diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06ff8eb..ee57d35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,6 +62,23 @@ jobs: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22 + - name: Resolve the real held rustc image (Unix) + if: runner.os != 'Windows' + run: echo "RUSTC=$(rustup which rustc)" >> "$GITHUB_ENV" + - name: Resolve the real held rustc image (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + "RUSTC=$(rustup which rustc)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + - name: Resolve the authenticated Windows SDK and MSVC environment + if: runner.os == 'Windows' + shell: cmd + run: | + for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VSINSTALL=%%i" + if not defined VSINSTALL exit /b 1 + call "%VSINSTALL%\Common7\Tools\VsDevCmd.bat" -no_logo -arch=x64 -host_arch=x64 + echo INCLUDE=%INCLUDE%>>"%GITHUB_ENV%" + echo LIB=%LIB%>>"%GITHUB_ENV%" - run: cargo fmt --all --check - run: cargo clippy --locked --workspace --all-targets --all-features -- -D warnings - run: cargo test --locked --workspace --all-targets --all-features @@ -75,6 +92,20 @@ jobs: run: cargo test --locked -p semaprax --lib agent_runtime::tests -- --nocapture - name: Require public bounded Agent Runtime injected-host evidence run: cargo test --locked -p semaprax --test agent_runtime_v1 -- --nocapture + - name: Require private Economic Agent deterministic fake-host evidence + run: cargo test --locked -p semaprax --lib economic_agent::tests -- --nocapture + - name: Require private Economic Agent durable marker process-termination evidence + run: cargo test --locked -p semaprax --lib economic_agent::tests::economic_process_kill_markers_never_repeat_sign_or_broadcast -- --exact --nocapture + - name: Require public Economic Agent injected-host evidence + run: cargo test --locked -p semaprax --test economic_agent_v1 -- --nocapture + - name: Require private Native Rust Interop language, HIR, Graph, and Wasm preservation evidence + run: | + cargo test --locked -p semaprax --test native_rust_interop_v1 -- --nocapture + cargo test --locked -p semaprax --test native_rust_interop_ci_contract -- --nocapture + - name: Require private Native Rust Interop A+B replay, static-link, runtime, and hostile evidence + run: cargo test --locked -p semaprax-native-rust-interop -- --nocapture + - name: Require private Native Rust Interop platform authority evidence + run: cargo test --locked -p semaprax-native-rust-interop-platform --all-targets -- --nocapture - name: Require Windows callable-v2 and private callable-v3 physical evidence if: runner.os == 'Windows' shell: pwsh @@ -117,6 +148,13 @@ jobs: run: | cargo test --locked -p semaprax --lib codegen::native_callable_provider_v3::tests::authoritative_fourteen_case_graph_providers_execute_and_settle_at_o0_o2 -- --exact cargo test --locked -p semaprax --lib codegen::native_callable_provider_v3::tests::physical_failure_injection_and_durable_settlement_boundaries_are_exact_at_o0_o2 -- --exact + - name: Require private Native Rust Interop ASan + UBSan round trip (Linux) + if: runner.os == 'Linux' + env: + SEMAPRAX_REQUIRE_NATIVE_RUST_INTEROP_SANITIZERS: "1" + ASAN_OPTIONS: detect_leaks=0:halt_on_error=1:abort_on_error=1 + UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1 + run: cargo test --locked -p semaprax-native-rust-interop --lib implementation::tests::linked_bridge_round_trips_rust_to_semaprax_to_rust_and_closes_failures -- --exact --nocapture - run: cargo test --locked --workspace --all-features --doc - run: cargo doc --locked --workspace --all-features --no-deps env: @@ -315,5 +353,8 @@ jobs: - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master with: toolchain: "1.85" + - name: Resolve the real held rustc image (Unix) + shell: bash + run: echo "RUSTC=$(rustup which rustc)" >> "$GITHUB_ENV" - run: cargo check --locked --workspace --all-targets --all-features - run: cargo test --locked --workspace --all-targets --all-features diff --git a/AGENTS.md b/AGENTS.md index d954676..dc4c4dd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -80,6 +80,10 @@ proof scaffolding, not a wired native-runtime claim. - `src/agent_runtime.rs`, `src/agent_runtime/`: bounded injected-host Agent profile, runtime-owned streaming sinks, cancellation, Trace, and Evidence; no built-in transport, write tool, durable memory, or economic authority. +- `src/economic_agent.rs`: public injected-host test-network/native-asset Economic Agent + policy, intent, chain-plan, approval, custody, journal, reconciliation, + Trace, and Evidence core; no built-in transport, key, or + mainnet authority. - `src/repair.rs`: bounded read-only Diagnostic Repair v1 discovery and instantiation plus the independently replayed Patch-v3 identity-rebase gate. - `src/review.rs`: bounded read-only Semantic Review v1 over complete Impact-v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index d5306d7..3b709cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +- Private Native Rust Interoperability v1 A+B design and local implementation + are green. The additive `import rust fn` scalar profile now resolves an + explicitly configured absolute Rust launcher only to discover one bounded + sysroot, independently holds the direct compiler at that sysroot, requires a + fixed-point sysroot check, and admits Rust artifacts only from that held + direct image. Phase B uses one pre-effect 12-use process arena whose Windows + attribute storage is queried, bounded, reserved, and materialized exactly, + prepared filesystem inventories/names, one fixed-capacity no-growth store for the four + authenticated `rustc -vV` fields, allocation-free final comparison/publication, + and fail-stop process/handle settlement. Private A now has named pre-HIR and + post-HIR retained/scratch envelopes, iterative renderer/replayer traversal, + exact persistent allocation transfers, and minimum-minus-one entry gates; + prepared Phase-B target arguments admit current-host underscore components + without opening other punctuation, and the Linux link plan freezes the + target's native-static library tail. Local builder 99/99, platform-system + 22/22, platform 10/10, source-contract 6/6, strict-Clippy, formatting, and + security gates are green; Windows directory authority now excludes mutable + directory length while retaining full file identity and reparse rejection. + Windows runtime, exact-head three-OS, and Linux sanitizer evidence + remain pending. Public C remains held; compiler sysroot/dynamic-library + descendant provenance, callable v2/v3, loader/host, `SPX-B104`, and existing + wires/KATs are unchanged. + - Added Bounded Native Agent Runtime v1 A+B proof and the additive C1 injected- host Rust API: canonical Profile/Task/Action/Trace/Evidence, deterministic routing, injected @@ -7,11 +30,7 @@ checks, cooperative cancellation, cumulative budgets, and independent replay. C1 exposes opaque Agent/run/sink types and no CLI, provider transport, ambient authority, language or backend semantics, durable memory, wallet, payment, or signing surface. - The exact `cd2f6393bb84657f7ef4f0094e1136eb5a401355` A+B matrix is hosted green - in [run 31585682213](https://github.com/wavect/semaprax/actions/runs/31585682213), - with all 12 jobs passing including Ubuntu/macOS/Windows fake-host evidence. - C1 public integration is locally green 4/4 and hosted promotion remains - pending. Totals remain 38 Partial/18 Missing. + Public Agent Runtime v1 is hosted GREEN at 8cf29aff8d1be3ccf74c36bc8c837f0c666ca067 (run 31591039261, 12/12 jobs, private and public deterministic fake-host gates on Ubuntu, macOS, and Windows). Private Economic Agent v1 A+B is exact-head hosted green at fe75c38d898b71e3ed5c57411fb46d0dbd4fc34b in run 31611748969, including both Economic gates on Ubuntu, macOS, and Windows. Public Economic Agent v1 C is exact-head hosted green at 03f1f2736de23d03b298f265f93409de89a6be95 in run 31616168124 (12/12 jobs), including the private, process-termination, and public Economic gates on Ubuntu, macOS, and Windows. Totals remain 38 Partial/18 Missing. All notable changes to SEMAPRAX are documented here. diff --git a/Cargo.lock b/Cargo.lock index 7eb0afe..feececd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -255,6 +255,32 @@ dependencies = [ "libloading", ] +[[package]] +name = "semaprax-native-rust-interop" +version = "0.1.0" +dependencies = [ + "semaprax", + "semaprax-native-rust-interop-platform", + "serde_json", + "sha2", +] + +[[package]] +name = "semaprax-native-rust-interop-platform" +version = "0.1.0" +dependencies = [ + "semaprax-native-rust-interop-platform-sys", +] + +[[package]] +name = "semaprax-native-rust-interop-platform-sys" +version = "0.1.0" +dependencies = [ + "libc", + "sha2", + "windows-sys", +] + [[package]] name = "semver" version = "1.0.28" diff --git a/Cargo.toml b/Cargo.toml index fe4a976..706e253 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,13 @@ keywords = ["compiler", "agents", "systems-programming", "semantic-graph"] categories = ["compilers", "development-tools"] [workspace] -members = ["crates/semaprax-native-host", "crates/semaprax-native-loader"] +members = [ + "crates/semaprax-native-host", + "crates/semaprax-native-loader", + "crates/semaprax-native-rust-interop-platform", + "crates/semaprax-native-rust-interop-platform-sys", + "crates/semaprax-native-rust-interop-builder", +] default-members = ["."] resolver = "2" diff --git a/README.md b/README.md index 110e3bb..5b8b6c4 100644 --- a/README.md +++ b/README.md @@ -172,10 +172,14 @@ does not satisfy a broader product gate. | Agent runtime | A bounded injected-host Rust API has hosted deterministic fake-host evidence. It is not a live provider transport, CLI agent, durable-memory system, wallet, payment, signing, or ambient-authority surface. | The bounded public Agent Runtime v1 gate is hosted green at -[`8cf29aff`](https://github.com/wavect/semaprax/commit/8cf29aff8d1be3ccf74c36bc8c837f0c666ca067) -([12/12 CI jobs](https://github.com/wavect/semaprax/actions/runs/31591039261)). -Private Economic Agent v1 A+B work is in progress; its promotion evidence is -pending and its public surface remains held. Neither changes the matrix totals. +Public Agent Runtime v1 is hosted GREEN at 8cf29aff8d1be3ccf74c36bc8c837f0c666ca067 (run 31591039261, 12/12 jobs, private and public deterministic fake-host gates on Ubuntu, macOS, and Windows). +Private Economic Agent v1 A+B is exact-head hosted green at fe75c38d898b71e3ed5c57411fb46d0dbd4fc34b in run 31611748969, including both Economic gates on Ubuntu, macOS, and Windows. Public Economic Agent v1 C is exact-head hosted green at 03f1f2736de23d03b298f265f93409de89a6be95 in run 31616168124 (12/12 jobs), including the private, process-termination, and public Economic gates on Ubuntu, macOS, and Windows. + +Private Native Rust Interoperability v1 A+B is locally green under the frozen +scalar/static-link profile; public C and exact-head Ubuntu/macOS/Windows plus +Linux-sanitizer promotion remain held. See [Native Rust Interoperability +v1](docs/NATIVE-RUST-INTEROP-V1.md). +Neither changes the matrix totals. For precise evidence, boundaries, and non-claims, use these documents: diff --git a/crates/semaprax-native-rust-interop-builder/Cargo.toml b/crates/semaprax-native-rust-interop-builder/Cargo.toml new file mode 100644 index 0000000..be6a464 --- /dev/null +++ b/crates/semaprax-native-rust-interop-builder/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "semaprax-native-rust-interop" +version = "0.1.0" +edition = "2021" +rust-version = "1.85" +publish = false +description = "Private bounded Native Rust Interop builder for SEMAPRAX" +license = "Apache-2.0" + +[dependencies] +semaprax = { version = "=0.2.0", path = "../..", default-features = false } +semaprax-native-rust-interop-platform = { version = "=0.1.0", path = "../semaprax-native-rust-interop-platform" } +serde_json = "=1.0.151" +sha2 = "=0.10.9" + +[lints.rust] +unsafe_code = "forbid" diff --git a/crates/semaprax-native-rust-interop-builder/src/implementation.rs b/crates/semaprax-native-rust-interop-builder/src/implementation.rs new file mode 100644 index 0000000..34340d7 --- /dev/null +++ b/crates/semaprax-native-rust-interop-builder/src/implementation.rs @@ -0,0 +1,27836 @@ +// Private Native Rust Interoperability v1 preparation and static-bundle lane. +// Compiled only by the unpublished interop crate during private A+B. + +use std::collections::{BTreeMap, BTreeSet}; +use std::ffi::{OsStr, OsString}; +use std::fmt::Write as _; +use std::path::{Path, PathBuf}; + +use serde_json::Value; +use sha2::{Digest, Sha256}; + +use crate::platform; + +use crate::ast::{ParamMode, Program, Type}; +use crate::diagnostic::{quote_json, Diagnostic}; +use crate::hir::{ + self, DeclarationId, OwnershipMode, ResolvedExpr, ResolvedExprKind, ResolvedFunction, + ResolvedImport, ResolvedImportFailure, ResolvedImportResultKind, ResolvedProgram, + ResolvedStatement, ResolvedType, +}; + +const SPEC_SCHEMA: &str = "semaprax.native-rust-interop-spec.v1"; +const DESCRIPTOR_SCHEMA: &str = "semaprax.native-rust-interop-descriptor.v1"; +const BUNDLE_SCHEMA: &str = "semaprax.native-rust-interop-bundle.v1"; +const SOURCE_DOMAIN: &[u8] = b"semaprax.native-rust-interop.source-revision.v1\0"; +const HIR_DOMAIN: &[u8] = b"semaprax.native-rust-interop.hir-digest.v1\0"; +const SPEC_DIGEST_DOMAIN: &[u8] = b"semaprax.native-rust-interop.spec-digest.v1\0"; +const DESCRIPTOR_DIGEST_DOMAIN: &[u8] = b"semaprax.native-rust-interop.descriptor-digest.v1\0"; +const CALL_DOMAIN: &[u8] = b"semaprax.native-rust-interop.call-contract.v1\0"; +const BUNDLE_DIGEST_DOMAIN: &[u8] = b"semaprax.native-rust-interop.bundle-digest.v1\0"; +const CAPABILITIES_DOMAIN: &[u8] = b"semaprax.native-rust-interop.capabilities.v1\0"; + +const MAX_EXPORTS: usize = 32; +const MAX_IMPORTS: usize = 32; +const MAX_PARAMETERS: usize = 8; +const MAX_CLOSURE_FUNCTIONS: usize = 256; +const MAX_STATUS_DOMAINS: usize = 64; +const MAX_EFFECTS: usize = 64; +const MAX_IDENTIFIER_BYTES: usize = 128; +const MAX_SOURCE_BYTES: usize = 16_777_216; +const MAX_SPEC_BYTES: usize = 1_048_576; +const MAX_DESCRIPTOR_BYTES: usize = 1_048_576; +const MAX_GENERATED_C_BYTES: usize = 4_194_304; +const MAX_GENERATED_HEADER_BYTES: usize = 1_048_576; +const MAX_GENERATED_RUST_BYTES: usize = 4_194_304; +const MAX_MANIFEST_BYTES: usize = 1_048_576; +const MAX_BUILDER_BYTES: usize = 33_554_432; +const SHA256_TEXT_BYTES: usize = "sha256:".len() + 64; +const PHASE_B_STAGE_NAME_CAPACITY: usize = 96; +const FINGERPRINT_ACTION_SLOTS: usize = MAX_SEMANTIC_EXPRESSION_DEPTH * 4 + 8; +// Pinned by module-local assertions beside the private iterative enums. +const HIR_RESOLVER_FRAME_BYTES: usize = 552; +const HIR_VALIDATOR_FRAME_BYTES: usize = 288; +const SOURCE_VERIFIER_FRAME_BYTES: usize = 320; +const SOURCE_VARIANT_MATCH_STATE_BYTES: usize = 312; +const CLEANUP_INVENTORY_SHAPE_FRAME_BYTES: usize = 40; +const CLEANUP_INVENTORY_EXPR_FRAME_BYTES: usize = 24; +const CLEANUP_LOWER_FRAME_BYTES: usize = 344; +const CLEANUP_EVAL_RESULT_BYTES: usize = 128; +const CALL_INDEX_FRAME_BYTES: usize = 16; +const C_EXPRESSION_FRAME_BYTES: usize = std::mem::size_of::>(); +const REPLAY_C_EXPRESSION_FRAME_BYTES: usize = + std::mem::size_of::>(); +const MAX_FORMAT_NESTING: usize = MAX_SEMANTIC_EXPRESSION_DEPTH + 1; + +#[cfg(test)] +thread_local! { + static CANONICAL_FORMAT_PASS_COUNT: std::cell::Cell = const { std::cell::Cell::new(0) }; + static HIR_RESOLVE_PASS_COUNT: std::cell::Cell = const { std::cell::Cell::new(0) }; + static HIR_POST_RESOLVE_PHASE_COUNT: std::cell::Cell<[usize; 4]> = const { std::cell::Cell::new([0; 4]) }; + static HIR_POST_RESOLVE_CAPACITY_HIGH_WATER: std::cell::Cell<[usize; 3]> = const { std::cell::Cell::new([0; 3]) }; + static POST_HIR_FACTS_ENTRY_COUNT: std::cell::Cell = const { std::cell::Cell::new(0) }; + static POST_HIR_FACTS_CAPACITY_HIGH_WATER: std::cell::Cell = const { std::cell::Cell::new(0) }; + static POST_HIR_FACTS_SCRATCH_HIGH_WATER: std::cell::Cell = const { std::cell::Cell::new(0) }; + static POST_HIR_AUTHORITY_TRANSFER_TERMS: std::cell::Cell<[usize; 5]> = const { std::cell::Cell::new([0; 5]) }; + static POST_HIR_RENDER_CAPACITY_HIGH_WATER: std::cell::Cell = const { std::cell::Cell::new(0) }; + static POST_HIR_REPLAY_CAPACITY_HIGH_WATER: std::cell::Cell = const { std::cell::Cell::new(0) }; + static EXACT_ARTIFACT_OUTPUT_ALLOCATION_COUNT: std::cell::Cell = const { std::cell::Cell::new(0) }; + static CLOSURE_CAPACITY_HIGH_WATER: std::cell::Cell = const { std::cell::Cell::new(0) }; + static RESOLVED_DISPOSE_HIGH_WATER: std::cell::Cell = const { std::cell::Cell::new(0) }; + static RESOLVED_DISPOSE_COMPLETIONS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static RESOLVED_DISPOSE_CAPACITIES: std::cell::Cell<[usize; 2]> = const { std::cell::Cell::new([0; 2]) }; + static PREPARE_FAILURE_INJECTION: std::cell::Cell> = const { std::cell::Cell::new(None) }; + static CREATE_AUTH_DISAGREEMENT: std::cell::Cell> = const { std::cell::Cell::new(None) }; + static CREATE_AUTH_DISCARD_ATTEMPTS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_EFFECT_STARTED: std::cell::Cell = const { std::cell::Cell::new(false) }; + static PHASE_B_POST_EFFECT_ERROR_MATERIALIZATIONS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_NATIVE_STAGE_ARENA_ALLOCATIONS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_NATIVE_STAGE_ARENA_SETS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_NATIVE_STAGE_ARENA_CONSUMPTIONS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_PREPARED_CARRIER_IDENTITIES: std::cell::Cell<[usize; 7]> = const { std::cell::Cell::new([0; 7]) }; + static PHASE_B_LOCAL_FAILURE_INJECTION: std::cell::Cell> = const { std::cell::Cell::new(None) }; + static PHASE_B_DISCARD_FAILURE_AFTER_DELETE: std::cell::Cell> = const { std::cell::Cell::new(None) }; + static PHASE_B_DISCARD_ATTEMPTS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_OVERSIZE_MANIFEST_INJECTION: std::cell::Cell = const { std::cell::Cell::new(false) }; + static PHASE_B_OUTPUT_PROBES: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_TOOL_HOLDS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_TOOL_PROCESSES: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_PROCESS_ARENA_DROPS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_PROCESS_ARENA_BUDGET_DROPS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_PROCESS_ARENA_DROP_ORDER: std::cell::Cell<[u8; 2]> = const { std::cell::Cell::new([0; 2]) }; + static PHASE_B_PROCESS_ARENA_DROP_ORDER_LENGTH: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_INVALID_TOOL_ENV_INJECTION: std::cell::Cell = const { std::cell::Cell::new(false) }; + static PHASE_B_DIRECT_SYSROOT_MISMATCH_INJECTION: std::cell::Cell = const { std::cell::Cell::new(false) }; + static PHASE_B_BUILD_INVOCATION_PLANS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_BUILD_INVOCATION_CONSUMPTIONS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_LINK_COPY_PLANS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_LINK_COPY_CONSUMPTIONS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_LINK_COPY_FAIL_BEFORE_AUTHENTICATION: std::cell::Cell = const { std::cell::Cell::new(false) }; + static PHASE_B_INVENTORY_EXACT_PLANS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_INVENTORY_EXACT_SCANS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_PUBLISH_PLANS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_PUBLISH_CONSUMPTIONS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_PUBLISH_FAILURE: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_OBJECT_AUTHORITY_TRANSFERS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_OBJECT_AUTHORITY_DROPS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_OBJECT_AUTHORITY_LIVE: std::cell::Cell = const { std::cell::Cell::new(false) }; + static PHASE_B_OBJECT_AUTHORITY_MANIFEST_OBSERVATIONS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_OBJECT_AUTHORITY_PUBLISH_OBSERVATIONS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_OBJECT_BYTES_DROPS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_OBJECT_DROP_ORDER: std::cell::Cell<[u8; 2]> = const { std::cell::Cell::new([0; 2]) }; + static PHASE_B_OBJECT_DROP_ORDER_LENGTH: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_MANIFEST_PLAN_CAPACITY: std::cell::Cell = const { std::cell::Cell::new(MAX_MANIFEST_BYTES) }; + static PHASE_B_MANIFEST_ARENA_ALLOCATIONS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_MANIFEST_ARENA_GROWTHS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_MANIFEST_AUTHORITY_TRANSFERS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_MANIFEST_AUTHORITY_DROPS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_MANIFEST_AUTHORITY_LIVE: std::cell::Cell = const { std::cell::Cell::new(false) }; + static PHASE_B_MANIFEST_BYTES_DROPS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PHASE_B_MANIFEST_DROP_ORDER: std::cell::Cell<[u8; 2]> = const { std::cell::Cell::new([0; 2]) }; + static PHASE_B_MANIFEST_DROP_ORDER_LENGTH: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +#[cfg(test)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum PrepareFailurePoint { + Closure, + Facts, + Render, + Replay, +} + +#[cfg(test)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum CreateAuthDisagreement { + Clean, + Substituted, +} + +#[cfg(test)] +fn inject_prepare_failure(point: PrepareFailurePoint) -> Result<(), Diagnostic> { + if PREPARE_FAILURE_INJECTION.with(std::cell::Cell::get) == Some(point) { + Err(b107("injected private preparation failure")) + } else { + Ok(()) + } +} + +#[cfg(test)] +fn note_canonical_format_pass() { + CANONICAL_FORMAT_PASS_COUNT.with(|count| count.set(count.get() + 1)); +} + +#[cfg(test)] +fn note_hir_resolve_pass() { + HIR_RESOLVE_PASS_COUNT.with(|count| count.set(count.get() + 1)); +} + +#[cfg(test)] +fn note_hir_post_resolve_phase(index: usize) { + HIR_POST_RESOLVE_PHASE_COUNT.with(|counts| { + let mut values = counts.get(); + values[index] += 1; + counts.set(values); + }); +} + +#[cfg(test)] +fn note_hir_post_resolve_capacity(index: usize, bytes: usize) { + HIR_POST_RESOLVE_CAPACITY_HIGH_WATER.with(|water| { + let mut values = water.get(); + values[index] = values[index].max(bytes); + water.set(values); + }); +} + +#[cfg(test)] +fn note_post_hir_facts_entry() { + POST_HIR_FACTS_ENTRY_COUNT.with(|count| count.set(count.get() + 1)); +} + +#[cfg(test)] +fn note_post_hir_facts_capacity(bytes: usize) { + POST_HIR_FACTS_CAPACITY_HIGH_WATER.with(|water| water.set(water.get().max(bytes))); +} + +#[cfg(test)] +fn note_post_hir_facts_scratch(bytes: usize) { + POST_HIR_FACTS_SCRATCH_HIGH_WATER.with(|water| water.set(water.get().max(bytes))); +} + +#[cfg(test)] +fn note_post_hir_render_capacity(bytes: usize) { + POST_HIR_RENDER_CAPACITY_HIGH_WATER.with(|water| water.set(water.get().max(bytes))); +} + +#[cfg(test)] +fn note_post_hir_replay_capacity(bytes: usize) { + POST_HIR_REPLAY_CAPACITY_HIGH_WATER.with(|water| water.set(water.get().max(bytes))); +} + +#[cfg(not(test))] +fn note_hir_post_resolve_phase(_index: usize) {} + +#[cfg(not(test))] +fn note_hir_post_resolve_capacity(_index: usize, _bytes: usize) {} + +#[cfg(not(test))] +fn note_hir_resolve_pass() {} + +#[cfg(test)] +fn reset_closure_capacity_high_water() { + CLOSURE_CAPACITY_HIGH_WATER.with(|water| water.set(0)); +} + +#[cfg(test)] +fn closure_capacity_high_water() -> usize { + CLOSURE_CAPACITY_HIGH_WATER.with(std::cell::Cell::get) +} + +#[cfg(test)] +fn note_closure_capacity_high_water(bytes: usize) { + CLOSURE_CAPACITY_HIGH_WATER.with(|water| water.set(water.get().max(bytes))); +} + +#[cfg(test)] +fn note_resolved_dispose_high_water(len: usize) { + RESOLVED_DISPOSE_HIGH_WATER.with(|water| water.set(water.get().max(len))); +} + +#[cfg(not(test))] +fn note_resolved_dispose_high_water(_len: usize) {} + +#[cfg(test)] +fn note_resolved_dispose_completion() { + RESOLVED_DISPOSE_COMPLETIONS.with(|count| count.set(count.get() + 1)); +} + +#[cfg(test)] +fn note_resolved_dispose_capacity(index: usize, capacity: usize) { + RESOLVED_DISPOSE_CAPACITIES.with(|capacities| { + let mut values = capacities.get(); + values[index] = capacity; + capacities.set(values); + }); +} + +#[cfg(not(test))] +fn note_resolved_dispose_capacity(_index: usize, _capacity: usize) {} + +#[cfg(not(test))] +fn note_resolved_dispose_completion() {} + +#[cfg(not(test))] +fn note_canonical_format_pass() {} +const MAX_JSON_DEPTH: usize = 8; +const MAX_SEMANTIC_EXPRESSION_DEPTH: usize = 512; +const MAX_CALL_DEPTH: usize = 32; +const MAX_CALLS_PER_BRIDGE: usize = 4_096; +const LIMIT_ROWS: [(&str, usize); 20] = [ + ("max_exports", MAX_EXPORTS), + ("max_imports", MAX_IMPORTS), + ("max_parameters", MAX_PARAMETERS), + ("max_closure_functions", MAX_CLOSURE_FUNCTIONS), + ("max_status_domains", MAX_STATUS_DOMAINS), + ("max_effects", MAX_EFFECTS), + ("max_identifier_bytes", MAX_IDENTIFIER_BYTES), + ("max_source_bytes", MAX_SOURCE_BYTES), + ("max_spec_bytes", MAX_SPEC_BYTES), + ("max_descriptor_bytes", MAX_DESCRIPTOR_BYTES), + ("max_generated_c_bytes", MAX_GENERATED_C_BYTES), + ("max_generated_header_bytes", MAX_GENERATED_HEADER_BYTES), + ("max_generated_rust_bytes", MAX_GENERATED_RUST_BYTES), + ("max_manifest_bytes", MAX_MANIFEST_BYTES), + ("max_builder_bytes", MAX_BUILDER_BYTES), + ("max_json_depth", MAX_JSON_DEPTH), + ( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + ), + ("max_call_depth", MAX_CALL_DEPTH), + ("max_calls_per_bridge", MAX_CALLS_PER_BRIDGE), + ("max_unexpected_inventory_entries", 0), +]; + +const NONCLAIMS: &[&str] = &[ + "no_resource_owned_borrow_shared_or_aggregate_abi", + "no_pointer_reference_slice_string_trait_object_or_generic_abi", + "no_cross_boundary_allocator_or_deallocator", + "no_wasm_component_or_canonical_abi_detour", + "no_dynamic_loading_symbol_lookup_unload_or_hot_reload", + "no_public_execution_or_spx_b104_change_in_private_ab", + "no_callable_v2_v3_proof_bundle_or_loader_wire_change", + "no_graph_schema_api_kat_or_semantic_projection_change", + "no_agent_runtime_economic_workspace_or_patch_wire_change", + "no_untrusted_native_code_sandbox_or_memory_safety", + "no_same_uid_process_signal_or_task_port_isolation", + "no_same_uid_active_filesystem_mutation_or_namespace_race_isolation", + "no_same_user_process_handle_or_thread_resume_isolation", + "no_abi_compatibility_outside_exact_descriptor_target_toolchain", + "no_cross_target_cross_toolchain_or_cross_build_bundle_reuse", + "no_panic_or_unwind_across_ffi", + "no_abort_oom_stack_overflow_signal_seh_or_process_crash_recovery", + "no_power_loss_durability_or_crash_atomicity", + "no_async_reentrant_parallel_cross_thread_or_send_sync_bridge", + "no_host_capability_provenance_or_os_authority", + "no_ambient_effect_capability_or_callback_discovery", + "no_host_error_text_panic_payload_secret_or_pointer_evidence", + "no_exactly_once_external_effect", + "no_exception_cpp_rust_unwind_translation", + "no_dynamic_library_code_signing_supply_chain_or_linker_provenance", + "no_dynamic_dependency_identity_or_filesystem_race_isolation", + "no_c_cpp_objective_c_swift_kotlin_jni_or_other_ecosystem_binding", + "no_stable_rust_abi_claim_beyond_generated_c_abi_wrapper", + "no_public_cli_package_registry_or_build_script_network", + "no_general_interop_or_production_readiness", + "no_completion_matrix_status_promotion", +]; + +#[derive(Clone)] +struct Spec { + module: String, + source_revision: String, + target: Target, + exports: Vec, + imports: Vec, + capabilities: Vec, +} + +#[derive(Clone, Eq, PartialEq)] +struct Target { + triple: String, + pointer_width: u32, + endian: String, + panic_strategy: String, + thread_policy: String, +} + +fn checked_spec_owned_capacity(spec: &Spec) -> Option { + std::mem::size_of::() + .checked_add(spec.module.capacity()) + .and_then(|bytes| bytes.checked_add(spec.source_revision.capacity())) + .and_then(|bytes| bytes.checked_add(spec.target.triple.capacity())) + .and_then(|bytes| bytes.checked_add(spec.target.endian.capacity())) + .and_then(|bytes| bytes.checked_add(spec.target.panic_strategy.capacity())) + .and_then(|bytes| bytes.checked_add(spec.target.thread_policy.capacity())) + .and_then(|bytes| { + [&spec.exports, &spec.imports, &spec.capabilities] + .into_iter() + .try_fold(bytes, |bytes, values| { + bytes + .checked_add( + values + .capacity() + .checked_mul(std::mem::size_of::())?, + ) + .and_then(|bytes| { + values + .iter() + .try_fold(bytes, |bytes, value| bytes.checked_add(value.capacity())) + }) + }) + }) +} + +fn prepared_spec_transfer_capacity(spec: &Spec) -> Option { + spec.source_revision + .capacity() + .checked_add(spec.target.triple.capacity()) + .and_then(|bytes| bytes.checked_add(spec.target.endian.capacity())) + .and_then(|bytes| bytes.checked_add(spec.target.panic_strategy.capacity())) + .and_then(|bytes| bytes.checked_add(spec.target.thread_policy.capacity())) +} + +#[derive(Clone)] +struct ParameterFact { + name: String, + ty: ScalarType, +} + +#[derive(Clone, Copy, Eq, PartialEq)] +enum ScalarType { + Unit, + I64, + Bool, +} + +#[derive(Clone)] +struct ExportFact { + id: String, + rust_method: String, + c_symbol: String, + parameters: Vec, + result: ScalarType, + effects: Vec, + capabilities: Vec, + required_imports: Vec, + status_domain_ordinals: Vec, + call_contract_digest: String, +} + +#[derive(Clone)] +struct ImportFact { + id: String, + interface: String, + import_key: String, + rust_method: String, + c_field: String, + parameters: Vec, + result: ScalarType, + effects: Vec, + capabilities: Vec, + failure: Option, + call_contract_digest: String, +} + +#[derive(Clone, Copy)] +struct PostHirFactsCapacity { + retained_upper: usize, + facts_scratch_upper: usize, + render_scratch_upper: usize, + replay_scratch_upper: usize, + traversal_pending_capacity: usize, +} + +impl PostHirFactsCapacity { + fn scratch_upper(self) -> usize { + self.facts_scratch_upper + .max(self.render_scratch_upper) + .max(self.replay_scratch_upper) + } + + fn complete(self) -> Option { + self.retained_upper.checked_add(self.scratch_upper()) + } +} + +fn checked_btree_allocation_upper(len: usize) -> Option { + len.checked_mul( + std::mem::size_of::<(K, V)>().checked_add(std::mem::size_of::>())?, + ) +} + +#[cfg(test)] +fn checked_owned_string_vec(values: &[String], capacity: usize) -> Option { + values.iter().try_fold( + capacity.checked_mul(std::mem::size_of::())?, + |bytes, value| bytes.checked_add(value.capacity()), + ) +} + +#[cfg(test)] +fn checked_owned_string_pairs(values: &Vec<(String, String)>) -> Option { + values.iter().try_fold( + values + .capacity() + .checked_mul(std::mem::size_of::<(String, String)>())?, + |bytes, (left, right)| { + bytes + .checked_add(left.capacity())? + .checked_add(right.capacity()) + }, + ) +} + +#[cfg(test)] +fn checked_u16_vec(values: &Vec) -> Option { + values.capacity().checked_mul(std::mem::size_of::()) +} + +#[cfg(test)] +fn note_post_hir_facts_live(_baseline: usize, scratch: usize) { + note_post_hir_facts_scratch(scratch); + note_post_hir_facts_capacity(_baseline.saturating_add(scratch)); +} + +#[cfg(test)] +fn checked_owned_string_set(values: &BTreeSet) -> Option { + values.iter().try_fold( + checked_btree_allocation_upper::(values.len())?, + |bytes, value| bytes.checked_add(value.capacity()), + ) +} + +#[cfg(test)] +fn checked_json_value_owned(value: &Value) -> Option { + match value { + Value::Null | Value::Bool(_) | Value::Number(_) => Some(0), + Value::String(value) => Some(value.capacity()), + Value::Array(values) => values.iter().try_fold( + values + .capacity() + .checked_mul(std::mem::size_of::())?, + |bytes, value| bytes.checked_add(checked_json_value_owned(value)?), + ), + Value::Object(values) => values.iter().try_fold( + checked_btree_allocation_upper::(values.len())?, + |bytes, (key, value)| { + bytes + .checked_add(key.capacity())? + .checked_add(checked_json_value_owned(value)?) + }, + ), + } +} + +#[cfg(test)] +fn checked_json_string_payload(value: &Value) -> Option { + match value { + Value::Null | Value::Bool(_) | Value::Number(_) => Some(0), + Value::String(value) => Some(value.capacity()), + Value::Array(values) => values.iter().try_fold(0usize, |bytes, value| { + bytes.checked_add(checked_json_string_payload(value)?) + }), + Value::Object(values) => values.iter().try_fold(0usize, |bytes, (key, value)| { + bytes + .checked_add(key.capacity())? + .checked_add(checked_json_string_payload(value)?) + }), + } +} + +fn post_hir_facts_capacity( + _source_bytes: usize, + spec_bytes: usize, + resolved: &ResolvedProgram, + closure: &[&ResolvedFunction], + spec: &Spec, +) -> Result { + let selected = closure.len().max(1); + let exports = spec.exports.len(); + let imports = spec.imports.len(); + let capabilities = spec.capabilities.len(); + let resolved_import_count = resolved + .interfaces + .iter() + .try_fold(0usize, |count, interface| { + count.checked_add(interface.imports.len()) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let digest_text_capacity = "sha256:" + .len() + .checked_add(64) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let parameter_slots = closure + .iter() + .try_fold(0usize, |count, function| { + count.checked_add(function.params.len()) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + // This census executes before the post-HIR reservation. Traverse borrowed + // imports directly: no Vec or map may be materialized until `complete()` + // has been admitted. + let ( + import_parameter_slots, + import_retained_payload, + import_effect_entries, + selected_import_id_bytes, + selected_import_effect_bytes, + ) = resolved + .interfaces + .iter() + .flat_map(|interface| &interface.imports) + .filter(|import| spec.imports.iter().any(|id| id == import.id.as_str())) + .try_fold((0usize, 0usize, 0usize, 0usize, 0usize), |state, import| { + let parameter_names = import + .parameters + .iter() + .try_fold(0usize, |total, parameter| { + total.checked_add(parameter.name.capacity()) + })?; + let effects = import + .effects + .iter() + .try_fold(0usize, |total, effect| total.checked_add(effect.capacity()))?; + let failure = match &import.failure { + ResolvedImportFailure::Infallible => 0, + ResolvedImportFailure::Status { domain_id, .. } => domain_id.len(), + }; + let parameter_backing = import + .parameters + .len() + .checked_mul(std::mem::size_of::())?; + let effect_backing = import + .effects + .len() + .checked_mul(std::mem::size_of::())? + .checked_mul(2)?; + let retained = import + .id + .as_str() + .len() + .checked_add(import.interface.as_str().len())? + .checked_add(import.import_key.capacity())? + .checked_add("import_".len().checked_add(64)?)? + .checked_add("spxnr1_i_".len().checked_add(64)?)? + .checked_add(parameter_backing)? + .checked_add(parameter_names)? + .checked_add(effect_backing)? + .checked_add(effects.checked_mul(2)?)? + .checked_add(failure)? + .checked_add(digest_text_capacity)?; + Some(( + state.0.checked_add(import.parameters.len())?, + state.1.checked_add(retained)?, + state.2.checked_add(import.effects.len())?, + state.3.checked_add(import.id.as_str().len())?, + state.4.checked_add(effects)?, + )) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let import_effect_conversion_scratch = resolved + .interfaces + .iter() + .flat_map(|interface| &interface.imports) + .filter(|import| spec.imports.iter().any(|id| id == import.id.as_str())) + .try_fold(0usize, |maximum, import| { + let effect_payload = import + .effects + .iter() + .try_fold(0usize, |bytes, effect| bytes.checked_add(effect.capacity()))?; + let parameter_payload = import + .parameters + .iter() + .try_fold(0usize, |bytes, parameter| { + bytes.checked_add(parameter.name.capacity()) + })?; + let failure_payload = match &import.failure { + ResolvedImportFailure::Infallible => 0, + ResolvedImportFailure::Status { domain_id, .. } => domain_id.as_str().len(), + }; + let scratch = checked_btree_allocation_upper::(import.effects.len())? + .checked_add(effect_payload)? + .checked_add( + import + .effects + .len() + .checked_mul(std::mem::size_of::())?, + )? + .checked_add(effect_payload)? + .checked_add( + import + .parameters + .len() + .checked_mul(std::mem::size_of::())?, + )? + .checked_add(parameter_payload)? + .checked_add(failure_payload)? + .checked_add(digest_text_capacity)?; + Some(maximum.max(scratch)) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let checked_string_bytes = |values: &[String]| { + values + .iter() + .try_fold(0usize, |bytes, value| bytes.checked_add(value.capacity())) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES)) + }; + let import_id_bytes = checked_string_bytes(&spec.imports)?; + let capability_bytes = checked_string_bytes(&spec.capabilities)?; + let closure_id_bytes = closure + .iter() + .try_fold(0usize, |bytes, function| { + bytes.checked_add(function.id.as_str().len()) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let export_retained_payload = spec + .exports + .iter() + .try_fold(0usize, |bytes, id| { + let function = closure + .iter() + .find(|function| function.id.as_str() == id) + .copied()?; + let parameter_payload = function + .params + .iter() + .try_fold(0usize, |payload, parameter| { + payload.checked_add(parameter.name.capacity()) + })?; + let parameter_backing = function + .params + .len() + .checked_mul(std::mem::size_of::())?; + let effect_payload = function + .effects + .iter() + .try_fold(0usize, |payload, effect| { + payload.checked_add(effect.capacity()) + })?; + let effect_backing = function + .effects + .len() + .checked_mul(std::mem::size_of::())?; + let capability_backing = capabilities.checked_mul(std::mem::size_of::())?; + let required_import_backing = imports.checked_mul(std::mem::size_of::())?; + let status_ordinal_backing = imports + .checked_add(3)? + .checked_mul(std::mem::size_of::())?; + let retained = id + .len() + .checked_add("export_".len().checked_add(64)?)? + .checked_add("spxnr1_e_".len().checked_add(64)?)? + .checked_add(parameter_backing)? + .checked_add(parameter_payload)? + .checked_add(effect_backing)? + .checked_add(effect_payload)? + .checked_add(capability_backing)? + .checked_add(capability_bytes)? + .checked_add(required_import_backing)? + .checked_add(import_id_bytes)? + .checked_add(status_ordinal_backing)? + .checked_add(digest_text_capacity)?; + bytes.checked_add(retained) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let target_retained_payload = spec + .target + .triple + .capacity() + .checked_add(spec.target.endian.capacity()) + .and_then(|bytes| bytes.checked_add(spec.target.panic_strategy.capacity())) + .and_then(|bytes| bytes.checked_add(spec.target.thread_policy.capacity())) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let final_digest_payload = digest_text_capacity + .checked_mul(3) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let retained_upper = exports + .checked_mul(std::mem::size_of::()) + .and_then(|bytes| { + bytes.checked_add(imports.checked_mul(std::mem::size_of::())?) + }) + .and_then(|bytes| bytes.checked_add(import_retained_payload)) + .and_then(|bytes| bytes.checked_add(export_retained_payload)) + .and_then(|bytes| { + bytes.checked_add(closure.len().checked_mul(std::mem::size_of::())?) + }) + .and_then(|bytes| bytes.checked_add(closure_id_bytes)) + .and_then(|bytes| bytes.checked_add(spec.source_revision.capacity())) + .and_then(|bytes| bytes.checked_add(final_digest_payload)) + .and_then(|bytes| bytes.checked_add(target_retained_payload)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let (maximum_c_nodes, maximum_c_depth, maximum_parameter_owned) = closure + .iter() + .try_fold((1usize, 1usize, 0usize), |maximum, function| { + let function_shape = function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures) + .try_fold((1usize, 1usize), |current, expression| { + let (nodes, depth) = c_expression_shape(expression).ok()?; + Some((current.0.max(nodes), current.1.max(depth))) + })?; + let parameter_owned = function + .params + .iter() + .try_fold(0usize, |bytes, parameter| { + bytes.checked_add(parameter.name.len()) + })? + .checked_add( + function + .params + .len() + .checked_mul(std::mem::size_of::())?, + )?; + Some(( + maximum.0.max(function_shape.0), + maximum.1.max(function_shape.1), + maximum.2.max(parameter_owned), + )) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let selected_effects_backing = + checked_btree_allocation_upper::<&str, ()>(import_effect_entries) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let source_function_backing = + checked_btree_allocation_upper::<&str, &ResolvedFunction>(resolved.functions.len()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let resolved_import_backing = resolved_import_count + .checked_mul(std::mem::size_of::<(&str, &ResolvedImport)>()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let by_function_backing = + checked_btree_allocation_upper::<&str, &ResolvedFunction>(closure.len()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let selection_scratch = selected_effects_backing + .checked_add(source_function_backing) + .and_then(|bytes| bytes.checked_add(resolved_import_backing)) + .and_then(|bytes| bytes.checked_add(by_function_backing)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + + let traversal_calls = traversal_call_site_census(closure)?; + let traversal_pending_capacity = traversal_calls + .function_sites + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let current_id_payload = closure + .iter() + .map(|function| function.id.as_str().len()) + .max() + .unwrap_or(0); + let pending_backing = traversal_pending_capacity + .checked_mul(std::mem::size_of::()) + .and_then(|bytes| bytes.checked_add(traversal_calls.function_id_bytes)) + .and_then(|bytes| bytes.checked_add(current_id_payload)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let visited_backing = checked_btree_allocation_upper::(closure.len()) + .and_then(|bytes| bytes.checked_add(closure_id_bytes)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let direct_function_backing = + checked_btree_allocation_upper::(traversal_calls.function_sites) + .and_then(|bytes| bytes.checked_mul(2)) + .and_then(|bytes| bytes.checked_add(traversal_calls.function_id_bytes.checked_mul(2)?)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let direct_import_backing = + checked_btree_allocation_upper::(traversal_calls.import_sites) + .and_then(|bytes| bytes.checked_mul(2)) + .and_then(|bytes| bytes.checked_add(traversal_calls.import_id_bytes.checked_mul(2)?)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let transitive_import_backing = checked_btree_allocation_upper::(imports) + .and_then(|bytes| bytes.checked_add(selected_import_id_bytes)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let traversal_scratch = pending_backing + .checked_add(visited_backing) + .and_then(|bytes| bytes.checked_add(direct_function_backing)) + .and_then(|bytes| bytes.checked_add(direct_import_backing)) + .and_then(|bytes| bytes.checked_add(transitive_import_backing)) + .and_then(|bytes| bytes.checked_add(current_id_payload)) + .and_then(|bytes| { + bytes.checked_add( + (MAX_SEMANTIC_EXPRESSION_DEPTH + 1) + .checked_mul(std::mem::size_of::<(&ResolvedExpr, usize)>())?, + ) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let facts_cross_product_entry = digest_text_capacity + .checked_mul(2) + .and_then(|bytes| bytes.checked_add(std::mem::size_of::<(String, String)>())) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let facts_cross_product = exports + .checked_mul(imports) + .and_then(|rows| rows.checked_mul(facts_cross_product_entry)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let export_construction_scratch = export_retained_payload + .checked_add(facts_cross_product) + .and_then(|bytes| bytes.checked_add(import_retained_payload)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let facts_general_scratch = selection_scratch + .checked_add(traversal_scratch) + .and_then(|bytes| bytes.checked_add(export_construction_scratch)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let import_phase_scratch = selection_scratch + .checked_add(import_effect_conversion_scratch) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + // Status-domain canonicalization owns the complete BTreeSet while the + // exact-capacity Vec is filled. Charge both container allocations and a + // conservative copy of every bounded key payload; the actual conversion + // moves each String, so this is an upper rather than an amortized claim. + let status_payload = imports + .checked_mul(MAX_IDENTIFIER_BYTES) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let selected_capabilities_owned = import_effect_entries + .checked_mul(std::mem::size_of::()) + .and_then(|bytes| bytes.checked_add(selected_import_effect_bytes)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let status_set_vec_scratch = checked_btree_allocation_upper::(imports) + .and_then(|bytes| bytes.checked_add(status_payload)) + .and_then(|bytes| bytes.checked_add(imports.checked_mul(std::mem::size_of::())?)) + .and_then(|bytes| bytes.checked_add(status_payload)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let status_conversion_scratch = status_set_vec_scratch + .checked_add(selection_scratch) + .and_then(|bytes| bytes.checked_add(selected_capabilities_owned)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let fingerprint_action_scratch = FINGERPRINT_ACTION_SLOTS + .checked_mul(std::mem::size_of::>()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let fingerprint_scratch = fingerprint_action_scratch + .checked_add(fingerprint_type_scratch_upper(closure)?) + .and_then(|bytes| bytes.checked_add(digest_text_capacity)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let facts_scratch_upper = facts_general_scratch + .max(import_phase_scratch) + .max(status_conversion_scratch) + .max(fingerprint_scratch); + // Artifact outputs have independent retained reservations. These terms + // authorize only simultaneously live renderer/replay scratch: final sink + // plus branch/argument fragments for C, and the descriptor JSON DOM plus + // exact-replay hash/escape temporaries. Fixed output maxima are admission + // limits, not empirical multipliers. + // The shared C generator/replay machine has one continuation per semantic + // ancestor, one result slot per depth, and one flat argument slot per + // expression node. A fixed line arena and the disjoint live value payload + // each have the generated-C byte ceiling; neither can grow geometrically. + let c_machine_scratch = maximum_c_depth + .checked_add(1) + .and_then(|slots| { + slots.checked_mul(C_EXPRESSION_FRAME_BYTES.max(REPLAY_C_EXPRESSION_FRAME_BYTES)) + }) + .and_then(|bytes| { + bytes.checked_add( + maximum_c_depth + .checked_add(1)? + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add(maximum_c_nodes.checked_mul(std::mem::size_of::())?) + }) + .and_then(|bytes| bytes.checked_add(MAX_GENERATED_C_BYTES)) + .and_then(|bytes| bytes.checked_add(MAX_GENERATED_C_BYTES)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + // Persistent generator locals that coexist with the expression machine: + // exact selected parameter facts, two borrowed import indexes, and the + // bounded capability/parameter/hash strings. Final output is excluded. + let c_outer_scratch = maximum_parameter_owned + .checked_add( + checked_btree_allocation_upper::<&String, ()>(imports) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .and_then(|bytes| { + bytes.checked_add(checked_btree_allocation_upper::<&str, usize>(imports)?) + }) + .and_then(|bytes| bytes.checked_add(MAX_IDENTIFIER_BYTES.checked_mul(12)?)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let render_entries = selected + .checked_add(exports) + .and_then(|entries| entries.checked_add(imports)) + .and_then(|entries| entries.checked_add(capabilities)) + .and_then(|entries| entries.checked_add(parameter_slots)) + .and_then(|entries| entries.checked_add(import_parameter_slots)) + .and_then(|entries| entries.checked_add(imports.checked_add(4)?)) + .and_then(|entries| entries.checked_add(exports.checked_mul(imports.checked_add(3)?)?)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let descriptor_collection_bytes = render_entries + .checked_mul( + std::mem::size_of::() + .checked_add(std::mem::size_of::()) + .and_then(|bytes| bytes.checked_add(std::mem::size_of::>())) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + // Descriptor row fragments coexist with their joined row strings before + // the separately reserved final descriptor sink is materialized. + let descriptor_render_scratch = MAX_DESCRIPTOR_BYTES + .checked_mul(2) + .and_then(|bytes| bytes.checked_add(descriptor_collection_bytes)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + // The final generated-C output has its own retained reservation. One + // MAX_C term here authorizes only the transient line payload; lines are + // drained directly into the output so a second joined copy never exists. + let c_render_scratch = c_machine_scratch + .checked_add(c_outer_scratch) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + // Safe Rust keeps the quoted capability row plus at most one parameter or + // argument row. Private FFI keeps its 32 digest-byte strings and import + // table rows, then at most one callback/argument pair. Charge these Vec + // headers fieldwise; MAX_RUST below covers only their joined/string + // payloads, never either separately retained final sink. + let safe_rust_vec_headers = capabilities + .checked_add(MAX_PARAMETERS) + .and_then(|entries| entries.checked_mul(std::mem::size_of::())) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let private_ffi_vec_headers = 32usize + .checked_add(imports) + .and_then(|entries| { + entries.checked_add( + MAX_PARAMETERS + .checked_mul(2)? + .max(imports.checked_add(MAX_PARAMETERS)?), + ) + }) + .and_then(|entries| entries.checked_mul(std::mem::size_of::())) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let rust_render_scratch = MAX_GENERATED_RUST_BYTES + .checked_add(safe_rust_vec_headers.max(private_ffi_vec_headers)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let render_scratch_upper = descriptor_render_scratch + .max(c_render_scratch) + .max(rust_render_scratch); + // Descriptor replay owns one serde_json DOM plus independent expected + // status/limit collections. Charge every schema-derived object entry as a + // separately allocated BTree node, every admitted array Value slot at a + // geometric two-times capacity upper, and the status Set→Vec overlap. + // The two descriptor-byte terms cover decoded key/string payload capacity + // and exact-replay escape/number temporaries; the final artifact is held by + // its independent retained reservation. + let descriptor_object_entries = 56usize + .checked_add( + exports + .checked_mul(12) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .and_then(|entries| entries.checked_add(imports.checked_mul(17)?)) + .and_then(|entries| { + entries.checked_add( + parameter_slots + .checked_add(import_parameter_slots)? + .checked_mul(3)?, + ) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let descriptor_array_values = imports + .checked_add(4) + .and_then(|values| values.checked_add(exports)) + .and_then(|values| values.checked_add(imports)) + .and_then(|values| values.checked_add(parameter_slots)) + .and_then(|values| values.checked_add(import_parameter_slots)) + .and_then(|values| values.checked_add(exports.checked_mul(3)?)) + .and_then(|values| values.checked_add(exports.checked_mul(capabilities.checked_mul(2)?)?)) + .and_then(|values| values.checked_add(exports.checked_mul(imports.checked_mul(2)?)?)) + .and_then(|values| values.checked_add(imports.checked_mul(capabilities.checked_mul(2)?)?)) + .and_then(|values| values.checked_add(NONCLAIMS.len())) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let descriptor_dom_backing = + checked_btree_allocation_upper::(descriptor_object_entries) + .and_then(|bytes| { + bytes.checked_add( + descriptor_array_values + .checked_mul(2)? + .checked_mul(std::mem::size_of::())?, + ) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let expected_status_backing = imports + .checked_add(4) + .and_then(|entries| entries.checked_mul(std::mem::size_of::<(u64, &str)>())) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + // Locked serde_json 1.0.151 owns one reusable `Deserializer::scratch` + // Vec (`src/de.rs`) whose string/number paths clear and reuse the same + // buffer (`src/read.rs`). Decoded bytes cannot exceed the admitted input; + // geometric Vec capacity is therefore at most twice that input. Returned + // DOM string/key payload is a distinct at-most-input term. Exact replay's + // escape/hash temporary is separate and begins only after parsing ends. + let descriptor_dom_string_payload = MAX_DESCRIPTOR_BYTES; + let serde_parser_vec_scratch = MAX_DESCRIPTOR_BYTES + .checked_mul(2) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let exact_descriptor_replay_temp = MAX_DESCRIPTOR_BYTES; + let descriptor_parse_scratch = descriptor_dom_backing + .checked_add(descriptor_dom_string_payload) + .and_then(|bytes| bytes.checked_add(serde_parser_vec_scratch)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let descriptor_validation_scratch = descriptor_dom_backing + .checked_add(descriptor_dom_string_payload) + .and_then(|bytes| bytes.checked_add(status_set_vec_scratch)) + .and_then(|bytes| bytes.checked_add(expected_status_backing)) + .and_then(|bytes| bytes.checked_add(exact_descriptor_replay_temp)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let descriptor_replay_scratch = descriptor_parse_scratch.max(descriptor_validation_scratch); + let c_replay_scratch = c_machine_scratch + .checked_add(c_outer_scratch) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let exact_replay_scratch = spec_bytes + .checked_add( + MAX_IDENTIFIER_BYTES + .checked_mul(4) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let replay_scratch_upper = descriptor_replay_scratch + .max(c_replay_scratch) + .max(exact_replay_scratch); + Ok(PostHirFactsCapacity { + retained_upper, + facts_scratch_upper, + render_scratch_upper, + replay_scratch_upper, + traversal_pending_capacity, + }) +} + +fn string_vec_owned_capacity(values: &[String], capacity: usize) -> usize { + capacity * std::mem::size_of::() + values.iter().map(String::capacity).sum::() +} + +fn parameter_facts_owned_capacity(values: &[ParameterFact], capacity: usize) -> usize { + capacity * std::mem::size_of::() + + values + .iter() + .map(|value| value.name.capacity()) + .sum::() +} + +fn string_vec_owned_capacity_checked(values: &[String], capacity: usize) -> Option { + values.iter().try_fold( + capacity.checked_mul(std::mem::size_of::())?, + |bytes, value| bytes.checked_add(value.capacity()), + ) +} + +fn parameter_facts_owned_capacity_checked( + values: &[ParameterFact], + capacity: usize, +) -> Option { + values.iter().try_fold( + capacity.checked_mul(std::mem::size_of::())?, + |bytes, value| bytes.checked_add(value.name.capacity()), + ) +} + +fn borrowed_string_set_owned_capacity(values: &BTreeSet<&str>) -> usize { + values.len() * (std::mem::size_of::<(&str, ())>() + std::mem::size_of::>()) +} + +fn owned_string_set_owned_capacity(values: &BTreeSet) -> usize { + values.len() + * (std::mem::size_of::<(String, ())>() + std::mem::size_of::>()) + + values.iter().map(String::capacity).sum::() +} + +#[cfg(test)] +fn borrowed_map_owned_capacity(len: usize) -> usize { + btree_allocation_upper::(len) +} + +#[cfg(test)] +fn post_hir_selection_scratch_capacity( + selected_effects: &BTreeSet<&str>, + source_functions: &BTreeMap<&str, &crate::ast::Function>, + resolved_imports: &Vec<(&str, &ResolvedImport)>, +) -> usize { + borrowed_string_set_owned_capacity(selected_effects) + .saturating_add(borrowed_map_owned_capacity::<&str, &crate::ast::Function>( + source_functions.len(), + )) + .saturating_add( + resolved_imports.capacity() * std::mem::size_of::<(&str, &ResolvedImport)>(), + ) +} + +#[cfg(test)] +#[allow(clippy::too_many_arguments)] +fn post_hir_live_facts_capacity( + export_facts: &Vec, + import_facts: &Vec, + selected_effects: &BTreeSet<&str>, + source_functions: &BTreeMap<&str, &crate::ast::Function>, + resolved_imports: &Vec<(&str, &ResolvedImport)>, + selected_capabilities: &Vec, + status_domains: &Vec, + ordinals: &BTreeMap<&str, u16>, + by_function: &BTreeMap<&str, &ResolvedFunction>, +) -> usize { + post_hir_facts_owned_capacity(export_facts, import_facts) + .saturating_add(post_hir_selection_scratch_capacity( + selected_effects, + source_functions, + resolved_imports, + )) + .saturating_add(string_vec_owned_capacity( + selected_capabilities, + selected_capabilities.capacity(), + )) + .saturating_add(string_vec_owned_capacity( + status_domains, + status_domains.capacity(), + )) + .saturating_add(borrowed_map_owned_capacity::<&str, u16>(ordinals.len())) + .saturating_add(borrowed_map_owned_capacity::<&str, &ResolvedFunction>( + by_function.len(), + )) +} + +fn post_hir_facts_owned_capacity(exports: &Vec, imports: &Vec) -> usize { + let export_bytes = exports.iter().map(|fact| { + fact.id.capacity() + + fact.rust_method.capacity() + + fact.c_symbol.capacity() + + parameter_facts_owned_capacity(&fact.parameters, fact.parameters.capacity()) + + string_vec_owned_capacity(&fact.effects, fact.effects.capacity()) + + string_vec_owned_capacity(&fact.capabilities, fact.capabilities.capacity()) + + string_vec_owned_capacity(&fact.required_imports, fact.required_imports.capacity()) + + fact.status_domain_ordinals.capacity() * std::mem::size_of::() + + fact.call_contract_digest.capacity() + }); + let import_bytes = imports.iter().map(|fact| { + fact.id.capacity() + + fact.interface.capacity() + + fact.import_key.capacity() + + fact.rust_method.capacity() + + fact.c_field.capacity() + + parameter_facts_owned_capacity(&fact.parameters, fact.parameters.capacity()) + + string_vec_owned_capacity(&fact.effects, fact.effects.capacity()) + + string_vec_owned_capacity(&fact.capabilities, fact.capabilities.capacity()) + + fact.failure.as_ref().map_or(0, String::capacity) + + fact.call_contract_digest.capacity() + }); + exports.capacity() * std::mem::size_of::() + + imports.capacity() * std::mem::size_of::() + + export_bytes.sum::() + + import_bytes.sum::() +} + +fn post_hir_facts_owned_capacity_checked( + exports: &Vec, + imports: &Vec, +) -> Option { + let mut bytes = exports + .capacity() + .checked_mul(std::mem::size_of::())? + .checked_add( + imports + .capacity() + .checked_mul(std::mem::size_of::())?, + )?; + for fact in exports { + bytes = bytes + .checked_add(fact.id.capacity())? + .checked_add(fact.rust_method.capacity())? + .checked_add(fact.c_symbol.capacity())? + .checked_add(parameter_facts_owned_capacity_checked( + &fact.parameters, + fact.parameters.capacity(), + )?)? + .checked_add(string_vec_owned_capacity_checked( + &fact.effects, + fact.effects.capacity(), + )?)? + .checked_add(string_vec_owned_capacity_checked( + &fact.capabilities, + fact.capabilities.capacity(), + )?)? + .checked_add(string_vec_owned_capacity_checked( + &fact.required_imports, + fact.required_imports.capacity(), + )?)? + .checked_add( + fact.status_domain_ordinals + .capacity() + .checked_mul(std::mem::size_of::())?, + )? + .checked_add(fact.call_contract_digest.capacity())?; + } + for fact in imports { + bytes = bytes + .checked_add(fact.id.capacity())? + .checked_add(fact.interface.capacity())? + .checked_add(fact.import_key.capacity())? + .checked_add(fact.rust_method.capacity())? + .checked_add(fact.c_field.capacity())? + .checked_add(parameter_facts_owned_capacity_checked( + &fact.parameters, + fact.parameters.capacity(), + )?)? + .checked_add(string_vec_owned_capacity_checked( + &fact.effects, + fact.effects.capacity(), + )?)? + .checked_add(string_vec_owned_capacity_checked( + &fact.capabilities, + fact.capabilities.capacity(), + )?)? + .checked_add(fact.failure.as_ref().map_or(0, String::capacity))? + .checked_add(fact.call_contract_digest.capacity())?; + } + Some(bytes) +} + +fn string_slice_owned_capacity(values: &[String]) -> usize { + std::mem::size_of_val(values) + values.iter().map(String::capacity).sum::() +} + +fn spec_owned_capacity(spec: &Spec) -> usize { + spec.module.capacity() + + spec.source_revision.capacity() + + spec.target.triple.capacity() + + spec.target.endian.capacity() + + spec.target.panic_strategy.capacity() + + spec.target.thread_policy.capacity() + + string_slice_owned_capacity(&spec.exports) + + string_slice_owned_capacity(&spec.imports) + + string_slice_owned_capacity(&spec.capabilities) +} + +/// Opaque private phase-A facts. Fields intentionally have no getters before C. +pub(crate) struct PreparedNativeRustInterop { + canonical_spec: String, + spec_digest: String, + descriptor: String, + descriptor_digest: String, + source_revision: String, + hir_digest: String, + target: Target, + exports: Vec, + imports: Vec, + closure: Vec, + generated_c: String, + generated_header: String, + generated_rust: String, + private_ffi_source: String, +} + +impl PreparedNativeRustInterop { + pub(crate) fn canonical_spec(&self) -> &str { + &self.canonical_spec + } + pub(crate) fn spec_digest(&self) -> &str { + &self.spec_digest + } + pub(crate) fn descriptor(&self) -> &str { + &self.descriptor + } + pub(crate) fn descriptor_digest(&self) -> &str { + &self.descriptor_digest + } + pub(crate) fn source_revision(&self) -> &str { + &self.source_revision + } + pub(crate) fn hir_digest(&self) -> &str { + &self.hir_digest + } + pub(crate) fn target_triple(&self) -> &str { + &self.target.triple + } + pub(crate) fn generated_c(&self) -> &str { + &self.generated_c + } + pub(crate) fn generated_header(&self) -> &str { + &self.generated_header + } + pub(crate) fn generated_rust(&self) -> &str { + &self.generated_rust + } + pub(crate) fn private_ffi_source(&self) -> &str { + &self.private_ffi_source + } + pub(crate) fn closure(&self) -> &[String] { + &self.closure + } +} + +/// Opaque private phase-B facts. No execution or loader handle escapes. +pub(crate) struct NativeRustInteropBundleFacts { + output_directory: PathBuf, + object_path: PathBuf, + descriptor_path: PathBuf, + manifest_path: PathBuf, + manifest_digest: String, +} + +struct PendingBundleFacts { + output_directory: PathBuf, + object_path: PathBuf, + descriptor_path: PathBuf, + manifest_path: PathBuf, + manifest_digest: String, +} + +impl PendingBundleFacts { + fn new(output: &Path, object_name: &'static str) -> Result { + use std::path::Component; + + let parent = output.parent().ok_or_else(platform_publication_error)?; + let output_name = output.file_name().ok_or_else(platform_publication_error)?; + let mut components = Path::new(output_name).components(); + if !matches!(components.next(), Some(Component::Normal(_))) + || components.next().is_some() + || output.strip_prefix(parent).ok() != Some(Path::new(output_name)) + { + return Err(platform_publication_error()); + } + + let output_bytes = output.as_os_str().as_encoded_bytes().len(); + let child_capacity = |name: &str| output_bytes.checked_add(1)?.checked_add(name.len()); + let object_capacity = child_capacity(object_name) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let descriptor_capacity = child_capacity("descriptor.json") + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let manifest_capacity = child_capacity("semaprax.native-rust-interop.json") + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let retained = output_bytes + .checked_add(object_capacity) + .and_then(|bytes| bytes.checked_add(descriptor_capacity)) + .and_then(|bytes| bytes.checked_add(manifest_capacity)) + .and_then(|bytes| bytes.checked_add(SHA256_TEXT_BYTES)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let authority = reserve_temporary_exact(retained)?; + + let output_directory = exact_path_copy(output, output_bytes)?; + let object_path = exact_child_path(output, object_name, object_capacity)?; + let descriptor_path = exact_child_path(output, "descriptor.json", descriptor_capacity)?; + let manifest_path = exact_child_path( + output, + "semaprax.native-rust-interop.json", + manifest_capacity, + )?; + let manifest_digest = String::with_capacity(SHA256_TEXT_BYTES); + if manifest_digest.capacity() != SHA256_TEXT_BYTES { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + authority.retain(retained)?; + Ok(Self { + output_directory, + object_path, + descriptor_path, + manifest_path, + manifest_digest, + }) + } + + fn bind_manifest_digest(&mut self, manifest: &[u8]) -> Result<(), PhaseBLocalError> { + if !self.manifest_digest.is_empty() || self.manifest_digest.capacity() != SHA256_TEXT_BYTES + { + return Err(PhaseBLocalError::Replay); + } + self.manifest_digest.push_str("sha256:"); + let mut hasher = Sha256::new(); + hasher.update(BUNDLE_DIGEST_DOMAIN); + hasher.update(manifest); + let digest = hasher.finalize(); + for byte in digest { + const HEX: &[u8; 16] = b"0123456789abcdef"; + self.manifest_digest + .push(char::from(HEX[usize::from(byte >> 4)])); + self.manifest_digest + .push(char::from(HEX[usize::from(byte & 0x0f)])); + } + if self.manifest_digest.len() != SHA256_TEXT_BYTES + || self.manifest_digest.capacity() != SHA256_TEXT_BYTES + { + return Err(PhaseBLocalError::Replay); + } + Ok(()) + } + + fn finish(self) -> NativeRustInteropBundleFacts { + NativeRustInteropBundleFacts { + output_directory: self.output_directory, + object_path: self.object_path, + descriptor_path: self.descriptor_path, + manifest_path: self.manifest_path, + manifest_digest: self.manifest_digest, + } + } +} + +fn exact_path_copy(path: &Path, capacity: usize) -> Result { + let mut output = PathBuf::with_capacity(capacity); + output.push(path); + if output != path || output.capacity() != capacity { + return Err(platform_publication_error()); + } + Ok(output) +} + +fn exact_child_path(parent: &Path, name: &str, capacity: usize) -> Result { + let mut output = PathBuf::with_capacity(capacity); + output.push(parent); + output.push(name); + if output.capacity() != capacity { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + Ok(output) +} + +impl NativeRustInteropBundleFacts { + pub(crate) fn output_directory(&self) -> &Path { + &self.output_directory + } + pub(crate) fn object_path(&self) -> &Path { + &self.object_path + } + pub(crate) fn descriptor_path(&self) -> &Path { + &self.descriptor_path + } + pub(crate) fn manifest_path(&self) -> &Path { + &self.manifest_path + } + pub(crate) fn manifest_digest(&self) -> &str { + &self.manifest_digest + } +} + +fn b106() -> Diagnostic { + Diagnostic::io( + "SPX-B106", + "Native Rust Interop specification is not canonical semaprax.native-rust-interop-spec.v1 JSON", + ) +} + +fn b107(reason: &'static str) -> Diagnostic { + Diagnostic::io( + "SPX-B107", + format!("Native Rust Interop declaration set is unsupported: {reason}"), + ) +} + +fn b108() -> Diagnostic { + Diagnostic::io( + "SPX-B108", + "Native Rust Interop descriptor disagrees with validated source and HIR", + ) +} + +fn b109(field: &'static str, maximum: usize) -> Diagnostic { + Diagnostic::io( + "SPX-B109", + format!("Native Rust Interop {field} exceeds {maximum}"), + ) +} + +fn b110() -> Diagnostic { + Diagnostic::io( + "SPX-B110", + "Native Rust Interop target or toolchain is unsupported", + ) +} + +fn b111() -> Diagnostic { + Diagnostic::io( + "SPX-B111", + "Native Rust Interop generated artifact replay failed", + ) +} + +fn debit(bytes: usize) -> Result<(), Diagnostic> { + if crate::bounded_output::reserve_active(bytes) { + Ok(()) + } else { + Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)) + } +} + +fn reserve_temporary_exact(maximum: usize) -> Result { + let remaining = crate::bounded_output::remaining_active().unwrap_or(MAX_BUILDER_BYTES); + if maximum > remaining { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + debit(maximum)?; + Ok(TemporaryBudget { reserved: maximum }) +} + +struct TemporaryBudget { + reserved: usize, +} + +enum ResolvedDisposeFrame { + ExprBox(Box), + Exprs(Vec), + Statements(Vec), + Fields(Vec), + Arms(Vec), + RecordPatternFields(Vec), + VariantPatternFields(Vec), + Type(ResolvedType), + Types(Vec), + Shape(semaprax::cleanup::FieldLivenessShape), + Shapes(Vec), +} + +const _: () = assert!(std::mem::size_of::() == 56); + +struct ResolvedProgramOwner { + program: Option, + frames: Vec, +} + +impl ResolvedProgramOwner { + fn new(program: ResolvedProgram, frames: Vec, capacity: usize) -> Self { + if frames.capacity() != capacity || !frames.is_empty() { + std::process::abort(); + } + note_resolved_dispose_capacity(0, capacity); + Self { + program: Some(program), + frames, + } + } + + fn program(&self) -> &ResolvedProgram { + self.program.as_ref().expect("resolved program retained") + } +} + +fn disposal_push(frames: &mut Vec, frame: ResolvedDisposeFrame) { + if frames.len() == frames.capacity() { + // The owner is created only after the admitted-depth census reserved + // this fixed workspace. Exhaustion is an internal invariant failure; + // aborting avoids both recursive fallback and allocation during Drop. + std::process::abort(); + } + frames.push(frame); + note_resolved_dispose_high_water(frames.len()); +} + +impl Drop for ResolvedProgramOwner { + fn drop(&mut self) { + let Some(program) = self.program.take() else { + return; + }; + let ResolvedProgram { + module, + permits, + entrypoint, + declarations, + types, + interfaces, + function_templates, + functions, + function_instances, + } = program; + // Scalars, strings, declarations, and non-recursive declaration + // containers may drop directly after every recursive HIR tree has + // been moved into the preallocated disposal machine. + for interface in interfaces { + for import in interface.imports { + for parameter in import.parameters { + disposal_push(&mut self.frames, ResolvedDisposeFrame::Type(parameter.ty)); + drain_disposal_frames(&mut self.frames, None); + } + } + } + drop((module, permits, entrypoint, declarations)); + for declaration in types { + match declaration.kind { + crate::hir::ResolvedTypeDeclarationKind::Resource { .. } => {} + crate::hir::ResolvedTypeDeclarationKind::Record { fields } => { + for field in fields { + disposal_push(&mut self.frames, ResolvedDisposeFrame::Type(field.ty)); + drain_disposal_frames(&mut self.frames, None); + } + } + crate::hir::ResolvedTypeDeclarationKind::Variant { cases } => { + for case in cases { + for field in case.fields { + disposal_push(&mut self.frames, ResolvedDisposeFrame::Type(field.ty)); + drain_disposal_frames(&mut self.frames, None); + } + } + } + } + } + for template in function_templates { + disposal_push( + &mut self.frames, + ResolvedDisposeFrame::Type(template.return_type), + ); + drain_disposal_frames(&mut self.frames, None); + disposal_push( + &mut self.frames, + ResolvedDisposeFrame::Exprs(template.requires), + ); + drain_disposal_frames(&mut self.frames, None); + disposal_push( + &mut self.frames, + ResolvedDisposeFrame::Exprs(template.ensures), + ); + drain_disposal_frames(&mut self.frames, None); + for parameter in template.params { + disposal_push(&mut self.frames, ResolvedDisposeFrame::Type(parameter.ty)); + drain_disposal_frames(&mut self.frames, None); + } + drain_disposal_frames(&mut self.frames, Some(template.body)); + } + for function in functions { + push_function_for_disposal(&mut self.frames, function); + } + for instance in function_instances { + disposal_push( + &mut self.frames, + ResolvedDisposeFrame::Types(instance.type_arguments), + ); + drain_disposal_frames(&mut self.frames, None); + push_function_for_disposal(&mut self.frames, instance.function); + } + drain_disposal_frames(&mut self.frames, None); + note_resolved_dispose_capacity(1, self.frames.capacity()); + note_resolved_dispose_completion(); + } +} + +fn push_function_for_disposal(frames: &mut Vec, function: ResolvedFunction) { + disposal_push(frames, ResolvedDisposeFrame::Type(function.return_type)); + drain_disposal_frames(frames, None); + disposal_push(frames, ResolvedDisposeFrame::Exprs(function.requires)); + drain_disposal_frames(frames, None); + disposal_push(frames, ResolvedDisposeFrame::Exprs(function.ensures)); + drain_disposal_frames(frames, None); + for parameter in function.params { + disposal_push(frames, ResolvedDisposeFrame::Type(parameter.ty)); + drain_disposal_frames(frames, None); + } + for slot in function.cleanup.slots { + disposal_push(frames, ResolvedDisposeFrame::Type(slot.ty)); + drain_disposal_frames(frames, None); + disposal_push(frames, ResolvedDisposeFrame::Shape(slot.shape)); + drain_disposal_frames(frames, None); + } + for slot in function.cleanup_plan.slots { + disposal_push(frames, ResolvedDisposeFrame::Type(slot.ty)); + drain_disposal_frames(frames, None); + disposal_push( + frames, + ResolvedDisposeFrame::Shape(slot.field_liveness_shape), + ); + drain_disposal_frames(frames, None); + } + for block in function.cleanup_plan.blocks { + for transition in block.transitions { + if let crate::cleanup_plan::CleanupTransition::StageCopyResult { source } = transition { + match source { + crate::cleanup_plan::StagedCopyResultSource::Body { instance, .. } => { + disposal_push(frames, ResolvedDisposeFrame::Type(instance)); + drain_disposal_frames(frames, None); + } + crate::cleanup_plan::StagedCopyResultSource::TryResidual { + source_instance, + target_instance, + .. + } + | crate::cleanup_plan::StagedCopyResultSource::TryOptionNone { + source_instance, + target_instance, + .. + } => { + disposal_push(frames, ResolvedDisposeFrame::Type(source_instance)); + drain_disposal_frames(frames, None); + disposal_push(frames, ResolvedDisposeFrame::Type(target_instance)); + drain_disposal_frames(frames, None); + } + } + } + } + } + drain_disposal_frames(frames, Some(function.body)); +} + +fn drain_disposal_frames( + frames: &mut Vec, + mut pending_expression: Option, +) { + loop { + if let Some(expression) = pending_expression.take() { + disposal_push(frames, ResolvedDisposeFrame::Type(expression.ty)); + match expression.kind { + ResolvedExprKind::Int(_) + | ResolvedExprKind::Bool(_) + | ResolvedExprKind::Place(_) => {} + ResolvedExprKind::Call { + type_arguments, + args, + .. + } => { + disposal_push(frames, ResolvedDisposeFrame::Types(type_arguments)); + disposal_push(frames, ResolvedDisposeFrame::Exprs(args)); + } + ResolvedExprKind::NativeRustImportCall(call) => { + disposal_push(frames, ResolvedDisposeFrame::Exprs(call.args)); + } + ResolvedExprKind::Unary { value, .. } + | ResolvedExprKind::Project { base: value, .. } => { + pending_expression = Some(*value); + } + ResolvedExprKind::Binary { left, right, .. } => { + disposal_push(frames, ResolvedDisposeFrame::ExprBox(right)); + pending_expression = Some(*left); + } + ResolvedExprKind::Block { statements, tail } => { + disposal_push(frames, ResolvedDisposeFrame::ExprBox(tail)); + disposal_push(frames, ResolvedDisposeFrame::Statements(statements)); + } + ResolvedExprKind::If { + condition, + then_branch, + else_branch, + } => { + disposal_push(frames, ResolvedDisposeFrame::ExprBox(else_branch)); + disposal_push(frames, ResolvedDisposeFrame::ExprBox(then_branch)); + pending_expression = Some(*condition); + } + ResolvedExprKind::ConstructRecord { fields, .. } + | ResolvedExprKind::ConstructVariant { fields, .. } => { + disposal_push(frames, ResolvedDisposeFrame::Fields(fields)); + } + ResolvedExprKind::Match { scrutinee, arms } => { + disposal_push(frames, ResolvedDisposeFrame::Arms(arms)); + pending_expression = Some(*scrutinee); + } + ResolvedExprKind::Try { + operand, + residual_type, + .. + } + | ResolvedExprKind::TryOption { + operand, + residual_type, + .. + } => { + disposal_push(frames, ResolvedDisposeFrame::Type(residual_type)); + pending_expression = Some(*operand); + } + ResolvedExprKind::UpdateRecord { base, fields, .. } => { + disposal_push(frames, ResolvedDisposeFrame::Fields(fields)); + pending_expression = Some(*base); + } + } + continue; + } + let Some(frame) = frames.pop() else { break }; + match frame { + ResolvedDisposeFrame::ExprBox(expression) => pending_expression = Some(*expression), + ResolvedDisposeFrame::Exprs(mut expressions) => { + if let Some(expression) = expressions.pop() { + disposal_push(frames, ResolvedDisposeFrame::Exprs(expressions)); + pending_expression = Some(expression); + } + } + ResolvedDisposeFrame::Statements(mut statements) => { + if let Some(statement) = statements.pop() { + disposal_push(frames, ResolvedDisposeFrame::Statements(statements)); + let ResolvedStatement::Let { binding, value, .. } = statement; + disposal_push(frames, ResolvedDisposeFrame::Type(binding.ty)); + pending_expression = Some(value); + } + } + ResolvedDisposeFrame::Fields(mut fields) => { + if let Some(field) = fields.pop() { + disposal_push(frames, ResolvedDisposeFrame::Fields(fields)); + pending_expression = Some(field.value); + } + } + ResolvedDisposeFrame::Arms(mut arms) => { + if let Some(arm) = arms.pop() { + disposal_push(frames, ResolvedDisposeFrame::Arms(arms)); + dispose_match_pattern(frames, arm.pattern); + pending_expression = Some(arm.value); + } + } + ResolvedDisposeFrame::RecordPatternFields(mut fields) => { + if let Some(field) = fields.pop() { + disposal_push(frames, ResolvedDisposeFrame::RecordPatternFields(fields)); + match field.pattern { + crate::hir::ResolvedRecordMatchFieldPattern::Binding(binding) => { + disposal_push(frames, ResolvedDisposeFrame::Type(binding.ty)); + } + crate::hir::ResolvedRecordMatchFieldPattern::Wildcard => {} + crate::hir::ResolvedRecordMatchFieldPattern::Record { + instance, + fields, + .. + } => { + disposal_push(frames, ResolvedDisposeFrame::Type(instance)); + disposal_push( + frames, + ResolvedDisposeFrame::RecordPatternFields(fields), + ); + } + } + } + } + ResolvedDisposeFrame::VariantPatternFields(mut fields) => { + if let Some(field) = fields.pop() { + disposal_push(frames, ResolvedDisposeFrame::VariantPatternFields(fields)); + disposal_push(frames, ResolvedDisposeFrame::Type(field.binding.ty)); + } + } + ResolvedDisposeFrame::Type(ty) => { + if let ResolvedType::Nominal { arguments, .. } = ty { + disposal_push(frames, ResolvedDisposeFrame::Types(arguments)); + } + } + ResolvedDisposeFrame::Types(mut types) => { + if let Some(ty) = types.pop() { + disposal_push(frames, ResolvedDisposeFrame::Types(types)); + disposal_push(frames, ResolvedDisposeFrame::Type(ty)); + } + } + ResolvedDisposeFrame::Shape(shape) => { + if let semaprax::cleanup::FieldLivenessShape::Record { fields, .. } = shape { + disposal_push(frames, ResolvedDisposeFrame::Shapes(fields)); + } + } + ResolvedDisposeFrame::Shapes(mut shapes) => { + if let Some(field) = shapes.pop() { + disposal_push(frames, ResolvedDisposeFrame::Shapes(shapes)); + disposal_push(frames, ResolvedDisposeFrame::Shape(field.shape)); + } + } + } + } +} + +fn dispose_match_pattern( + frames: &mut Vec, + pattern: crate::hir::ResolvedMatchPattern, +) { + match pattern { + crate::hir::ResolvedMatchPattern::Wildcard => {} + crate::hir::ResolvedMatchPattern::Variant { fields, .. } => { + disposal_push(frames, ResolvedDisposeFrame::VariantPatternFields(fields)); + } + crate::hir::ResolvedMatchPattern::Record { + instance, fields, .. + } => { + disposal_push(frames, ResolvedDisposeFrame::Type(instance)); + disposal_push(frames, ResolvedDisposeFrame::RecordPatternFields(fields)); + } + } +} + +impl TemporaryBudget { + fn maximum(&self) -> usize { + self.reserved + } + + fn retain(mut self, actual: usize) -> Result<(), Diagnostic> { + if actual > self.reserved { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + crate::bounded_output::release_active(self.reserved - actual); + self.reserved = 0; + Ok(()) + } + + fn shrink_held(&mut self, actual: usize) -> Result<(), Diagnostic> { + if actual > self.reserved { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + crate::bounded_output::release_active(self.reserved - actual); + self.reserved = actual; + Ok(()) + } + + fn check(&self, actual: usize) -> Result<(), Diagnostic> { + if actual > self.reserved { + Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)) + } else { + Ok(()) + } + } +} + +impl Drop for TemporaryBudget { + fn drop(&mut self) { + crate::bounded_output::release_active(self.reserved); + } +} + +fn debit_source(source: &str) -> Result<(), Diagnostic> { + debit(source.len()) +} + +#[cfg(test)] +thread_local! { + static TEST_TARGET_OVERRIDE: std::cell::RefCell> = const { std::cell::RefCell::new(None) }; +} + +#[cfg(test)] +fn with_test_target(target: Target, run: impl FnOnce() -> T) -> T { + struct Reset; + impl Drop for Reset { + fn drop(&mut self) { + TEST_TARGET_OVERRIDE.with(|slot| *slot.borrow_mut() = None); + } + } + TEST_TARGET_OVERRIDE.with(|slot| { + assert!(slot.borrow().is_none(), "test target override nested"); + *slot.borrow_mut() = Some(target); + }); + let reset = Reset; + let result = run(); + drop(reset); + result +} + +fn current_target() -> Option { + #[cfg(test)] + if let Some(target) = TEST_TARGET_OVERRIDE.with(|slot| slot.borrow().clone()) { + return Some(target); + } + let triple = if cfg!(all(target_arch = "x86_64", target_os = "linux")) { + "x86_64-unknown-linux-gnu" + } else if cfg!(all(target_arch = "aarch64", target_os = "linux")) { + "aarch64-unknown-linux-gnu" + } else if cfg!(all(target_arch = "x86_64", target_os = "macos")) { + "x86_64-apple-darwin" + } else if cfg!(all(target_arch = "aarch64", target_os = "macos")) { + "aarch64-apple-darwin" + } else if cfg!(all(target_arch = "x86_64", target_os = "windows")) { + "x86_64-pc-windows-msvc" + } else if cfg!(all(target_arch = "aarch64", target_os = "windows")) { + "aarch64-pc-windows-msvc" + } else { + return None; + }; + Some(Target { + triple: triple.to_owned(), + pointer_width: 64, + endian: "little".to_owned(), + panic_strategy: "unwind".to_owned(), + thread_policy: "same_thread".to_owned(), + }) +} + +fn domain_digest(domain: &[u8], bytes: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(domain); + hasher.update(bytes); + format!("sha256:{:x}", hasher.finalize()) +} + +fn raw_digest(bytes: &[u8]) -> String { + format!("sha256:{:x}", Sha256::digest(bytes)) +} + +fn identifier_gate(value: &str) -> Result<(), Diagnostic> { + if value.is_empty() || value.len() > MAX_IDENTIFIER_BYTES || value.contains('\0') { + Err(b109("max_identifier_bytes", MAX_IDENTIFIER_BYTES)) + } else { + Ok(()) + } +} + +fn identifier_audit(program: &Program, spec: &Spec) -> Result<(), Diagnostic> { + identifier_gate(&program.module)?; + if spec.capabilities.len() > MAX_EFFECTS { + return Err(b109("max_effects", MAX_EFFECTS)); + } + for value in spec + .exports + .iter() + .chain(&spec.imports) + .chain(&spec.capabilities) + { + identifier_gate(value)?; + } + Ok(()) +} + +fn full_hash(value: &str) -> String { + format!("{:x}", Sha256::digest(value.as_bytes())) +} + +fn frame(hasher: &mut Sha256, bytes: &[u8]) { + hasher.update(u64::try_from(bytes.len()).unwrap_or(u64::MAX).to_be_bytes()); + hasher.update(bytes); +} + +fn framed_digest<'a>(domain: &[u8], fields: impl IntoIterator) -> String { + let mut hasher = Sha256::new(); + hasher.update(domain); + for field in fields { + frame(&mut hasher, field); + } + format!("sha256:{:x}", hasher.finalize()) +} + +fn sorted_unique(values: &[String]) -> bool { + values.windows(2).all(|pair| pair[0] < pair[1]) +} + +fn json_depth(bytes: &[u8]) -> Result { + let mut depth = 0_usize; + let mut maximum = 0_usize; + let mut quoted = false; + let mut escaped = false; + for byte in bytes { + if quoted { + if escaped { + escaped = false; + } else if *byte == b'\\' { + escaped = true; + } else if *byte == b'"' { + quoted = false; + } + continue; + } + match *byte { + b'"' => quoted = true, + b'{' | b'[' => { + depth = depth.checked_add(1).ok_or_else(b106)?; + maximum = maximum.max(depth); + } + b'}' | b']' => depth = depth.checked_sub(1).ok_or_else(b106)?, + _ => {} + } + } + if quoted || depth != 0 { + return Err(b106()); + } + Ok(maximum) +} + +fn maximum_spec_strings() -> Result { + MAX_EXPORTS + .checked_add(MAX_IMPORTS) + .and_then(|count| count.checked_add(MAX_EFFECTS)) + .and_then(|count| count.checked_add(NONCLAIMS.len())) + .and_then(|count| count.checked_add(64)) + .ok_or_else(b106) +} + +struct CountingSink { + bytes: usize, + maximum: usize, + overflowed: bool, +} + +impl std::fmt::Write for CountingSink { + fn write_str(&mut self, value: &str) -> std::fmt::Result { + let Some(bytes) = self.bytes.checked_add(value.len()) else { + self.overflowed = true; + return Ok(()); + }; + if bytes > self.maximum { + self.overflowed = true; + } else { + self.bytes = bytes; + } + Ok(()) + } +} + +fn count_exact_artifact( + field: &'static str, + maximum: usize, + render: &mut F, +) -> Result +where + F: FnMut(&mut dyn std::fmt::Write) -> Result<(), Diagnostic>, +{ + let mut counter = CountingSink { + bytes: 0, + maximum, + overflowed: false, + }; + render(&mut counter)?; + if counter.overflowed { + return Err(b109(field, maximum)); + } + Ok(counter.bytes) +} + +fn render_counted_artifact( + field: &'static str, + maximum: usize, + exact_bytes: usize, + render: &mut F, +) -> Result +where + F: FnMut(&mut dyn std::fmt::Write) -> Result<(), Diagnostic>, +{ + #[cfg(test)] + EXACT_ARTIFACT_OUTPUT_ALLOCATION_COUNT.with(|count| count.set(count.get() + 1)); + let mut output = String::with_capacity(exact_bytes); + let initial_capacity = output.capacity(); + if initial_capacity != exact_bytes { + return Err(b109(field, maximum)); + } + render(&mut output)?; + if output.len() != exact_bytes || output.capacity() != initial_capacity { + return Err(b109(field, maximum)); + } + Ok(output) +} + +fn render_exact_artifact( + field: &'static str, + maximum: usize, + mut render: F, +) -> Result +where + F: FnMut(&mut dyn std::fmt::Write) -> Result<(), Diagnostic>, +{ + let exact_bytes = count_exact_artifact(field, maximum, &mut render)?; + render_counted_artifact(field, maximum, exact_bytes, &mut render) +} + +fn canonical_format_scratch_capacity( + program: &Program, +) -> Result { + let mut expression_stack = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let expressions = program.functions.iter().flat_map(|function| { + function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures) + }); + let expression_depth = scan_ast_capacity(expressions, program, false, &mut expression_stack)? + .max_depth + .max(1); + let mut type_depth = 1usize; + for expression in program.functions.iter().flat_map(|function| { + function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures) + }) { + type_depth = type_depth.max(ast_expression_type_depth(expression)?); + } + for function in &program.functions { + type_depth = type_depth.max(ast_type_depth(&function.return_type)?); + for parameter in &function.params { + type_depth = type_depth.max(ast_type_depth(¶meter.ty)?); + } + } + for interface in &program.interfaces { + for import in &interface.imports { + for parameter in &import.params { + type_depth = type_depth.max(ast_type_depth(¶meter.ty)?); + } + } + } + for declaration in &program.types { + match &declaration.kind { + crate::ast::TypeDeclarationKind::Resource { .. } => {} + crate::ast::TypeDeclarationKind::Record { fields } => { + for field in fields { + type_depth = type_depth.max(ast_type_depth(&field.ty)?); + } + } + crate::ast::TypeDeclarationKind::Variant { cases } => { + for case in cases { + for field in &case.fields { + type_depth = type_depth.max(ast_type_depth(&field.ty)?); + } + } + } + } + } + let mut pattern_depth = 1usize; + for function in &program.functions { + let roots = function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures); + for expression in roots { + pattern_depth = pattern_depth.max(ast_pattern_depth(expression)?); + } + } + crate::private_format::private_scratch_capacity(expression_depth, type_depth, pattern_depth) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES)) +} + +fn ast_expression_type_depth(root: &crate::ast::Expr) -> Result { + let mut expressions = [None; MAX_FORMAT_NESTING]; + expressions[0] = Some((root, 0usize)); + let mut len = 1usize; + let mut maximum = 1usize; + while len != 0 { + len -= 1; + let (expression, next) = expressions[len] + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if next == 0 { + match &expression.kind { + crate::ast::ExprKind::Call { type_arguments, .. } => { + for ty in type_arguments { + maximum = maximum.max(ast_type_depth(ty)?); + } + } + crate::ast::ExprKind::ConstructRecord { type_arguments, .. } + | crate::ast::ExprKind::ConstructVariant { type_arguments, .. } => { + for ty in type_arguments { + maximum = maximum.max(ast_type_depth(ty)?); + } + } + _ => {} + } + } + if let Some(child) = ast_child(expression, next) { + if len + 2 > expressions.len() { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + expressions[len] = Some((expression, next + 1)); + expressions[len + 1] = Some((child, 0)); + len += 2; + } + } + Ok(maximum) +} + +fn ast_type_depth(root: &crate::ast::Type) -> Result { + let mut stack: [Option<(&crate::ast::Type, usize, usize)>; MAX_FORMAT_NESTING] = + [None; MAX_FORMAT_NESTING]; + stack[0] = Some((root, 1, 0)); + let mut len = 1usize; + let mut maximum = 1usize; + while len != 0 { + len -= 1; + let (ty, depth, next_child) = stack[len] + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + maximum = maximum.max(depth); + if let crate::ast::Type::Named { arguments, .. } = ty { + if let Some(argument) = arguments.get(next_child) { + if len + 2 > stack.len() { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + stack[len] = Some((ty, depth, next_child + 1)); + stack[len + 1] = Some((argument, depth + 1, 0)); + len += 2; + } + } + } + Ok(maximum) +} + +fn ast_pattern_depth(root: &crate::ast::Expr) -> Result { + let mut expressions = [None; MAX_FORMAT_NESTING]; + expressions[0] = Some((root, 0usize)); + let mut expression_len = 1usize; + let mut maximum = 1usize; + while expression_len != 0 { + expression_len -= 1; + let (expression, next) = expressions[expression_len] + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if next == 0 { + if let crate::ast::ExprKind::Match { arms, .. } = &expression.kind { + for arm in arms { + maximum = maximum.max(match_pattern_depth(&arm.pattern)?); + } + } + } + if let Some(child) = ast_child(expression, next) { + if expression_len + 2 > expressions.len() { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + expressions[expression_len] = Some((expression, next + 1)); + expressions[expression_len + 1] = Some((child, 0)); + expression_len += 2; + } + } + Ok(maximum) +} + +fn match_pattern_depth(pattern: &crate::ast::MatchPattern) -> Result { + let crate::ast::MatchPattern::Record { fields, .. } = pattern else { + return Ok(1); + }; + let mut stack: [Option<(&[crate::ast::RecordMatchPatternField], usize, usize)>; + MAX_FORMAT_NESTING] = [None; MAX_FORMAT_NESTING]; + stack[0] = Some((fields, 1, 0)); + let mut len = 1usize; + let mut maximum = 1usize; + while len != 0 { + len -= 1; + let (fields, depth, next_child) = stack[len] + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + maximum = maximum.max(depth); + if let Some(field) = fields.get(next_child) { + if let crate::ast::RecordMatchFieldPattern::Record { fields: nested, .. } = + &field.pattern + { + if len + 2 > stack.len() { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + stack[len] = Some((fields, depth, next_child + 1)); + stack[len + 1] = Some((nested, depth + 1, 0)); + len += 2; + } else if next_child + 1 < fields.len() { + stack[len] = Some((fields, depth, next_child + 1)); + len += 1; + } + } + } + Ok(maximum) +} + +fn canonical_source_bounded(program: &Program) -> Result { + let scratch_bytes = canonical_format_scratch_capacity(program)?; + let scratch_budget = reserve_temporary_exact(scratch_bytes.bytes())?; + // Pass one establishes the exact final capacity while its frame scratch is + // already authorized. Pass two holds the same scratch and exact String. + let mut counter = CountingSink { + bytes: 0, + maximum: MAX_SOURCE_BYTES, + overflowed: false, + }; + note_canonical_format_pass(); + crate::private_format::write_canonical_with_scratch(program, &mut counter, scratch_bytes); + if counter.overflowed { + return Err(b109("max_source_bytes", MAX_SOURCE_BYTES)); + } + let budget = reserve_temporary_exact(counter.bytes)?; + let mut source = String::with_capacity(counter.bytes); + note_canonical_format_pass(); + crate::private_format::write_canonical_with_scratch(program, &mut source, scratch_bytes); + if source.len() != counter.bytes || source.capacity() != counter.bytes { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + budget.retain(source.capacity())?; + drop(scratch_budget); + Ok(source) +} + +/// A single-pass parser for the exact canonical Spec shape. It admits every +/// container, member, scalar, and array element before allocating the decoded +/// value, so hostile generic JSON never reaches a serde DOM. +struct SpecCursor<'a> { + bytes: &'a [u8], + offset: usize, +} + +impl<'a> SpecCursor<'a> { + fn new(bytes: &'a [u8]) -> Self { + Self { bytes, offset: 0 } + } + + fn expect(&mut self, expected: &[u8]) -> Result<(), Diagnostic> { + let end = self.offset.checked_add(expected.len()).ok_or_else(b106)?; + if self.bytes.get(self.offset..end) != Some(expected) { + return Err(b106()); + } + self.offset = end; + Ok(()) + } + + fn string(&mut self) -> Result { + let start = self.offset; + self.expect(b"\"")?; + let mut escaped = false; + loop { + let byte = *self.bytes.get(self.offset).ok_or_else(b106)?; + self.offset = self.offset.checked_add(1).ok_or_else(b106)?; + if escaped { + escaped = false; + } else if byte == b'\\' { + escaped = true; + } else if byte == b'"' { + break; + } else if byte < 0x20 { + return Err(b106()); + } + } + let value: String = + serde_json::from_slice(&self.bytes[start..self.offset]).map_err(|_| b106())?; + if value.contains('\0') { + return Err(b106()); + } + Ok(value) + } + + fn string_array( + &mut self, + maximum: usize, + field: &'static str, + ) -> Result, Diagnostic> { + self.expect(b"[")?; + let mut values = Vec::new(); + if self.bytes.get(self.offset) == Some(&b']') { + self.offset += 1; + return Ok(values); + } + loop { + if values.len() == maximum { + return Err(b109(field, maximum)); + } + values.push(self.string()?); + match self.bytes.get(self.offset) { + Some(b',') => self.offset += 1, + Some(b']') => { + self.offset += 1; + return Ok(values); + } + _ => return Err(b106()), + } + } + } + + fn finish(self) -> Result<(), Diagnostic> { + if self.offset == self.bytes.len() { + Ok(()) + } else { + Err(b106()) + } + } +} + +fn parse_spec(program: &Program, bytes: &[u8]) -> Result { + let source = canonical_source_bounded(program)?; + parse_spec_with_source(program, bytes, &source) +} + +fn parse_spec_with_source( + program: &Program, + bytes: &[u8], + source: &str, +) -> Result { + let (spec, authority) = parse_spec_with_source_authority(program, bytes, source)?; + let retained = authority.maximum(); + authority.retain(retained)?; + Ok(spec) +} + +fn parse_spec_with_source_authority( + program: &Program, + bytes: &[u8], + source: &str, +) -> Result<(Spec, TemporaryBudget), Diagnostic> { + if bytes.len() > MAX_SPEC_BYTES { + return Err(b109("max_spec_bytes", MAX_SPEC_BYTES)); + } + if json_depth(bytes)? > MAX_JSON_DEPTH { + return Err(b109("max_json_depth", MAX_JSON_DEPTH)); + } + let container_overhead = maximum_spec_strings()? + .checked_mul(256) + .and_then(|bytes| bytes.checked_add(65_536)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + // The exact-shape cursor can own at most one decoded copy of every input + // string plus the three bounded vectors. Reserve that complete capacity + // before decoding the first string. + let spec_upper = bytes + .len() + .checked_add(container_overhead) + .and_then(|bytes| bytes.checked_add(std::mem::size_of::())) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let spec_budget = reserve_temporary_exact(spec_upper)?; + let mut cursor = SpecCursor::new(bytes); + cursor.expect(b"{\"schema\":")?; + if cursor.string()? != SPEC_SCHEMA { + return Err(b106()); + } + cursor.expect(b",\"module\":")?; + let module = cursor.string()?; + cursor.expect(b",\"source_revision\":")?; + let source_revision = cursor.string()?; + cursor.expect(b",\"target\":{\"triple\":")?; + let triple = cursor.string()?; + cursor.expect(b",\"pointer_width\":")?; + let pointer_width = if cursor.bytes[cursor.offset..].starts_with(b"64") { + cursor.offset += 2; + 64 + } else { + return Err(b106()); + }; + cursor.expect(b",\"endian\":")?; + let endian = cursor.string()?; + cursor.expect(b",\"panic_strategy\":")?; + let panic_strategy = cursor.string()?; + cursor.expect(b",\"thread_policy\":")?; + let thread_policy = cursor.string()?; + cursor.expect(b"},\"exports\":")?; + let exports = cursor.string_array(MAX_EXPORTS, "max_exports")?; + cursor.expect(b",\"imports\":")?; + let imports = cursor.string_array(MAX_IMPORTS, "max_imports")?; + cursor.expect(b",\"capabilities\":")?; + let capabilities = cursor.string_array(MAX_EFFECTS, "max_effects")?; + cursor.expect(b",\"limits\":")?; + cursor.expect(limits_json().as_bytes())?; + cursor.expect(b",\"nonclaims\":[")?; + for (index, expected) in NONCLAIMS.iter().enumerate() { + if index != 0 { + cursor.expect(b",")?; + } + if cursor.string()? != *expected { + return Err(b106()); + } + } + cursor.expect(b"]}\n")?; + cursor.finish()?; + let target = Target { + triple, + pointer_width, + endian, + panic_strategy, + thread_policy, + }; + let spec = Spec { + module, + source_revision, + target, + exports, + imports, + capabilities, + }; + if spec.exports.is_empty() + || !sorted_unique(&spec.exports) + || !sorted_unique(&spec.imports) + || !sorted_unique(&spec.capabilities) + { + return Err(b106()); + } + let canonical_budget = reserve_temporary_exact(MAX_SPEC_BYTES)?; + let canonical = render_spec(&spec); + canonical_budget.check(canonical.capacity())?; + if canonical.as_bytes() != bytes { + return Err(b106()); + } + drop(canonical); + drop(canonical_budget); + let spec_owned = checked_spec_owned_capacity(&spec) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + for (actual, field, maximum) in [ + (spec.exports.len(), "max_exports", MAX_EXPORTS), + (spec.imports.len(), "max_imports", MAX_IMPORTS), + (spec.capabilities.len(), "max_effects", MAX_EFFECTS), + ] { + if actual > maximum { + return Err(b109(field, maximum)); + } + } + identifier_gate(&spec.module)?; + for value in spec + .exports + .iter() + .chain(&spec.imports) + .chain(&spec.capabilities) + { + identifier_gate(value)?; + } + if current_target().as_ref() != Some(&spec.target) { + return Err(b107("target profile mismatch")); + } + if source.len() > MAX_SOURCE_BYTES { + return Err(b109("max_source_bytes", MAX_SOURCE_BYTES)); + } + if spec.module != program.module + || spec.source_revision != domain_digest(SOURCE_DOMAIN, source.as_bytes()) + { + return Err(b107("selected identity missing")); + } + // Keep the complete decode reservation live through target construction, + // source-digest materialization, and validation; only then transfer the + // exact retained Spec capacity into the invocation-wide ledger. + let mut spec_budget = spec_budget; + spec_budget.shrink_held(spec_owned)?; + Ok((spec, spec_budget)) +} + +fn limits_json() -> String { + format!( + "{{\"max_exports\":{MAX_EXPORTS},\"max_imports\":{MAX_IMPORTS},\"max_parameters\":{MAX_PARAMETERS},\"max_closure_functions\":{MAX_CLOSURE_FUNCTIONS},\"max_status_domains\":{MAX_STATUS_DOMAINS},\"max_effects\":{MAX_EFFECTS},\"max_identifier_bytes\":{MAX_IDENTIFIER_BYTES},\"max_source_bytes\":{MAX_SOURCE_BYTES},\"max_spec_bytes\":{MAX_SPEC_BYTES},\"max_descriptor_bytes\":{MAX_DESCRIPTOR_BYTES},\"max_generated_c_bytes\":{MAX_GENERATED_C_BYTES},\"max_generated_header_bytes\":{MAX_GENERATED_HEADER_BYTES},\"max_generated_rust_bytes\":{MAX_GENERATED_RUST_BYTES},\"max_manifest_bytes\":{MAX_MANIFEST_BYTES},\"max_builder_bytes\":{MAX_BUILDER_BYTES},\"max_json_depth\":{MAX_JSON_DEPTH},\"max_semantic_expression_depth\":{MAX_SEMANTIC_EXPRESSION_DEPTH},\"max_call_depth\":{MAX_CALL_DEPTH},\"max_calls_per_bridge\":{MAX_CALLS_PER_BRIDGE},\"max_unexpected_inventory_entries\":0}}" + ) +} + +fn render_string_array(values: &[String]) -> String { + let mut output = String::new(); + for (index, value) in values.iter().enumerate() { + if index != 0 { + output.push(','); + } + write_json_string(&mut output, value).expect("writing JSON cannot fail"); + } + output +} + +fn nonclaims_json() -> String { + let mut output = String::new(); + for (index, value) in NONCLAIMS.iter().enumerate() { + if index != 0 { + output.push(','); + } + write_json_string(&mut output, value).expect("writing JSON cannot fail"); + } + output +} + +fn write_limits_json(output: &mut impl std::fmt::Write) -> std::fmt::Result { + output.write_char('{')?; + for (index, (name, value)) in LIMIT_ROWS.iter().enumerate() { + if index != 0 { + output.write_char(',')?; + } + write_json_string(output, name)?; + output.write_char(':')?; + write_usize_decimal(output, *value)?; + } + output.write_char('}') +} + +fn target_json(target: &Target) -> String { + format!( + "{{\"triple\":{},\"pointer_width\":{},\"endian\":{},\"panic_strategy\":{},\"thread_policy\":{}}}", + quote_json(&target.triple), + target.pointer_width, + quote_json(&target.endian), + quote_json(&target.panic_strategy), + quote_json(&target.thread_policy) + ) +} + +fn write_json_string(output: &mut impl std::fmt::Write, value: &str) -> std::fmt::Result { + output.write_char('"')?; + for character in value.chars() { + match character { + '"' => output.write_str("\\\"")?, + '\\' => output.write_str("\\\\")?, + '\u{08}' => output.write_str("\\b")?, + '\u{0c}' => output.write_str("\\f")?, + '\n' => output.write_str("\\n")?, + '\r' => output.write_str("\\r")?, + '\t' => output.write_str("\\t")?, + character if character <= '\u{1f}' => { + write!(output, "\\u{:04x}", u32::from(character))? + } + character => output.write_char(character)?, + } + } + output.write_char('"') +} + +fn write_spec_string_array( + output: &mut impl std::fmt::Write, + values: &[String], +) -> std::fmt::Result { + output.write_char('[')?; + for (index, value) in values.iter().enumerate() { + if index != 0 { + output.write_char(',')?; + } + write_json_string(output, value)?; + } + output.write_char(']') +} + +fn write_spec(spec: &Spec, output: &mut impl std::fmt::Write) -> std::fmt::Result { + output.write_str("{\"schema\":")?; + write_json_string(output, SPEC_SCHEMA)?; + output.write_str(",\"module\":")?; + write_json_string(output, &spec.module)?; + output.write_str(",\"source_revision\":")?; + write_json_string(output, &spec.source_revision)?; + output.write_str(",\"target\":{\"triple\":")?; + write_json_string(output, &spec.target.triple)?; + write!(output, ",\"pointer_width\":{}", spec.target.pointer_width)?; + output.write_str(",\"endian\":")?; + write_json_string(output, &spec.target.endian)?; + output.write_str(",\"panic_strategy\":")?; + write_json_string(output, &spec.target.panic_strategy)?; + output.write_str(",\"thread_policy\":")?; + write_json_string(output, &spec.target.thread_policy)?; + output.write_str("},\"exports\":")?; + write_spec_string_array(output, &spec.exports)?; + output.write_str(",\"imports\":")?; + write_spec_string_array(output, &spec.imports)?; + output.write_str(",\"capabilities\":")?; + write_spec_string_array(output, &spec.capabilities)?; + output.write_str(",\"limits\":")?; + write_limits_json(output)?; + output.write_str(",\"nonclaims\":[")?; + for (index, value) in NONCLAIMS.iter().enumerate() { + if index != 0 { + output.write_char(',')?; + } + write_json_string(output, value)?; + } + output.write_str("]}\n") +} + +fn render_spec(spec: &Spec) -> String { + let mut counter = CountingSink { + bytes: 0, + maximum: MAX_SPEC_BYTES, + overflowed: false, + }; + write_spec(spec, &mut counter).expect("counting Spec output cannot fail"); + if counter.overflowed { + return String::new(); + } + let mut output = String::with_capacity(counter.bytes); + write_spec(spec, &mut output).expect("writing Spec output cannot fail"); + output +} + +fn scalar_type(ty: &ResolvedType) -> Option { + match ty { + ResolvedType::Unit => Some(ScalarType::Unit), + ResolvedType::I64 => Some(ScalarType::I64), + ResolvedType::Bool => Some(ScalarType::Bool), + _ => None, + } +} + +fn source_scalar_type(ty: &Type) -> Option { + match ty { + Type::I64 => Some(ScalarType::I64), + Type::Bool => Some(ScalarType::Bool), + Type::Named { .. } => None, + } +} + +fn scalar_text(ty: ScalarType) -> &'static str { + match ty { + ScalarType::Unit => "unit", + ScalarType::I64 => "i64", + ScalarType::Bool => "bool", + } +} + +fn c_type(ty: ScalarType) -> &'static str { + match ty { + ScalarType::Unit => "void", + ScalarType::I64 => "int64_t", + ScalarType::Bool => "uint8_t", + } +} + +fn rust_type(ty: ScalarType) -> &'static str { + match ty { + ScalarType::Unit => "()", + ScalarType::I64 => "i64", + ScalarType::Bool => "bool", + } +} + +fn rust_ffi_wire_type(ty: ScalarType) -> &'static str { + match ty { + ScalarType::Unit => "()", + ScalarType::I64 => "i64", + ScalarType::Bool => "u8", + } +} + +#[allow(clippy::too_many_arguments)] +fn call_digest( + direction: &str, + id: &str, + parameters: &[ParameterFact], + result: ScalarType, + effects: &[String], + capabilities: &[String], + required_imports: &[String], + required_import_contracts: &[(String, String)], + failure: &str, + _capacity_baseline: usize, + target: &Target, +) -> Result { + let parameter_values = parameters + .iter() + .map(|parameter| format!("{}:{}:value", parameter.name, scalar_text(parameter.ty))) + .collect::>(); + let params = parameter_values.join("\0"); + let target = target_json(target); + let effects = effects.join("\0"); + let capabilities = capabilities.join("\0"); + #[cfg(test)] + { + let scratch = checked_owned_string_vec(¶meter_values, parameter_values.capacity()) + .and_then(|bytes| bytes.checked_add(params.capacity())) + .and_then(|bytes| bytes.checked_add(target.capacity())) + .and_then(|bytes| bytes.checked_add(effects.capacity())) + .and_then(|bytes| bytes.checked_add(capabilities.capacity())) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + note_post_hir_facts_live(_capacity_baseline, scratch); + } + let abi = "1\0C\0u64-domain16-code32-class8-retry1-reserved7\0u8-0-or-1\0signed-two-complement-i64\0SPXNRCTX1\0SPXNRIMP1\0caller-owned-uninitialized-success-only\0none-across-boundary\0caught-before-ffi-return\0same-thread\0rejected"; + let mut hasher = Sha256::new(); + hasher.update(CALL_DOMAIN); + for value in [ + direction.as_bytes(), + id.as_bytes(), + params.as_bytes(), + scalar_text(result).as_bytes(), + effects.as_bytes(), + capabilities.as_bytes(), + failure.as_bytes(), + target.as_bytes(), + abi.as_bytes(), + ] { + frame(&mut hasher, value); + } + hash_count(&mut hasher, "required-imports", required_imports.len()); + for import in required_imports { + frame(&mut hasher, import.as_bytes()); + } + hash_count( + &mut hasher, + "required-import-contracts", + required_import_contracts.len(), + ); + for (id, digest) in required_import_contracts { + frame(&mut hasher, id.as_bytes()); + frame(&mut hasher, digest.as_bytes()); + } + let digest = format!("sha256:{:x}", hasher.finalize()); + #[cfg(test)] + { + let scratch = checked_owned_string_vec(¶meter_values, parameter_values.capacity()) + .and_then(|bytes| bytes.checked_add(params.capacity())) + .and_then(|bytes| bytes.checked_add(target.capacity())) + .and_then(|bytes| bytes.checked_add(effects.capacity())) + .and_then(|bytes| bytes.checked_add(capabilities.capacity())) + .and_then(|bytes| bytes.checked_add(digest.capacity())) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + note_post_hir_facts_live(_capacity_baseline, scratch); + } + Ok(digest) +} + +fn visit_calls( + expression: &ResolvedExpr, + functions: &mut BTreeSet, + imports: &mut BTreeSet, + _capacity_baseline: usize, + _scratch_baseline: usize, +) -> Result<(), Diagnostic> { + fn child(expression: &ResolvedExpr, index: usize) -> Option<&ResolvedExpr> { + resolved_call_child(expression, index) + } + let mut frames = Vec::with_capacity(MAX_SEMANTIC_EXPRESSION_DEPTH + 1); + frames.push((expression, 0usize)); + while let Some((expression, next)) = frames.pop() { + if next == 0 { + match &expression.kind { + ResolvedExprKind::Call { callee, .. } => { + functions.insert(callee.clone()); + } + ResolvedExprKind::NativeRustImportCall(call) => { + imports.insert(call.import.clone()); + } + _ => {} + } + } + if let Some(child) = child(expression, next) { + if frames.len() + 2 > frames.capacity() { + return Err(b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + )); + } + frames.push((expression, next + 1)); + frames.push((child, 0)); + } + #[cfg(test)] + { + let scratch = frames.capacity() * std::mem::size_of::<(&ResolvedExpr, usize)>() + + declaration_set_capacity(functions) + + declaration_set_capacity(imports); + note_post_hir_facts_scratch(_scratch_baseline.saturating_add(scratch)); + note_post_hir_facts_capacity(_capacity_baseline.saturating_add(scratch)); + } + } + Ok(()) +} + +fn resolved_call_child(expression: &ResolvedExpr, index: usize) -> Option<&ResolvedExpr> { + match &expression.kind { + ResolvedExprKind::Call { args, .. } => args.get(index), + ResolvedExprKind::NativeRustImportCall(call) => call.args.get(index), + ResolvedExprKind::Unary { value, .. } + | ResolvedExprKind::Try { operand: value, .. } + | ResolvedExprKind::TryOption { operand: value, .. } + | ResolvedExprKind::Project { base: value, .. } => (index == 0).then_some(value), + ResolvedExprKind::Binary { left, right, .. } => { + [left.as_ref(), right.as_ref()].get(index).copied() + } + ResolvedExprKind::Block { statements, tail } => statements + .get(index) + .map(|statement| { + let ResolvedStatement::Let { value, .. } = statement; + value + }) + .or_else(|| (index == statements.len()).then_some(tail)), + ResolvedExprKind::If { + condition, + then_branch, + else_branch, + } => [ + condition.as_ref(), + then_branch.as_ref(), + else_branch.as_ref(), + ] + .get(index) + .copied(), + ResolvedExprKind::ConstructRecord { fields, .. } + | ResolvedExprKind::ConstructVariant { fields, .. } => { + fields.get(index).map(|field| &field.value) + } + ResolvedExprKind::Match { scrutinee, arms } => { + if index == 0 { + Some(scrutinee) + } else { + arms.get(index - 1).map(|arm| &arm.value) + } + } + ResolvedExprKind::UpdateRecord { base, fields, .. } => { + if index == 0 { + Some(base) + } else { + fields.get(index - 1).map(|field| &field.value) + } + } + ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) | ResolvedExprKind::Place(_) => None, + } +} + +#[derive(Clone, Copy, Default)] +struct TraversalCallSiteCensus { + function_sites: usize, + function_id_bytes: usize, + import_sites: usize, + import_id_bytes: usize, +} + +fn expression_call_site_census(root: &ResolvedExpr) -> Result { + let mut frames = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let mut frame_len = 1usize; + frames[0] = Some((root, 0usize)); + let mut census = TraversalCallSiteCensus::default(); + while frame_len > 0 { + let (expression, next) = frames[frame_len - 1] + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + frame_len -= 1; + if next == 0 { + match &expression.kind { + ResolvedExprKind::Call { callee, .. } => { + census.function_sites = census + .function_sites + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + census.function_id_bytes = census + .function_id_bytes + .checked_add(callee.as_str().len()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + ResolvedExprKind::NativeRustImportCall(call) => { + census.import_sites = census + .import_sites + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + census.import_id_bytes = census + .import_id_bytes + .checked_add(call.import.as_str().len()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + _ => {} + } + } + if let Some(child) = resolved_call_child(expression, next) { + if frame_len + 2 > frames.len() { + return Err(b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + )); + } + frames[frame_len] = Some((expression, next + 1)); + frames[frame_len + 1] = Some((child, 0)); + frame_len += 2; + } + } + Ok(census) +} + +fn traversal_call_site_census( + closure: &[&ResolvedFunction], +) -> Result { + closure + .iter() + .try_fold(TraversalCallSiteCensus::default(), |mut total, function| { + for expression in function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures) + { + let current = expression_call_site_census(expression)?; + total.function_sites = total + .function_sites + .checked_add(current.function_sites) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + total.function_id_bytes = total + .function_id_bytes + .checked_add(current.function_id_bytes) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + total.import_sites = total + .import_sites + .checked_add(current.import_sites) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + total.import_id_bytes = total + .import_id_bytes + .checked_add(current.import_id_bytes) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + Ok(total) + }) +} + +fn direct_calls( + function: &ResolvedFunction, + _capacity_baseline: usize, + _scratch_baseline: usize, +) -> Result<(BTreeSet, BTreeSet), Diagnostic> { + let mut functions = BTreeSet::new(); + let mut imports = BTreeSet::new(); + for contract in &function.requires { + let mut contract_functions = BTreeSet::new(); + let mut contract_imports = BTreeSet::new(); + #[cfg(test)] + let nested_baseline = _capacity_baseline + + declaration_set_capacity(&functions) + + declaration_set_capacity(&imports); + #[cfg(not(test))] + let nested_baseline = 0; + #[cfg(test)] + let nested_scratch = _scratch_baseline + + declaration_set_capacity(&functions) + + declaration_set_capacity(&imports); + #[cfg(not(test))] + let nested_scratch = 0; + visit_calls( + contract, + &mut contract_functions, + &mut contract_imports, + nested_baseline, + nested_scratch, + )?; + #[cfg(test)] + note_post_hir_facts_live( + _capacity_baseline, + _scratch_baseline + .saturating_add(declaration_set_capacity(&functions)) + .saturating_add(declaration_set_capacity(&imports)) + .saturating_add(declaration_set_capacity(&contract_functions)) + .saturating_add(declaration_set_capacity(&contract_imports)), + ); + if !contract_imports.is_empty() { + imports.insert(DeclarationId::new("\0native-rust-contract-call".to_owned())); + } + functions.extend(contract_functions); + } + visit_calls( + &function.body, + &mut functions, + &mut imports, + _capacity_baseline, + _scratch_baseline, + )?; + for contract in &function.ensures { + let mut contract_functions = BTreeSet::new(); + let mut contract_imports = BTreeSet::new(); + #[cfg(test)] + let nested_baseline = _capacity_baseline + + declaration_set_capacity(&functions) + + declaration_set_capacity(&imports); + #[cfg(not(test))] + let nested_baseline = 0; + #[cfg(test)] + let nested_scratch = _scratch_baseline + + declaration_set_capacity(&functions) + + declaration_set_capacity(&imports); + #[cfg(not(test))] + let nested_scratch = 0; + visit_calls( + contract, + &mut contract_functions, + &mut contract_imports, + nested_baseline, + nested_scratch, + )?; + #[cfg(test)] + note_post_hir_facts_live( + _capacity_baseline, + _scratch_baseline + .saturating_add(declaration_set_capacity(&functions)) + .saturating_add(declaration_set_capacity(&imports)) + .saturating_add(declaration_set_capacity(&contract_functions)) + .saturating_add(declaration_set_capacity(&contract_imports)), + ); + if !contract_imports.is_empty() { + imports.insert(DeclarationId::new("\0native-rust-contract-call".to_owned())); + } + functions.extend(contract_functions); + } + Ok((functions, imports)) +} + +#[cfg(test)] +fn btree_allocation_upper(len: usize) -> usize { + // A BTree allocation contains inline key/value slots plus links and node + // metadata. Charging one complete map header per live entry is a + // conservative upper for the separately allocated node/link storage: a + // non-root node always contains multiple entries, while a singleton root + // needs only one header. + len.saturating_mul( + std::mem::size_of::<(K, V)>().saturating_add(std::mem::size_of::>()), + ) +} + +#[cfg(test)] +fn declaration_set_capacity(set: &BTreeSet) -> usize { + btree_allocation_upper::(set.len()) + .saturating_add(set.iter().map(|id| id.as_str().len()).sum::()) +} + +struct SelectedClosureFrame { + id: String, + calls: Vec, + next: usize, + longest: usize, +} + +const _: () = assert!(std::mem::size_of::() == 64); + +#[cfg(test)] +fn selected_closure_live_capacity( + by_id: &BTreeMap<&str, &ResolvedFunction>, + state: &BTreeMap, + depths: &BTreeMap, + closure: &Vec<&ResolvedFunction>, + reached_imports: &BTreeSet, + stack: &Vec, + pending: &Option, +) -> usize { + let stack_bytes = stack.iter().fold( + stack.capacity() * std::mem::size_of::(), + |bytes, frame| { + bytes + .saturating_add(frame.id.capacity()) + .saturating_add(frame.calls.capacity() * std::mem::size_of::()) + .saturating_add(frame.calls.iter().map(String::capacity).sum::()) + }, + ); + let map_bytes = btree_allocation_upper::<&str, &ResolvedFunction>(by_id.len()) + .saturating_add(btree_allocation_upper::(state.len())) + .saturating_add(state.keys().map(String::capacity).sum::()) + .saturating_add(btree_allocation_upper::(depths.len())) + .saturating_add(depths.keys().map(String::capacity).sum::()) + .saturating_add(btree_allocation_upper::(reached_imports.len())) + .saturating_add(reached_imports.iter().map(String::capacity).sum::()); + let closure_bytes = closure.capacity() * std::mem::size_of::<&ResolvedFunction>(); + stack_bytes + .saturating_add(map_bytes) + .saturating_add(closure_bytes) + .saturating_add(pending.as_ref().map_or(0, String::capacity)) +} + +fn contract_reaches_native_import( + function: &ResolvedFunction, + by_id: &BTreeMap<&str, &ResolvedFunction>, + #[cfg(test)] retained_outer_bytes: usize, +) -> Result { + let mut pending = BTreeSet::new(); + for contract in function.requires.iter().chain(&function.ensures) { + let mut calls = BTreeSet::new(); + let mut imports = BTreeSet::new(); + visit_calls( + contract, + &mut calls, + &mut imports, + #[cfg(test)] + retained_outer_bytes.saturating_add(declaration_set_capacity(&pending)), + #[cfg(not(test))] + 0, + 0, + )?; + #[cfg(test)] + note_closure_capacity_high_water( + retained_outer_bytes + .saturating_add(declaration_set_capacity(&pending)) + .saturating_add(declaration_set_capacity(&calls)) + .saturating_add(declaration_set_capacity(&imports)), + ); + if !imports.is_empty() { + return Ok(true); + } + pending.extend(calls); + } + let mut visited = BTreeSet::new(); + while let Some(id) = pending.pop_first() { + if !visited.insert(id.clone()) { + continue; + } + let helper = by_id + .get(id.as_str()) + .ok_or_else(|| b107("selected identity missing"))?; + let (calls, imports) = direct_calls(helper, 0, 0)?; + #[cfg(test)] + note_closure_capacity_high_water( + retained_outer_bytes + .saturating_add(declaration_set_capacity(&pending)) + .saturating_add(declaration_set_capacity(&visited)) + .saturating_add(declaration_set_capacity(&calls)) + .saturating_add(declaration_set_capacity(&imports)) + .saturating_add(id.as_str().len()), + ); + if !imports.is_empty() { + return Ok(true); + } + pending.extend(calls); + } + Ok(false) +} + +fn selected_closure<'a>( + resolved: &'a ResolvedProgram, + selected: &[String], +) -> Result<(Vec<&'a ResolvedFunction>, BTreeSet), Diagnostic> { + note_hir_post_resolve_phase(0); + let by_id = resolved + .functions + .iter() + .map(|function| (function.id.as_str(), function)) + .collect::>(); + let mut state = BTreeMap::::new(); + let mut depths = BTreeMap::::new(); + let mut closure = Vec::new(); + let mut reached_imports = BTreeSet::new(); + + for root in selected { + if state.get(root).copied() == Some(2) { + continue; + } + let mut stack = Vec::::new(); + let mut pending = Some(root.clone()); + loop { + #[cfg(test)] + note_closure_capacity_high_water(selected_closure_live_capacity( + &by_id, + &state, + &depths, + &closure, + &reached_imports, + &stack, + &pending, + )); + if let Some(id) = pending.take() { + match state.get(&id).copied() { + Some(1) => return Err(b107("selected closure is cyclic")), + Some(2) => { + let child_depth = *depths + .get(&id) + .ok_or_else(|| b107("selected identity missing"))?; + if let Some(parent) = stack.last_mut() { + parent.longest = parent.longest.max( + child_depth + .checked_add(1) + .ok_or_else(|| b109("max_call_depth", MAX_CALL_DEPTH))?, + ); + if parent.longest > MAX_CALL_DEPTH { + return Err(b109("max_call_depth", MAX_CALL_DEPTH)); + } + continue; + } + break; + } + _ => {} + } + let function = by_id + .get(id.as_str()) + .ok_or_else(|| b107("selected identity missing"))?; + if contract_reaches_native_import( + function, + &by_id, + #[cfg(test)] + selected_closure_live_capacity( + &by_id, + &state, + &depths, + &closure, + &reached_imports, + &stack, + &pending, + ), + )? { + return Err(b107("effect or capability mismatch")); + } + if state.len() >= MAX_CLOSURE_FUNCTIONS { + return Err(b109("max_closure_functions", MAX_CLOSURE_FUNCTIONS)); + } + state.insert(id.clone(), 1); + let (calls, imports) = direct_calls(function, 0, 0)?; + #[cfg(test)] + note_closure_capacity_high_water( + selected_closure_live_capacity( + &by_id, + &state, + &depths, + &closure, + &reached_imports, + &stack, + &pending, + ) + .saturating_add(declaration_set_capacity(&calls)) + .saturating_add(declaration_set_capacity(&imports)), + ); + if imports.iter().any(|id| id.as_str().starts_with('\0')) { + return Err(b107("effect or capability mismatch")); + } + reached_imports.extend(imports.into_iter().map(|id| id.as_str().to_owned())); + let mut call_ids = Vec::with_capacity(calls.len()); + let mut remaining_calls = calls; + while let Some(id) = remaining_calls.pop_first() { + call_ids.push(id.as_str().to_owned()); + #[cfg(test)] + note_closure_capacity_high_water( + selected_closure_live_capacity( + &by_id, + &state, + &depths, + &closure, + &reached_imports, + &stack, + &pending, + ) + .saturating_add(declaration_set_capacity(&remaining_calls)) + .saturating_add( + call_ids.capacity() * std::mem::size_of::() + + call_ids.iter().map(String::capacity).sum::(), + ), + ); + } + stack.push(SelectedClosureFrame { + id, + calls: call_ids, + next: 0, + longest: 1, + }); + if stack.len() > MAX_CALL_DEPTH { + return Err(b109("max_call_depth", MAX_CALL_DEPTH)); + } + } + + let Some(frame) = stack.last_mut() else { break }; + if let Some(call) = frame.calls.get(frame.next).cloned() { + frame.next += 1; + pending = Some(call); + continue; + } + let frame = stack.pop().expect("checked nonempty"); + let function = by_id + .get(frame.id.as_str()) + .ok_or_else(|| b107("selected identity missing"))?; + state.insert(frame.id.clone(), 2); + depths.insert(frame.id, frame.longest); + closure.push(*function); + if let Some(parent) = stack.last_mut() { + parent.longest = parent.longest.max( + frame + .longest + .checked_add(1) + .ok_or_else(|| b109("max_call_depth", MAX_CALL_DEPTH))?, + ); + if parent.longest > MAX_CALL_DEPTH { + return Err(b109("max_call_depth", MAX_CALL_DEPTH)); + } + } else { + break; + } + } + } + closure.sort_by(|left, right| left.id.cmp(&right.id)); + Ok((closure, reached_imports)) +} + +fn transitive_imports( + root: &ResolvedFunction, + functions: &BTreeMap<&str, &ResolvedFunction>, + pending_capacity: usize, + _capacity_baseline: usize, +) -> Result, Diagnostic> { + let mut pending = Vec::with_capacity(pending_capacity); + pending.push(root.id.as_str().to_owned()); + let mut visited = BTreeSet::new(); + let mut imports = BTreeSet::new(); + while let Some(id) = pending.pop() { + #[cfg(test)] + note_post_hir_facts_live( + _capacity_baseline, + checked_owned_string_vec(&pending, pending.capacity()) + .and_then(|bytes| bytes.checked_add(owned_string_set_owned_capacity(&visited))) + .and_then(|bytes| bytes.checked_add(owned_string_set_owned_capacity(&imports))) + .and_then(|bytes| bytes.checked_add(id.capacity())) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ); + if !visited.insert(id.clone()) { + continue; + } + let function = functions + .get(id.as_str()) + .ok_or_else(|| b107("selected identity missing"))?; + #[cfg(test)] + let traversal_scratch = checked_owned_string_vec(&pending, pending.capacity()) + .and_then(|bytes| bytes.checked_add(owned_string_set_owned_capacity(&visited))) + .and_then(|bytes| bytes.checked_add(owned_string_set_owned_capacity(&imports))) + .and_then(|bytes| bytes.checked_add(id.capacity())) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + #[cfg(not(test))] + let traversal_scratch = 0; + #[cfg(test)] + note_post_hir_facts_live(_capacity_baseline, traversal_scratch); + #[cfg(test)] + let traversal_baseline = _capacity_baseline.saturating_add(traversal_scratch); + #[cfg(not(test))] + let traversal_baseline = 0; + let (calls, reached) = direct_calls(function, traversal_baseline, traversal_scratch)?; + #[cfg(test)] + note_post_hir_facts_live( + _capacity_baseline, + traversal_scratch + .saturating_add(declaration_set_capacity(&calls)) + .saturating_add(declaration_set_capacity(&reached)), + ); + if reached.iter().any(|id| id.as_str().starts_with('\0')) { + return Err(b107("effect or capability mismatch")); + } + imports.extend(reached.into_iter().map(|id| id.as_str().to_owned())); + pending.extend(calls.into_iter().map(|id| id.as_str().to_owned())); + } + Ok(imports) +} + +fn parameter_facts(function: &ResolvedFunction) -> Result, Diagnostic> { + if function.params.len() > MAX_PARAMETERS { + return Err(b109("max_parameters", MAX_PARAMETERS)); + } + let mut facts = Vec::with_capacity(function.params.len()); + for parameter in &function.params { + if parameter.ownership != OwnershipMode::Value + || parameter.name.len() > MAX_IDENTIFIER_BYTES + { + return Err(b107("scalar value signature required")); + } + facts.push(ParameterFact { + name: parameter.name.clone(), + ty: scalar_type(¶meter.ty) + .filter(|ty| *ty != ScalarType::Unit) + .ok_or_else(|| b107("scalar value signature required"))?, + }); + } + Ok(facts) +} + +fn import_parameter_facts(import: &ResolvedImport) -> Result, Diagnostic> { + if import.parameters.len() > MAX_PARAMETERS { + return Err(b109("max_parameters", MAX_PARAMETERS)); + } + let mut facts = Vec::with_capacity(import.parameters.len()); + for parameter in &import.parameters { + if parameter.ownership != OwnershipMode::Value + || parameter.consumes_on_failure + || parameter.name.len() > MAX_IDENTIFIER_BYTES + { + return Err(b107("scalar value signature required")); + } + facts.push(ParameterFact { + name: parameter.name.clone(), + ty: scalar_type(¶meter.ty) + .filter(|ty| *ty != ScalarType::Unit) + .ok_or_else(|| b107("scalar value signature required"))?, + }); + } + Ok(facts) +} + +#[derive(Clone, Copy, Debug)] +struct TypeIdentityMetrics { + nodes: usize, + all_key_bytes: usize, + root_bytes: usize, + maximum_encoded_bytes: usize, +} + +enum TypeIdentityFrame<'a> { + Enter(&'a ResolvedType), + Finish(&'a DeclarationId, usize, usize, usize), +} + +#[derive(Clone, Copy)] +enum TypeIdentityMetricFrame<'a> { + Enter(&'a ResolvedType, usize), + Finish(&'a DeclarationId, usize), +} + +fn decimal_bytes(mut value: usize) -> usize { + let mut bytes = 1usize; + while value >= 10 { + value /= 10; + bytes += 1; + } + bytes +} + +fn type_identity_metrics( + ty: &ResolvedType, + initial_depth: usize, +) -> Result { + let leaf = |root_bytes| TypeIdentityMetrics { + nodes: 1, + all_key_bytes: root_bytes, + root_bytes, + maximum_encoded_bytes: 0, + }; + let mut frames = [None; FINGERPRINT_ACTION_SLOTS]; + let mut frame_len = 1usize; + frames[0] = Some(TypeIdentityMetricFrame::Enter(ty, initial_depth)); + let mut results = [None; FINGERPRINT_ACTION_SLOTS]; + let mut result_len = 0usize; + let mut work = 0usize; + while frame_len > 0 { + frame_len -= 1; + let frame = frames[frame_len] + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + match frame { + TypeIdentityMetricFrame::Enter(ty, depth) => { + if depth > MAX_SEMANTIC_EXPRESSION_DEPTH { + return Err(b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + )); + } + work = work + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if work > FINGERPRINT_ACTION_SLOTS { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + let metric = match ty { + ResolvedType::Unit => Some(leaf("unit".len())), + ResolvedType::I64 => Some(leaf("i64".len())), + ResolvedType::Bool => Some(leaf("bool".len())), + ResolvedType::TypeParameter { owner, index } => { + let owner_bytes = owner.as_str().len(); + let root_bytes = "parameter:" + .len() + .checked_add(decimal_bytes(owner_bytes)) + .and_then(|bytes| bytes.checked_add(1)) + .and_then(|bytes| bytes.checked_add(owner_bytes)) + .and_then(|bytes| bytes.checked_add(1)) + .and_then(|bytes| bytes.checked_add(decimal_bytes(*index as usize))) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + Some(leaf(root_bytes)) + } + ResolvedType::Nominal { + declaration, + arguments, + } => { + if frame_len + .checked_add(arguments.len()) + .and_then(|len| len.checked_add(1)) + .is_none_or(|len| len > frames.len()) + { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + frames[frame_len] = Some(TypeIdentityMetricFrame::Finish( + declaration, + arguments.len(), + )); + frame_len += 1; + for argument in arguments.iter().rev() { + frames[frame_len] = + Some(TypeIdentityMetricFrame::Enter(argument, depth + 1)); + frame_len += 1; + } + None + } + }; + if let Some(metric) = metric { + let slot = results + .get_mut(result_len) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + *slot = Some(metric); + result_len += 1; + } + } + TypeIdentityMetricFrame::Finish(declaration, count) => { + let split = result_len + .checked_sub(count) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let mut nodes = 1usize; + let mut all_key_bytes = 0usize; + let mut encoded_bytes = 0usize; + let mut maximum_encoded_bytes = 0usize; + for slot in &mut results[split..result_len] { + let child = slot + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + nodes = nodes + .checked_add(child.nodes) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + all_key_bytes = all_key_bytes + .checked_add(child.all_key_bytes) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + encoded_bytes = encoded_bytes + .checked_add(decimal_bytes(child.root_bytes)) + .and_then(|bytes| bytes.checked_add(1)) + .and_then(|bytes| bytes.checked_add(child.root_bytes)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + maximum_encoded_bytes = maximum_encoded_bytes.max(child.maximum_encoded_bytes); + } + let declaration_bytes = declaration.as_str().len(); + let root_bytes = "nominal:" + .len() + .checked_add(decimal_bytes(declaration_bytes)) + .and_then(|bytes| bytes.checked_add(1)) + .and_then(|bytes| bytes.checked_add(declaration_bytes)) + .and_then(|bytes| bytes.checked_add(1)) + .and_then(|bytes| bytes.checked_add(decimal_bytes(count))) + .and_then(|bytes| bytes.checked_add(1)) + .and_then(|bytes| bytes.checked_add(encoded_bytes)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + result_len = split; + results[result_len] = Some(TypeIdentityMetrics { + nodes, + all_key_bytes: all_key_bytes + .checked_add(root_bytes) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + root_bytes, + maximum_encoded_bytes: maximum_encoded_bytes.max(encoded_bytes), + }); + result_len += 1; + } + } + } + if result_len != 1 { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + results[0] + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES)) +} + +fn type_identity_scratch_upper(ty: &ResolvedType) -> Result { + let metrics = type_identity_metrics(ty, 1)?; + metrics + .nodes + .checked_mul(std::mem::size_of::>()) + .and_then(|bytes| { + bytes.checked_add(metrics.nodes.checked_mul(std::mem::size_of::())?) + }) + .and_then(|bytes| bytes.checked_add(metrics.all_key_bytes)) + .and_then(|bytes| bytes.checked_add(metrics.maximum_encoded_bytes)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES)) +} + +fn fingerprint_type_identity( + ty: &ResolvedType, + _capacity_baseline: usize, + _outer_scratch: usize, +) -> Result { + let metrics = type_identity_metrics(ty, 1)?; + let mut frames = Vec::with_capacity(metrics.nodes); + let mut keys = Vec::::with_capacity(metrics.nodes); + frames.push(TypeIdentityFrame::Enter(ty)); + while let Some(frame) = frames.pop() { + match frame { + TypeIdentityFrame::Enter(ty) => match ty { + ResolvedType::Unit | ResolvedType::I64 | ResolvedType::Bool => { + let text = match ty { + ResolvedType::Unit => "unit", + ResolvedType::I64 => "i64", + ResolvedType::Bool => "bool", + _ => unreachable!(), + }; + let mut key = String::with_capacity(text.len()); + key.push_str(text); + keys.push(key); + } + ResolvedType::TypeParameter { owner, index } => { + let key_bytes = type_identity_metrics(ty, 1)?.root_bytes; + let mut key = String::with_capacity(key_bytes); + write!(key, "parameter:{}:{}:{index}", owner.as_str().len(), owner) + .map_err(|_| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + keys.push(key); + } + ResolvedType::Nominal { + declaration, + arguments, + } => { + let node = type_identity_metrics(ty, 1)?; + let encoded_bytes = arguments + .iter() + .try_fold(0usize, |bytes, argument| { + let child_bytes = type_identity_metrics(argument, 1).ok()?.root_bytes; + bytes + .checked_add(decimal_bytes(child_bytes))? + .checked_add(1)? + .checked_add(child_bytes) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + frames.push(TypeIdentityFrame::Finish( + declaration, + arguments.len(), + encoded_bytes, + node.root_bytes, + )); + frames.extend(arguments.iter().rev().map(TypeIdentityFrame::Enter)); + } + }, + TypeIdentityFrame::Finish(declaration, count, encoded_bytes, result_bytes) => { + let split = keys + .len() + .checked_sub(count) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let mut encoded = String::with_capacity(encoded_bytes); + for key in &keys[split..] { + write!(encoded, "{}:{key}", key.len()) + .map_err(|_| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + let mut result = String::with_capacity(result_bytes); + write!( + result, + "nominal:{}:{}:{}:{}", + declaration.as_str().len(), + declaration, + count, + encoded + ) + .map_err(|_| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + #[cfg(test)] + note_post_hir_facts_live( + _capacity_baseline, + _outer_scratch + .saturating_add( + frames + .capacity() + .saturating_mul(std::mem::size_of::>()), + ) + .saturating_add( + keys.capacity() + .saturating_mul(std::mem::size_of::()), + ) + .saturating_add(keys.iter().map(String::capacity).sum::()) + .saturating_add(encoded.capacity()) + .saturating_add(result.capacity()), + ); + keys.truncate(split); + keys.push(result); + } + } + #[cfg(test)] + note_post_hir_facts_live( + _capacity_baseline, + _outer_scratch + .saturating_add( + frames + .capacity() + .saturating_mul(std::mem::size_of::>()), + ) + .saturating_add( + keys.capacity() + .saturating_mul(std::mem::size_of::()), + ) + .saturating_add(keys.iter().map(String::capacity).sum::()), + ); + } + if keys.len() != 1 { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + Ok(keys.pop().expect("one checked type identity")) +} + +fn fingerprint_binding_type_scratch( + binding: &crate::hir::ResolvedBinding, +) -> Result { + type_identity_scratch_upper(&binding.ty) +} + +fn fingerprint_record_pattern_types_scratch( + fields: &[crate::hir::ResolvedRecordMatchPatternField], +) -> Result { + fields.iter().try_fold(0usize, |maximum, field| { + let current = match &field.pattern { + crate::hir::ResolvedRecordMatchFieldPattern::Binding(binding) => { + fingerprint_binding_type_scratch(binding)? + } + crate::hir::ResolvedRecordMatchFieldPattern::Wildcard => 0, + crate::hir::ResolvedRecordMatchFieldPattern::Record { + instance, fields, .. + } => type_identity_scratch_upper(instance)? + .max(fingerprint_record_pattern_types_scratch(fields)?), + }; + Ok(maximum.max(current)) + }) +} + +fn fingerprint_pattern_types_scratch( + pattern: &crate::hir::ResolvedMatchPattern, +) -> Result { + match pattern { + crate::hir::ResolvedMatchPattern::Wildcard => Ok(0), + crate::hir::ResolvedMatchPattern::Variant { fields, .. } => { + fields.iter().try_fold(0usize, |maximum, field| { + Ok(maximum.max(fingerprint_binding_type_scratch(&field.binding)?)) + }) + } + crate::hir::ResolvedMatchPattern::Record { + instance, fields, .. + } => Ok(type_identity_scratch_upper(instance)? + .max(fingerprint_record_pattern_types_scratch(fields)?)), + } +} + +fn fingerprint_expression_types_scratch( + expression: &ResolvedExpr, + depth: usize, +) -> Result { + #[derive(Clone, Copy)] + enum Frame<'a> { + Expr(&'a ResolvedExpr, usize), + Exprs(&'a [ResolvedExpr], usize, usize), + Statements(&'a [ResolvedStatement], usize, usize), + Fields(&'a [crate::hir::ResolvedFieldInitializer], usize, usize), + Arms(&'a [crate::hir::ResolvedMatchArm], usize, usize), + } + fn push<'a>( + stack: &mut [Option>], + stack_len: &mut usize, + frame: Frame<'a>, + ) -> Result<(), Diagnostic> { + let slot = stack.get_mut(*stack_len).ok_or_else(|| { + b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + ) + })?; + *slot = Some(frame); + *stack_len += 1; + Ok(()) + } + + let mut stack = [None; FINGERPRINT_ACTION_SLOTS]; + let mut stack_len = 0usize; + push(&mut stack, &mut stack_len, Frame::Expr(expression, depth))?; + let mut maximum = 0usize; + while stack_len > 0 { + stack_len -= 1; + let frame = stack[stack_len] + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + match frame { + Frame::Expr(expression, depth) => { + if depth > MAX_SEMANTIC_EXPRESSION_DEPTH { + return Err(b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + )); + } + let child_depth = depth.checked_add(1).ok_or_else(|| { + b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + ) + })?; + maximum = maximum.max(type_identity_scratch_upper(&expression.ty)?); + match &expression.kind { + ResolvedExprKind::Int(_) + | ResolvedExprKind::Bool(_) + | ResolvedExprKind::Place(_) => {} + ResolvedExprKind::Call { + type_arguments, + args, + .. + } => { + for ty in type_arguments { + maximum = maximum.max(type_identity_scratch_upper(ty)?); + } + push( + &mut stack, + &mut stack_len, + Frame::Exprs(args, 0, child_depth), + )?; + } + ResolvedExprKind::NativeRustImportCall(call) => push( + &mut stack, + &mut stack_len, + Frame::Exprs(&call.args, 0, child_depth), + )?, + ResolvedExprKind::Unary { value, .. } => { + push(&mut stack, &mut stack_len, Frame::Expr(value, child_depth))? + } + ResolvedExprKind::Binary { left, right, .. } => { + push(&mut stack, &mut stack_len, Frame::Expr(right, child_depth))?; + push(&mut stack, &mut stack_len, Frame::Expr(left, child_depth))?; + } + ResolvedExprKind::Block { statements, tail } => { + push(&mut stack, &mut stack_len, Frame::Expr(tail, child_depth))?; + push( + &mut stack, + &mut stack_len, + Frame::Statements(statements, 0, child_depth), + )?; + } + ResolvedExprKind::If { + condition, + then_branch, + else_branch, + } => { + push( + &mut stack, + &mut stack_len, + Frame::Expr(else_branch, child_depth), + )?; + push( + &mut stack, + &mut stack_len, + Frame::Expr(then_branch, child_depth), + )?; + push( + &mut stack, + &mut stack_len, + Frame::Expr(condition, child_depth), + )?; + } + ResolvedExprKind::ConstructRecord { fields, .. } + | ResolvedExprKind::ConstructVariant { fields, .. } => push( + &mut stack, + &mut stack_len, + Frame::Fields(fields, 0, child_depth), + )?, + ResolvedExprKind::Match { scrutinee, arms } => { + push( + &mut stack, + &mut stack_len, + Frame::Arms(arms, 0, child_depth), + )?; + push( + &mut stack, + &mut stack_len, + Frame::Expr(scrutinee, child_depth), + )?; + } + ResolvedExprKind::Try { + operand, + residual_type, + .. + } + | ResolvedExprKind::TryOption { + operand, + residual_type, + .. + } => { + maximum = maximum.max(type_identity_scratch_upper(residual_type)?); + push( + &mut stack, + &mut stack_len, + Frame::Expr(operand, child_depth), + )?; + } + ResolvedExprKind::UpdateRecord { base, fields, .. } => { + push( + &mut stack, + &mut stack_len, + Frame::Fields(fields, 0, child_depth), + )?; + push(&mut stack, &mut stack_len, Frame::Expr(base, child_depth))?; + } + ResolvedExprKind::Project { base, .. } => { + push(&mut stack, &mut stack_len, Frame::Expr(base, child_depth))? + } + } + } + Frame::Exprs(expressions, index, depth) => { + if let Some(expression) = expressions.get(index) { + push( + &mut stack, + &mut stack_len, + Frame::Exprs(expressions, index + 1, depth), + )?; + push(&mut stack, &mut stack_len, Frame::Expr(expression, depth))?; + } + } + Frame::Statements(statements, index, depth) => { + if let Some(statement) = statements.get(index) { + let ResolvedStatement::Let { binding, value, .. } = statement; + maximum = maximum.max(fingerprint_binding_type_scratch(binding)?); + push( + &mut stack, + &mut stack_len, + Frame::Statements(statements, index + 1, depth), + )?; + push(&mut stack, &mut stack_len, Frame::Expr(value, depth))?; + } + } + Frame::Fields(fields, index, depth) => { + if let Some(field) = fields.get(index) { + push( + &mut stack, + &mut stack_len, + Frame::Fields(fields, index + 1, depth), + )?; + push(&mut stack, &mut stack_len, Frame::Expr(&field.value, depth))?; + } + } + Frame::Arms(arms, index, depth) => { + if let Some(arm) = arms.get(index) { + maximum = maximum.max(fingerprint_pattern_types_scratch(&arm.pattern)?); + push( + &mut stack, + &mut stack_len, + Frame::Arms(arms, index + 1, depth), + )?; + push(&mut stack, &mut stack_len, Frame::Expr(&arm.value, depth))?; + } + } + } + } + Ok(maximum) +} + +fn fingerprint_type_scratch_upper(closure: &[&ResolvedFunction]) -> Result { + closure.iter().try_fold(0usize, |mut maximum, function| { + maximum = maximum.max(type_identity_scratch_upper(&function.return_type)?); + for parameter in &function.params { + maximum = maximum.max(type_identity_scratch_upper(¶meter.ty)?); + } + for expression in function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures) + { + maximum = maximum.max(fingerprint_expression_types_scratch(expression, 1)?); + } + Ok(maximum) + }) +} + +fn hir_fingerprint( + closure: &[&ResolvedFunction], + imports: &[ImportFact], + _capacity_baseline: usize, +) -> Result { + let mut hasher = Sha256::new(); + hasher.update(HIR_DOMAIN); + hash_count(&mut hasher, "functions", closure.len()); + for function in closure { + frame(&mut hasher, b"function"); + frame(&mut hasher, function.id.as_str().as_bytes()); + frame(&mut hasher, function.name.as_bytes()); + frame(&mut hasher, function.result_id.as_str().as_bytes()); + frame(&mut hasher, function.body.id.as_str().as_bytes()); + let return_identity = + fingerprint_type_identity(&function.return_type, _capacity_baseline, 0)?; + #[cfg(test)] + note_post_hir_facts_live(_capacity_baseline, return_identity.capacity()); + frame(&mut hasher, return_identity.as_bytes()); + hash_count(&mut hasher, "effects", function.effects.len()); + for effect in &function.effects { + frame(&mut hasher, effect.as_bytes()); + } + hash_count(&mut hasher, "parameters", function.params.len()); + for parameter in &function.params { + frame(&mut hasher, parameter.id.as_str().as_bytes()); + frame(&mut hasher, parameter.name.as_bytes()); + frame( + &mut hasher, + match parameter.ownership { + OwnershipMode::Value => b"value", + OwnershipMode::Own => b"own", + OwnershipMode::Borrow => b"borrow", + OwnershipMode::Shared => b"shared", + }, + ); + let parameter_identity = + fingerprint_type_identity(¶meter.ty, _capacity_baseline, 0)?; + #[cfg(test)] + note_post_hir_facts_live(_capacity_baseline, parameter_identity.capacity()); + frame(&mut hasher, parameter_identity.as_bytes()); + } + hash_count(&mut hasher, "requires", function.requires.len()); + for requirement in &function.requires { + hash_expr(&mut hasher, requirement, _capacity_baseline)?; + } + frame(&mut hasher, b"body"); + hash_expr(&mut hasher, &function.body, _capacity_baseline)?; + hash_count(&mut hasher, "ensures", function.ensures.len()); + for guarantee in &function.ensures { + hash_expr(&mut hasher, guarantee, _capacity_baseline)?; + } + } + hash_count(&mut hasher, "imports", imports.len()); + for import in imports { + frame(&mut hasher, b"import"); + frame(&mut hasher, import.id.as_bytes()); + frame(&mut hasher, import.interface.as_bytes()); + frame(&mut hasher, import.import_key.as_bytes()); + frame(&mut hasher, scalar_text(import.result).as_bytes()); + hash_count(&mut hasher, "import-parameters", import.parameters.len()); + for parameter in &import.parameters { + frame(&mut hasher, parameter.name.as_bytes()); + frame(&mut hasher, scalar_text(parameter.ty).as_bytes()); + } + hash_count(&mut hasher, "import-effects", import.effects.len()); + for effect in &import.effects { + frame(&mut hasher, effect.as_bytes()); + } + frame( + &mut hasher, + import.failure.as_deref().unwrap_or("infallible").as_bytes(), + ); + frame(&mut hasher, import.call_contract_digest.as_bytes()); + } + let digest = format!("sha256:{:x}", hasher.finalize()); + #[cfg(test)] + note_post_hir_facts_live(_capacity_baseline, digest.capacity()); + Ok(digest) +} + +fn hash_count(hasher: &mut Sha256, label: &str, count: usize) { + frame(hasher, label.as_bytes()); + frame( + hasher, + &u64::try_from(count).unwrap_or(u64::MAX).to_be_bytes(), + ); +} + +enum HirFingerprintAction<'a> { + Expr(&'a ResolvedExpr, usize), + Exprs(&'a [ResolvedExpr], usize, usize), + Statement(&'a ResolvedStatement, usize), + Statements(&'a [ResolvedStatement], usize, usize), + Field(&'a crate::hir::ResolvedFieldInitializer, usize), + Fields(&'a [crate::hir::ResolvedFieldInitializer], usize, usize), + Pattern(&'a crate::hir::ResolvedMatchPattern), + RecordPatternField(&'a crate::hir::ResolvedRecordMatchPatternField), + RecordPatternFields(&'a [crate::hir::ResolvedRecordMatchPatternField], usize), + Arms(&'a [crate::hir::ResolvedMatchArm], usize, usize), + TryIds([&'a DeclarationId; 5], usize), + OptionIds([&'a DeclarationId; 4], usize), + Bytes(&'a [u8]), + Type(&'a ResolvedType), +} + +fn hash_expr( + hasher: &mut Sha256, + expression: &ResolvedExpr, + _capacity_baseline: usize, +) -> Result<(), Diagnostic> { + let ownership = |ownership| match ownership { + OwnershipMode::Value => b"value".as_slice(), + OwnershipMode::Own => b"own".as_slice(), + OwnershipMode::Borrow => b"borrow".as_slice(), + OwnershipMode::Shared => b"shared".as_slice(), + }; + let mut actions = Vec::with_capacity(MAX_SEMANTIC_EXPRESSION_DEPTH * 4 + 8); + actions.push(HirFingerprintAction::Expr(expression, 1)); + while let Some(action) = actions.pop() { + if actions.len() + 4 > actions.capacity() { + return Err(b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + )); + } + match action { + HirFingerprintAction::Bytes(value) => frame(hasher, value), + HirFingerprintAction::Type(ty) => { + let action_bytes = actions + .capacity() + .saturating_mul(std::mem::size_of::>()); + let identity = fingerprint_type_identity(ty, _capacity_baseline, action_bytes)?; + #[cfg(test)] + note_post_hir_facts_live( + _capacity_baseline, + actions + .capacity() + .saturating_mul(std::mem::size_of::>()) + .saturating_add(identity.capacity()), + ); + frame(hasher, identity.as_bytes()); + } + HirFingerprintAction::Statement(statement, depth) => { + let ResolvedStatement::Let { binding, value, .. } = statement; + frame(hasher, b"let"); + frame(hasher, binding.id.as_str().as_bytes()); + frame(hasher, binding.name.as_bytes()); + let action_bytes = actions + .capacity() + .saturating_mul(std::mem::size_of::>()); + let binding_identity = + fingerprint_type_identity(&binding.ty, _capacity_baseline, action_bytes)?; + #[cfg(test)] + note_post_hir_facts_live( + _capacity_baseline, + actions + .capacity() + .saturating_mul(std::mem::size_of::>()) + .saturating_add(binding_identity.capacity()), + ); + frame(hasher, binding_identity.as_bytes()); + frame(hasher, ownership(binding.ownership)); + actions.push(HirFingerprintAction::Expr(value, depth)); + } + HirFingerprintAction::Statements(statements, index, depth) => { + if let Some(statement) = statements.get(index) { + actions.push(HirFingerprintAction::Statements( + statements, + index + 1, + depth, + )); + actions.push(HirFingerprintAction::Statement(statement, depth)); + } + } + HirFingerprintAction::Exprs(expressions, index, depth) => { + if let Some(expression) = expressions.get(index) { + actions.push(HirFingerprintAction::Exprs(expressions, index + 1, depth)); + actions.push(HirFingerprintAction::Expr(expression, depth)); + } + } + HirFingerprintAction::TryIds(ids, index) => { + if let Some(id) = ids.get(index) { + actions.push(HirFingerprintAction::TryIds(ids, index + 1)); + actions.push(HirFingerprintAction::Bytes(id.as_str().as_bytes())); + } + } + HirFingerprintAction::OptionIds(ids, index) => { + if let Some(id) = ids.get(index) { + actions.push(HirFingerprintAction::OptionIds(ids, index + 1)); + actions.push(HirFingerprintAction::Bytes(id.as_str().as_bytes())); + } + } + HirFingerprintAction::Field(field, depth) => { + frame(hasher, field.field.as_str().as_bytes()); + actions.push(HirFingerprintAction::Expr(&field.value, depth)); + } + HirFingerprintAction::Fields(fields, index, depth) => { + if index == 0 { + hash_count(hasher, "fields", fields.len()); + } + if let Some(field) = fields.get(index) { + actions.push(HirFingerprintAction::Fields(fields, index + 1, depth)); + actions.push(HirFingerprintAction::Field(field, depth)); + } + } + HirFingerprintAction::Arms(arms, index, depth) => { + if index == 0 { + hash_count(hasher, "arms", arms.len()); + } + if let Some(arm) = arms.get(index) { + actions.push(HirFingerprintAction::Arms(arms, index + 1, depth)); + actions.push(HirFingerprintAction::Expr(&arm.value, depth)); + actions.push(HirFingerprintAction::Pattern(&arm.pattern)); + } + } + HirFingerprintAction::RecordPatternFields(fields, index) => { + if let Some(field) = fields.get(index) { + actions.push(HirFingerprintAction::RecordPatternFields(fields, index + 1)); + actions.push(HirFingerprintAction::RecordPatternField(field)); + } + } + HirFingerprintAction::RecordPatternField(field) => { + frame(hasher, field.field.as_str().as_bytes()); + match &field.pattern { + crate::hir::ResolvedRecordMatchFieldPattern::Binding(binding) => { + frame(hasher, b"binding"); + hash_binding( + hasher, + binding, + _capacity_baseline, + actions + .capacity() + .saturating_mul(std::mem::size_of::>()), + )?; + } + crate::hir::ResolvedRecordMatchFieldPattern::Wildcard => { + frame(hasher, b"wildcard"); + } + crate::hir::ResolvedRecordMatchFieldPattern::Record { + record, + instance, + fields, + } => { + frame(hasher, b"record"); + frame(hasher, record.as_str().as_bytes()); + let action_bytes = actions + .capacity() + .saturating_mul(std::mem::size_of::>()); + let instance_identity = + fingerprint_type_identity(instance, _capacity_baseline, action_bytes)?; + #[cfg(test)] + note_post_hir_facts_live( + _capacity_baseline, + actions + .capacity() + .saturating_mul(std::mem::size_of::>()) + .saturating_add(instance_identity.capacity()), + ); + frame(hasher, instance_identity.as_bytes()); + hash_count(hasher, "record-pattern-fields", fields.len()); + actions.push(HirFingerprintAction::RecordPatternFields(fields, 0)); + } + } + } + HirFingerprintAction::Pattern(pattern) => match pattern { + crate::hir::ResolvedMatchPattern::Wildcard => frame(hasher, b"wildcard"), + crate::hir::ResolvedMatchPattern::Variant { + variant, + case, + fields, + } => { + frame(hasher, b"variant"); + frame(hasher, variant.as_str().as_bytes()); + frame(hasher, case.as_str().as_bytes()); + hash_count(hasher, "variant-pattern-fields", fields.len()); + for field in fields { + frame(hasher, field.field.as_str().as_bytes()); + hash_binding( + hasher, + &field.binding, + _capacity_baseline, + actions + .capacity() + .saturating_mul(std::mem::size_of::>()), + )?; + } + } + crate::hir::ResolvedMatchPattern::Record { + record, + instance, + fields, + } => { + frame(hasher, b"record"); + frame(hasher, record.as_str().as_bytes()); + let action_bytes = actions + .capacity() + .saturating_mul(std::mem::size_of::>()); + let instance_identity = + fingerprint_type_identity(instance, _capacity_baseline, action_bytes)?; + #[cfg(test)] + note_post_hir_facts_live( + _capacity_baseline, + actions + .capacity() + .saturating_mul(std::mem::size_of::>()) + .saturating_add(instance_identity.capacity()), + ); + frame(hasher, instance_identity.as_bytes()); + hash_count(hasher, "record-pattern-fields", fields.len()); + actions.push(HirFingerprintAction::RecordPatternFields(fields, 0)); + } + }, + HirFingerprintAction::Expr(expression, depth) => { + if depth > MAX_SEMANTIC_EXPRESSION_DEPTH { + return Err(b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + )); + } + let child_depth = depth.checked_add(1).ok_or_else(|| { + b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + ) + })?; + frame(hasher, b"expression"); + frame(hasher, expression.id.as_str().as_bytes()); + let action_bytes = actions + .capacity() + .saturating_mul(std::mem::size_of::>()); + let identity = + fingerprint_type_identity(&expression.ty, _capacity_baseline, action_bytes)?; + #[cfg(test)] + note_post_hir_facts_live( + _capacity_baseline, + actions + .capacity() + .saturating_mul(std::mem::size_of::>()) + .saturating_add(identity.capacity()), + ); + frame(hasher, identity.as_bytes()); + frame(hasher, ownership(expression.ownership)); + match &expression.kind { + ResolvedExprKind::Int(value) => { + frame(hasher, b"int"); + frame(hasher, &value.to_be_bytes()); + } + ResolvedExprKind::Bool(value) => { + frame(hasher, b"bool"); + frame(hasher, &[*value as u8]); + } + ResolvedExprKind::Place(place) => { + frame(hasher, b"place"); + frame(hasher, place.root.as_str().as_bytes()); + hash_count(hasher, "projections", place.projections.len()); + for projection in &place.projections { + match projection { + crate::hir::PlaceProjection::Field(field) => { + frame(hasher, b"field"); + frame(hasher, field.as_str().as_bytes()); + } + crate::hir::PlaceProjection::VariantField { case, field } => { + frame(hasher, b"variant-field"); + frame(hasher, case.as_str().as_bytes()); + frame(hasher, field.as_str().as_bytes()); + } + } + } + } + ResolvedExprKind::Call { + callee, + type_arguments, + instance, + args, + } => { + frame(hasher, b"call"); + frame(hasher, callee.as_str().as_bytes()); + hash_count(hasher, "type-arguments", type_arguments.len()); + for argument in type_arguments { + let action_bytes = actions + .capacity() + .saturating_mul(std::mem::size_of::>()); + let argument_identity = fingerprint_type_identity( + argument, + _capacity_baseline, + action_bytes, + )?; + #[cfg(test)] + note_post_hir_facts_live( + _capacity_baseline, + actions + .capacity() + .saturating_mul(std::mem::size_of::>()) + .saturating_add(argument_identity.capacity()), + ); + frame(hasher, argument_identity.as_bytes()); + } + frame( + hasher, + instance + .as_ref() + .map_or(b"".as_slice(), |value| value.as_str().as_bytes()), + ); + hash_count(hasher, "arguments", args.len()); + actions.push(HirFingerprintAction::Exprs(args, 0, child_depth)); + } + ResolvedExprKind::NativeRustImportCall(call) => { + frame(hasher, b"native-rust-import"); + frame(hasher, call.expression.as_str().as_bytes()); + frame(hasher, call.import.as_str().as_bytes()); + frame( + hasher, + match call.result { + ResolvedImportResultKind::Unit => b"unit", + ResolvedImportResultKind::I64 => b"i64", + ResolvedImportResultKind::Bool => b"bool", + }, + ); + hash_count(hasher, "arguments", call.args.len()); + actions.push(HirFingerprintAction::Exprs(&call.args, 0, child_depth)); + } + ResolvedExprKind::Unary { op, value } => { + frame( + hasher, + match op { + crate::ast::UnaryOp::Neg => b"unary-neg", + crate::ast::UnaryOp::Not => b"unary-not", + }, + ); + actions.push(HirFingerprintAction::Expr(value, child_depth)); + } + ResolvedExprKind::Binary { op, left, right } => { + frame( + hasher, + match op { + crate::ast::BinaryOp::Add => b"binary-add", + crate::ast::BinaryOp::Sub => b"binary-sub", + crate::ast::BinaryOp::Mul => b"binary-mul", + crate::ast::BinaryOp::Div => b"binary-div", + crate::ast::BinaryOp::Rem => b"binary-rem", + crate::ast::BinaryOp::Eq => b"binary-eq", + crate::ast::BinaryOp::Ne => b"binary-ne", + crate::ast::BinaryOp::Lt => b"binary-lt", + crate::ast::BinaryOp::Le => b"binary-le", + crate::ast::BinaryOp::Gt => b"binary-gt", + crate::ast::BinaryOp::Ge => b"binary-ge", + crate::ast::BinaryOp::And => b"binary-and", + crate::ast::BinaryOp::Or => b"binary-or", + }, + ); + actions.push(HirFingerprintAction::Expr(right, child_depth)); + actions.push(HirFingerprintAction::Expr(left, child_depth)); + } + ResolvedExprKind::Block { statements, tail } => { + frame(hasher, b"block"); + hash_count(hasher, "statements", statements.len()); + actions.push(HirFingerprintAction::Expr(tail, child_depth)); + actions.push(HirFingerprintAction::Statements(statements, 0, child_depth)); + } + ResolvedExprKind::If { + condition, + then_branch, + else_branch, + } => { + frame(hasher, b"if"); + actions.push(HirFingerprintAction::Expr(else_branch, child_depth)); + actions.push(HirFingerprintAction::Expr(then_branch, child_depth)); + actions.push(HirFingerprintAction::Expr(condition, child_depth)); + } + ResolvedExprKind::ConstructRecord { record, fields } => { + frame(hasher, b"construct-record"); + frame(hasher, record.as_str().as_bytes()); + actions.push(HirFingerprintAction::Fields(fields, 0, child_depth)); + } + ResolvedExprKind::ConstructVariant { + variant, + case, + fields, + } => { + frame(hasher, b"construct-variant"); + frame(hasher, variant.as_str().as_bytes()); + frame(hasher, case.as_str().as_bytes()); + actions.push(HirFingerprintAction::Fields(fields, 0, child_depth)); + } + ResolvedExprKind::Match { scrutinee, arms } => { + frame(hasher, b"match"); + actions.push(HirFingerprintAction::Arms(arms, 0, child_depth)); + actions.push(HirFingerprintAction::Expr(scrutinee, child_depth)); + } + ResolvedExprKind::Try { + operand, + result, + ok_case, + ok_field, + err_case, + err_field, + residual_type, + } => { + frame(hasher, b"try"); + let ids = [result, ok_case, ok_field, err_case, err_field]; + actions.push(HirFingerprintAction::Type(residual_type)); + actions.push(HirFingerprintAction::TryIds(ids, 0)); + actions.push(HirFingerprintAction::Expr(operand, child_depth)); + } + ResolvedExprKind::TryOption { + operand, + option, + some_case, + some_field, + none_case, + residual_type, + } => { + frame(hasher, b"try-option"); + let ids = [option, some_case, some_field, none_case]; + actions.push(HirFingerprintAction::Type(residual_type)); + actions.push(HirFingerprintAction::OptionIds(ids, 0)); + actions.push(HirFingerprintAction::Expr(operand, child_depth)); + } + ResolvedExprKind::UpdateRecord { + base, + record, + fields, + } => { + frame(hasher, b"update-record"); + actions.push(HirFingerprintAction::Fields(fields, 0, child_depth)); + actions.push(HirFingerprintAction::Bytes(record.as_str().as_bytes())); + actions.push(HirFingerprintAction::Expr(base, child_depth)); + } + ResolvedExprKind::Project { base, field } => { + frame(hasher, b"project"); + actions.push(HirFingerprintAction::Bytes(field.as_str().as_bytes())); + actions.push(HirFingerprintAction::Expr(base, child_depth)); + } + } + } + } + #[cfg(test)] + note_post_hir_facts_live( + _capacity_baseline, + actions + .capacity() + .saturating_mul(std::mem::size_of::>()), + ); + } + Ok(()) +} + +fn hash_binding( + hasher: &mut Sha256, + binding: &crate::hir::ResolvedBinding, + _capacity_baseline: usize, + _action_bytes: usize, +) -> Result<(), Diagnostic> { + frame(hasher, binding.id.as_str().as_bytes()); + frame(hasher, binding.name.as_bytes()); + let binding_identity = + fingerprint_type_identity(&binding.ty, _capacity_baseline, _action_bytes)?; + #[cfg(test)] + note_post_hir_facts_live( + _capacity_baseline, + _action_bytes.saturating_add(binding_identity.capacity()), + ); + frame(hasher, binding_identity.as_bytes()); + frame( + hasher, + match binding.ownership { + OwnershipMode::Value => b"value", + OwnershipMode::Own => b"own", + OwnershipMode::Borrow => b"borrow", + OwnershipMode::Shared => b"shared", + }, + ); + Ok(()) +} + +/// Pure private phase-A preparation. It performs no filesystem, process, or +/// network operation. +pub(crate) fn prepare_native_rust_interop( + program: &Program, + spec_bytes: &[u8], +) -> Result> { + let result = crate::bounded_output::with_limit(MAX_BUILDER_BYTES, || { + prepare_native_rust_interop_bounded(program, spec_bytes) + }); + if result.1 { + return Err(vec![b109("max_builder_bytes", MAX_BUILDER_BYTES)]); + } + result.0.map_err(|error| vec![error]) +} + +#[cfg(test)] +fn prepare_native_rust_interop_with_test_limit( + program: &Program, + spec_bytes: &[u8], + limit: usize, +) -> Result> { + assert!(limit <= MAX_BUILDER_BYTES); + let (result, overflowed) = crate::bounded_output::with_limit(limit, || { + prepare_native_rust_interop_bounded(program, spec_bytes) + }); + if overflowed { + return Err(vec![b109("max_builder_bytes", MAX_BUILDER_BYTES)]); + } + result.map_err(|error| vec![error]) +} + +fn prepare_native_rust_interop_bounded( + program: &Program, + spec_bytes: &[u8], +) -> Result { + validate_native_rust_source_expression_budget(program)?; + debit(spec_bytes.len())?; + let canonical_source = canonical_source_bounded(program)?; + let (spec, spec_authority) = + parse_spec_with_source_authority(program, spec_bytes, &canonical_source)?; + #[cfg(test)] + let spec_transfer_allocations = ( + spec.source_revision.as_ptr(), + spec.target.triple.as_ptr(), + spec.target.endian.as_ptr(), + spec.target.panic_strategy.as_ptr(), + spec.target.thread_policy.as_ptr(), + ); + identifier_audit(program, &spec)?; + let canonical_spec_budget = reserve_temporary_exact(MAX_SPEC_BYTES)?; + let canonical_spec = render_spec(&spec); + canonical_spec_budget.retain(canonical_spec.capacity())?; + let mut hir_scan_stack = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let hir_capacity = + hir_pre_resolve_capacity(program, canonical_source.len(), &mut hir_scan_stack)?; + let mut hir_budget = reserve_temporary_exact( + hir_capacity + .complete() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + )?; + let dispose_frames = Vec::with_capacity(hir_capacity.disposal_frames); + note_hir_resolve_pass(); + let resolved_owner = ResolvedProgramOwner::new( + hir::resolve(program).map_err(|_| b107("selected identity missing"))?, + dispose_frames, + hir_capacity.disposal_frames, + ); + let resolved = resolved_owner.program(); + let (closure, reached_imports) = selected_closure(resolved, &spec.exports)?; + validate_native_rust_expression_budget_for_closure(&closure, true)?; + validate_selected_scalar_closure(&closure)?; + validate_native_unit_discard_bindings(&closure)?; + #[cfg(test)] + inject_prepare_failure(PrepareFailurePoint::Closure)?; + // Keep the complete reservation through the post-resolution closure and + // validation phases: their maps, DFS stacks, and pending vectors are part + // of `scratch_upper`. Only after every such phase settles may the shared + // sequential scratch be released while the conservative retained HIR and + // selected-function clone ceiling remain authorized. + // `DeclarationIndex` is intentionally opaque across the crate boundary. + // Its maps contain only declaration identities/type facts derived from + // canonical source; charge a separate source-derived upper while every + // public ResolvedProgram field and selected clone is exact-censused. + let declaration_index_upper = hir_capacity.declaration_index_upper; + let actual_hir_retained = hir_owned_capacity(resolved)? + .checked_add(declaration_index_upper) + .and_then(|bytes| { + bytes.checked_add( + hir_capacity + .disposal_frames + .checked_mul(std::mem::size_of::())?, + ) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if actual_hir_retained > hir_capacity.retained_upper { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + hir_budget.shrink_held(actual_hir_retained)?; + let facts_capacity = post_hir_facts_capacity( + canonical_source.len(), + canonical_spec.len(), + resolved, + &closure, + &spec, + )?; + let spec_transfer_capacity = prepared_spec_transfer_capacity(&spec) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + // The source revision and target strings are still owned by `spec` and + // therefore already covered by `spec_authority`. Reserve only the new + // facts topology here; the existing authority is narrowed and retained + // when those exact allocations move into Prepared below. + let facts_complete_without_spec_transfer = facts_capacity + .complete() + .and_then(|complete| complete.checked_sub(spec_transfer_capacity)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let facts_budget = reserve_temporary_exact(facts_complete_without_spec_transfer)?; + #[cfg(test)] + note_post_hir_facts_entry(); + if reached_imports != spec.imports.iter().cloned().collect() { + return Err(b107("unselected import reached")); + } + let mut selected_effects = BTreeSet::new(); + for function in &closure { + identifier_gate(function.id.as_str())?; + identifier_gate(&function.name)?; + if function.effects.len() > MAX_EFFECTS { + return Err(b109("max_effects", MAX_EFFECTS)); + } + for effect in &function.effects { + identifier_gate(effect)?; + selected_effects.insert(effect.as_str()); + } + } + let source_functions = program + .functions + .iter() + .map(|function| (function.stable_id.as_str(), function)) + .collect::>(); + for id in &spec.exports { + let function = source_functions + .get(id.as_str()) + .ok_or_else(|| b107("selected identity missing"))?; + if !function.explicit_id + || function.name == "main" + || !function.type_parameters.is_empty() + || function.params.len() > MAX_PARAMETERS + || function.params.iter().any(|parameter| { + parameter.mode != ParamMode::Value || source_scalar_type(¶meter.ty).is_none() + }) + || source_scalar_type(&function.return_type).is_none() + { + return Err(b107(if !function.explicit_id { + "explicit persistent ID required" + } else { + "scalar value signature required" + })); + } + } + + let resolved_import_count = resolved + .interfaces + .iter() + .try_fold(0usize, |count, interface| { + count.checked_add(interface.imports.len()) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let mut resolved_imports = Vec::with_capacity(resolved_import_count); + resolved_imports.extend(resolved.interfaces.iter().flat_map(|interface| { + interface + .imports + .iter() + .map(move |import| (interface.id.as_str(), import)) + })); + #[cfg(test)] + note_post_hir_facts_capacity(post_hir_selection_scratch_capacity( + &selected_effects, + &source_functions, + &resolved_imports, + )); + #[cfg(test)] + note_post_hir_facts_scratch(post_hir_selection_scratch_capacity( + &selected_effects, + &source_functions, + &resolved_imports, + )); + let mut import_facts = Vec::with_capacity(spec.imports.len()); + for id in &spec.imports { + let (interface, import) = resolved_imports + .iter() + .find(|(_, import)| import.id.as_str() == id) + .copied() + .ok_or_else(|| b107("selected identity missing"))?; + if !import.native_rust { + return Err(b107("selected identity missing")); + } + identifier_gate(interface)?; + identifier_gate(import.id.as_str())?; + identifier_gate(&import.name)?; + if import.effects.len() > MAX_EFFECTS { + return Err(b109("max_effects", MAX_EFFECTS)); + } + for effect in &import.effects { + identifier_gate(effect)?; + selected_effects.insert(effect.as_str()); + } + let parameters = import_parameter_facts(import)?; + let result = match import.result.kind { + ResolvedImportResultKind::Unit => ScalarType::Unit, + ResolvedImportResultKind::I64 => ScalarType::I64, + ResolvedImportResultKind::Bool => ScalarType::Bool, + }; + let failure = match &import.failure { + ResolvedImportFailure::Infallible => None, + ResolvedImportFailure::Status { domain_id, .. } => Some(domain_id.clone()), + }; + let hash = full_hash(id); + let effect_set = import.effects.iter().cloned().collect::>(); + #[cfg(test)] + let import_effect_baseline = post_hir_facts_owned_capacity(&Vec::new(), &import_facts); + #[cfg(test)] + let import_effect_outer_scratch = post_hir_selection_scratch_capacity( + &selected_effects, + &source_functions, + &resolved_imports, + ); + #[cfg(test)] + let import_effect_locals = + parameter_facts_owned_capacity(¶meters, parameters.capacity()) + .saturating_add(failure.as_ref().map_or(0, String::capacity)) + .saturating_add(hash.capacity()); + #[cfg(test)] + note_post_hir_facts_live( + import_effect_baseline, + import_effect_outer_scratch + .saturating_add(import_effect_locals) + .saturating_add( + checked_owned_string_set(&effect_set) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ), + ); + let mut effects = Vec::with_capacity(effect_set.len()); + let mut remaining_effects = effect_set; + while let Some(effect) = remaining_effects.pop_first() { + effects.push(effect); + #[cfg(test)] + note_post_hir_facts_live( + import_effect_baseline, + import_effect_outer_scratch + .saturating_add(import_effect_locals) + .saturating_add( + checked_owned_string_set(&remaining_effects) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .saturating_add( + checked_owned_string_vec(&effects, effects.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ), + ); + } + import_facts.push(ImportFact { + id: id.clone(), + interface: interface.to_owned(), + import_key: import.import_key.clone(), + rust_method: format!("import_{hash}"), + c_field: format!("spxnr1_i_{hash}"), + parameters, + result, + effects: effects.clone(), + capabilities: effects, + failure, + call_contract_digest: String::new(), + }); + #[cfg(test)] + note_post_hir_facts_live( + post_hir_facts_owned_capacity(&Vec::new(), &import_facts), + post_hir_selection_scratch_capacity( + &selected_effects, + &source_functions, + &resolved_imports, + ) + .saturating_add(hash.capacity()), + ); + } + import_facts.sort_by(|left, right| left.id.cmp(&right.id)); + if selected_effects.len() > MAX_EFFECTS { + return Err(b109("max_effects", MAX_EFFECTS)); + } + let selected_capability_set = import_facts + .iter() + .flat_map(|import| import.capabilities.iter().cloned()) + .collect::>(); + #[cfg(test)] + let selected_capability_baseline = post_hir_facts_owned_capacity(&Vec::new(), &import_facts) + .saturating_add(post_hir_selection_scratch_capacity( + &selected_effects, + &source_functions, + &resolved_imports, + )); + #[cfg(test)] + let selected_capability_set_owned = checked_owned_string_set(&selected_capability_set) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + #[cfg(test)] + note_post_hir_facts_live(selected_capability_baseline, selected_capability_set_owned); + let mut selected_capabilities = Vec::with_capacity(selected_capability_set.len()); + for capability in selected_capability_set { + selected_capabilities.push(capability); + #[cfg(test)] + note_post_hir_facts_live( + selected_capability_baseline, + selected_capability_set_owned.saturating_add( + checked_owned_string_vec(&selected_capabilities, selected_capabilities.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ), + ); + } + if selected_capabilities != spec.capabilities { + return Err(b107("effect or capability mismatch")); + } + + let status_domain_set = import_facts + .iter() + .filter_map(|import| import.failure.clone()) + .collect::>(); + #[cfg(test)] + let status_domain_set_owned = checked_owned_string_set(&status_domain_set) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + #[cfg(test)] + let status_phase_outer_scratch = post_hir_selection_scratch_capacity( + &selected_effects, + &source_functions, + &resolved_imports, + ) + .checked_add( + checked_owned_string_vec(&selected_capabilities, selected_capabilities.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + #[cfg(test)] + let status_domain_conversion_baseline = + post_hir_facts_owned_capacity(&Vec::new(), &import_facts) + .checked_add(status_phase_outer_scratch) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + #[cfg(test)] + note_post_hir_facts_capacity( + status_domain_conversion_baseline + .checked_add(status_domain_set_owned) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ); + #[cfg(test)] + note_post_hir_facts_scratch( + status_phase_outer_scratch + .checked_add(status_domain_set_owned) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ); + let mut status_domains = Vec::with_capacity(status_domain_set.len()); + for domain in status_domain_set { + status_domains.push(domain); + #[cfg(test)] + { + let status_domain_conversion_scratch = status_domain_set_owned + .checked_add( + checked_owned_string_vec(&status_domains, status_domains.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + note_post_hir_facts_scratch( + status_phase_outer_scratch + .checked_add(status_domain_conversion_scratch) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ); + note_post_hir_facts_capacity( + status_domain_conversion_baseline + .checked_add(status_domain_conversion_scratch) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ); + } + } + if status_domains + .len() + .checked_add(4) + .is_none_or(|count| count > MAX_STATUS_DOMAINS) + { + return Err(b109("max_status_domains", MAX_STATUS_DOMAINS)); + } + let ordinals = status_domains + .iter() + .enumerate() + .map(|(index, domain)| { + ( + domain.as_str(), + u16::try_from(index + 1).unwrap_or(u16::MAX), + ) + }) + .collect::>(); + #[cfg(test)] + note_post_hir_facts_capacity( + post_hir_facts_owned_capacity(&Vec::new(), &import_facts) + + post_hir_selection_scratch_capacity( + &selected_effects, + &source_functions, + &resolved_imports, + ) + + checked_owned_string_vec(&selected_capabilities, selected_capabilities.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))? + + checked_owned_string_vec(&status_domains, status_domains.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))? + + borrowed_map_owned_capacity::<&str, u16>(ordinals.len()), + ); + #[allow(clippy::needless_range_loop)] + for index in 0..import_facts.len() { + #[cfg(test)] + let import_digest_baseline = post_hir_facts_owned_capacity(&Vec::new(), &import_facts) + + post_hir_selection_scratch_capacity( + &selected_effects, + &source_functions, + &resolved_imports, + ) + + checked_owned_string_vec(&selected_capabilities, selected_capabilities.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))? + + checked_owned_string_vec(&status_domains, status_domains.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))? + + borrowed_map_owned_capacity::<&str, u16>(ordinals.len()) + + owned_string_set_owned_capacity(&reached_imports); + #[cfg(not(test))] + let import_digest_baseline = 0; + let import = &mut import_facts[index]; + let failure = import.failure.as_ref().map_or_else( + || "infallible".to_owned(), + |domain| { + format!( + "{}:{domain}", + ordinals.get(domain.as_str()).copied().unwrap_or(u16::MAX) + ) + }, + ); + import.call_contract_digest = call_digest( + "import", + &import.id, + &import.parameters, + import.result, + &import.effects, + &import.capabilities, + &[], + &[], + &failure, + import_digest_baseline + failure.capacity(), + &spec.target, + )?; + } + + let by_function = closure + .iter() + .map(|function| (function.id.as_str(), *function)) + .collect::>(); + for function in &closure { + #[cfg(test)] + let traversal_baseline = post_hir_live_facts_capacity( + &Vec::new(), + &import_facts, + &selected_effects, + &source_functions, + &resolved_imports, + &selected_capabilities, + &status_domains, + &ordinals, + &by_function, + ); + #[cfg(not(test))] + let traversal_baseline = 0; + let reachable = transitive_imports( + function, + &by_function, + facts_capacity.traversal_pending_capacity, + traversal_baseline, + )?; + let reachable_effects = reachable + .iter() + .filter_map(|id| import_facts.iter().find(|import| import.id == id.as_str())) + .flat_map(|import| import.effects.iter().cloned()) + .collect::>(); + let declared = function.effects.iter().cloned().collect::>(); + #[cfg(test)] + note_post_hir_facts_live( + traversal_baseline, + owned_string_set_owned_capacity(&reachable) + .saturating_add(owned_string_set_owned_capacity(&reachable_effects)) + .saturating_add(owned_string_set_owned_capacity(&declared)), + ); + if declared != reachable_effects { + return Err(b107("effect or capability mismatch")); + } + } + let mut export_facts = Vec::with_capacity(spec.exports.len()); + for id in &spec.exports { + let function = by_function + .get(id.as_str()) + .ok_or_else(|| b107("selected identity missing"))?; + let parameters = parameter_facts(function)?; + let result = scalar_type(&function.return_type) + .filter(|ty| *ty != ScalarType::Unit) + .ok_or_else(|| b107("scalar value signature required"))?; + #[cfg(test)] + let traversal_baseline = post_hir_live_facts_capacity( + &export_facts, + &import_facts, + &selected_effects, + &source_functions, + &resolved_imports, + &selected_capabilities, + &status_domains, + &ordinals, + &by_function, + ); + #[cfg(not(test))] + let traversal_baseline = 0; + let reachable_imports = transitive_imports( + function, + &by_function, + facts_capacity.traversal_pending_capacity, + traversal_baseline, + )?; + let capabilities = spec.capabilities.clone(); + let mut required_imports = Vec::with_capacity(import_facts.len()); + required_imports.extend(import_facts.iter().map(|import| import.id.clone())); + let mut required_import_contracts = Vec::with_capacity(import_facts.len()); + required_import_contracts.extend( + import_facts + .iter() + .map(|import| (import.id.clone(), import.call_contract_digest.clone())), + ); + #[cfg(test)] + let export_prefix_baseline = traversal_baseline + .saturating_add(parameter_facts_owned_capacity( + ¶meters, + parameters.capacity(), + )) + .saturating_add(owned_string_set_owned_capacity(&reachable_imports)) + .saturating_add( + checked_owned_string_vec(&capabilities, capabilities.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .saturating_add( + checked_owned_string_vec(&required_imports, required_imports.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .saturating_add( + checked_owned_string_pairs(&required_import_contracts) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ); + let effect_set = function.effects.iter().cloned().collect::>(); + #[cfg(test)] + let effect_set_owned = checked_owned_string_set(&effect_set) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + #[cfg(test)] + note_post_hir_facts_live(export_prefix_baseline, effect_set_owned); + let mut effects = Vec::with_capacity(effect_set.len()); + for effect in effect_set { + effects.push(effect); + #[cfg(test)] + note_post_hir_facts_live( + export_prefix_baseline, + effect_set_owned.saturating_add( + checked_owned_string_vec(&effects, effects.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ), + ); + } + let status_domain_ordinal_set = reachable_imports + .iter() + .filter_map(|id| import_facts.iter().find(|import| import.id == id.as_str())) + .filter_map(|import| import.failure.as_deref()) + .filter_map(|domain| ordinals.get(domain).copied()) + .collect::>(); + #[cfg(test)] + let status_domain_ordinal_set_owned = + btree_allocation_upper::(status_domain_ordinal_set.len()); + #[cfg(test)] + let status_ordinal_baseline = export_prefix_baseline.saturating_add( + checked_owned_string_vec(&effects, effects.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ); + #[cfg(test)] + note_post_hir_facts_live(status_ordinal_baseline, status_domain_ordinal_set_owned); + let mut status_domain_ordinals = Vec::with_capacity( + status_domain_ordinal_set + .len() + .checked_add(3) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ); + for ordinal in status_domain_ordinal_set { + status_domain_ordinals.push(ordinal); + #[cfg(test)] + note_post_hir_facts_live( + status_ordinal_baseline, + status_domain_ordinal_set_owned.saturating_add( + checked_u16_vec(&status_domain_ordinals) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ), + ); + } + status_domain_ordinals.extend([65_533, 65_534, 65_535]); + status_domain_ordinals.sort_unstable(); + let status_contract_values = status_domain_ordinals + .iter() + .map(|ordinal| match *ordinal { + 65_533 => Ok::<_, Diagnostic>("65533:semaprax.native-rust-semantics.v1".to_owned()), + 65_534 => Ok("65534:semaprax.native-rust-host.v1".to_owned()), + 65_535 => Ok("65535:semaprax.native-rust-adapter.v1".to_owned()), + _ => { + let domain = status_domains + .get(usize::from(*ordinal).saturating_sub(1)) + .ok_or_else(b111)?; + Ok(format!("{ordinal}:{domain}")) + } + }) + .collect::, _>>()?; + #[cfg(test)] + let status_contract_baseline = status_ordinal_baseline.saturating_add( + checked_u16_vec(&status_domain_ordinals) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ); + #[cfg(test)] + note_post_hir_facts_live( + status_contract_baseline, + checked_owned_string_vec(&status_contract_values, status_contract_values.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ); + let status_contract = status_contract_values.join(";"); + #[cfg(test)] + note_post_hir_facts_live( + status_contract_baseline, + checked_owned_string_vec(&status_contract_values, status_contract_values.capacity()) + .and_then(|bytes| bytes.checked_add(status_contract.capacity())) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ); + let hash = full_hash(id); + #[cfg(test)] + let export_digest_baseline = status_contract_baseline + .saturating_add( + checked_owned_string_vec( + &status_contract_values, + status_contract_values.capacity(), + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .saturating_add(status_contract.capacity()) + .saturating_add(hash.capacity()); + #[cfg(not(test))] + let export_digest_baseline = 0; + let call_contract_digest = call_digest( + "export", + id, + ¶meters, + result, + &effects, + &capabilities, + &required_imports, + &required_import_contracts, + &status_contract, + export_digest_baseline, + &spec.target, + )?; + export_facts.push(ExportFact { + id: id.clone(), + rust_method: format!("export_{hash}"), + c_symbol: format!("spxnr1_e_{hash}"), + parameters: parameters.clone(), + result, + effects: effects.clone(), + capabilities: capabilities.clone(), + required_imports: required_imports.clone(), + status_domain_ordinals, + call_contract_digest, + }); + #[cfg(test)] + { + let export_clone_overlap_scratch = + parameter_facts_owned_capacity(¶meters, parameters.capacity()) + .saturating_add(owned_string_set_owned_capacity(&reachable_imports)) + .saturating_add( + checked_owned_string_vec(&capabilities, capabilities.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .saturating_add( + checked_owned_string_vec(&required_imports, required_imports.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .saturating_add( + checked_owned_string_pairs(&required_import_contracts) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .saturating_add( + checked_owned_string_vec(&effects, effects.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .saturating_add( + checked_owned_string_vec( + &status_contract_values, + status_contract_values.capacity(), + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .saturating_add(status_contract.capacity()) + .saturating_add(hash.capacity()); + note_post_hir_facts_live( + post_hir_live_facts_capacity( + &export_facts, + &import_facts, + &selected_effects, + &source_functions, + &resolved_imports, + &selected_capabilities, + &status_domains, + &ordinals, + &by_function, + ), + export_clone_overlap_scratch, + ); + } + } + export_facts.sort_by(|left, right| left.id.cmp(&right.id)); + #[cfg(test)] + note_post_hir_facts_capacity(post_hir_facts_owned_capacity(&export_facts, &import_facts)); + drop(by_function); + drop(ordinals); + drop(selected_capabilities); + drop(resolved_imports); + drop(source_functions); + drop(selected_effects); + drop(reached_imports); + status_domains.shrink_to_fit(); + #[cfg(test)] + let fingerprint_baseline = post_hir_facts_owned_capacity(&export_facts, &import_facts) + + string_vec_owned_capacity(&status_domains, status_domains.capacity()); + #[cfg(not(test))] + let fingerprint_baseline = 0; + let hir_digest = hir_fingerprint(&closure, &import_facts, fingerprint_baseline)?; + #[cfg(test)] + inject_prepare_failure(PrepareFailurePoint::Facts)?; + let descriptor_budget = reserve_temporary_exact(MAX_DESCRIPTOR_BYTES)?; + let descriptor = render_descriptor( + &spec, + &hir_digest, + &status_domains, + &export_facts, + &import_facts, + )?; + descriptor_budget.retain(descriptor.capacity())?; + if descriptor.len() > MAX_DESCRIPTOR_BYTES { + return Err(b109("max_descriptor_bytes", MAX_DESCRIPTOR_BYTES)); + } + replay_descriptor( + &descriptor, + &spec, + &hir_digest, + &export_facts, + &import_facts, + )?; + let header_budget = reserve_temporary_exact(MAX_GENERATED_HEADER_BYTES)?; + let generated_header = generate_header(&export_facts, &import_facts)?; + header_budget.retain(generated_header.capacity())?; + let c_budget = reserve_temporary_exact(MAX_GENERATED_C_BYTES)?; + let generated_c = generate_c(&spec, &closure, &export_facts, &import_facts)?; + c_budget.retain(generated_c.capacity())?; + let rust_budget = reserve_temporary_exact(MAX_GENERATED_RUST_BYTES)?; + let (generated_rust, private_ffi_source) = + generate_rust_artifacts(&spec, &export_facts, &import_facts)?; + let rust_capacity = generated_rust + .capacity() + .checked_add(private_ffi_source.capacity()) + .ok_or_else(|| b109("max_generated_rust_bytes", MAX_GENERATED_RUST_BYTES))?; + rust_budget.retain(rust_capacity)?; + #[cfg(test)] + inject_prepare_failure(PrepareFailurePoint::Render)?; + for (field, bytes, maximum) in [ + ( + "max_generated_c_bytes", + generated_c.len(), + MAX_GENERATED_C_BYTES, + ), + ( + "max_generated_header_bytes", + generated_header.len(), + MAX_GENERATED_HEADER_BYTES, + ), + ] { + if bytes > maximum { + return Err(b109(field, maximum)); + } + } + replay_generated_exact( + &spec, + &closure, + &export_facts, + &import_facts, + &generated_header, + &generated_c, + &generated_rust, + &private_ffi_source, + )?; + #[cfg(test)] + inject_prepare_failure(PrepareFailurePoint::Replay)?; + drop(status_domains); + let mut closure_ids = Vec::with_capacity(closure.len()); + closure_ids.extend( + closure + .iter() + .map(|function| function.id.as_str().to_owned()), + ); + let spec_digest = domain_digest(SPEC_DIGEST_DOMAIN, canonical_spec.as_bytes()); + let descriptor_digest = domain_digest(DESCRIPTOR_DIGEST_DOMAIN, descriptor.as_bytes()); + let spec_authority_bytes = checked_spec_owned_capacity(&spec) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if spec_authority.maximum() != spec_authority_bytes { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + let closure_id_bytes = closure_ids + .iter() + .try_fold(0usize, |bytes, id| bytes.checked_add(id.capacity())) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let persistent_without_spec_transfer = + post_hir_facts_owned_capacity_checked(&export_facts, &import_facts) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))? + .checked_add( + closure_ids + .capacity() + .checked_mul(std::mem::size_of::()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .and_then(|bytes| bytes.checked_add(closure_id_bytes)) + .and_then(|bytes| bytes.checked_add(hir_digest.capacity())) + .and_then(|bytes| bytes.checked_add(spec_digest.capacity())) + .and_then(|bytes| bytes.checked_add(descriptor_digest.capacity())) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let persistent_facts = persistent_without_spec_transfer + .checked_add(spec_transfer_capacity) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + #[cfg(test)] + POST_HIR_AUTHORITY_TRANSFER_TERMS.with(|terms| { + terms.set([ + facts_capacity.complete().expect("checked facts capacity"), + spec_transfer_capacity, + facts_complete_without_spec_transfer, + persistent_without_spec_transfer, + persistent_facts, + ]); + }); + if persistent_facts > facts_capacity.retained_upper { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + if spec_transfer_capacity > spec_authority_bytes { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + let retained_without_spec_transfer_upper = facts_capacity + .retained_upper + .checked_sub(spec_transfer_capacity) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if persistent_without_spec_transfer > retained_without_spec_transfer_upper { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + #[cfg(test)] + let ledger_before_transfer = crate::bounded_output::remaining_active() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + #[cfg(test)] + let facts_reserved_before_transfer = facts_budget.maximum(); + let Spec { + module, + source_revision, + target, + exports: spec_exports, + imports: spec_imports, + capabilities: spec_capabilities, + } = spec; + #[cfg(test)] + assert_eq!( + spec_transfer_allocations, + ( + source_revision.as_ptr(), + target.triple.as_ptr(), + target.endian.as_ptr(), + target.panic_strategy.as_ptr(), + target.thread_policy.as_ptr(), + ), + "Spec source/target allocations must move into Prepared without clones", + ); + let moved_transfer_capacity = source_revision + .capacity() + .checked_add(target.triple.capacity()) + .and_then(|bytes| bytes.checked_add(target.endian.capacity())) + .and_then(|bytes| bytes.checked_add(target.panic_strategy.capacity())) + .and_then(|bytes| bytes.checked_add(target.thread_policy.capacity())) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if moved_transfer_capacity != spec_transfer_capacity { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + // Destroy every non-transferred Spec allocation before narrowing its + // authority; the five moved allocations remain continuously covered. + drop((module, spec_exports, spec_imports, spec_capabilities)); + #[cfg(test)] + let expected_remaining_after_transfer = ledger_before_transfer + .checked_add( + spec_authority_bytes + .checked_sub(spec_transfer_capacity) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .and_then(|bytes| { + bytes.checked_add( + facts_reserved_before_transfer.checked_sub(persistent_without_spec_transfer)?, + ) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + spec_authority.retain(spec_transfer_capacity)?; + facts_budget.retain(persistent_without_spec_transfer)?; + #[cfg(test)] + assert_eq!( + crate::bounded_output::remaining_active(), + Some(expected_remaining_after_transfer), + "Spec authority must release exactly once before Prepared facts retain", + ); + drop(closure); + let _ = resolved; + drop(resolved_owner); + drop(hir_budget); + Ok(PreparedNativeRustInterop { + spec_digest, + canonical_spec, + descriptor_digest, + descriptor, + source_revision, + hir_digest, + target, + exports: export_facts, + imports: import_facts, + closure: closure_ids, + generated_c, + generated_header, + generated_rust, + private_ffi_source, + }) +} + +fn validate_selected_scalar_closure(functions: &[&ResolvedFunction]) -> Result<(), Diagnostic> { + note_hir_post_resolve_phase(2); + let mut pending = Vec::new(); + for function in functions { + if function.params.len() > MAX_PARAMETERS + || function.params.iter().any(|parameter| { + parameter.ownership != hir::OwnershipMode::Value + || scalar_type(¶meter.ty).is_none() + }) + || scalar_type(&function.return_type).is_none() + || !function.cleanup.slots.is_empty() + || !function.cleanup.flags.is_empty() + || !function.cleanup_plan.slots.is_empty() + { + return Err(b107("scalar value signature required")); + } + pending.extend(function.requires.iter()); + pending.push(&function.body); + pending.extend(function.ensures.iter()); + } + while let Some(expression) = pending.pop() { + note_hir_post_resolve_capacity( + 1, + pending.capacity() * std::mem::size_of::<&ResolvedExpr>(), + ); + let direct_unit_import = expression.ty == ResolvedType::Unit + && matches!(expression.kind, ResolvedExprKind::NativeRustImportCall(_)); + if scalar_type(&expression.ty).is_none() && !direct_unit_import { + return Err(b107("scalar value signature required")); + } + match &expression.kind { + ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) => {} + ResolvedExprKind::Place(place) + if place.projections.is_empty() + && expression.ownership == hir::OwnershipMode::Value => {} + ResolvedExprKind::Call { + type_arguments, + instance, + args, + .. + } if type_arguments.is_empty() && instance.is_none() => pending.extend(args), + ResolvedExprKind::NativeRustImportCall(call) => pending.extend(&call.args), + ResolvedExprKind::Unary { value, .. } => pending.push(value), + ResolvedExprKind::Binary { left, right, .. } => { + pending.push(left); + pending.push(right); + } + ResolvedExprKind::Block { statements, tail } => { + for statement in statements { + let ResolvedStatement::Let { binding, value, .. } = statement; + let unit_discard = binding.ty == ResolvedType::Unit + && value.ty == ResolvedType::Unit + && matches!(value.kind, ResolvedExprKind::NativeRustImportCall(_)); + if binding.ownership != hir::OwnershipMode::Value + || (scalar_type(&binding.ty).is_none() && !unit_discard) + { + return Err(b107("scalar value signature required")); + } + pending.push(value); + } + pending.push(tail); + } + ResolvedExprKind::If { + condition, + then_branch, + else_branch, + } => { + pending.push(condition); + pending.push(then_branch); + pending.push(else_branch); + } + ResolvedExprKind::ConstructRecord { .. } + | ResolvedExprKind::Call { .. } + | ResolvedExprKind::ConstructVariant { .. } + | ResolvedExprKind::Match { .. } + | ResolvedExprKind::Try { .. } + | ResolvedExprKind::TryOption { .. } + | ResolvedExprKind::UpdateRecord { .. } + | ResolvedExprKind::Project { .. } + | ResolvedExprKind::Place(_) => { + return Err(b107("scalar value signature required")); + } + } + } + Ok(()) +} + +fn validate_native_unit_discard_bindings( + functions: &[&ResolvedFunction], +) -> Result<(), Diagnostic> { + note_hir_post_resolve_phase(3); + for function in functions { + let mut discarded = BTreeSet::::new(); + let mut pending = vec![(&function.body, false)]; + while let Some((expression, direct_let_rhs)) = pending.pop() { + note_hir_post_resolve_capacity( + 2, + pending.capacity() * std::mem::size_of::<(&ResolvedExpr, bool)>() + + discarded.len() + * (std::mem::size_of::() + + std::mem::size_of::>()) + + discarded.iter().map(|id| id.as_str().len()).sum::(), + ); + if expression.ty == ResolvedType::Unit && !direct_let_rhs { + return Err(b107("scalar value signature required")); + } + match &expression.kind { + ResolvedExprKind::Block { statements, tail } => { + for statement in statements { + let ResolvedStatement::Let { binding, value, .. } = statement; + if value.ty == ResolvedType::Unit { + if !matches!(value.kind, ResolvedExprKind::NativeRustImportCall(_)) + || binding.ty != ResolvedType::Unit + || !discarded.insert(binding.id.clone()) + { + return Err(b107("scalar value signature required")); + } + pending.push((value, true)); + } else { + pending.push((value, false)); + } + } + pending.push((tail, false)); + } + ResolvedExprKind::Place(place) if discarded.contains(&place.root) => { + return Err(b107("scalar value signature required")); + } + ResolvedExprKind::Call { args, .. } => { + pending.extend(args.iter().map(|child| (child, false))); + } + ResolvedExprKind::NativeRustImportCall(call) => { + pending.extend(call.args.iter().map(|child| (child, false))); + } + ResolvedExprKind::Unary { value, .. } + | ResolvedExprKind::Try { operand: value, .. } + | ResolvedExprKind::TryOption { operand: value, .. } + | ResolvedExprKind::Project { base: value, .. } => pending.push((value, false)), + ResolvedExprKind::Binary { left, right, .. } => { + pending.push((left, false)); + pending.push((right, false)); + } + ResolvedExprKind::If { + condition, + then_branch, + else_branch, + } => { + pending.push((condition, false)); + pending.push((then_branch, false)); + pending.push((else_branch, false)); + } + ResolvedExprKind::ConstructRecord { fields, .. } + | ResolvedExprKind::ConstructVariant { fields, .. } => { + pending.extend(fields.iter().map(|field| (&field.value, false))); + } + ResolvedExprKind::Match { scrutinee, arms } => { + pending.push((scrutinee, false)); + pending.extend(arms.iter().map(|arm| (&arm.value, false))); + } + ResolvedExprKind::UpdateRecord { base, fields, .. } => { + pending.push((base, false)); + pending.extend(fields.iter().map(|field| (&field.value, false))); + } + _ => {} + } + } + } + Ok(()) +} + +fn validate_native_rust_source_expression_budget(program: &Program) -> Result<(), Diagnostic> { + let mut stack = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + for function in &program.functions { + for root in function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures) + { + let mut stack_len = 1; + stack[0] = Some((root, 1_usize, 0_usize)); + while stack_len != 0 { + stack_len -= 1; + let (expression, depth, next_child) = stack[stack_len] + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if next_child == 0 { + debit(std::mem::size_of::<&crate::ast::Expr>())?; + if depth > MAX_SEMANTIC_EXPRESSION_DEPTH { + return Err(b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + )); + } + } + if let Some(child) = ast_child(expression, next_child) { + if stack_len + 2 > stack.len() { + return Err(b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + )); + } + stack[stack_len] = Some((expression, depth, next_child + 1)); + stack[stack_len + 1] = Some((child, depth + 1, 0)); + stack_len += 2; + } + } + } + } + Ok(()) +} + +#[derive(Clone, Copy, Default)] +struct AstCapacityStats { + nodes: usize, + cumulative_depth: usize, + generic_calls: usize, + max_depth: usize, + max_match_arms: usize, + max_indexed_children: usize, + depth_arm_product_sum: usize, + depth_width_product_sum: usize, + local_bindings: usize, + pattern_bindings: usize, + binding_name_bytes: usize, + binding_depth_sum: usize, + max_index_digits: usize, +} + +fn ast_child(expression: &crate::ast::Expr, index: usize) -> Option<&crate::ast::Expr> { + match &expression.kind { + crate::ast::ExprKind::Call { args, .. } => args.get(index), + crate::ast::ExprKind::Unary { value, .. } + | crate::ast::ExprKind::Try { operand: value } + | crate::ast::ExprKind::Project { base: value, .. } => (index == 0).then_some(value), + crate::ast::ExprKind::Binary { left, right, .. } => { + [left.as_ref(), right.as_ref()].get(index).copied() + } + crate::ast::ExprKind::Block { statements, tail } => statements + .get(index) + .map(|statement| { + let crate::ast::Statement::Let { value, .. } = statement; + value + }) + .or_else(|| (index == statements.len()).then_some(tail)), + crate::ast::ExprKind::If { + condition, + then_branch, + else_branch, + } => [ + condition.as_ref(), + then_branch.as_ref(), + else_branch.as_ref(), + ] + .get(index) + .copied(), + crate::ast::ExprKind::ConstructRecord { fields, .. } + | crate::ast::ExprKind::ConstructVariant { fields, .. } => { + fields.get(index).map(|field| &field.value) + } + crate::ast::ExprKind::Match { scrutinee, arms } => { + if index == 0 { + Some(scrutinee) + } else { + arms.get(index - 1).map(|arm| &arm.value) + } + } + crate::ast::ExprKind::UpdateRecord { base, fields } => { + if index == 0 { + Some(base) + } else { + fields.get(index - 1).map(|field| &field.value) + } + } + crate::ast::ExprKind::Int(_) + | crate::ast::ExprKind::Bool(_) + | crate::ast::ExprKind::Var(_) => None, + } +} + +fn ast_child_identity_path_increment( + expression: &crate::ast::Expr, + child_index: usize, + program: &Program, +) -> usize { + match &expression.kind { + crate::ast::ExprKind::Call { name, .. } => { + let prefix = if program + .interfaces + .iter() + .any(|interface| interface.imports.iter().any(|import| import.name == *name)) + { + ".native-rust-arg." + } else { + ".arg." + }; + prefix.len() + decimal_digits(child_index) + } + crate::ast::ExprKind::Unary { .. } => ".value".len(), + crate::ast::ExprKind::Binary { .. } => { + if child_index == 0 { ".left" } else { ".right" }.len() + } + crate::ast::ExprKind::Block { statements, .. } => { + if child_index < statements.len() { + ".s".len() + decimal_digits(child_index) + ".value".len() + } else { + ".tail".len() + } + } + crate::ast::ExprKind::If { .. } => [".condition", ".then", ".else"] + .get(child_index) + .map_or(0, |segment| segment.len()), + crate::ast::ExprKind::ConstructRecord { .. } + | crate::ast::ExprKind::ConstructVariant { .. } => { + ".field.".len() + decimal_digits(child_index) + ".value".len() + } + crate::ast::ExprKind::Match { .. } => { + if child_index == 0 { + ".scrutinee".len() + } else { + ".arm.".len() + decimal_digits(child_index - 1) + ".value".len() + } + } + crate::ast::ExprKind::UpdateRecord { .. } => { + if child_index == 0 { + ".base".len() + } else { + ".field.".len() + decimal_digits(child_index - 1) + ".value".len() + } + } + crate::ast::ExprKind::Try { .. } => ".operand".len(), + crate::ast::ExprKind::Project { .. } => ".base".len(), + crate::ast::ExprKind::Int(_) + | crate::ast::ExprKind::Bool(_) + | crate::ast::ExprKind::Var(_) => 0, + } +} + +fn ast_root_identity_path_len(function: &crate::ast::Function, root_index: usize) -> usize { + match root_index.cmp(&function.requires.len()) { + std::cmp::Ordering::Less => "requires.".len() + decimal_digits(root_index), + std::cmp::Ordering::Equal => "body".len(), + std::cmp::Ordering::Greater => { + "ensures.".len() + decimal_digits(root_index - function.requires.len() - 1) + } + } +} + +fn ast_type_identity_key_len(program: &Program, root: &crate::ast::Type) -> Option { + #[derive(Clone, Copy)] + enum Frame<'a> { + Enter(&'a crate::ast::Type), + Finish(&'a crate::ast::TypeDeclaration, usize), + } + + let mut frames = [None; MAX_FORMAT_NESTING * 2]; + let mut results = [0usize; MAX_FORMAT_NESTING]; + frames[0] = Some(Frame::Enter(root)); + let mut frame_len = 1usize; + let mut result_len = 0usize; + while frame_len != 0 { + frame_len -= 1; + match frames[frame_len].take()? { + Frame::Enter(crate::ast::Type::I64) => { + results[result_len] = "i64".len(); + result_len = result_len.checked_add(1)?; + } + Frame::Enter(crate::ast::Type::Bool) => { + results[result_len] = "bool".len(); + result_len = result_len.checked_add(1)?; + } + Frame::Enter(crate::ast::Type::Named { name, arguments }) => { + let declaration = program + .types + .iter() + .find(|declaration| declaration.name == *name)?; + if frame_len.checked_add(arguments.len())?.checked_add(1)? > frames.len() { + return None; + } + frames[frame_len] = Some(Frame::Finish(declaration, arguments.len())); + frame_len += 1; + for argument in arguments.iter().rev() { + frames[frame_len] = Some(Frame::Enter(argument)); + frame_len += 1; + } + } + Frame::Finish(declaration, argument_count) => { + let start = result_len.checked_sub(argument_count)?; + let encoded_arguments = + results[start..result_len] + .iter() + .try_fold(0usize, |bytes, key_len| { + bytes + .checked_add(decimal_digits(*key_len))? + .checked_add(1)? + .checked_add(*key_len) + })?; + result_len = start; + let declaration_len = declaration.stable_id.len(); + let key_len = "nominal:" + .len() + .checked_add(decimal_digits(declaration_len))? + .checked_add(1)? + .checked_add(declaration_len)? + .checked_add(1)? + .checked_add(decimal_digits(argument_count))? + .checked_add(1)? + .checked_add(encoded_arguments)?; + results[result_len] = key_len; + result_len = result_len.checked_add(1)?; + } + } + } + (result_len == 1).then_some(results[0]) +} + +fn function_instance_identity_len( + program: &Program, + function: &crate::ast::Function, + type_arguments: &[crate::ast::Type], +) -> Option { + if type_arguments.len() != function.type_parameters.len() { + return None; + } + let encoded_arguments = type_arguments.iter().try_fold(0usize, |bytes, ty| { + let key_len = ast_type_identity_key_len(program, ty)?; + bytes + .checked_add(decimal_digits(key_len))? + .checked_add(1)? + .checked_add(key_len) + })?; + "semaprax.function-instance.v1:" + .len() + .checked_add(decimal_digits(function.stable_id.len()))? + .checked_add(1)? + .checked_add(function.stable_id.len())? + .checked_add(1)? + .checked_add(decimal_digits(type_arguments.len()))? + .checked_add(1)? + .checked_add(encoded_arguments) +} + +fn generic_function_instance_identity_upper( + program: &Program, + function: &crate::ast::Function, +) -> Option { + if function.type_parameters.is_empty() { + return Some(0); + } + let mut maximum = 0usize; + let mut traversal = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + for caller in program + .functions + .iter() + .filter(|caller| caller.type_parameters.is_empty()) + { + for root in caller + .requires + .iter() + .chain(std::iter::once(&caller.body)) + .chain(&caller.ensures) + { + let mut len = 1usize; + traversal[0] = Some((root, 0usize, 0usize)); + while len != 0 { + len -= 1; + let (expression, next_child, _) = traversal[len].take()?; + if next_child == 0 { + if let crate::ast::ExprKind::Call { + name, + type_arguments, + .. + } = &expression.kind + { + if *name == function.name { + if let Some(identity_len) = + function_instance_identity_len(program, function, type_arguments) + { + maximum = maximum.max(identity_len); + } + } + } + } + if let Some(child) = ast_child(expression, next_child) { + if len + 2 > traversal.len() { + return None; + } + traversal[len] = Some((expression, next_child + 1, 0)); + traversal[len + 1] = Some((child, 0, 0)); + len += 2; + } + } + } + } + Some(maximum) +} + +fn scoped_identity_upper( + function: &crate::ast::Function, + generic_instance_identity_len: usize, + kind_len: usize, + path_len: usize, +) -> Option { + let monomorphic = "declaration:" + .len() + .checked_add(decimal_digits(function.stable_id.len()))? + .checked_add(1)? + .checked_add(function.stable_id.len())? + .checked_add(1)? + .checked_add(kind_len)? + .checked_add(1)? + .checked_add(decimal_digits(path_len))? + .checked_add(1)? + .checked_add(path_len)?; + if function.type_parameters.is_empty() { + return Some(monomorphic); + } + if generic_instance_identity_len == 0 { + return Some(monomorphic); + } + let owner_len = "semaprax.function-execution.v1:generic:" + .len() + .checked_add(decimal_digits(generic_instance_identity_len))? + .checked_add(1)? + .checked_add(generic_instance_identity_len)?; + let generic = "function-execution:" + .len() + .checked_add(decimal_digits(owner_len))? + .checked_add(1)? + .checked_add(owner_len)? + .checked_add(1)? + .checked_add(kind_len)? + .checked_add(1)? + .checked_add(decimal_digits(path_len))? + .checked_add(1)? + .checked_add(path_len)?; + Some(monomorphic.max(generic)) +} + +fn scoped_value_identity_upper( + function: &crate::ast::Function, + generic_instance_identity_len: usize, + path_len: usize, +) -> Option { + scoped_identity_upper( + function, + generic_instance_identity_len, + "value:result".len().max("value:local".len()), + path_len, + ) +} + +fn scoped_expression_identity_upper( + function: &crate::ast::Function, + generic_instance_identity_len: usize, + path_len: usize, +) -> Option { + scoped_identity_upper( + function, + generic_instance_identity_len, + "expression".len(), + path_len, + ) +} + +fn scan_ast_capacity<'a>( + roots: impl IntoIterator, + program: &Program, + count_generic_calls: bool, + stack: &mut [Option<(&'a crate::ast::Expr, usize, usize)>; MAX_SEMANTIC_EXPRESSION_DEPTH + 1], +) -> Result { + let mut stats = AstCapacityStats::default(); + for root in roots { + let mut stack_len = 1; + stack[0] = Some((root, 1, 0)); + while stack_len != 0 { + stack_len -= 1; + let (expression, depth, next_child) = stack[stack_len] + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if next_child == 0 { + stats.nodes = stats + .nodes + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + stats.cumulative_depth = stats + .cumulative_depth + .checked_add(depth) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + stats.max_depth = stats.max_depth.max(depth); + let indexed_children = match &expression.kind { + crate::ast::ExprKind::Call { args, .. } => args.len(), + crate::ast::ExprKind::Block { statements, .. } => statements.len() + 1, + crate::ast::ExprKind::ConstructRecord { fields, .. } + | crate::ast::ExprKind::ConstructVariant { fields, .. } => fields.len(), + crate::ast::ExprKind::Match { arms, .. } => { + stats.max_match_arms = stats.max_match_arms.max(arms.len()); + arms.len() + 1 + } + crate::ast::ExprKind::UpdateRecord { fields, .. } => fields.len() + 1, + crate::ast::ExprKind::If { .. } => 3, + crate::ast::ExprKind::Binary { .. } => 2, + crate::ast::ExprKind::Unary { .. } + | crate::ast::ExprKind::Try { .. } + | crate::ast::ExprKind::Project { .. } => 1, + crate::ast::ExprKind::Int(_) + | crate::ast::ExprKind::Bool(_) + | crate::ast::ExprKind::Var(_) => 0, + }; + stats.max_indexed_children = stats.max_indexed_children.max(indexed_children); + stats.max_index_digits = stats + .max_index_digits + .max(decimal_digits(indexed_children.saturating_sub(1))); + if let crate::ast::ExprKind::Block { statements, .. } = &expression.kind { + stats.local_bindings = stats + .local_bindings + .checked_add(statements.len()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + stats.binding_name_bytes = statements + .iter() + .try_fold(stats.binding_name_bytes, |bytes, statement| { + let crate::ast::Statement::Let { name, .. } = statement; + bytes.checked_add(name.len()) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + stats.binding_depth_sum = stats + .binding_depth_sum + .checked_add( + depth + .checked_mul(statements.len()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + stats.depth_width_product_sum = stats + .depth_width_product_sum + .checked_add( + depth + .checked_mul(indexed_children) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if let crate::ast::ExprKind::Match { arms, .. } = &expression.kind { + for arm in arms { + let (bindings, names) = ast_pattern_binding_stats(&arm.pattern)?; + stats.pattern_bindings = stats + .pattern_bindings + .checked_add(bindings) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + stats.binding_name_bytes = stats + .binding_name_bytes + .checked_add(names) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + stats.binding_depth_sum = stats + .binding_depth_sum + .checked_add( + depth + .checked_mul(bindings) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + stats.max_index_digits = stats + .max_index_digits + .max(ast_pattern_index_digits(&arm.pattern)?); + } + stats.depth_arm_product_sum = stats + .depth_arm_product_sum + .checked_add( + depth + .checked_mul(arms.len()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + if let crate::ast::ExprKind::Call { + name, + type_arguments, + .. + } = &expression.kind + { + if count_generic_calls + && !type_arguments.is_empty() + && program.functions.iter().any(|function| { + !function.type_parameters.is_empty() && function.name == *name + }) + { + stats.generic_calls = stats + .generic_calls + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + } + } + if let Some(child) = ast_child(expression, next_child) { + if stack_len + 2 > stack.len() { + return Err(b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + )); + } + stack[stack_len] = Some((expression, depth, next_child + 1)); + stack[stack_len + 1] = Some((child, depth + 1, 0)); + stack_len += 2; + } + } + } + Ok(stats) +} + +fn ast_pattern_index_digits(pattern: &crate::ast::MatchPattern) -> Result { + let crate::ast::MatchPattern::Record { fields, .. } = pattern else { + return Ok(match pattern { + crate::ast::MatchPattern::Variant { fields, .. } => { + decimal_digits(fields.len().saturating_sub(1)) + } + _ => 1, + }); + }; + let mut pending: [Option<(&[crate::ast::RecordMatchPatternField], usize)>; MAX_FORMAT_NESTING] = + [None; MAX_FORMAT_NESTING]; + pending[0] = Some((fields, 0)); + let mut len = 1; + let mut digits = 1; + while len != 0 { + len -= 1; + let (fields, next) = pending[len] + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + digits = digits.max(decimal_digits(fields.len().saturating_sub(1))); + let Some(field) = fields.get(next) else { + continue; + }; + pending[len] = Some((fields, next + 1)); + len += 1; + if let crate::ast::RecordMatchFieldPattern::Record { fields, .. } = &field.pattern { + if len == pending.len() { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + pending[len] = Some((fields, 0)); + len += 1; + } + } + Ok(digits) +} + +fn decimal_digits(mut value: usize) -> usize { + let mut digits = 1; + while value >= 10 { + value /= 10; + digits += 1; + } + digits +} + +fn ast_pattern_binding_stats( + pattern: &crate::ast::MatchPattern, +) -> Result<(usize, usize), Diagnostic> { + match pattern { + crate::ast::MatchPattern::Wildcard { .. } => Ok((0, 0)), + crate::ast::MatchPattern::Variant { fields, .. } => Ok(( + fields.len(), + fields.iter().map(|field| field.binding.len()).sum(), + )), + crate::ast::MatchPattern::Record { fields, .. } => { + let mut pending: [Option<(&[crate::ast::RecordMatchPatternField], usize)>; + MAX_FORMAT_NESTING] = [None; MAX_FORMAT_NESTING]; + pending[0] = Some((fields, 0)); + let mut len = 1; + let mut count = 0usize; + let mut names = 0usize; + while len != 0 { + len -= 1; + let (fields, next) = pending[len] + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let Some(field) = fields.get(next) else { + continue; + }; + pending[len] = Some((fields, next + 1)); + len += 1; + match &field.pattern { + crate::ast::RecordMatchFieldPattern::Binding { .. } => { + count = count + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if let crate::ast::RecordMatchFieldPattern::Binding { name, .. } = + &field.pattern + { + names = names + .checked_add(name.len()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + } + crate::ast::RecordMatchFieldPattern::Record { fields, .. } => { + if len == pending.len() { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + pending[len] = Some((fields, 0)); + len += 1; + } + crate::ast::RecordMatchFieldPattern::Wildcard { .. } => {} + } + } + Ok((count, names)) + } + } +} + +fn declaration_field_type( + declaration: &crate::ast::TypeDeclaration, + mut index: usize, +) -> Option<&crate::ast::Type> { + match &declaration.kind { + crate::ast::TypeDeclarationKind::Resource { .. } => None, + crate::ast::TypeDeclarationKind::Record { fields } => { + fields.get(index).map(|field| &field.ty) + } + crate::ast::TypeDeclarationKind::Variant { cases } => { + for case in cases { + if index < case.fields.len() { + return Some(&case.fields[index].ty); + } + index -= case.fields.len(); + } + None + } + } +} + +fn declaration_field_identity_bytes( + declaration: &crate::ast::TypeDeclaration, + mut index: usize, +) -> Option { + match &declaration.kind { + crate::ast::TypeDeclarationKind::Resource { .. } => None, + crate::ast::TypeDeclarationKind::Record { fields } => { + fields.get(index).map(|field| field.stable_id.len()) + } + crate::ast::TypeDeclarationKind::Variant { cases } => { + for case in cases { + if index < case.fields.len() { + return case + .stable_id + .len() + .checked_add(case.fields[index].stable_id.len()); + } + index -= case.fields.len(); + } + None + } + } +} + +fn ast_resource_leaf_count( + root: &crate::ast::Type, + program: &Program, +) -> Result { + enum Frame<'a> { + Enter(&'a crate::ast::Type, usize), + Children(&'a crate::ast::TypeDeclaration, usize, usize, usize), + Add(&'a crate::ast::TypeDeclaration, usize, usize, usize), + } + let mut frames: [Option>; MAX_FORMAT_NESTING] = std::array::from_fn(|_| None); + let mut ancestors: [Option<&str>; MAX_FORMAT_NESTING] = [None; MAX_FORMAT_NESTING]; + let mut values = [0usize; MAX_FORMAT_NESTING]; + frames[0] = Some(Frame::Enter(root, 0)); + let (mut frame_len, mut value_len) = (1usize, 0usize); + while frame_len != 0 { + frame_len -= 1; + match frames[frame_len] + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))? + { + Frame::Enter(crate::ast::Type::I64 | crate::ast::Type::Bool, _) => { + values[value_len] = 0; + value_len += 1; + } + Frame::Enter(crate::ast::Type::Named { name, .. }, depth) => { + let Some(declaration) = program.types.iter().find(|value| value.name == *name) + else { + values[value_len] = 0; + value_len += 1; + continue; + }; + if ancestors[..depth].contains(&Some(name.as_str())) { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + ancestors[depth] = Some(name); + if matches!( + declaration.kind, + crate::ast::TypeDeclarationKind::Resource { .. } + ) { + values[value_len] = 1; + value_len += 1; + ancestors[depth] = None; + } else { + frames[frame_len] = Some(Frame::Children(declaration, 0, 0, depth)); + frame_len += 1; + } + } + Frame::Children(declaration, index, total, depth) => { + if let Some(child) = declaration_field_type(declaration, index) { + if frame_len + 2 > frames.len() || depth + 1 >= ancestors.len() { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + frames[frame_len] = Some(Frame::Add(declaration, index + 1, total, depth)); + frames[frame_len + 1] = Some(Frame::Enter(child, depth + 1)); + frame_len += 2; + } else { + ancestors[depth] = None; + values[value_len] = total; + value_len += 1; + } + } + Frame::Add(declaration, index, total, depth) => { + value_len = value_len + .checked_sub(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let total = total + .checked_add(values[value_len]) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if total > MAX_BUILDER_BYTES { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + frames[frame_len] = Some(Frame::Children(declaration, index, total, depth)); + frame_len += 1; + } + } + } + (value_len == 1) + .then_some(values[0]) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES)) +} + +fn maximum_resource_leaf_count(program: &Program) -> Result { + let mut maximum = 1usize; + for declaration in &program.types { + let leaves = match &declaration.kind { + crate::ast::TypeDeclarationKind::Resource { .. } => 1, + crate::ast::TypeDeclarationKind::Record { fields } => { + fields + .iter() + .try_fold(0usize, |total, field| -> Result { + total + .checked_add(ast_resource_leaf_count(&field.ty, program)?) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES)) + })? + } + crate::ast::TypeDeclarationKind::Variant { cases } => { + cases + .iter() + .try_fold(0usize, |total, case| -> Result { + case.fields.iter().try_fold( + total, + |total, field| -> Result { + total + .checked_add(ast_resource_leaf_count(&field.ty, program)?) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES)) + }, + ) + })? + } + }; + maximum = maximum.max(leaves); + } + Ok(maximum) +} + +#[derive(Clone, Copy, Debug)] +struct DeclarationDagExpansion { + maximum_resource_leaves: usize, + maximum_type_occurrences: usize, + maximum_shape_fields: usize, + maximum_projection_segments: usize, + maximum_shape_identity_bytes: usize, + maximum_lifecycle_identity_bytes: usize, + maximum_projection_identity_bytes: usize, + cleanup_retained: CleanupRetainedStats, +} + +#[derive(Clone, Copy, Default)] +struct CleanupTypeFacts { + leaves: usize, + occurrences: usize, + shape_fields: usize, + projection_segments: usize, + shape_ids: usize, + lifecycle_ids: usize, + projection_ids: usize, +} + +#[derive(Clone, Copy, Debug, Default)] +struct CleanupRetainedStats { + roots: usize, + occurrences: usize, + shape_fields: usize, + leaves: usize, + projection_segments: usize, + shape_ids: usize, + lifecycle_ids: usize, + projection_ids: usize, + finalizer_copies: usize, + finalizer_projection_segments: usize, + finalizer_lifecycle_ids: usize, + finalizer_projection_ids: usize, + place_copies: usize, + place_projection_segments: usize, + place_projection_ids: usize, + call_arguments: usize, + call_argument_owned_bytes: usize, + parent_local_epochs: usize, + parent_local_zero_lifetime_transfers: usize, + parent_local_partial_fields: usize, + parent_local_finalizer_copies: usize, + parent_local_finalizer_projection_segments: usize, + parent_local_finalizer_lifecycle_ids: usize, + parent_local_finalizer_projection_ids: usize, + parent_local_finalizer_storage_bytes: usize, + parent_local_projection_epochs: usize, + parent_local_projection_exit_groups: usize, + parent_local_projection_finalizer_copies: usize, + parent_local_projection_finalizer_projection_segments: usize, + parent_local_projection_finalizer_lifecycle_ids: usize, + parent_local_projection_finalizer_projection_ids: usize, + parent_local_projection_finalizer_storage_bytes: usize, + parent_local_update_prefix_fields: usize, + parent_local_update_prefix_exit_groups: usize, + parent_local_update_prefix_finalizer_copies: usize, + parent_local_update_prefix_finalizer_projection_segments: usize, + parent_local_update_prefix_finalizer_lifecycle_ids: usize, + parent_local_update_prefix_finalizer_projection_ids: usize, + parent_local_update_prefix_finalizer_storage_bytes: usize, + ordinary_slot_payload_bytes: usize, + ordinary_place_storage_bytes: usize, + ordinary_finalizer_storage_bytes: usize, + staged_results: usize, + variant_edges: usize, + stage_identity_and_type_bytes: usize, + variant_identity_bytes: usize, + fallback_roots: usize, + exit_events: usize, +} + +impl CleanupRetainedStats { + fn add_root(&mut self, facts: CleanupTypeFacts) -> Option<()> { + if facts.leaves == 0 { + return Some(()); + } + self.roots = self.roots.checked_add(1)?; + self.occurrences = self.occurrences.checked_add(facts.occurrences)?; + self.shape_fields = self.shape_fields.checked_add(facts.shape_fields)?; + self.leaves = self.leaves.checked_add(facts.leaves)?; + self.projection_segments = self + .projection_segments + .checked_add(facts.projection_segments)?; + self.shape_ids = self.shape_ids.checked_add(facts.shape_ids)?; + self.lifecycle_ids = self.lifecycle_ids.checked_add(facts.lifecycle_ids)?; + self.projection_ids = self.projection_ids.checked_add(facts.projection_ids)?; + Some(()) + } + + fn merge(&mut self, other: Self) -> Option<()> { + self.roots = self.roots.checked_add(other.roots)?; + self.occurrences = self.occurrences.checked_add(other.occurrences)?; + self.shape_fields = self.shape_fields.checked_add(other.shape_fields)?; + self.leaves = self.leaves.checked_add(other.leaves)?; + self.projection_segments = self + .projection_segments + .checked_add(other.projection_segments)?; + self.shape_ids = self.shape_ids.checked_add(other.shape_ids)?; + self.lifecycle_ids = self.lifecycle_ids.checked_add(other.lifecycle_ids)?; + self.projection_ids = self.projection_ids.checked_add(other.projection_ids)?; + self.finalizer_copies = self.finalizer_copies.checked_add(other.finalizer_copies)?; + self.finalizer_projection_segments = self + .finalizer_projection_segments + .checked_add(other.finalizer_projection_segments)?; + self.finalizer_lifecycle_ids = self + .finalizer_lifecycle_ids + .checked_add(other.finalizer_lifecycle_ids)?; + self.finalizer_projection_ids = self + .finalizer_projection_ids + .checked_add(other.finalizer_projection_ids)?; + self.place_copies = self.place_copies.checked_add(other.place_copies)?; + self.place_projection_segments = self + .place_projection_segments + .checked_add(other.place_projection_segments)?; + self.place_projection_ids = self + .place_projection_ids + .checked_add(other.place_projection_ids)?; + self.call_arguments = self.call_arguments.checked_add(other.call_arguments)?; + self.call_argument_owned_bytes = self + .call_argument_owned_bytes + .checked_add(other.call_argument_owned_bytes)?; + self.parent_local_epochs = self + .parent_local_epochs + .checked_add(other.parent_local_epochs)?; + self.parent_local_zero_lifetime_transfers = self + .parent_local_zero_lifetime_transfers + .checked_add(other.parent_local_zero_lifetime_transfers)?; + self.parent_local_partial_fields = self + .parent_local_partial_fields + .checked_add(other.parent_local_partial_fields)?; + self.parent_local_finalizer_copies = self + .parent_local_finalizer_copies + .checked_add(other.parent_local_finalizer_copies)?; + self.parent_local_finalizer_projection_segments = self + .parent_local_finalizer_projection_segments + .checked_add(other.parent_local_finalizer_projection_segments)?; + self.parent_local_finalizer_lifecycle_ids = self + .parent_local_finalizer_lifecycle_ids + .checked_add(other.parent_local_finalizer_lifecycle_ids)?; + self.parent_local_finalizer_projection_ids = self + .parent_local_finalizer_projection_ids + .checked_add(other.parent_local_finalizer_projection_ids)?; + self.parent_local_finalizer_storage_bytes = self + .parent_local_finalizer_storage_bytes + .checked_add(other.parent_local_finalizer_storage_bytes)?; + self.parent_local_projection_epochs = self + .parent_local_projection_epochs + .checked_add(other.parent_local_projection_epochs)?; + self.parent_local_projection_exit_groups = self + .parent_local_projection_exit_groups + .checked_add(other.parent_local_projection_exit_groups)?; + self.parent_local_projection_finalizer_copies = self + .parent_local_projection_finalizer_copies + .checked_add(other.parent_local_projection_finalizer_copies)?; + self.parent_local_projection_finalizer_projection_segments = self + .parent_local_projection_finalizer_projection_segments + .checked_add(other.parent_local_projection_finalizer_projection_segments)?; + self.parent_local_projection_finalizer_lifecycle_ids = self + .parent_local_projection_finalizer_lifecycle_ids + .checked_add(other.parent_local_projection_finalizer_lifecycle_ids)?; + self.parent_local_projection_finalizer_projection_ids = self + .parent_local_projection_finalizer_projection_ids + .checked_add(other.parent_local_projection_finalizer_projection_ids)?; + self.parent_local_projection_finalizer_storage_bytes = self + .parent_local_projection_finalizer_storage_bytes + .checked_add(other.parent_local_projection_finalizer_storage_bytes)?; + self.parent_local_update_prefix_fields = self + .parent_local_update_prefix_fields + .checked_add(other.parent_local_update_prefix_fields)?; + self.parent_local_update_prefix_exit_groups = + self.parent_local_update_prefix_exit_groups + .checked_add(other.parent_local_update_prefix_exit_groups)?; + self.parent_local_update_prefix_finalizer_copies = self + .parent_local_update_prefix_finalizer_copies + .checked_add(other.parent_local_update_prefix_finalizer_copies)?; + self.parent_local_update_prefix_finalizer_projection_segments = self + .parent_local_update_prefix_finalizer_projection_segments + .checked_add(other.parent_local_update_prefix_finalizer_projection_segments)?; + self.parent_local_update_prefix_finalizer_lifecycle_ids = self + .parent_local_update_prefix_finalizer_lifecycle_ids + .checked_add(other.parent_local_update_prefix_finalizer_lifecycle_ids)?; + self.parent_local_update_prefix_finalizer_projection_ids = self + .parent_local_update_prefix_finalizer_projection_ids + .checked_add(other.parent_local_update_prefix_finalizer_projection_ids)?; + self.parent_local_update_prefix_finalizer_storage_bytes = self + .parent_local_update_prefix_finalizer_storage_bytes + .checked_add(other.parent_local_update_prefix_finalizer_storage_bytes)?; + self.ordinary_slot_payload_bytes = self + .ordinary_slot_payload_bytes + .checked_add(other.ordinary_slot_payload_bytes)?; + self.ordinary_place_storage_bytes = self + .ordinary_place_storage_bytes + .checked_add(other.ordinary_place_storage_bytes)?; + self.ordinary_finalizer_storage_bytes = self + .ordinary_finalizer_storage_bytes + .checked_add(other.ordinary_finalizer_storage_bytes)?; + self.staged_results = self.staged_results.checked_add(other.staged_results)?; + self.variant_edges = self.variant_edges.checked_add(other.variant_edges)?; + self.stage_identity_and_type_bytes = self + .stage_identity_and_type_bytes + .checked_add(other.stage_identity_and_type_bytes)?; + self.variant_identity_bytes = self + .variant_identity_bytes + .checked_add(other.variant_identity_bytes)?; + self.fallback_roots = self.fallback_roots.checked_add(other.fallback_roots)?; + self.exit_events = self.exit_events.checked_add(other.exit_events)?; + Some(()) + } + + fn scaled(self, multiplier: usize) -> Option { + Some(Self { + roots: self.roots.checked_mul(multiplier)?, + occurrences: self.occurrences.checked_mul(multiplier)?, + shape_fields: self.shape_fields.checked_mul(multiplier)?, + leaves: self.leaves.checked_mul(multiplier)?, + projection_segments: self.projection_segments.checked_mul(multiplier)?, + shape_ids: self.shape_ids.checked_mul(multiplier)?, + lifecycle_ids: self.lifecycle_ids.checked_mul(multiplier)?, + projection_ids: self.projection_ids.checked_mul(multiplier)?, + finalizer_copies: self.finalizer_copies.checked_mul(multiplier)?, + finalizer_projection_segments: self + .finalizer_projection_segments + .checked_mul(multiplier)?, + finalizer_lifecycle_ids: self.finalizer_lifecycle_ids.checked_mul(multiplier)?, + finalizer_projection_ids: self.finalizer_projection_ids.checked_mul(multiplier)?, + place_copies: self.place_copies.checked_mul(multiplier)?, + place_projection_segments: self.place_projection_segments.checked_mul(multiplier)?, + place_projection_ids: self.place_projection_ids.checked_mul(multiplier)?, + call_arguments: self.call_arguments.checked_mul(multiplier)?, + call_argument_owned_bytes: self.call_argument_owned_bytes.checked_mul(multiplier)?, + parent_local_epochs: self.parent_local_epochs.checked_mul(multiplier)?, + parent_local_zero_lifetime_transfers: self + .parent_local_zero_lifetime_transfers + .checked_mul(multiplier)?, + parent_local_partial_fields: self + .parent_local_partial_fields + .checked_mul(multiplier)?, + parent_local_finalizer_copies: self + .parent_local_finalizer_copies + .checked_mul(multiplier)?, + parent_local_finalizer_projection_segments: self + .parent_local_finalizer_projection_segments + .checked_mul(multiplier)?, + parent_local_finalizer_lifecycle_ids: self + .parent_local_finalizer_lifecycle_ids + .checked_mul(multiplier)?, + parent_local_finalizer_projection_ids: self + .parent_local_finalizer_projection_ids + .checked_mul(multiplier)?, + parent_local_finalizer_storage_bytes: self + .parent_local_finalizer_storage_bytes + .checked_mul(multiplier)?, + parent_local_projection_epochs: self + .parent_local_projection_epochs + .checked_mul(multiplier)?, + parent_local_projection_exit_groups: self + .parent_local_projection_exit_groups + .checked_mul(multiplier)?, + parent_local_projection_finalizer_copies: self + .parent_local_projection_finalizer_copies + .checked_mul(multiplier)?, + parent_local_projection_finalizer_projection_segments: self + .parent_local_projection_finalizer_projection_segments + .checked_mul(multiplier)?, + parent_local_projection_finalizer_lifecycle_ids: self + .parent_local_projection_finalizer_lifecycle_ids + .checked_mul(multiplier)?, + parent_local_projection_finalizer_projection_ids: self + .parent_local_projection_finalizer_projection_ids + .checked_mul(multiplier)?, + parent_local_projection_finalizer_storage_bytes: self + .parent_local_projection_finalizer_storage_bytes + .checked_mul(multiplier)?, + parent_local_update_prefix_fields: self + .parent_local_update_prefix_fields + .checked_mul(multiplier)?, + parent_local_update_prefix_exit_groups: self + .parent_local_update_prefix_exit_groups + .checked_mul(multiplier)?, + parent_local_update_prefix_finalizer_copies: self + .parent_local_update_prefix_finalizer_copies + .checked_mul(multiplier)?, + parent_local_update_prefix_finalizer_projection_segments: self + .parent_local_update_prefix_finalizer_projection_segments + .checked_mul(multiplier)?, + parent_local_update_prefix_finalizer_lifecycle_ids: self + .parent_local_update_prefix_finalizer_lifecycle_ids + .checked_mul(multiplier)?, + parent_local_update_prefix_finalizer_projection_ids: self + .parent_local_update_prefix_finalizer_projection_ids + .checked_mul(multiplier)?, + parent_local_update_prefix_finalizer_storage_bytes: self + .parent_local_update_prefix_finalizer_storage_bytes + .checked_mul(multiplier)?, + ordinary_slot_payload_bytes: self + .ordinary_slot_payload_bytes + .checked_mul(multiplier)?, + ordinary_place_storage_bytes: self + .ordinary_place_storage_bytes + .checked_mul(multiplier)?, + ordinary_finalizer_storage_bytes: self + .ordinary_finalizer_storage_bytes + .checked_mul(multiplier)?, + staged_results: self.staged_results.checked_mul(multiplier)?, + variant_edges: self.variant_edges.checked_mul(multiplier)?, + stage_identity_and_type_bytes: self + .stage_identity_and_type_bytes + .checked_mul(multiplier)?, + variant_identity_bytes: self.variant_identity_bytes.checked_mul(multiplier)?, + fallback_roots: self.fallback_roots.checked_mul(multiplier)?, + exit_events: self.exit_events.checked_mul(multiplier)?, + }) + } +} + +fn retained_vec_capacity_extra(logical_entries: usize, container_upper: usize) -> Option { + if logical_entries == 0 { + return Some(0); + } + let nonempty_containers = container_upper.min(logical_entries); + nonempty_containers + .checked_mul(8) + .and_then(|capacity| capacity.checked_add(logical_entries.checked_mul(2)?)) + .and_then(|capacity| capacity.checked_sub(logical_entries)) +} + +fn declaration_dag_expansion( + program: &Program, + generic_instance_upper: usize, +) -> Result { + fn add_child( + parent: &mut CleanupTypeFacts, + child: CleanupTypeFacts, + edge_ids: usize, + ) -> Option<()> { + parent.leaves = parent.leaves.checked_add(child.leaves)?; + parent.occurrences = parent.occurrences.checked_add(child.occurrences)?; + parent.shape_fields = parent + .shape_fields + .checked_add(1)? + .checked_add(child.shape_fields)?; + parent.projection_segments = parent + .projection_segments + .checked_add(child.projection_segments)? + .checked_add(child.leaves)?; + parent.shape_ids = parent + .shape_ids + .checked_add(edge_ids)? + .checked_add(child.shape_ids)?; + parent.lifecycle_ids = parent.lifecycle_ids.checked_add(child.lifecycle_ids)?; + parent.projection_ids = parent + .projection_ids + .checked_add(child.projection_ids)? + .checked_add(child.leaves.checked_mul(edge_ids)?)?; + Some(()) + } + + let mut cleanup_node_count = 0usize; + let mut cleanup_scan = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + for function in &program.functions { + for root in function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures) + { + let mut len = 1usize; + cleanup_scan[0] = Some((root, 0usize, 0usize)); + while len != 0 { + len -= 1; + let (expression, next_child, _) = cleanup_scan[len] + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if next_child == 0 { + cleanup_node_count = cleanup_node_count + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + if let Some(child) = ast_child(expression, next_child) { + if len + 2 > cleanup_scan.len() { + return Err(b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + )); + } + cleanup_scan[len] = Some((expression, next_child + 1, 0)); + cleanup_scan[len + 1] = Some((child, 0, 0)); + len += 2; + } + } + } + } + let cleanup_node_capacity = cleanup_node_count.max(1); + let count = program.types.len().max(1); + let table_bytes = count + .checked_mul( + std::mem::size_of::() + + std::mem::size_of::() + + std::mem::size_of::>(), + ) + .and_then(|bytes| { + bytes.checked_add( + cleanup_node_capacity.checked_mul(std::mem::size_of::())?, + ) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let _table_budget = reserve_temporary_exact(table_bytes)?; + let mut state = Vec::with_capacity(count); + let mut facts = Vec::with_capacity(count); + let mut stack: Vec> = Vec::with_capacity(count); + state.resize(count, 0u8); + facts.resize(count, CleanupTypeFacts::default()); + stack.resize(count, None); + if state.capacity() != count || facts.capacity() != count || stack.capacity() != count { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + let mut maximum_resource_leaves = 0usize; + let mut maximum_type_occurrences = 1usize; + let mut maximum_shape_fields = 0usize; + let mut maximum_projection_segments = 0usize; + let mut maximum_shape_identity_bytes = 0usize; + let mut maximum_lifecycle_identity_bytes = 0usize; + let mut maximum_projection_identity_bytes = 0usize; + for root in 0..program.types.len() { + if state[root] == 2 { + continue; + } + stack[0] = Some(( + root, + 0, + CleanupTypeFacts { + occurrences: 1, + shape_ids: program.types[root].stable_id.len(), + ..CleanupTypeFacts::default() + }, + )); + state[root] = 1; + let mut len = 1usize; + while len != 0 { + len -= 1; + let (index, next, total) = stack[len] + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let declaration = &program.types[index]; + if matches!( + declaration.kind, + crate::ast::TypeDeclarationKind::Resource { .. } + ) { + let lifecycle_bytes = match &declaration.kind { + crate::ast::TypeDeclarationKind::Resource { lifecycles } => lifecycles + .iter() + .filter_map(|lifecycle| lifecycle.stable_id.as_deref()) + .try_fold(0usize, |bytes, id| bytes.checked_add(id.len())) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + _ => unreachable!(), + }; + facts[index] = CleanupTypeFacts { + leaves: 1, + occurrences: 1, + shape_ids: lifecycle_bytes, + lifecycle_ids: lifecycle_bytes, + projection_ids: 0, + ..CleanupTypeFacts::default() + }; + maximum_resource_leaves = maximum_resource_leaves.max(1); + maximum_type_occurrences = maximum_type_occurrences.max(1); + maximum_shape_identity_bytes = maximum_shape_identity_bytes.max(lifecycle_bytes); + maximum_lifecycle_identity_bytes = + maximum_lifecycle_identity_bytes.max(lifecycle_bytes); + state[index] = 2; + if let Some(parent) = len.checked_sub(1).and_then(|parent| stack[parent].as_mut()) { + let parent_decl = &program.types[parent.0]; + let edge = declaration_field_identity_bytes(parent_decl, parent.1 - 1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + add_child(&mut parent.2, facts[index], edge) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + continue; + } + let Some(child) = declaration_field_type(declaration, next) else { + facts[index] = total; + state[index] = 2; + maximum_resource_leaves = maximum_resource_leaves.max(total.leaves); + maximum_type_occurrences = maximum_type_occurrences.max(total.occurrences); + maximum_shape_fields = maximum_shape_fields.max(total.shape_fields); + maximum_projection_segments = + maximum_projection_segments.max(total.projection_segments); + maximum_shape_identity_bytes = maximum_shape_identity_bytes.max(total.shape_ids); + maximum_lifecycle_identity_bytes = + maximum_lifecycle_identity_bytes.max(total.lifecycle_ids); + maximum_projection_identity_bytes = + maximum_projection_identity_bytes.max(total.projection_ids); + if let Some(parent) = len.checked_sub(1).and_then(|parent| stack[parent].as_mut()) { + let parent_decl = &program.types[parent.0]; + let edge = declaration_field_identity_bytes(parent_decl, parent.1 - 1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + add_child(&mut parent.2, total, edge) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + continue; + }; + stack[len] = Some((index, next + 1, total)); + len += 1; + let crate::ast::Type::Named { name, .. } = child else { + let parent = stack[len - 1].as_mut().expect("parent retained"); + parent.2.occurrences = parent + .2 + .occurrences + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + parent.2.shape_fields = parent + .2 + .shape_fields + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + parent.2.shape_ids = parent + .2 + .shape_ids + .checked_add( + declaration_field_identity_bytes(declaration, next) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + continue; + }; + let Some(child_index) = program.types.iter().position(|value| value.name == *name) + else { + let parent = stack[len - 1].as_mut().expect("parent retained"); + parent.2.occurrences = parent + .2 + .occurrences + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + parent.2.shape_fields = parent + .2 + .shape_fields + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + parent.2.shape_ids = parent + .2 + .shape_ids + .checked_add( + declaration_field_identity_bytes(declaration, next) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + continue; + }; + match state[child_index] { + 2 => { + let parent = stack[len - 1].as_mut().expect("parent retained"); + let edge = declaration_field_identity_bytes(declaration, next) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + add_child(&mut parent.2, facts[child_index], edge) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + 1 => return Err(b107("selected identity missing")), + _ => { + if len == stack.len() { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + state[child_index] = 1; + stack[len] = Some(( + child_index, + 0, + CleanupTypeFacts { + occurrences: 1, + shape_ids: program.types[child_index].stable_id.len(), + ..CleanupTypeFacts::default() + }, + )); + len += 1; + } + } + } + } + let cleanup_retained = cleanup_retained_stats( + program, + &facts, + cleanup_node_capacity, + generic_instance_upper, + )?; + Ok(DeclarationDagExpansion { + maximum_resource_leaves, + maximum_type_occurrences, + maximum_shape_fields, + maximum_projection_segments, + maximum_shape_identity_bytes, + maximum_lifecycle_identity_bytes, + maximum_projection_identity_bytes, + cleanup_retained, + }) +} + +#[derive(Clone, Copy)] +enum CleanupTypeKey { + Scalar, + Declaration(usize), + Unknown, +} + +fn cleanup_source_exit_events(expression: &crate::ast::Expr) -> usize { + match &expression.kind { + crate::ast::ExprKind::Call { .. } + | crate::ast::ExprKind::Unary { + op: crate::ast::UnaryOp::Neg, + .. + } + | crate::ast::ExprKind::Binary { + op: + crate::ast::BinaryOp::Add + | crate::ast::BinaryOp::Sub + | crate::ast::BinaryOp::Mul + | crate::ast::BinaryOp::Div + | crate::ast::BinaryOp::Rem, + .. + } + | crate::ast::ExprKind::Block { .. } + | crate::ast::ExprKind::Try { .. } + | crate::ast::ExprKind::UpdateRecord { .. } => 1, + // If, lazy boolean, and Match are lowered in their active region. + // Their authored Block children, when present, own the corresponding + // lexical scope exits and are counted independently above. + _ => 0, + } +} + +fn cleanup_source_failure_events(expression: &crate::ast::Expr) -> usize { + match &expression.kind { + crate::ast::ExprKind::Call { .. } + | crate::ast::ExprKind::Unary { + op: crate::ast::UnaryOp::Neg, + .. + } + | crate::ast::ExprKind::Binary { + op: + crate::ast::BinaryOp::Add + | crate::ast::BinaryOp::Sub + | crate::ast::BinaryOp::Mul + | crate::ast::BinaryOp::Div + | crate::ast::BinaryOp::Rem, + .. + } + | crate::ast::ExprKind::Try { .. } => 1, + _ => 0, + } +} + +fn cleanup_function_exit_events<'a>( + function: &'a crate::ast::Function, + traversal: &mut [Option<(&'a crate::ast::Expr, usize, usize)>; + MAX_SEMANTIC_EXPRESSION_DEPTH + 1], +) -> Result { + let mut events = function + .requires + .len() + .checked_add(function.ensures.len()) + .and_then(|contracts| contracts.checked_mul(2)) + .and_then(|events| events.checked_add(1)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + for root in function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures) + { + let mut len = 1usize; + traversal[0] = Some((root, 0, 0)); + while len != 0 { + len -= 1; + let (expression, next_child, _) = traversal[len] + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if next_child == 0 { + // lower_root_body reuses the function's root region instead + // of creating an authored Block region for the outer body. + if !std::ptr::eq(expression, &function.body) { + events = events + .checked_add(cleanup_source_exit_events(expression)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + } + if let Some(child) = ast_child(expression, next_child) { + if len + 2 > traversal.len() { + return Err(b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + )); + } + traversal[len] = Some((expression, next_child + 1, 0)); + traversal[len + 1] = Some((child, 0, 0)); + len += 2; + } + } + } + Ok(events) +} + +fn cleanup_expression_exit_events<'a>( + root: &'a crate::ast::Expr, + traversal: &mut [Option<(&'a crate::ast::Expr, usize, usize)>; + MAX_SEMANTIC_EXPRESSION_DEPTH + 1], +) -> Result { + let mut events = 0usize; + let mut len = 1usize; + traversal[0] = Some((root, 0, 0)); + while len != 0 { + len -= 1; + let (expression, next_child, _) = traversal[len] + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if next_child == 0 { + events = events + .checked_add(cleanup_source_exit_events(expression)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + if let Some(child) = ast_child(expression, next_child) { + if len + 2 > traversal.len() { + return Err(b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + )); + } + traversal[len] = Some((expression, next_child + 1, 0)); + traversal[len + 1] = Some((child, 0, 0)); + len += 2; + } + } + Ok(events) +} + +fn cleanup_expression_failure_events<'a>( + root: &'a crate::ast::Expr, + traversal: &mut [Option<(&'a crate::ast::Expr, usize, usize)>; + MAX_SEMANTIC_EXPRESSION_DEPTH + 1], +) -> Result { + let mut events = 0usize; + let mut len = 1usize; + traversal[0] = Some((root, 0, 0)); + while len != 0 { + len -= 1; + let (expression, next_child, _) = traversal[len] + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if next_child == 0 { + events = events + .checked_add(cleanup_source_failure_events(expression)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + if let Some(child) = ast_child(expression, next_child) { + if len + 2 > traversal.len() { + return Err(b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + )); + } + traversal[len] = Some((expression, next_child + 1, 0)); + traversal[len + 1] = Some((child, 0, 0)); + len += 2; + } + } + Ok(events) +} + +fn cleanup_expression_call_events<'a>( + root: &'a crate::ast::Expr, + program: &Program, + traversal: &mut [Option<(&'a crate::ast::Expr, usize, usize)>; + MAX_SEMANTIC_EXPRESSION_DEPTH + 1], +) -> Result { + let mut events = 0usize; + let mut len = 1usize; + traversal[0] = Some((root, 0, 0)); + while len != 0 { + len -= 1; + let (expression, next_child, _) = traversal[len] + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if next_child == 0 { + if let crate::ast::ExprKind::Call { name, .. } = &expression.kind { + if !program + .interfaces + .iter() + .any(|interface| interface.imports.iter().any(|import| import.name == *name)) + { + events = events + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + } + } + if let Some(child) = ast_child(expression, next_child) { + if len + 2 > traversal.len() { + return Err(b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + )); + } + traversal[len] = Some((expression, next_child + 1, 0)); + traversal[len + 1] = Some((child, 0, 0)); + len += 2; + } + } + Ok(events) +} + +fn cleanup_expression_boolean_branch_events<'a>( + root: &'a crate::ast::Expr, + traversal: &mut [Option<(&'a crate::ast::Expr, usize, usize)>; + MAX_SEMANTIC_EXPRESSION_DEPTH + 1], +) -> Result { + let mut events = 0usize; + let mut len = 1usize; + traversal[0] = Some((root, 0, 0)); + while len != 0 { + len -= 1; + let (expression, next_child, _) = traversal[len] + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if next_child == 0 + && matches!( + expression.kind, + crate::ast::ExprKind::If { .. } + | crate::ast::ExprKind::Binary { + op: crate::ast::BinaryOp::And | crate::ast::BinaryOp::Or, + .. + } + ) + { + events = events + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + if let Some(child) = ast_child(expression, next_child) { + if len + 2 > traversal.len() { + return Err(b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + )); + } + traversal[len] = Some((expression, next_child + 1, 0)); + traversal[len + 1] = Some((child, 0, 0)); + len += 2; + } + } + Ok(events) +} + +fn cleanup_plan_variable_identity_bytes( + function: &crate::ast::Function, + program: &Program, + cleanup_path_copies: usize, +) -> Result<(usize, usize), Diagnostic> { + fn child_path_increment( + expression: &crate::ast::Expr, + child_index: usize, + program: &Program, + ) -> usize { + match &expression.kind { + crate::ast::ExprKind::Call { name, .. } => { + let prefix = + if program.interfaces.iter().any(|interface| { + interface.imports.iter().any(|import| import.name == *name) + }) { + ".native-rust-arg." + } else { + ".arg." + }; + prefix.len() + decimal_digits(child_index) + } + crate::ast::ExprKind::Unary { .. } => ".value".len(), + crate::ast::ExprKind::Binary { .. } => { + if child_index == 0 { ".left" } else { ".right" }.len() + } + crate::ast::ExprKind::Block { statements, .. } => { + if child_index < statements.len() { + ".s".len() + decimal_digits(child_index) + ".value".len() + } else { + ".tail".len() + } + } + crate::ast::ExprKind::If { .. } => [".condition", ".then", ".else"] + .get(child_index) + .map_or(0, |segment| segment.len()), + crate::ast::ExprKind::ConstructRecord { .. } + | crate::ast::ExprKind::ConstructVariant { .. } => { + ".field.".len() + decimal_digits(child_index) + ".value".len() + } + crate::ast::ExprKind::Match { .. } => { + if child_index == 0 { + ".scrutinee".len() + } else { + ".arm.".len() + decimal_digits(child_index - 1) + ".value".len() + } + } + crate::ast::ExprKind::UpdateRecord { .. } => { + if child_index == 0 { + ".base".len() + } else { + ".field.".len() + decimal_digits(child_index - 1) + ".value".len() + } + } + crate::ast::ExprKind::Try { .. } => ".operand".len(), + crate::ast::ExprKind::Project { .. } => ".base".len(), + crate::ast::ExprKind::Int(_) + | crate::ast::ExprKind::Bool(_) + | crate::ast::ExprKind::Var(_) => 0, + } + } + + let generic_instance_identity_len = generic_function_instance_identity_upper(program, function) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let mut bytes = 0usize; + let mut all_expression_bytes = 0usize; + for (root_index, (root, contract)) in function + .requires + .iter() + .map(|root| (root, true)) + .chain(std::iter::once((&function.body, false))) + .chain(function.ensures.iter().map(|root| (root, true))) + .enumerate() + { + let path_len = match root_index.cmp(&function.requires.len()) { + std::cmp::Ordering::Less => "requires.".len() + decimal_digits(root_index), + std::cmp::Ordering::Equal => "body".len(), + std::cmp::Ordering::Greater => { + "ensures.".len() + decimal_digits(root_index - function.requires.len() - 1) + } + }; + let mut traversal = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let mut len = 1usize; + traversal[0] = Some((root, path_len, 0)); + while len != 0 { + len -= 1; + let (expression, path_len, next_child) = traversal[len] + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if next_child == 0 { + let mut copies = usize::from(contract && std::ptr::eq(expression, root)) + .checked_mul(5) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + match &expression.kind { + crate::ast::ExprKind::Call { name, .. } => { + if !program.interfaces.iter().any(|interface| { + interface.imports.iter().any(|import| import.name == *name) + }) { + // StatusSource, two status edges, SelectFailure, + // ReturnFailure, and CallCommit. + copies = copies + .checked_add(6) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + } + crate::ast::ExprKind::Unary { + op: crate::ast::UnaryOp::Neg, + .. + } + | crate::ast::ExprKind::Binary { + op: + crate::ast::BinaryOp::Add + | crate::ast::BinaryOp::Sub + | crate::ast::BinaryOp::Mul + | crate::ast::BinaryOp::Div + | crate::ast::BinaryOp::Rem, + .. + } => { + copies = copies + .checked_add(5) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + crate::ast::ExprKind::If { .. } + | crate::ast::ExprKind::Binary { + op: crate::ast::BinaryOp::And | crate::ast::BinaryOp::Or, + .. + } => { + copies = copies + .checked_add(2) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + _ => {} + } + if std::ptr::eq(expression, &function.body) { + copies = copies + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + let uncovered = copies + .checked_sub(copies.min(cleanup_path_copies)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let identity_bytes = scoped_expression_identity_upper( + function, + generic_instance_identity_len, + path_len, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + all_expression_bytes = all_expression_bytes + .checked_add(identity_bytes) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + bytes = bytes + .checked_add( + uncovered + .checked_mul(identity_bytes) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + if let Some(child) = ast_child(expression, next_child) { + if len + 2 > traversal.len() { + return Err(b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + )); + } + let child_path_len = path_len + .checked_add(child_path_increment(expression, next_child, program)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + traversal[len] = Some((expression, path_len, next_child + 1)); + traversal[len + 1] = Some((child, child_path_len, 0)); + len += 2; + } + } + } + Ok((all_expression_bytes, bytes)) +} + +fn cleanup_function_finalizer_events<'a>( + function: &'a crate::ast::Function, + traversal: &mut [Option<(&'a crate::ast::Expr, usize, usize)>; + MAX_SEMANTIC_EXPRESSION_DEPTH + 1], +) -> Result { + let mut events = function + .requires + .len() + .checked_add(function.ensures.len()) + .and_then(|events| events.checked_add(1)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + for root in function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures) + { + events = events + .checked_add(cleanup_expression_failure_events(root, traversal)?) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + Ok(events) +} + +fn cleanup_function_region_depth<'a>( + function: &'a crate::ast::Function, + traversal: &mut [Option<(&'a crate::ast::Expr, usize, usize)>; + MAX_SEMANTIC_EXPRESSION_DEPTH + 1], +) -> Result { + let mut maximum = 1usize; + for (root, contract_region) in function + .requires + .iter() + .map(|root| (root, true)) + .chain(std::iter::once((&function.body, false))) + .chain(function.ensures.iter().map(|root| (root, true))) + { + let root_region = 1usize + .checked_add(usize::from(contract_region)) + .and_then(|depth| { + depth.checked_add(usize::from( + !std::ptr::eq(root, &function.body) + && matches!( + root.kind, + crate::ast::ExprKind::Block { .. } + | crate::ast::ExprKind::UpdateRecord { .. } + ), + )) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + maximum = maximum.max(root_region); + let mut len = 1usize; + traversal[0] = Some((root, 0, root_region)); + while len != 0 { + len -= 1; + let (expression, next_child, region_depth) = traversal[len] + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if let Some(child) = ast_child(expression, next_child) { + if len + 2 > traversal.len() { + return Err(b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + )); + } + let child_depth = region_depth + .checked_add(usize::from(matches!( + child.kind, + crate::ast::ExprKind::Block { .. } + | crate::ast::ExprKind::UpdateRecord { .. } + ))) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + maximum = maximum.max(child_depth); + traversal[len] = Some((expression, next_child + 1, region_depth)); + traversal[len + 1] = Some((child, 0, child_depth)); + len += 2; + } + } + } + Ok(maximum) +} + +#[derive(Clone, Copy, Default)] +struct CleanupBindingFlow { + failure_finalizers: usize, + live_after: bool, +} + +fn cleanup_binding_flow<'a>( + root: &'a crate::ast::Expr, + binding: &str, + consumes_result: bool, + program: &Program, + traversal: &mut [Option<(&'a crate::ast::Expr, usize, usize)>; + MAX_SEMANTIC_EXPRESSION_DEPTH + 1], +) -> Result { + let mut consumes = [false; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let mut flows = [CleanupBindingFlow::default(); MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let mut branch_live = [false; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let mut stack_len = 1usize; + traversal[0] = Some((root, 0, 0)); + consumes[0] = consumes_result; + flows[0].live_after = true; + let mut returned: Option = None; + while stack_len != 0 { + let frame_index = stack_len - 1; + let consume = consumes[frame_index]; + let (expression, next_child, _) = + traversal[frame_index].ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + + if let Some(child) = returned.take() { + let child_index = next_child + .checked_sub(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let flow = &mut flows[frame_index]; + let sequence = |flow: &mut CleanupBindingFlow, + child: CleanupBindingFlow| + -> Result<(), Diagnostic> { + if flow.live_after { + flow.failure_finalizers = flow + .failure_finalizers + .checked_add(child.failure_finalizers) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + flow.live_after = child.live_after; + } + Ok(()) + }; + match &expression.kind { + crate::ast::ExprKind::If { .. } | crate::ast::ExprKind::Match { .. } + if child_index != 0 => + { + if flow.live_after { + flow.failure_finalizers = flow + .failure_finalizers + .checked_add(child.failure_finalizers) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + branch_live[frame_index] |= child.live_after; + } + } + crate::ast::ExprKind::Binary { + op: crate::ast::BinaryOp::And | crate::ast::BinaryOp::Or, + .. + } if child_index == 1 => { + if flow.live_after { + flow.failure_finalizers = flow + .failure_finalizers + .checked_add(child.failure_finalizers) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + // The lazy short-circuit path retains the binding even + // if the right operand consumes it. + } + } + _ => sequence(flow, child)?, + } + } + + if let Some(child) = ast_child(expression, next_child) { + if stack_len == traversal.len() { + return Err(b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + )); + } + let child_consumes = match &expression.kind { + crate::ast::ExprKind::Call { name, .. } => program + .functions + .iter() + .find(|function| function.name == *name) + .and_then(|function| function.params.get(next_child)) + .is_some_and(|parameter| parameter.mode == crate::ast::ParamMode::Own), + crate::ast::ExprKind::Block { statements, .. } => { + next_child < statements.len() || consume + } + crate::ast::ExprKind::If { .. } => next_child != 0 && consume, + crate::ast::ExprKind::ConstructRecord { .. } + | crate::ast::ExprKind::ConstructVariant { .. } + | crate::ast::ExprKind::UpdateRecord { .. } => true, + crate::ast::ExprKind::Match { .. } => next_child == 0 || consume, + crate::ast::ExprKind::Try { .. } => true, + crate::ast::ExprKind::Project { .. } + | crate::ast::ExprKind::Unary { .. } + | crate::ast::ExprKind::Binary { .. } => false, + crate::ast::ExprKind::Int(_) + | crate::ast::ExprKind::Bool(_) + | crate::ast::ExprKind::Var(_) => false, + }; + traversal[frame_index] = Some((expression, next_child + 1, 0)); + traversal[stack_len] = Some((child, 0, 0)); + consumes[stack_len] = child_consumes; + flows[stack_len] = CleanupBindingFlow { + failure_finalizers: 0, + live_after: true, + }; + branch_live[stack_len] = false; + stack_len += 1; + continue; + } + + let mut flow = flows[frame_index]; + match &expression.kind { + crate::ast::ExprKind::If { .. } | crate::ast::ExprKind::Match { .. } + if flow.live_after => + { + flow.live_after = branch_live[frame_index]; + } + _ => {} + } + if flow.live_after { + flow.failure_finalizers = flow + .failure_finalizers + .checked_add(cleanup_source_failure_events(expression)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if consume + && matches!(&expression.kind, crate::ast::ExprKind::Var(name) if name == binding) + { + flow.live_after = false; + } + } + traversal[frame_index] = None; + stack_len -= 1; + returned = Some(flow); + } + returned.ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES)) +} + +fn cleanup_block_binding_finalizer_events<'a>( + function: &'a crate::ast::Function, + block: &'a crate::ast::Expr, + next_child: usize, + binding: &str, + program: &Program, + traversal: &mut [Option<(&'a crate::ast::Expr, usize, usize)>; + MAX_SEMANTIC_EXPRESSION_DEPTH + 1], +) -> Result { + let mut events = 0usize; + let mut live = true; + let mut child_index = next_child; + while let Some(child) = ast_child(block, child_index) { + if live { + let flow = cleanup_binding_flow(child, binding, true, program, traversal)?; + events = events + .checked_add(flow.failure_finalizers) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + live = flow.live_after; + } + child_index = child_index + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + if live && std::ptr::eq(block, &function.body) { + for ensure in &function.ensures { + let flow = cleanup_binding_flow(ensure, binding, false, program, traversal)?; + events = events + .checked_add(flow.failure_finalizers) + .and_then(|events| events.checked_add(1)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + live = flow.live_after; + if !live { + break; + } + } + } + events + .checked_add(usize::from(live)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES)) +} + +fn cleanup_parameter_finalizer_events<'a>( + function: &'a crate::ast::Function, + binding: &str, + program: &Program, + traversal: &mut [Option<(&'a crate::ast::Expr, usize, usize)>; + MAX_SEMANTIC_EXPRESSION_DEPTH + 1], +) -> Result { + let mut events = 0usize; + let mut live = true; + for require in &function.requires { + let flow = cleanup_binding_flow(require, binding, false, program, traversal)?; + events = events + .checked_add(flow.failure_finalizers) + .and_then(|events| events.checked_add(usize::from(live))) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + live &= flow.live_after; + } + if matches!(function.body.kind, crate::ast::ExprKind::Block { .. }) { + return events + .checked_add(cleanup_block_binding_finalizer_events( + function, + &function.body, + 0, + binding, + program, + traversal, + )?) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + if live { + let flow = cleanup_binding_flow(&function.body, binding, true, program, traversal)?; + events = events + .checked_add(flow.failure_finalizers) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + live = flow.live_after; + } + for ensure in &function.ensures { + if live { + let flow = cleanup_binding_flow(ensure, binding, false, program, traversal)?; + events = events + .checked_add(flow.failure_finalizers) + .and_then(|events| events.checked_add(1)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + live = flow.live_after; + } + } + events + .checked_add(usize::from(live)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES)) +} + +fn cleanup_parent_local_remaining_finalizer_events<'a>( + function: &'a crate::ast::Function, + root: &'a crate::ast::Expr, + traversal: &[Option<(&'a crate::ast::Expr, usize, usize)>], + stack_len: usize, + event_traversal: &mut [Option<(&'a crate::ast::Expr, usize, usize)>; + MAX_SEMANTIC_EXPRESSION_DEPTH + 1], +) -> Result { + let mut events = 0usize; + for (ancestor, next_child, _) in traversal[..stack_len].iter().rev().flatten().copied() { + let active_child = next_child + .checked_sub(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let mut add_later_child = |child_index: usize| -> Result<(), Diagnostic> { + if let Some(child) = ast_child(ancestor, child_index) { + events = events + .checked_add(cleanup_expression_failure_events(child, event_traversal)?) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + Ok(()) + }; + match &ancestor.kind { + crate::ast::ExprKind::If { .. } => { + if active_child == 0 { + add_later_child(1)?; + add_later_child(2)?; + } + } + crate::ast::ExprKind::Match { arms, .. } => { + if active_child == 0 { + for arm_index in 0..arms.len() { + add_later_child(arm_index + 1)?; + } + } + } + crate::ast::ExprKind::Binary { + op: crate::ast::BinaryOp::And | crate::ast::BinaryOp::Or, + .. + } => { + if active_child == 0 { + add_later_child(1)?; + } + } + _ => { + let mut child_index = next_child; + while ast_child(ancestor, child_index).is_some() { + add_later_child(child_index)?; + child_index = child_index + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + } + } + events = events + .checked_add(cleanup_source_failure_events(ancestor)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if matches!( + ancestor.kind, + crate::ast::ExprKind::Block { .. } | crate::ast::ExprKind::UpdateRecord { .. } + ) { + if matches!(ancestor.kind, crate::ast::ExprKind::Block { .. }) + && std::ptr::eq(ancestor, &function.body) + { + for ensure in &function.ensures { + events = events + .checked_add(cleanup_expression_failure_events(ensure, event_traversal)?) + .and_then(|events| events.checked_add(1)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + } + return events + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + } + if std::ptr::eq(root, &function.body) { + for ensure in &function.ensures { + events = events + .checked_add(cleanup_expression_failure_events(ensure, event_traversal)?) + .and_then(|events| events.checked_add(1)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + } + events + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES)) +} + +fn cleanup_retained_stats( + program: &Program, + declaration_facts: &[CleanupTypeFacts], + node_capacity: usize, + generic_instance_upper: usize, +) -> Result { + fn key_for_type(program: &Program, ty: &crate::ast::Type) -> CleanupTypeKey { + match ty { + crate::ast::Type::I64 | crate::ast::Type::Bool => CleanupTypeKey::Scalar, + crate::ast::Type::Named { name, .. } => { + if let Some(index) = program + .types + .iter() + .position(|declaration| declaration.name == *name) + { + CleanupTypeKey::Declaration(index) + } else if matches!(name.as_str(), "Option" | "Result") + || program.types.iter().any(|declaration| { + declaration + .type_parameters + .iter() + .any(|parameter| parameter.name == *name) + }) + || program.functions.iter().any(|function| { + function + .type_parameters + .iter() + .any(|parameter| parameter.name == *name) + }) + { + // Prelude Option/Result and admitted direct generic + // arguments are Copy-only at this boundary. + CleanupTypeKey::Scalar + } else { + CleanupTypeKey::Unknown + } + } + } + } + + fn pattern_binding_key( + program: &Program, + pattern: &crate::ast::MatchPattern, + name: &str, + ) -> Result, Diagnostic> { + Ok(match pattern { + crate::ast::MatchPattern::Variant { + type_name, + case_name, + fields, + .. + } => { + let Some(declaration) = program + .types + .iter() + .find(|declaration| declaration.name == *type_name) + else { + return Ok(None); + }; + let crate::ast::TypeDeclarationKind::Variant { cases } = &declaration.kind else { + return Ok(None); + }; + let Some(case) = cases.iter().find(|case| case.name == *case_name) else { + return Ok(None); + }; + fields.iter().find_map(|binding| { + (binding.binding == name).then(|| { + case.fields + .iter() + .find(|field| field.name == binding.name) + .map(|field| key_for_type(program, &field.ty)) + })? + }) + } + crate::ast::MatchPattern::Record { + type_name, fields, .. + } => { + let Some(declaration) = program + .types + .iter() + .find(|declaration| declaration.name == *type_name) + else { + return Ok(None); + }; + let mut stack = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let mut len = 1usize; + stack[0] = Some((declaration, fields.as_slice(), 0usize, 1usize)); + let mut found = None; + while len != 0 { + len -= 1; + let (declaration, fields, index, depth) = stack[len] + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let crate::ast::TypeDeclarationKind::Record { + fields: declarations, + } = &declaration.kind + else { + continue; + }; + let Some(field) = fields.get(index) else { + continue; + }; + let Some(declaration_field) = declarations + .iter() + .find(|candidate| candidate.name == field.name) + else { + continue; + }; + match &field.pattern { + crate::ast::RecordMatchFieldPattern::Binding { name: binding, .. } + if binding == name => + { + found = Some(key_for_type(program, &declaration_field.ty)); + break; + } + crate::ast::RecordMatchFieldPattern::Record { + type_name, + fields: child_fields, + .. + } => { + let Some(child) = program + .types + .iter() + .find(|candidate| candidate.name == *type_name) + else { + continue; + }; + let child_depth = depth.checked_add(1).ok_or_else(|| { + b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + ) + })?; + if child_depth > MAX_SEMANTIC_EXPRESSION_DEPTH || len + 2 > stack.len() + { + return Err(b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + )); + } + stack[len] = Some((declaration, fields, index + 1, depth)); + stack[len + 1] = Some((child, child_fields.as_slice(), 0, child_depth)); + len += 2; + } + _ => { + stack[len] = Some((declaration, fields, index + 1, depth)); + len += 1; + } + } + } + found + } + crate::ast::MatchPattern::Wildcard { .. } => None, + }) + } + + fn facts_for_key( + key: CleanupTypeKey, + declaration_facts: &[CleanupTypeFacts], + fallback: CleanupTypeFacts, + ) -> CleanupTypeFacts { + match key { + CleanupTypeKey::Scalar => CleanupTypeFacts::default(), + CleanupTypeKey::Declaration(index) => declaration_facts[index], + CleanupTypeKey::Unknown => fallback, + } + } + + fn add_root( + target: &mut CleanupRetainedStats, + key: CleanupTypeKey, + declaration_facts: &[CleanupTypeFacts], + fallback: CleanupTypeFacts, + storage_identity_bytes: usize, + resolved_type_bytes: usize, + ) -> Result<(), Diagnostic> { + if matches!(key, CleanupTypeKey::Unknown) { + target.fallback_roots = target + .fallback_roots + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + let facts = facts_for_key(key, declaration_facts, fallback); + if facts.leaves != 0 { + target.ordinary_slot_payload_bytes = target + .ordinary_slot_payload_bytes + .checked_add( + storage_identity_bytes + .checked_add(resolved_type_bytes) + .and_then(|bytes| bytes.checked_mul(2)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.ordinary_place_storage_bytes = target + .ordinary_place_storage_bytes + .checked_add( + storage_identity_bytes + // Initialize, Transfer source/destination, and the + // region's raw StorageId each own the full identity. + .checked_mul(4) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + target + .add_root(facts) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES)) + } + + fn add_finalizer_upper( + target: &mut CleanupRetainedStats, + key: CleanupTypeKey, + declaration_facts: &[CleanupTypeFacts], + fallback: CleanupTypeFacts, + exits_after_initialization: usize, + storage_identity_bytes: usize, + ) -> Result<(), Diagnostic> { + let facts = facts_for_key(key, declaration_facts, fallback); + target.finalizer_copies = target + .finalizer_copies + .checked_add( + facts + .leaves + .checked_mul(exits_after_initialization) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.finalizer_projection_segments = target + .finalizer_projection_segments + .checked_add( + facts + .projection_segments + .checked_mul(exits_after_initialization) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.finalizer_lifecycle_ids = target + .finalizer_lifecycle_ids + .checked_add( + facts + .lifecycle_ids + .checked_mul(exits_after_initialization) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.finalizer_projection_ids = target + .finalizer_projection_ids + .checked_add( + facts + .projection_ids + .checked_mul(exits_after_initialization) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.ordinary_finalizer_storage_bytes = target + .ordinary_finalizer_storage_bytes + .checked_add( + storage_identity_bytes + .checked_mul(facts.leaves) + .and_then(|bytes| bytes.checked_mul(exits_after_initialization)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + Ok(()) + } + + fn add_parent_local_record_prefix( + target: &mut CleanupRetainedStats, + facts: CleanupTypeFacts, + later_failure_events: usize, + storage_identity_bytes: usize, + field_identity_bytes: usize, + ) -> Result<(), Diagnostic> { + if facts.leaves == 0 || later_failure_events == 0 { + return Ok(()); + } + let finalizer_copies = facts + .leaves + .checked_mul(later_failure_events) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let projection_segments = facts + .projection_segments + .checked_add(facts.leaves) + .and_then(|segments| segments.checked_mul(later_failure_events)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let lifecycle_ids = facts + .lifecycle_ids + .checked_mul(later_failure_events) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let projection_ids = facts + .projection_ids + .checked_add( + facts + .leaves + .checked_mul(field_identity_bytes) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .and_then(|bytes| bytes.checked_mul(later_failure_events)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let storage_bytes = storage_identity_bytes + .checked_mul(finalizer_copies) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + + target.finalizer_copies = target + .finalizer_copies + .checked_add(finalizer_copies) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.finalizer_projection_segments = target + .finalizer_projection_segments + .checked_add(projection_segments) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.finalizer_lifecycle_ids = target + .finalizer_lifecycle_ids + .checked_add(lifecycle_ids) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.finalizer_projection_ids = target + .finalizer_projection_ids + .checked_add(projection_ids) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.ordinary_finalizer_storage_bytes = target + .ordinary_finalizer_storage_bytes + .checked_add(storage_bytes) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + + target.parent_local_partial_fields = target + .parent_local_partial_fields + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.parent_local_finalizer_copies = target + .parent_local_finalizer_copies + .checked_add(finalizer_copies) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.parent_local_finalizer_projection_segments = target + .parent_local_finalizer_projection_segments + .checked_add(projection_segments) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.parent_local_finalizer_lifecycle_ids = target + .parent_local_finalizer_lifecycle_ids + .checked_add(lifecycle_ids) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.parent_local_finalizer_projection_ids = target + .parent_local_finalizer_projection_ids + .checked_add(projection_ids) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.parent_local_finalizer_storage_bytes = target + .parent_local_finalizer_storage_bytes + .checked_add(storage_bytes) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + Ok(()) + } + + fn add_parent_local_update_prefix( + target: &mut CleanupRetainedStats, + facts: CleanupTypeFacts, + later_failure_events: usize, + storage_identity_bytes: usize, + field_identity_bytes: usize, + ) -> Result<(), Diagnostic> { + if facts.leaves == 0 || later_failure_events == 0 { + return Ok(()); + } + let finalizer_copies = facts + .leaves + .checked_mul(later_failure_events) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let projection_segments = facts + .projection_segments + .checked_add(facts.leaves) + .and_then(|segments| segments.checked_mul(later_failure_events)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let lifecycle_ids = facts + .lifecycle_ids + .checked_mul(later_failure_events) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let projection_ids = facts + .projection_ids + .checked_add( + facts + .leaves + .checked_mul(field_identity_bytes) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .and_then(|bytes| bytes.checked_mul(later_failure_events)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let storage_bytes = storage_identity_bytes + .checked_mul(finalizer_copies) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + + target.finalizer_copies = target + .finalizer_copies + .checked_add(finalizer_copies) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.finalizer_projection_segments = target + .finalizer_projection_segments + .checked_add(projection_segments) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.finalizer_lifecycle_ids = target + .finalizer_lifecycle_ids + .checked_add(lifecycle_ids) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.finalizer_projection_ids = target + .finalizer_projection_ids + .checked_add(projection_ids) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.ordinary_finalizer_storage_bytes = target + .ordinary_finalizer_storage_bytes + .checked_add(storage_bytes) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + + target.parent_local_update_prefix_fields = target + .parent_local_update_prefix_fields + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.parent_local_update_prefix_exit_groups = target + .parent_local_update_prefix_exit_groups + .checked_add(later_failure_events) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.parent_local_update_prefix_finalizer_copies = target + .parent_local_update_prefix_finalizer_copies + .checked_add(finalizer_copies) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.parent_local_update_prefix_finalizer_projection_segments = target + .parent_local_update_prefix_finalizer_projection_segments + .checked_add(projection_segments) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.parent_local_update_prefix_finalizer_lifecycle_ids = target + .parent_local_update_prefix_finalizer_lifecycle_ids + .checked_add(lifecycle_ids) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.parent_local_update_prefix_finalizer_projection_ids = target + .parent_local_update_prefix_finalizer_projection_ids + .checked_add(projection_ids) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.parent_local_update_prefix_finalizer_storage_bytes = target + .parent_local_update_prefix_finalizer_storage_bytes + .checked_add(storage_bytes) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + Ok(()) + } + + fn add_parent_local_projection_residual( + target: &mut CleanupRetainedStats, + residual: CleanupTypeFacts, + remaining_events: usize, + storage_identity_bytes: usize, + ) -> Result<(), Diagnostic> { + if residual.leaves == 0 || remaining_events == 0 { + return Ok(()); + } + let finalizer_copies = residual + .leaves + .checked_mul(remaining_events) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let projection_segments = residual + .projection_segments + .checked_mul(remaining_events) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let lifecycle_ids = residual + .lifecycle_ids + .checked_mul(remaining_events) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let projection_ids = residual + .projection_ids + .checked_mul(remaining_events) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let storage_bytes = storage_identity_bytes + .checked_mul(finalizer_copies) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + + target.finalizer_copies = target + .finalizer_copies + .checked_add(finalizer_copies) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.finalizer_projection_segments = target + .finalizer_projection_segments + .checked_add(projection_segments) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.finalizer_lifecycle_ids = target + .finalizer_lifecycle_ids + .checked_add(lifecycle_ids) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.finalizer_projection_ids = target + .finalizer_projection_ids + .checked_add(projection_ids) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.ordinary_finalizer_storage_bytes = target + .ordinary_finalizer_storage_bytes + .checked_add(storage_bytes) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + + target.parent_local_projection_epochs = target + .parent_local_projection_epochs + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.parent_local_projection_exit_groups = target + .parent_local_projection_exit_groups + .checked_add(remaining_events) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.parent_local_projection_finalizer_copies = target + .parent_local_projection_finalizer_copies + .checked_add(finalizer_copies) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.parent_local_projection_finalizer_projection_segments = target + .parent_local_projection_finalizer_projection_segments + .checked_add(projection_segments) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.parent_local_projection_finalizer_lifecycle_ids = target + .parent_local_projection_finalizer_lifecycle_ids + .checked_add(lifecycle_ids) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.parent_local_projection_finalizer_projection_ids = target + .parent_local_projection_finalizer_projection_ids + .checked_add(projection_ids) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + target.parent_local_projection_finalizer_storage_bytes = target + .parent_local_projection_finalizer_storage_bytes + .checked_add(storage_bytes) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + Ok(()) + } + + fn variable_key( + program: &Program, + function: &crate::ast::Function, + name: &str, + traversal: &[Option<(&crate::ast::Expr, usize, usize)>], + stack_len: usize, + results: &[CleanupTypeKey], + ) -> Result { + for (ancestor, next_child, result_start) in + traversal[..stack_len].iter().rev().flatten().copied() + { + match &ancestor.kind { + crate::ast::ExprKind::Block { statements, .. } => { + let active_child = next_child.saturating_sub(1); + let completed_statements = active_child.min(statements.len()); + for index in (0..completed_statements).rev() { + let crate::ast::Statement::Let { name: binding, .. } = &statements[index]; + if binding == name { + return Ok(results + .get(result_start + index) + .copied() + .unwrap_or(CleanupTypeKey::Unknown)); + } + } + } + crate::ast::ExprKind::Match { arms, .. } => { + let active_child = next_child.saturating_sub(1); + if let Some(arm_index) = active_child.checked_sub(1) { + if let Some(key) = arms + .get(arm_index) + .map(|arm| pattern_binding_key(program, &arm.pattern, name)) + .transpose()? + .flatten() + { + return Ok(key); + } + } + } + _ => {} + } + } + Ok(function + .params + .iter() + .rev() + .find(|parameter| parameter.name == name) + .map(|parameter| key_for_type(program, ¶meter.ty)) + .unwrap_or(CleanupTypeKey::Unknown)) + } + + let fallback = + declaration_facts + .iter() + .copied() + .fold(CleanupTypeFacts::default(), |maximum, facts| { + CleanupTypeFacts { + leaves: maximum.leaves.max(facts.leaves), + occurrences: maximum.occurrences.max(facts.occurrences), + shape_fields: maximum.shape_fields.max(facts.shape_fields), + projection_segments: maximum.projection_segments.max(facts.projection_segments), + shape_ids: maximum.shape_ids.max(facts.shape_ids), + lifecycle_ids: maximum.lifecycle_ids.max(facts.lifecycle_ids), + projection_ids: maximum.projection_ids.max(facts.projection_ids), + } + }); + // Staged Result/Option records retain compiler-owned identities even in a + // program with no user resource declarations. Keep this list adjacent to + // the source prelude contract; tests below bind its exact spellings. + let prelude_identity_bytes = crate::private_capacity_contract::PRELUDE_CAPACITY_IDENTITIES + .into_iter() + .map(str::len) + .max() + .expect("private prelude identities are nonempty"); + let authored_identity_bytes = program.types.iter().fold(0usize, |maximum, declaration| { + let maximum = maximum.max(declaration.stable_id.len()); + match &declaration.kind { + crate::ast::TypeDeclarationKind::Resource { lifecycles } => { + lifecycles.iter().fold(maximum, |maximum, lifecycle| { + maximum.max(lifecycle.stable_id.as_deref().map(str::len).unwrap_or(0)) + }) + } + crate::ast::TypeDeclarationKind::Record { fields } => fields + .iter() + .fold(maximum, |maximum, field| maximum.max(field.stable_id.len())), + crate::ast::TypeDeclarationKind::Variant { cases } => { + cases.iter().fold(maximum, |maximum, case| { + case.fields + .iter() + .fold(maximum.max(case.stable_id.len()), |maximum, field| { + maximum.max(field.stable_id.len()) + }) + }) + } + } + }); + let maximum_declaration_identity_bytes = authored_identity_bytes.max(prelude_identity_bytes); + let maximum_type_arguments = program + .types + .iter() + .map(|declaration| declaration.type_parameters.len()) + .max() + .unwrap_or(0) + .max(2); + let maximum_resolved_type_owned_bytes = maximum_declaration_identity_bytes + .checked_add( + maximum_type_arguments + .checked_mul(std::mem::size_of::()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let mut total = CleanupRetainedStats::default(); + let mut traversal = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let mut event_traversal = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + + for function in &program.functions { + let generic_instance_identity_len = + generic_function_instance_identity_upper(program, function) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let function_roots = function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures); + let function_node_total = + scan_ast_capacity(function_roots, program, false, &mut traversal)?.nodes; + let path_segment_bytes = 32usize + .checked_add(decimal_digits(function_node_total)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let value_storage_identity_bytes_for_path = |path_len: usize| { + scoped_value_identity_upper(function, generic_instance_identity_len, path_len) + }; + let expression_storage_identity_bytes_for_path = |path_len: usize| { + scoped_expression_identity_upper(function, generic_instance_identity_len, path_len) + }; + let type_bytes_for_key = |key: CleanupTypeKey| match key { + CleanupTypeKey::Scalar => Some(0), + CleanupTypeKey::Declaration(index) => program.types[index].stable_id.len().checked_add( + program.types[index] + .type_parameters + .len() + .checked_mul(std::mem::size_of::())?, + ), + CleanupTypeKey::Unknown => Some(maximum_resolved_type_owned_bytes), + }; + let function_exit_upper = cleanup_function_exit_events(function, &mut traversal)?; + // These are exactly the source forms that can ask the lowerer for + // an exit: operation failure, postfix residual, authored/update + // scope, contract false/scope, and final success. + let mut function_stats = CleanupRetainedStats { + exit_events: function_exit_upper, + ..CleanupRetainedStats::default() + }; + let mut function_nodes = 0usize; + let mut owned_parameters = 0usize; + let mut has_try = false; + for (parameter_index, parameter) in function.params.iter().enumerate() { + if parameter.mode == crate::ast::ParamMode::Own { + let key = key_for_type(program, ¶meter.ty); + let storage_identity_bytes = + value_storage_identity_bytes_for_path(decimal_digits(parameter_index)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + add_root( + &mut function_stats, + key, + declaration_facts, + fallback, + storage_identity_bytes, + type_bytes_for_key(key) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + )?; + add_finalizer_upper( + &mut function_stats, + key, + declaration_facts, + fallback, + cleanup_parameter_finalizer_events( + function, + ¶meter.name, + program, + &mut event_traversal, + )?, + storage_identity_bytes, + )?; + function_stats.ordinary_place_storage_bytes = function_stats + .ordinary_place_storage_bytes + .checked_add(storage_identity_bytes) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + owned_parameters = owned_parameters + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + } + + let roots = function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures); + let mut traversal_path_lengths = [0usize; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + for (root_index, root) in roots.enumerate() { + let mut stack_len = 1usize; + traversal[0] = Some((root, 0usize, 0usize)); + traversal_path_lengths[0] = ast_root_identity_path_len(function, root_index); + let mut results = Vec::::with_capacity(node_capacity); + if results.capacity() != node_capacity { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + while stack_len != 0 { + stack_len -= 1; + let expression_path_len = traversal_path_lengths[stack_len]; + let (expression, next_child, result_start) = traversal[stack_len] + .take() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if next_child != 0 { + if let crate::ast::ExprKind::Block { statements, .. } = &expression.kind { + let previous = next_child - 1; + if previous < statements.len() { + let crate::ast::Statement::Let { name, .. } = &statements[previous]; + let key = results + .last() + .copied() + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let storage_identity_bytes = value_storage_identity_bytes_for_path( + expression_path_len + .checked_add(".s".len()) + .and_then(|bytes| bytes.checked_add(decimal_digits(previous))) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + add_root( + &mut function_stats, + key, + declaration_facts, + fallback, + storage_identity_bytes, + type_bytes_for_key(key) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + )?; + if facts_for_key(key, declaration_facts, fallback).leaves != 0 { + function_stats.parent_local_epochs = function_stats + .parent_local_epochs + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + let remaining = cleanup_block_binding_finalizer_events( + function, + expression, + next_child, + name, + program, + &mut event_traversal, + )?; + add_finalizer_upper( + &mut function_stats, + key, + declaration_facts, + fallback, + remaining, + storage_identity_bytes, + )?; + } + } + } + if next_child == 0 { + function_nodes = function_nodes + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + match &expression.kind { + crate::ast::ExprKind::Call { args, .. } => { + if let crate::ast::ExprKind::Call { name, .. } = &expression.kind { + if let Some(candidate) = program + .functions + .iter() + .find(|candidate| candidate.name == *name) + { + for (argument_index, parameter) in + candidate.params.iter().take(args.len()).enumerate() + { + let key = key_for_type(program, ¶meter.ty); + if parameter.mode != crate::ast::ParamMode::Own + || facts_for_key(key, declaration_facts, fallback) + .leaves + == 0 + { + continue; + } + // The caller retains a distinct + // CallArgument epoch in addition to + // the argument expression temporary. + add_root( + &mut function_stats, + key, + declaration_facts, + fallback, + 0, + 0, + )?; + let later_argument_events = args[argument_index + 1..] + .iter() + .try_fold(0usize, |events, argument| { + events + .checked_add(cleanup_expression_failure_events( + argument, + &mut event_traversal, + )?) + .ok_or_else(|| { + b109("max_builder_bytes", MAX_BUILDER_BYTES) + }) + })?; + let argument_identity_bytes = function + .stable_id + .len() + .checked_add( + stack_len + .checked_add(2) + .and_then(|depth| { + depth.checked_mul(path_segment_bytes) + }) + .ok_or_else(|| { + b109("max_builder_bytes", MAX_BUILDER_BYTES) + })?, + ) + .and_then(|bytes| bytes.checked_mul(2)) + .ok_or_else(|| { + b109("max_builder_bytes", MAX_BUILDER_BYTES) + })?; + let argument_facts = + facts_for_key(key, declaration_facts, fallback); + // Four paired CallArgument StorageId + // copies coexist (slot, region, + // Transfer destination, CallCommit + // source). Transfer::at and + // CallCommit::call add two single + // expression IDs, equal to one more + // paired upper. + let fixed_storage_copies = argument_identity_bytes + .checked_mul(5) + .and_then(|bytes| { + bytes.checked_add(maximum_resolved_type_owned_bytes) + }) + .ok_or_else(|| { + b109("max_builder_bytes", MAX_BUILDER_BYTES) + })?; + let failure_storage_copies = argument_identity_bytes + .checked_mul(argument_facts.leaves) + .and_then(|bytes| { + bytes.checked_mul(later_argument_events) + }) + .ok_or_else(|| { + b109("max_builder_bytes", MAX_BUILDER_BYTES) + })?; + function_stats.call_argument_owned_bytes = function_stats + .call_argument_owned_bytes + .checked_add(fixed_storage_copies) + .and_then(|bytes| { + bytes.checked_add(failure_storage_copies) + }) + .ok_or_else(|| { + b109("max_builder_bytes", MAX_BUILDER_BYTES) + })?; + add_finalizer_upper( + &mut function_stats, + key, + declaration_facts, + fallback, + later_argument_events, + 0, + )?; + function_stats.call_arguments = function_stats + .call_arguments + .checked_add(1) + .ok_or_else(|| { + b109("max_builder_bytes", MAX_BUILDER_BYTES) + })?; + function_stats.parent_local_epochs = function_stats + .parent_local_epochs + .checked_add(1) + .ok_or_else(|| { + b109("max_builder_bytes", MAX_BUILDER_BYTES) + })?; + } + } + } + } + crate::ast::ExprKind::Match { arms, .. } => { + function_stats.variant_edges = + function_stats + .variant_edges + .checked_add(arms.len().checked_mul(2).ok_or_else(|| { + b109("max_builder_bytes", MAX_BUILDER_BYTES) + })?) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + crate::ast::ExprKind::Try { .. } => { + has_try = true; + function_stats.staged_results = function_stats + .staged_results + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + function_stats.variant_edges = function_stats + .variant_edges + .checked_add(2) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + _ => {} + } + } + if let Some(child) = ast_child(expression, next_child) { + if stack_len + 2 > traversal.len() { + return Err(b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + )); + } + traversal[stack_len] = Some((expression, next_child + 1, result_start)); + traversal_path_lengths[stack_len] = expression_path_len; + traversal[stack_len + 1] = Some((child, 0, results.len())); + traversal_path_lengths[stack_len + 1] = expression_path_len + .checked_add(ast_child_identity_path_increment( + expression, next_child, program, + )) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + stack_len += 2; + continue; + } + + let children = &results[result_start..]; + let key = match &expression.kind { + crate::ast::ExprKind::Int(_) | crate::ast::ExprKind::Bool(_) => { + CleanupTypeKey::Scalar + } + crate::ast::ExprKind::Var(name) => { + variable_key(program, function, name, &traversal, stack_len, &results)? + } + crate::ast::ExprKind::Call { name, .. } => program + .functions + .iter() + .find(|candidate| candidate.name == *name) + .map(|candidate| key_for_type(program, &candidate.return_type)) + .unwrap_or(CleanupTypeKey::Scalar), + crate::ast::ExprKind::Unary { .. } | crate::ast::ExprKind::Binary { .. } => { + CleanupTypeKey::Scalar + } + crate::ast::ExprKind::Block { .. } => { + children.last().copied().unwrap_or(CleanupTypeKey::Scalar) + } + crate::ast::ExprKind::If { .. } => { + children.get(1).copied().unwrap_or(CleanupTypeKey::Unknown) + } + crate::ast::ExprKind::ConstructRecord { + type_name, fields, .. + } => { + let declaration_index = program + .types + .iter() + .position(|declaration| declaration.name == *type_name); + if let Some(declaration_index) = declaration_index { + let declaration = &program.types[declaration_index]; + let declared_fields = match &declaration.kind { + crate::ast::TypeDeclarationKind::Record { fields } => fields, + _ => { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + }; + let storage_identity_bytes = + expression_storage_identity_bytes_for_path(expression_path_len) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + for (field_index, initializer) in fields.iter().enumerate() { + let field_key = children + .get(field_index) + .copied() + .unwrap_or(CleanupTypeKey::Unknown); + let facts = facts_for_key(field_key, declaration_facts, fallback); + if facts.leaves == 0 { + continue; + } + let later_failure_events = fields[field_index + 1..] + .iter() + .try_fold(0usize, |events, later| { + events + .checked_add(cleanup_expression_failure_events( + &later.value, + &mut event_traversal, + )?) + .ok_or_else(|| { + b109("max_builder_bytes", MAX_BUILDER_BYTES) + }) + })?; + let field_identity_bytes = declared_fields + .iter() + .find(|field| field.name == initializer.name) + .map(|field| field.stable_id.len()) + .unwrap_or(maximum_declaration_identity_bytes); + add_parent_local_record_prefix( + &mut function_stats, + facts, + later_failure_events, + storage_identity_bytes, + field_identity_bytes, + )?; + } + CleanupTypeKey::Declaration(declaration_index) + } else { + CleanupTypeKey::Unknown + } + } + crate::ast::ExprKind::ConstructVariant { type_name, .. } => program + .types + .iter() + .position(|declaration| declaration.name == *type_name) + .map(CleanupTypeKey::Declaration) + .unwrap_or_else(|| { + if matches!(type_name.as_str(), "Option" | "Result") { + CleanupTypeKey::Scalar + } else { + CleanupTypeKey::Unknown + } + }), + crate::ast::ExprKind::Match { arms, .. } => { + if let Some(arm) = arms.first() { + if let crate::ast::ExprKind::Var(name) = &arm.value.kind { + pattern_binding_key(program, &arm.pattern, name)? + .unwrap_or(CleanupTypeKey::Unknown) + } else { + children.get(1).copied().unwrap_or(CleanupTypeKey::Unknown) + } + } else { + CleanupTypeKey::Unknown + } + } + crate::ast::ExprKind::Try { .. } => CleanupTypeKey::Scalar, + crate::ast::ExprKind::UpdateRecord { fields, .. } => { + let base = children.first().copied().unwrap_or(CleanupTypeKey::Unknown); + let destination_storage_identity_bytes = + expression_storage_identity_bytes_for_path(expression_path_len) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + for (field_index, initializer) in fields.iter().enumerate() { + let replacement_key = children + .get(field_index + 1) + .copied() + .unwrap_or(CleanupTypeKey::Unknown); + let replacement_facts = + facts_for_key(replacement_key, declaration_facts, fallback); + if replacement_facts.leaves == 0 { + continue; + } + let later_failure_events = fields[field_index + 1..].iter().try_fold( + 0usize, + |events, later| { + events + .checked_add(cleanup_expression_failure_events( + &later.value, + &mut event_traversal, + )?) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES)) + }, + )?; + let field_identity_bytes = match base { + CleanupTypeKey::Declaration(index) => { + match &program.types[index].kind { + crate::ast::TypeDeclarationKind::Record { fields } => { + fields + .iter() + .find(|field| field.name == initializer.name) + .map(|field| field.stable_id.len()) + .unwrap_or(maximum_declaration_identity_bytes) + } + _ => maximum_declaration_identity_bytes, + } + } + _ => maximum_declaration_identity_bytes, + }; + add_parent_local_update_prefix( + &mut function_stats, + replacement_facts, + later_failure_events, + destination_storage_identity_bytes, + field_identity_bytes, + )?; + } + let storage_identity_bytes = expression_storage_identity_bytes_for_path( + expression_path_len + .checked_add(".base".len()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + add_root( + &mut function_stats, + base, + declaration_facts, + fallback, + storage_identity_bytes, + type_bytes_for_key(base) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + )?; + let staged_base_exits = fields.iter().try_fold( + 1usize, + |events, field| -> Result { + events + .checked_add(cleanup_expression_failure_events( + &field.value, + &mut event_traversal, + )?) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES)) + }, + )?; + add_finalizer_upper( + &mut function_stats, + base, + declaration_facts, + fallback, + staged_base_exits, + storage_identity_bytes, + )?; + if facts_for_key(base, declaration_facts, fallback).leaves != 0 { + function_stats.parent_local_epochs = function_stats + .parent_local_epochs + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + base + } + crate::ast::ExprKind::Project { base, field, .. } => { + let base_key = children.first().copied().unwrap_or(CleanupTypeKey::Unknown); + let selected = match base_key { + CleanupTypeKey::Declaration(index) => { + let declaration = &program.types[index]; + match &declaration.kind { + crate::ast::TypeDeclarationKind::Record { fields } => fields + .iter() + .find(|candidate| candidate.name == *field) + .map(|candidate| key_for_type(program, &candidate.ty)), + _ => None, + } + } + _ => None, + } + .unwrap_or(CleanupTypeKey::Unknown); + if !matches!(base.kind, crate::ast::ExprKind::Var(_)) { + let base_facts = facts_for_key(base_key, declaration_facts, fallback); + let residual = if let CleanupTypeKey::Declaration(index) = base_key { + let selected_facts = + facts_for_key(selected, declaration_facts, fallback); + let field_identity_bytes = match &program.types[index].kind { + crate::ast::TypeDeclarationKind::Record { fields } => fields + .iter() + .find(|candidate| candidate.name == *field) + .map(|candidate| candidate.stable_id.len()) + .unwrap_or(maximum_declaration_identity_bytes), + _ => maximum_declaration_identity_bytes, + }; + let selected_projection_segments = selected_facts + .projection_segments + .checked_add(selected_facts.leaves) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let selected_projection_ids = selected_facts + .projection_ids + .checked_add( + selected_facts + .leaves + .checked_mul(field_identity_bytes) + .ok_or_else(|| { + b109("max_builder_bytes", MAX_BUILDER_BYTES) + })?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if selected_facts.leaves <= base_facts.leaves + && selected_projection_segments + <= base_facts.projection_segments + && selected_facts.lifecycle_ids <= base_facts.lifecycle_ids + && selected_projection_ids <= base_facts.projection_ids + { + CleanupTypeFacts { + leaves: base_facts.leaves - selected_facts.leaves, + projection_segments: base_facts.projection_segments + - selected_projection_segments, + lifecycle_ids: base_facts.lifecycle_ids + - selected_facts.lifecycle_ids, + projection_ids: base_facts.projection_ids + - selected_projection_ids, + ..CleanupTypeFacts::default() + } + } else { + // Generic field substitution is not yet + // materialized in this source census. + // Keeping the complete base is the exact + // admitted fallback, never a subtraction + // from unrelated declaration facts. + base_facts + } + } else { + // A valid unresolved generic projection may + // still instantiate to the maximum admitted + // resource aggregate. Retain the whole fallback + // rather than assuming which field transferred. + base_facts + }; + let base_path_len = expression_path_len + .checked_add(ast_child_identity_path_increment( + expression, 0, program, + )) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let storage_identity_bytes = + expression_storage_identity_bytes_for_path(base_path_len) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let remaining_events = cleanup_parent_local_remaining_finalizer_events( + function, + root, + &traversal, + stack_len, + &mut event_traversal, + )?; + add_parent_local_projection_residual( + &mut function_stats, + residual, + remaining_events, + storage_identity_bytes, + )?; + } + selected + } + }; + if !matches!(expression.kind, crate::ast::ExprKind::Var(_)) { + let storage_identity_bytes = + expression_storage_identity_bytes_for_path(expression_path_len) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + add_root( + &mut function_stats, + key, + declaration_facts, + fallback, + storage_identity_bytes, + type_bytes_for_key(key) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + )?; + if facts_for_key(key, declaration_facts, fallback).leaves != 0 { + function_stats.parent_local_epochs = function_stats + .parent_local_epochs + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + function_stats.parent_local_zero_lifetime_transfers = function_stats + .parent_local_zero_lifetime_transfers + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + } + results.truncate(result_start); + results.push(key); + } + if results.len() != 1 { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + } + if function_nodes != function_node_total { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + + let result_key = key_for_type(program, &function.return_type); + add_root( + &mut function_stats, + result_key, + declaration_facts, + fallback, + 0, + type_bytes_for_key(result_key) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + )?; + if facts_for_key(result_key, declaration_facts, fallback).leaves != 0 { + function_stats.parent_local_epochs = function_stats + .parent_local_epochs + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + if facts_for_key(result_key, declaration_facts, fallback).leaves != 0 { + function_stats.ordinary_slot_payload_bytes = function_stats + .ordinary_slot_payload_bytes + .checked_add( + value_storage_identity_bytes_for_path(0) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + let result_finalizer_events = + function + .ensures + .iter() + .try_fold(function.ensures.len(), |events, ensure| { + events + .checked_add(cleanup_expression_failure_events( + ensure, + &mut event_traversal, + )?) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES)) + })?; + add_finalizer_upper( + &mut function_stats, + result_key, + declaration_facts, + fallback, + result_finalizer_events, + 0, + )?; + if has_try { + // The plan retains one Body staging source in addition to every + // residual source materialized by a postfix `?`. + function_stats.staged_results = function_stats + .staged_results + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + let expression_identity_bytes = function + .stable_id + .len() + .checked_add( + function_nodes + .checked_mul(path_segment_bytes) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .and_then(|bytes| bytes.checked_add(fallback.shape_ids)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let staged_owned_bytes = expression_identity_bytes + .checked_mul(2) + .and_then(|bytes| bytes.checked_add(maximum_resolved_type_owned_bytes.checked_mul(2)?)) + .and_then(|bytes| bytes.checked_add(maximum_declaration_identity_bytes.checked_mul(5)?)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + function_stats.stage_identity_and_type_bytes = function_stats + .staged_results + .checked_mul(staged_owned_bytes) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + function_stats.variant_identity_bytes = function_stats + .variant_edges + .checked_mul( + expression_identity_bytes + .checked_add(maximum_declaration_identity_bytes) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if function_stats.leaves != 0 { + // Each cleanup storage epoch is initialized once and transferred + // at most once; its inventory/plan slot is accounted separately. + // CallCommit argument sources are additional projected places. + let root_transition_copies = function_stats + .roots + .checked_mul(2) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let projected_place_copies = root_transition_copies + .checked_add(function_stats.call_arguments) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + function_stats.place_copies = function_stats + .roots + .checked_add(projected_place_copies) + .and_then(|value| value.checked_add(owned_parameters)) + .and_then(|value| value.checked_add(1)) + .and_then(|value| value.checked_add(function_stats.finalizer_copies)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + function_stats.place_projection_segments = function_stats + .projection_segments + .checked_mul(2) + .and_then(|segments| { + segments.checked_add( + fallback + .projection_segments + .checked_mul(function_stats.call_arguments)?, + ) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + function_stats.place_projection_ids = function_stats + .projection_ids + .checked_mul(2) + .and_then(|bytes| { + bytes.checked_add( + fallback + .projection_ids + .checked_mul(function_stats.call_arguments)?, + ) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + + let multiplicity = if function.type_parameters.is_empty() { + 1 + } else { + generic_instance_upper + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))? + }; + total + .merge( + function_stats + .scaled(multiplicity) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + Ok(total) +} + +#[derive(Clone, Copy)] +struct HirPreResolveCapacity { + retained_upper: usize, + scratch_upper: usize, + declaration_index_upper: usize, + cleanup_retained_upper: usize, + cleanup_authority_upper: usize, + cleanup_exit_events_upper: usize, + cleanup_fallback_roots: usize, + cleanup_call_argument_owned_upper: usize, + cleanup_plan_structural_upper: usize, + #[cfg(test)] + cleanup_parent_local_lifetime_upper: usize, + #[cfg(test)] + cleanup_parent_local_projection_lifetime_upper: usize, + #[cfg(test)] + cleanup_parent_local_update_prefix_lifetime_upper: usize, + #[cfg(test)] + cleanup_proof: CleanupCapacityProofTerms, + phase_peaks: [usize; 8], + disposal_frames: usize, +} + +#[cfg(test)] +#[derive(Clone, Copy, Debug)] +struct CleanupCapacityProofTerms { + stats: CleanupRetainedStats, + inventory_slot_capacity_entries: usize, + inventory_flag_capacity_entries: usize, + inventory_entry_capacity_entries: usize, + plan_slot_capacity_entries: usize, + plan_entry_capacity_entries: usize, + shape_field_capacity_entries: usize, + flag_projection_capacity_entries: usize, + place_projection_capacity_entries: usize, + finalizer_projection_capacity_entries: usize, + finalizer_capacity_entries: usize, + block_capacity_entries: usize, + edge_capacity_entries: usize, + region_capacity_entries: usize, + exit_capacity_entries: usize, + status_capacity_entries: usize, + transition_capacity_entries: usize, + branch_edge_capacity_entries: usize, + region_slot_capacity_entries: usize, + exit_region_capacity_entries: usize, + status_case_capacity_entries: usize, +} + +impl HirPreResolveCapacity { + fn complete(self) -> Option { + self.retained_upper.checked_add(self.scratch_upper) + } + + #[cfg(test)] + fn phase_peaks(self) -> [usize; 8] { + self.phase_peaks + } +} + +#[cfg(test)] +fn hir_capacity_terms_for_test( + program: &Program, + source_bytes: usize, +) -> Result<(usize, usize, usize), Diagnostic> { + let mut stack = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let capacity = hir_pre_resolve_capacity(program, source_bytes, &mut stack)?; + Ok(( + capacity.retained_upper, + capacity.scratch_upper, + capacity.cleanup_retained_upper, + )) +} + +fn hir_pre_resolve_capacity<'a>( + program: &'a Program, + source_bytes: usize, + stack: &mut [Option<(&'a crate::ast::Expr, usize, usize)>; MAX_SEMANTIC_EXPRESSION_DEPTH + 1], +) -> Result { + let all_roots = program.functions.iter().flat_map(|function| { + function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures) + }); + let stats = scan_ast_capacity(all_roots, program, false, stack)?; + let contract_index_digits = program.functions.iter().fold(1usize, |digits, function| { + digits + .max(decimal_digits(function.requires.len().saturating_sub(1))) + .max(decimal_digits(function.ensures.len().saturating_sub(1))) + .max(decimal_digits(function.params.len().saturating_sub(1))) + }); + let monomorphic_roots = program + .functions + .iter() + .filter(|function| function.type_parameters.is_empty()) + .flat_map(|function| { + function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures) + }); + let reachable_generic_calls = + scan_ast_capacity(monomorphic_roots, program, true, stack)?.generic_calls; + let mut largest_template = AstCapacityStats::default(); + for function in &program.functions { + if function.type_parameters.is_empty() { + continue; + } + let roots = function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures); + let template = scan_ast_capacity(roots, program, false, stack)?; + largest_template.nodes = largest_template.nodes.max(template.nodes); + largest_template.cumulative_depth = largest_template + .cumulative_depth + .max(template.cumulative_depth); + largest_template.max_depth = largest_template.max_depth.max(template.max_depth); + largest_template.max_match_arms = + largest_template.max_match_arms.max(template.max_match_arms); + largest_template.max_indexed_children = largest_template + .max_indexed_children + .max(template.max_indexed_children); + largest_template.depth_arm_product_sum = largest_template + .depth_arm_product_sum + .max(template.depth_arm_product_sum); + largest_template.depth_width_product_sum = largest_template + .depth_width_product_sum + .max(template.depth_width_product_sum); + largest_template.local_bindings = + largest_template.local_bindings.max(template.local_bindings); + largest_template.pattern_bindings = largest_template + .pattern_bindings + .max(template.pattern_bindings); + largest_template.binding_name_bytes = largest_template + .binding_name_bytes + .max(template.binding_name_bytes); + largest_template.binding_depth_sum = largest_template + .binding_depth_sum + .max(template.binding_depth_sum); + largest_template.max_index_digits = largest_template + .max_index_digits + .max(template.max_index_digits); + } + let declarations = program + .types + .len() + .checked_add(program.interfaces.len()) + .and_then(|value| value.checked_add(program.functions.len())) + .and_then(|value| { + program + .interfaces + .iter() + .try_fold(value, |value, interface| { + value.checked_add(interface.imports.len()) + }) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let nested_declarations = program + .types + .iter() + .try_fold(declarations, |count, declaration| { + let count = count.checked_add(declaration.type_parameters.len())?; + match &declaration.kind { + crate::ast::TypeDeclarationKind::Resource { lifecycles } => { + count.checked_add(lifecycles.len()) + } + crate::ast::TypeDeclarationKind::Record { fields } => { + count.checked_add(fields.len()) + } + crate::ast::TypeDeclarationKind::Variant { cases } => cases + .iter() + .try_fold(count.checked_add(cases.len())?, |count, case| { + count.checked_add(case.fields.len()) + }), + } + }) + .and_then(|count| { + program.functions.iter().try_fold(count, |count, function| { + count + .checked_add(function.type_parameters.len())? + .checked_add(function.params.len()) + }) + }) + .and_then(|count| { + program + .interfaces + .iter() + .try_fold(count, |count, interface| { + interface.imports.iter().try_fold(count, |count, import| { + count.checked_add(import.params.len()) + }) + }) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + // The longest indexed segment is `.arm..binding.`; derive its digit + // widths from the widest admitted authored node instead of assuming a + // machine-usize textual width. Resolved + // expression identity, value identity, cleanup inventory, cleanup plan, + // and validation/index ownership can retain at most six path-bearing + // copies. Fixed node/declaration terms cover enum/vector/BTree node bodies. + let maximum_index_digits = stats.max_index_digits.max(contract_index_digits); + let indexed_path_segment_bytes = 15usize + .checked_add( + maximum_index_digits + .checked_mul(2) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let cleanup_node_inline = std::mem::size_of::() + .checked_add(std::mem::size_of::()) + .and_then(|bytes| { + bytes.checked_add(std::mem::size_of::()) + }) + .and_then(|bytes| { + bytes.checked_add(std::mem::size_of::()) + }) + .and_then(|bytes| { + bytes.checked_add(std::mem::size_of::()) + }) + .and_then(|bytes| { + bytes.checked_add(std::mem::size_of::()) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let retained_node_inline = std::mem::size_of::() + .checked_add(cleanup_node_inline) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let type_expansion = declaration_dag_expansion(program, reachable_generic_calls)?; + let maximum_resource_leaves = type_expansion.maximum_resource_leaves; + let disposal_frames = stats + .max_depth + .checked_mul(4) + .and_then(|frames| { + frames.checked_add(type_expansion.maximum_type_occurrences.checked_mul(2)?) + }) + .and_then(|frames| frames.checked_add(16)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let path_copy_upper = 1usize + .checked_add(maximum_resource_leaves.min(5)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let cleanup_path_copies = maximum_resource_leaves.min(5); + let mut exact_expression_identity_bytes = 0usize; + let mut cleanup_plan_uncovered_identity_bytes = 0usize; + for function in &program.functions { + let multiplicity = if function.type_parameters.is_empty() { + 1 + } else { + reachable_generic_calls + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))? + }; + let (function_expression_bytes, function_plan_bytes) = + cleanup_plan_variable_identity_bytes(function, program, cleanup_path_copies)?; + exact_expression_identity_bytes = exact_expression_identity_bytes + .checked_add( + function_expression_bytes + .checked_mul(multiplicity) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + cleanup_plan_uncovered_identity_bytes = cleanup_plan_uncovered_identity_bytes + .checked_add( + function_plan_bytes + .checked_mul(multiplicity) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + let node_bytes = stats + .nodes + .checked_mul(retained_node_inline) + .and_then(|bytes| { + bytes.checked_add(exact_expression_identity_bytes.checked_mul(path_copy_upper)?) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + // Peak iterative resolver/validator/cleanup scratch. The declaration + // census is a conservative upper for simultaneously live bindings/flags. + // Branch continuations retain at most depth copies; Match retains one + // FlowState per authored arm. Indexed child vectors/commit lists are + // bounded by the widest authored node. + let parameter_bindings = program + .functions + .iter() + .try_fold(0usize, |count, function| { + count.checked_add(function.params.len()) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let maximum_declared_fields = program + .types + .iter() + .try_fold(0usize, |total, declaration| { + let fields = match &declaration.kind { + crate::ast::TypeDeclarationKind::Resource { .. } => 1, + crate::ast::TypeDeclarationKind::Record { fields } => fields.len(), + crate::ast::TypeDeclarationKind::Variant { cases } => cases + .iter() + .try_fold(0usize, |count, case| count.checked_add(case.fields.len()))?, + }; + total.checked_add(fields) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))? + .max(1); + let binding_slots = parameter_bindings + .checked_add(stats.local_bindings) + .and_then(|width| width.checked_add(stats.pattern_bindings)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + // A binding of an aggregate can contribute one ownership/partial-place + // fact per resource leaf. The declaration-field sum is a no-allocation + // upper for an acyclic declaration graph, while the declaration verifier + // rejects cycles before semantic admission. + let live_state_width = binding_slots + .checked_mul(maximum_resource_leaves) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))? + .max(1); + let branch_scope_copies = stats + .depth_arm_product_sum + .checked_add(stats.max_depth) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let parameter_name_bytes = program + .functions + .iter() + .try_fold(0usize, |bytes, function| { + function.params.iter().try_fold(bytes, |bytes, parameter| { + bytes.checked_add(parameter.name.len()) + }) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let binding_identity_bytes = stats + .binding_name_bytes + .checked_add(parameter_name_bytes) + .and_then(|bytes| { + bytes.checked_add( + stats + .binding_depth_sum + .checked_mul(indexed_path_segment_bytes)?, + ) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let scope_entry_inline = + std::mem::size_of::<(crate::hir::ValueId, ResolvedType, OwnershipMode)>(); + let scope_payload_bytes = live_state_width + .checked_mul(scope_entry_inline) + .and_then(|bytes| { + bytes.checked_add(binding_identity_bytes.checked_mul(maximum_declared_fields)?) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let indexed_result_bytes = std::mem::size_of::() + .checked_add(std::mem::size_of::()) + .and_then(|bytes| { + bytes.checked_add(std::mem::size_of::()) + }) + .and_then(|bytes| bytes.checked_add(CLEANUP_EVAL_RESULT_BYTES)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let declaration_work_bytes = std::mem::size_of::() + .checked_add(std::mem::size_of::()) + .and_then(|bytes| { + bytes.checked_add(std::mem::size_of::< + crate::hir::ResolvedVariantCaseDeclaration, + >()) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let source_phase = stats + .max_depth + .checked_mul(SOURCE_VERIFIER_FRAME_BYTES) + .and_then(|bytes| bytes.checked_add(branch_scope_copies.checked_mul(scope_payload_bytes)?)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let resolver_phase = stats + .max_depth + .checked_mul(HIR_RESOLVER_FRAME_BYTES) + .and_then(|bytes| { + bytes.checked_add( + stats + .max_depth + .checked_mul(stats.max_indexed_children.max(1))? + .checked_mul(indexed_result_bytes)?, + ) + }) + .and_then(|bytes| bytes.checked_add(branch_scope_copies.checked_mul(scope_payload_bytes)?)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let validator_phase = stats + .max_depth + .checked_mul(HIR_VALIDATOR_FRAME_BYTES) + .and_then(|bytes| bytes.checked_add(branch_scope_copies.checked_mul(scope_payload_bytes)?)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let inventory_phase = maximum_resource_leaves + .checked_mul( + CLEANUP_INVENTORY_SHAPE_FRAME_BYTES + + std::mem::size_of::() + + std::mem::size_of::(), + ) + .and_then(|bytes| { + bytes.checked_add( + stats + .max_depth + .checked_mul(CLEANUP_INVENTORY_EXPR_FRAME_BYTES)?, + ) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let plan_entry_bytes = std::mem::size_of::() + + std::mem::size_of::() + + std::mem::size_of::() + + std::mem::size_of::() + + std::mem::size_of::(); + let cleanup_phase = stats + .max_depth + .checked_mul(CLEANUP_LOWER_FRAME_BYTES) + .and_then(|bytes| { + bytes.checked_add( + stats + .max_depth + .checked_mul(stats.max_indexed_children.max(1))? + .checked_mul(CLEANUP_EVAL_RESULT_BYTES)?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + stats + .nodes + .checked_mul(maximum_resource_leaves)? + .checked_mul(plan_entry_bytes)?, + ) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let call_index_phase = stats + .max_depth + .checked_mul(CALL_INDEX_FRAME_BYTES) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let closure_identity_entries = program + .functions + .len() + .checked_add(program.interfaces.len()) + .and_then(|entries| entries.checked_add(nested_declarations)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))? + .max(1); + let closure_btree_entry_overhead = std::mem::size_of::>(); + let closure_reference_headers = program + .functions + .len() + .checked_mul(std::mem::size_of::<&ResolvedFunction>()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let closure_phase = Some(closure_reference_headers) + // The selected closure borrows functions from the live resolved + // program. Only the sorted reference vector is retained; expression + // and cleanup trees are neither cloned nor separately dropped. + // by_id, state, depths, reached-imports, pending/visited/direct-call + // sets, and contract traversal sets can overlap. One separately + // allocated BTree node per identity plus the full authored source as + // every key payload is conservative for each of the nine containers. + .and_then(|bytes| { + bytes.checked_add( + closure_identity_entries + .checked_mul( + std::mem::size_of::<(String, usize)>() + .checked_add(closure_btree_entry_overhead)?, + )? + .checked_mul(9)?, + ) + }) + .and_then(|bytes| bytes.checked_add(source_bytes.checked_mul(9)?)) + // DFS retains one ID and one indexed direct-call vector per depth. + .and_then(|bytes| { + bytes.checked_add( + MAX_CALL_DEPTH.checked_mul( + std::mem::size_of::() + .checked_add(indexed_path_segment_bytes)?, + )?, + ) + }) + // While converting a direct-call set into the frame Vec, both + // container backings and all ID strings coexist. + .and_then(|bytes| { + bytes.checked_add( + closure_identity_entries.checked_mul( + std::mem::size_of::() + .checked_add(std::mem::size_of::())? + .checked_add(closure_btree_entry_overhead)?, + )?, + ) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let frame_machine_scratch = source_phase + .max(resolver_phase) + .max(validator_phase) + .max(inventory_phase) + .max(cleanup_phase) + .max(call_index_phase) + .max(closure_phase) + .checked_add( + nested_declarations + .checked_mul(declaration_work_bytes) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let declaration_phase_overlap = nested_declarations + .checked_mul(declaration_work_bytes) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + // Each distinct reachable specialization may clone and resolve one whole + // template while the resolved template remains live. Count every call + // site (even duplicate instances) against the largest template, which is + // conservative without allocating a pre-resolution identity set. + let specialization_bytes = largest_template + .nodes + .checked_mul( + retained_node_inline + .checked_mul(2) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .and_then(|bytes| { + bytes.checked_add( + largest_template + .cumulative_depth + .checked_mul(indexed_path_segment_bytes.checked_mul(2 * 6)?)?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + largest_template + .max_depth + .checked_mul(HIR_RESOLVER_FRAME_BYTES)?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + largest_template + .depth_arm_product_sum + .checked_add(largest_template.max_depth)? + .checked_mul(live_state_width)? + .checked_mul(scope_entry_inline)?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + largest_template + .max_depth + .checked_mul(largest_template.max_indexed_children.max(1))? + .checked_mul(indexed_result_bytes)?, + ) + }) + .and_then(|bytes| bytes.checked_mul(reachable_generic_calls)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + // TypeFacts layout keys recursively embed each child key. The fixed + // per-occurrence syntax consists of four decimal lengths/separators plus + let type_fact_layout_upper = crate::private_capacity_contract::type_facts_layout_upper( + source_bytes, + program.types.len(), + type_expansion.maximum_type_occurrences, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let type_facts_frame_bytes = std::mem::size_of::<( + ResolvedType, + String, + DeclarationId, + crate::hir::DeclarationKind, + usize, + )>(); + let type_facts_scratch = type_expansion + .maximum_type_occurrences + .checked_mul(type_facts_frame_bytes) + .and_then(|bytes| bytes.checked_add(type_fact_layout_upper.checked_mul(2)?)) + .and_then(|bytes| { + bytes.checked_add( + program + .types + .len() + .checked_mul(std::mem::size_of::<(String, crate::hir::TypeFacts)>())?, + ) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let declaration_index_upper = crate::private_capacity_contract::declaration_index_upper( + source_bytes, + program.types.len(), + program.interfaces.len(), + program.functions.len(), + type_fact_layout_upper, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + // The declaration-DAG pass also performs a typed source-flow census while + // its exact temporary memo is still authorized. Unlike the former + // `all roots * largest type * all nodes` product, every persistent shape, + // flag and projection below is charged against the authored type of the + // storage root that can create it. Plan places and exits are separate + // copies because they coexist with the inventory and plan-slot shapes. + let cleanup = type_expansion.cleanup_retained; + let cleanup_function_instance_upper = program + .functions + .iter() + .try_fold(0usize, |instances, function| { + let multiplicity = if function.type_parameters.is_empty() { + 1 + } else { + reachable_generic_calls.checked_add(1)? + }; + instances.checked_add(multiplicity) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let flag_capacity_extra = + retained_vec_capacity_extra(cleanup.leaves, cleanup_function_instance_upper) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let shape_field_capacity_extra = retained_vec_capacity_extra( + cleanup.shape_fields, + cleanup.occurrences.min(cleanup.shape_fields), + ) + .and_then(|extra| extra.checked_mul(2)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let flag_projection_capacity_extra = retained_vec_capacity_extra( + cleanup.projection_segments, + cleanup.leaves.min(cleanup.projection_segments), + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let place_projection_capacity_extra = retained_vec_capacity_extra( + cleanup.place_projection_segments, + cleanup.place_copies.min(cleanup.place_projection_segments), + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let finalizer_projection_capacity_extra = retained_vec_capacity_extra( + cleanup.finalizer_projection_segments, + cleanup + .finalizer_copies + .min(cleanup.finalizer_projection_segments), + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let finalizer_capacity_extra = retained_vec_capacity_extra( + cleanup.finalizer_copies, + cleanup.exit_events.min(cleanup.finalizer_copies), + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let entry_state_capacity_extra = retained_vec_capacity_extra( + cleanup.roots, + cleanup_function_instance_upper.min(cleanup.roots), + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let inventory_slot_capacity_extra = retained_vec_capacity_extra( + cleanup.roots, + cleanup_function_instance_upper.min(cleanup.roots), + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let plan_slot_capacity_extra = retained_vec_capacity_extra( + cleanup.roots, + cleanup_function_instance_upper.min(cleanup.roots), + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let inventory_entry_capacity_entries = cleanup + .roots + .checked_add(entry_state_capacity_extra) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + #[cfg(test)] + let cleanup_parent_local_lifetime_upper = cleanup + .parent_local_finalizer_copies + .checked_mul(std::mem::size_of::()) + .and_then(|bytes| { + bytes.checked_add( + cleanup + .parent_local_finalizer_projection_segments + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| bytes.checked_add(cleanup.parent_local_finalizer_lifecycle_ids)) + .and_then(|bytes| bytes.checked_add(cleanup.parent_local_finalizer_projection_ids)) + .and_then(|bytes| bytes.checked_add(cleanup.parent_local_finalizer_storage_bytes)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + #[cfg(test)] + let cleanup_parent_local_projection_lifetime_upper = { + let action_capacity_extra = retained_vec_capacity_extra( + cleanup.parent_local_projection_finalizer_copies, + cleanup + .parent_local_projection_exit_groups + .min(cleanup.parent_local_projection_finalizer_copies), + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let projection_capacity_extra = retained_vec_capacity_extra( + cleanup.parent_local_projection_finalizer_projection_segments, + cleanup + .parent_local_projection_finalizer_copies + .min(cleanup.parent_local_projection_finalizer_projection_segments), + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + cleanup + .parent_local_projection_finalizer_copies + .checked_add(action_capacity_extra) + .and_then(|entries| { + entries.checked_mul(std::mem::size_of::()) + }) + .and_then(|bytes| { + bytes.checked_add( + cleanup + .parent_local_projection_finalizer_projection_segments + .checked_add(projection_capacity_extra)? + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add(cleanup.parent_local_projection_finalizer_lifecycle_ids) + }) + .and_then(|bytes| { + bytes.checked_add(cleanup.parent_local_projection_finalizer_projection_ids) + }) + .and_then(|bytes| { + bytes.checked_add(cleanup.parent_local_projection_finalizer_storage_bytes) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))? + }; + #[cfg(test)] + let cleanup_parent_local_update_prefix_lifetime_upper = { + let action_capacity_extra = retained_vec_capacity_extra( + cleanup.parent_local_update_prefix_finalizer_copies, + cleanup + .parent_local_update_prefix_exit_groups + .min(cleanup.parent_local_update_prefix_finalizer_copies), + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let projection_capacity_extra = retained_vec_capacity_extra( + cleanup.parent_local_update_prefix_finalizer_projection_segments, + cleanup + .parent_local_update_prefix_finalizer_copies + .min(cleanup.parent_local_update_prefix_finalizer_projection_segments), + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + cleanup + .parent_local_update_prefix_finalizer_copies + .checked_add(action_capacity_extra) + .and_then(|entries| { + entries.checked_mul(std::mem::size_of::()) + }) + .and_then(|bytes| { + bytes.checked_add( + cleanup + .parent_local_update_prefix_finalizer_projection_segments + .checked_add(projection_capacity_extra)? + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add(cleanup.parent_local_update_prefix_finalizer_lifecycle_ids) + }) + .and_then(|bytes| { + bytes.checked_add(cleanup.parent_local_update_prefix_finalizer_projection_ids) + }) + .and_then(|bytes| { + bytes.checked_add(cleanup.parent_local_update_prefix_finalizer_storage_bytes) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))? + }; + let cleanup_retained_upper = cleanup + .roots + .checked_mul( + std::mem::size_of::() + + std::mem::size_of::(), + ) + .and_then(|bytes| { + bytes.checked_add( + cleanup + .occurrences + .checked_mul(2)? + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + cleanup + .shape_fields + .checked_mul(2)? + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| bytes.checked_add(cleanup.shape_ids.checked_mul(2)?)) + .and_then(|bytes| { + bytes.checked_add( + cleanup + .leaves + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| bytes.checked_add(cleanup.lifecycle_ids)) + .and_then(|bytes| { + bytes.checked_add( + cleanup + .projection_segments + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| bytes.checked_add(cleanup.projection_ids)) + .and_then(|bytes| { + bytes.checked_add( + cleanup + .place_copies + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + cleanup + .place_projection_segments + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| bytes.checked_add(cleanup.place_projection_ids)) + .and_then(|bytes| { + bytes.checked_add( + cleanup + .finalizer_copies + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + cleanup + .finalizer_projection_segments + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| bytes.checked_add(cleanup.finalizer_lifecycle_ids)) + .and_then(|bytes| bytes.checked_add(cleanup.finalizer_projection_ids)) + .and_then(|bytes| { + bytes.checked_add(cleanup.staged_results.checked_mul(std::mem::size_of::< + semaprax::cleanup_plan::StagedCopyResultSource, + >())?) + }) + .and_then(|bytes| { + bytes.checked_add( + cleanup + .variant_edges + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| bytes.checked_add(cleanup.stage_identity_and_type_bytes)) + .and_then(|bytes| bytes.checked_add(cleanup.variant_identity_bytes)) + .and_then(|bytes| { + bytes.checked_add(cleanup.call_arguments.checked_mul(std::mem::size_of::< + semaprax::cleanup_plan::CallArgumentTransfer, + >())?) + }) + .and_then(|bytes| bytes.checked_add(cleanup.call_argument_owned_bytes)) + .and_then(|bytes| bytes.checked_add(cleanup.ordinary_slot_payload_bytes)) + .and_then(|bytes| bytes.checked_add(cleanup.ordinary_place_storage_bytes)) + .and_then(|bytes| bytes.checked_add(cleanup.ordinary_finalizer_storage_bytes)) + .and_then(|bytes| { + bytes.checked_add( + flag_capacity_extra + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + shape_field_capacity_extra + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + flag_projection_capacity_extra.checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + place_projection_capacity_extra.checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + finalizer_projection_capacity_extra + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + finalizer_capacity_extra + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + entry_state_capacity_extra + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + inventory_slot_capacity_extra + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + plan_slot_capacity_extra + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + inventory_entry_capacity_entries + .checked_mul(std::mem::size_of::())?, + ) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let mut cleanup_structural_nodes = 0usize; + let mut cleanup_structural_depth = 0usize; + let mut cleanup_failure_events = 0usize; + let mut cleanup_call_events = 0usize; + let mut cleanup_boolean_branch_events = 0usize; + let mut cleanup_contracts = 0usize; + let mut cleanup_function_instances = 0usize; + let cleanup_expression_identity_bytes = exact_expression_identity_bytes; + for function in &program.functions { + let function_stats = scan_ast_capacity( + function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures), + program, + false, + stack, + )?; + let multiplicity = if function.type_parameters.is_empty() { + 1 + } else { + reachable_generic_calls + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))? + }; + cleanup_structural_nodes = cleanup_structural_nodes + .checked_add( + function_stats + .nodes + .checked_mul(multiplicity) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + cleanup_structural_depth = + cleanup_structural_depth.max(cleanup_function_region_depth(function, stack)?); + cleanup_function_instances = cleanup_function_instances + .checked_add(multiplicity) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + cleanup_contracts = cleanup_contracts + .checked_add( + function + .requires + .len() + .checked_add(function.ensures.len()) + .and_then(|contracts| contracts.checked_mul(multiplicity)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + cleanup_failure_events = cleanup_failure_events + .checked_add( + cleanup_function_finalizer_events(function, stack)? + .checked_sub(1) + .and_then(|events| events.checked_mul(multiplicity)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let mut function_call_events = 0usize; + for root in function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures) + { + function_call_events = function_call_events + .checked_add(cleanup_expression_call_events(root, program, stack)?) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + cleanup_call_events = cleanup_call_events + .checked_add( + function_call_events + .checked_mul(multiplicity) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let mut function_boolean_branch_events = 0usize; + for root in function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures) + { + function_boolean_branch_events = function_boolean_branch_events + .checked_add(cleanup_expression_boolean_branch_events(root, stack)?) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + cleanup_boolean_branch_events = cleanup_boolean_branch_events + .checked_add( + function_boolean_branch_events + .checked_mul(multiplicity) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + let cleanup_structural_upper = cleanup_structural_nodes + .checked_mul(cleanup_node_inline) + .and_then(|bytes| { + bytes.checked_add(cleanup_expression_identity_bytes.checked_mul(cleanup_path_copies)?) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + // Retained CleanupPlan container backing and identity payloads are + // distinct from inventory/slot shapes above. Derive each family from the + // source events that can create it. Four headers per logical entry covers + // the current target's minimum-capacity floor for independently allocated + // small Vecs as well as geometric growth. + let transition_entries = cleanup + .occurrences + .checked_mul(2) + // Every failing status path owns one SelectFailure transition. Only + // ordinary calls additionally own CallCommit; checked arithmetic and + // contract-false paths do not. Native Rust imports have neither. + .and_then(|entries| entries.checked_add(cleanup_failure_events)) + .and_then(|entries| entries.checked_add(cleanup_call_events)) + .and_then(|entries| entries.checked_add(cleanup.call_arguments)) + .and_then(|entries| entries.checked_add(cleanup.staged_results)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let cleanup_callee_identity_bytes = program + .functions + .iter() + .map(|function| function.stable_id.len()) + .chain(program.interfaces.iter().flat_map(|interface| { + interface + .imports + .iter() + .map(|import| import.stable_id.len()) + })) + .max() + .unwrap_or(0); + let expression_identity_fixed_bytes = "function-execution:" + .len() + .checked_add("semaprax.function-execution.v1:generic:".len()) + .and_then(|bytes| bytes.checked_add("declaration:".len())) + .and_then(|bytes| bytes.checked_add(":expression:".len())) + .and_then(|bytes| bytes.checked_add(decimal_digits(source_bytes).checked_mul(4)?)) + .and_then(|bytes| bytes.checked_add(8)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let extra_block_headers = cleanup_structural_nodes + .checked_mul(2) + .and_then(|entries| entries.checked_add(cleanup_contracts)) + .and_then(|entries| entries.checked_add(cleanup_function_instances)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let extra_edge_headers = cleanup_structural_nodes + .checked_mul(3) + .and_then(|entries| entries.checked_add(cleanup_contracts.checked_mul(2)?)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let extra_region_headers = cleanup_contracts + .checked_add(cleanup_function_instances) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let extra_exit_headers = cleanup + .exit_events + .checked_sub(cleanup.exit_events.min(cleanup_structural_nodes)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let block_entries = cleanup_structural_nodes + .checked_add(extra_block_headers) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let edge_entries = cleanup_structural_nodes + .checked_add(extra_edge_headers) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let region_entries = cleanup_structural_nodes + .checked_add(extra_region_headers) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let exit_entries = cleanup_structural_nodes + .checked_add(extra_exit_headers) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let block_capacity_extra = + retained_vec_capacity_extra(block_entries, cleanup_function_instances.min(block_entries)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let edge_capacity_extra = + retained_vec_capacity_extra(edge_entries, cleanup_function_instances.min(edge_entries)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let region_capacity_extra = retained_vec_capacity_extra( + region_entries, + cleanup_function_instances.min(region_entries), + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let exit_capacity_extra = + retained_vec_capacity_extra(exit_entries, cleanup_function_instances.min(exit_entries)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let status_capacity_extra = retained_vec_capacity_extra( + cleanup_failure_events, + cleanup_function_instances.min(cleanup_failure_events), + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let plan_expression_identity_copies = cleanup + .occurrences + .checked_mul(2) + .and_then(|copies| copies.checked_add(cleanup_failure_events.checked_mul(5)?)) + .and_then(|copies| copies.checked_add(cleanup_call_events)) + .and_then(|copies| copies.checked_add(cleanup_boolean_branch_events.checked_mul(2)?)) + .and_then(|copies| copies.checked_add(cleanup_function_instances)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let transition_capacity_entries = transition_entries + .checked_add( + retained_vec_capacity_extra(transition_entries, block_entries.min(transition_entries)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let branch_edge_entries = cleanup_structural_nodes + .checked_mul(3) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let branch_edge_capacity_entries = branch_edge_entries + .checked_add( + retained_vec_capacity_extra( + branch_edge_entries, + block_entries.min(branch_edge_entries), + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let region_slot_capacity_entries = cleanup + .roots + .checked_add( + retained_vec_capacity_extra(cleanup.roots, region_entries.min(cleanup.roots)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let exit_region_entries = cleanup + .exit_events + .checked_mul(cleanup_structural_depth.max(1)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let exit_region_capacity_entries = exit_region_entries + .checked_add( + retained_vec_capacity_extra(exit_region_entries, exit_entries.min(exit_region_entries)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let status_case_capacity_entries = cleanup_failure_events + .checked_add( + retained_vec_capacity_extra(cleanup_failure_events, cleanup_failure_events) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let cleanup_plan_structural_upper = transition_capacity_entries + .checked_mul(std::mem::size_of::()) + .and_then(|bytes| { + bytes.checked_add( + branch_edge_capacity_entries + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + extra_block_headers + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + extra_edge_headers + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + extra_region_headers + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + extra_exit_headers + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + region_slot_capacity_entries + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + exit_region_capacity_entries + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + cleanup_failure_events + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + status_case_capacity_entries + .checked_mul(std::mem::size_of::())?, + ) + }) + // The full path payload has one source-derived copy in + // cleanup_structural_upper. Each status/edge/continuation clone also + // owns the fixed scoped-identity framing around that path. + .and_then(|bytes| { + bytes.checked_add( + plan_expression_identity_copies.checked_mul(expression_identity_fixed_bytes)?, + ) + }) + .and_then(|bytes| { + bytes.checked_add(cleanup_failure_events.checked_mul(cleanup_callee_identity_bytes)?) + }) + .and_then(|bytes| bytes.checked_add(cleanup_plan_uncovered_identity_bytes)) + .and_then(|bytes| { + bytes.checked_add( + block_capacity_extra + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + edge_capacity_extra + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + region_capacity_extra + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + exit_capacity_extra + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|bytes| { + bytes.checked_add( + status_capacity_extra + .checked_mul(std::mem::size_of::())?, + ) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let cleanup_authority_upper = cleanup_retained_upper + .checked_add(cleanup_structural_upper) + .and_then(|bytes| bytes.checked_add(cleanup_plan_structural_upper)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + #[cfg(test)] + let cleanup_proof = + CleanupCapacityProofTerms { + stats: cleanup, + inventory_slot_capacity_entries: cleanup + .roots + .checked_add(inventory_slot_capacity_extra) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + inventory_flag_capacity_entries: cleanup + .leaves + .checked_add(flag_capacity_extra) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + inventory_entry_capacity_entries, + plan_slot_capacity_entries: cleanup + .roots + .checked_add(plan_slot_capacity_extra) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + plan_entry_capacity_entries: cleanup + .roots + .checked_add(entry_state_capacity_extra) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + shape_field_capacity_entries: cleanup + .shape_fields + .checked_mul(2) + .and_then(|entries| entries.checked_add(shape_field_capacity_extra)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + flag_projection_capacity_entries: cleanup + .projection_segments + .checked_add(flag_projection_capacity_extra) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + place_projection_capacity_entries: cleanup + .place_projection_segments + .checked_add(place_projection_capacity_extra) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + finalizer_projection_capacity_entries: cleanup + .finalizer_projection_segments + .checked_add(finalizer_projection_capacity_extra) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + finalizer_capacity_entries: cleanup + .finalizer_copies + .checked_add(finalizer_capacity_extra) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + block_capacity_entries: block_entries + .checked_add(block_capacity_extra) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + edge_capacity_entries: edge_entries + .checked_add(edge_capacity_extra) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + region_capacity_entries: region_entries + .checked_add(region_capacity_extra) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + exit_capacity_entries: exit_entries + .checked_add(exit_capacity_extra) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + status_capacity_entries: cleanup_failure_events + .checked_add(status_capacity_extra) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + transition_capacity_entries, + branch_edge_capacity_entries, + region_slot_capacity_entries, + exit_region_capacity_entries, + status_case_capacity_entries, + }; + let disposal_workspace_bytes = disposal_frames + .checked_mul(std::mem::size_of::()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let resolved_function_headers = program + .functions + .len() + .checked_add(reachable_generic_calls) + .and_then(|functions| functions.checked_mul(std::mem::size_of::())) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let retained_upper = source_bytes + .checked_mul(8) + .and_then(|bytes| bytes.checked_add(node_bytes)) + .and_then(|bytes| bytes.checked_add(specialization_bytes)) + .and_then(|bytes| { + bytes.checked_add(nested_declarations.checked_mul(declaration_work_bytes)?) + }) + .and_then(|bytes| bytes.checked_add(declaration_index_upper)) + .and_then(|bytes| bytes.checked_add(cleanup_retained_upper)) + .and_then(|bytes| bytes.checked_add(cleanup_plan_structural_upper)) + .and_then(|bytes| bytes.checked_add(disposal_workspace_bytes)) + .and_then(|bytes| bytes.checked_add(resolved_function_headers)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + Ok(HirPreResolveCapacity { + retained_upper, + scratch_upper: frame_machine_scratch.max(type_facts_scratch), + declaration_index_upper, + cleanup_retained_upper, + cleanup_authority_upper, + cleanup_exit_events_upper: cleanup.exit_events, + cleanup_fallback_roots: cleanup.fallback_roots, + cleanup_call_argument_owned_upper: cleanup.call_argument_owned_bytes, + cleanup_plan_structural_upper, + #[cfg(test)] + cleanup_parent_local_lifetime_upper, + #[cfg(test)] + cleanup_parent_local_projection_lifetime_upper, + #[cfg(test)] + cleanup_parent_local_update_prefix_lifetime_upper, + #[cfg(test)] + cleanup_proof, + phase_peaks: [ + source_phase + .checked_add(declaration_phase_overlap) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + resolver_phase + .checked_add(declaration_phase_overlap) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + validator_phase + .checked_add(declaration_phase_overlap) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + inventory_phase + .checked_add(declaration_phase_overlap) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + cleanup_phase + .checked_add(declaration_phase_overlap) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + call_index_phase + .checked_add(declaration_phase_overlap) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + closure_phase + .checked_add(declaration_phase_overlap) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + type_facts_scratch, + ], + disposal_frames, + }) +} + +fn hir_type_owned_capacity(ty: &ResolvedType) -> Option { + match ty { + ResolvedType::Unit | ResolvedType::I64 | ResolvedType::Bool => Some(0), + ResolvedType::TypeParameter { owner, .. } => Some(owner.as_str().len()), + ResolvedType::Nominal { + declaration, + arguments, + } => arguments + .iter() + .try_fold(declaration.as_str().len(), |bytes, argument| { + bytes.checked_add(hir_type_owned_capacity(argument)?) + })? + .checked_add(arguments.capacity() * std::mem::size_of::()), + } +} + +fn add_capacity(total: &mut usize, capacity: usize, element: usize) -> Result<(), Diagnostic> { + *total = total + .checked_add( + capacity + .checked_mul(element) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + Ok(()) +} + +fn hir_binding_owned_capacity(binding: &crate::hir::ResolvedBinding) -> Option { + binding + .id + .as_str() + .len() + .checked_add(binding.name.capacity())? + .checked_add(hir_type_owned_capacity(&binding.ty)?) +} + +fn hir_match_pattern_owned_capacity(pattern: &crate::hir::ResolvedMatchPattern) -> Option { + match pattern { + crate::hir::ResolvedMatchPattern::Wildcard => Some(0), + crate::hir::ResolvedMatchPattern::Variant { + variant, + case, + fields, + } => fields + .iter() + .try_fold( + variant.as_str().len().checked_add(case.as_str().len())?, + |bytes, field| { + bytes + .checked_add(field.field.as_str().len())? + .checked_add(hir_binding_owned_capacity(&field.binding)?) + }, + )? + .checked_add( + fields.capacity() * std::mem::size_of::(), + ), + crate::hir::ResolvedMatchPattern::Record { + record, + instance, + fields, + } => fields + .iter() + .try_fold( + record + .as_str() + .len() + .checked_add(hir_type_owned_capacity(instance)?)?, + |bytes, field| { + bytes + .checked_add(field.field.as_str().len())? + .checked_add(hir_record_pattern_field_owned_capacity(&field.pattern)?) + }, + )? + .checked_add( + fields.capacity() + * std::mem::size_of::(), + ), + } +} + +fn hir_record_pattern_field_owned_capacity( + pattern: &crate::hir::ResolvedRecordMatchFieldPattern, +) -> Option { + match pattern { + crate::hir::ResolvedRecordMatchFieldPattern::Binding(binding) => { + hir_binding_owned_capacity(binding) + } + crate::hir::ResolvedRecordMatchFieldPattern::Wildcard => Some(0), + crate::hir::ResolvedRecordMatchFieldPattern::Record { + record, + instance, + fields, + } => fields + .iter() + .try_fold( + record + .as_str() + .len() + .checked_add(hir_type_owned_capacity(instance)?)?, + |bytes, field| { + bytes + .checked_add(field.field.as_str().len())? + .checked_add(hir_record_pattern_field_owned_capacity(&field.pattern)?) + }, + )? + .checked_add( + fields.capacity() + * std::mem::size_of::(), + ), + } +} + +fn hir_expr_owned_capacity(expression: &ResolvedExpr) -> Result { + let mut total = 0_usize; + let mut pending = vec![expression]; + while let Some(expression) = pending.pop() { + total = total + .checked_add(std::mem::size_of::()) + .and_then(|bytes| bytes.checked_add(expression.id.as_str().len())) + .and_then(|bytes| bytes.checked_add(hir_type_owned_capacity(&expression.ty)?)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + match &expression.kind { + ResolvedExprKind::Call { + callee, + type_arguments, + instance, + args, + } => { + total = total + .checked_add(callee.as_str().len()) + .and_then(|bytes| { + bytes.checked_add( + type_arguments.capacity() * std::mem::size_of::(), + ) + }) + .and_then(|bytes| { + instance.as_ref().map_or(Some(bytes), |instance| { + bytes.checked_add(instance.as_str().len()) + }) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + for ty in type_arguments { + let ty_bytes = hir_type_owned_capacity(ty) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + total = total + .checked_add(ty_bytes) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + add_capacity( + &mut total, + args.capacity(), + std::mem::size_of::(), + )?; + pending.extend(args); + } + ResolvedExprKind::NativeRustImportCall(call) => { + total = total + .checked_add(call.expression.as_str().len()) + .and_then(|bytes| bytes.checked_add(call.import.as_str().len())) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + add_capacity( + &mut total, + call.args.capacity(), + std::mem::size_of::(), + )?; + pending.extend(&call.args); + } + ResolvedExprKind::Unary { value, .. } => pending.push(value), + ResolvedExprKind::Try { + operand, + result, + ok_case, + ok_field, + err_case, + err_field, + residual_type, + } => { + for id in [result, ok_case, ok_field, err_case, err_field] { + total = total + .checked_add(id.as_str().len()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + total = total + .checked_add( + hir_type_owned_capacity(residual_type) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + pending.push(operand); + } + ResolvedExprKind::TryOption { + operand, + option, + some_case, + some_field, + none_case, + residual_type, + } => { + for id in [option, some_case, some_field, none_case] { + total = total + .checked_add(id.as_str().len()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + total = total + .checked_add( + hir_type_owned_capacity(residual_type) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + pending.push(operand); + } + ResolvedExprKind::Project { base, field } => { + total = total + .checked_add(field.as_str().len()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + pending.push(base); + } + ResolvedExprKind::Binary { left, right, .. } => { + pending.push(left); + pending.push(right); + } + ResolvedExprKind::Block { statements, tail } => { + add_capacity( + &mut total, + statements.capacity(), + std::mem::size_of::(), + )?; + for statement in statements { + let ResolvedStatement::Let { binding, value, .. } = statement; + total = total + .checked_add(binding.id.as_str().len()) + .and_then(|bytes| bytes.checked_add(binding.name.capacity())) + .and_then(|bytes| bytes.checked_add(hir_type_owned_capacity(&binding.ty)?)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + pending.push(value); + } + pending.push(tail); + } + ResolvedExprKind::If { + condition, + then_branch, + else_branch, + } => { + pending.push(condition); + pending.push(then_branch); + pending.push(else_branch); + } + ResolvedExprKind::ConstructRecord { record, fields } => { + total = total + .checked_add(record.as_str().len()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + add_capacity( + &mut total, + fields.capacity(), + std::mem::size_of::(), + )?; + for field in fields { + total = total + .checked_add(field.field.as_str().len()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + pending.extend(fields.iter().map(|field| &field.value)); + } + ResolvedExprKind::ConstructVariant { + variant, + case, + fields, + } => { + total = total + .checked_add(variant.as_str().len()) + .and_then(|bytes| bytes.checked_add(case.as_str().len())) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + add_capacity( + &mut total, + fields.capacity(), + std::mem::size_of::(), + )?; + for field in fields { + total = total + .checked_add(field.field.as_str().len()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + pending.extend(fields.iter().map(|field| &field.value)); + } + ResolvedExprKind::Match { scrutinee, arms } => { + add_capacity( + &mut total, + arms.capacity(), + std::mem::size_of::(), + )?; + for arm in arms { + total = total + .checked_add( + hir_match_pattern_owned_capacity(&arm.pattern) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + pending.push(scrutinee); + pending.extend(arms.iter().map(|arm| &arm.value)); + } + ResolvedExprKind::UpdateRecord { + base, + record, + fields, + } => { + total = total + .checked_add(record.as_str().len()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + add_capacity( + &mut total, + fields.capacity(), + std::mem::size_of::(), + )?; + for field in fields { + total = total + .checked_add(field.field.as_str().len()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + pending.push(base); + pending.extend(fields.iter().map(|field| &field.value)); + } + ResolvedExprKind::Place(place) => { + total = total + .checked_add(place.root.as_str().len()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + add_capacity( + &mut total, + place.projections.capacity(), + std::mem::size_of::(), + )?; + for projection in &place.projections { + total = total + .checked_add(match projection { + crate::hir::PlaceProjection::Field(field) => field.as_str().len(), + crate::hir::PlaceProjection::VariantField { case, field } => { + case.as_str().len().saturating_add(field.as_str().len()) + } + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + } + ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) => {} + } + } + Ok(total) +} + +fn hir_function_owned_capacity(function: &ResolvedFunction) -> Result { + let mut total = std::mem::size_of::() + .checked_add(function.id.as_str().len()) + .and_then(|bytes| bytes.checked_add(function.result_id.as_str().len())) + .and_then(|bytes| bytes.checked_add(function.name.capacity())) + .and_then(|bytes| bytes.checked_add(hir_type_owned_capacity(&function.return_type)?)) + .and_then(|bytes| { + bytes.checked_add( + function.params.capacity() * std::mem::size_of::(), + ) + }) + .and_then(|bytes| { + bytes.checked_add(function.effects.capacity() * std::mem::size_of::()) + }) + .and_then(|bytes| { + bytes.checked_add(function.requires.capacity() * std::mem::size_of::()) + }) + .and_then(|bytes| { + bytes.checked_add(function.ensures.capacity() * std::mem::size_of::()) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + for parameter in &function.params { + total = total + .checked_add(parameter.id.as_str().len()) + .and_then(|bytes| bytes.checked_add(parameter.name.capacity())) + .and_then(|bytes| bytes.checked_add(hir_type_owned_capacity(¶meter.ty)?)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + for effect in &function.effects { + total = total + .checked_add(effect.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + for expression in function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures) + { + total = total + .checked_add(hir_expr_owned_capacity(expression)?) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + total = total + .checked_add( + crate::private_capacity_contract::cleanup_inventory_owned_capacity(&function.cleanup) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .and_then(|bytes| { + bytes.checked_add( + crate::private_capacity_contract::cleanup_plan_owned_capacity( + &function.cleanup_plan, + )?, + ) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + Ok(total) +} + +fn hir_owned_capacity(resolved: &ResolvedProgram) -> Result { + // `declaration_index_upper` separately owns the opaque index's inline + // header and heap payload. Avoid charging its inline bytes twice here. + let mut total = (std::mem::size_of::() + - std::mem::size_of::()) + .checked_add(resolved.module.capacity()) + .and_then(|bytes| bytes.checked_add(resolved.entrypoint.as_str().len())) + .and_then(|bytes| { + bytes.checked_add(resolved.permits.capacity() * std::mem::size_of::()) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + for permit in &resolved.permits { + total = total + .checked_add(permit.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + total = total + .checked_add(resolved.functions.capacity() * std::mem::size_of::()) + .and_then(|bytes| { + bytes.checked_add( + resolved.interfaces.capacity() + * std::mem::size_of::(), + ) + }) + .and_then(|bytes| { + bytes.checked_add( + resolved.types.capacity() + * std::mem::size_of::(), + ) + }) + .and_then(|bytes| { + bytes.checked_add( + resolved.function_templates.capacity() + * std::mem::size_of::(), + ) + }) + .and_then(|bytes| { + bytes.checked_add( + resolved.function_instances.capacity() + * std::mem::size_of::(), + ) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + for interface in &resolved.interfaces { + total = total + .checked_add(interface.id.as_str().len()) + .and_then(|bytes| bytes.checked_add(interface.name.capacity())) + .and_then(|bytes| { + bytes.checked_add(interface.permits.capacity() * std::mem::size_of::()) + }) + .and_then(|bytes| { + bytes.checked_add( + interface.imports.capacity() + * std::mem::size_of::(), + ) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + for permit in &interface.permits { + total = total + .checked_add(permit.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + for import in &interface.imports { + total = total + .checked_add(import.id.as_str().len()) + .and_then(|bytes| bytes.checked_add(import.interface.as_str().len())) + .and_then(|bytes| bytes.checked_add(import.name.capacity())) + .and_then(|bytes| bytes.checked_add(import.import_key.capacity())) + .and_then(|bytes| { + bytes.checked_add( + import.parameters.capacity() + * std::mem::size_of::(), + ) + }) + .and_then(|bytes| { + bytes.checked_add(import.effects.capacity() * std::mem::size_of::()) + }) + .and_then(|bytes| { + bytes.checked_add( + import.required_authority.capacity() * std::mem::size_of::(), + ) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + for parameter in &import.parameters { + total = total + .checked_add(parameter.name.capacity()) + .and_then(|bytes| bytes.checked_add(hir_type_owned_capacity(¶meter.ty)?)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + for value in import.effects.iter().chain(&import.required_authority) { + total = total + .checked_add(value.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + if let ResolvedImportFailure::Status { domain_id, .. } = &import.failure { + total = total + .checked_add(domain_id.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + } + } + for function in &resolved.functions { + // The outer function vector already accounts for each inline struct; + // add only its recursively owned payload. + let whole_function = hir_function_owned_capacity(function)?; + total = total + .checked_add(whole_function.saturating_sub(std::mem::size_of::())) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + for declaration in &resolved.types { + total = total + .checked_add(declaration.id.as_str().len()) + .and_then(|bytes| bytes.checked_add(declaration.name.capacity())) + .and_then(|bytes| { + bytes.checked_add( + declaration.type_parameters.capacity() + * std::mem::size_of::(), + ) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + for parameter in &declaration.type_parameters { + total = total + .checked_add(parameter.name.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + match &declaration.kind { + crate::hir::ResolvedTypeDeclarationKind::Resource { drop } => { + total = total + .checked_add(drop.id.as_str().len()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if let crate::hir::ResolvedResourceDropKind::Imported { import, import_key } = + &drop.kind + { + total = total + .checked_add(import.as_str().len()) + .and_then(|bytes| bytes.checked_add(import_key.capacity())) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + } + crate::hir::ResolvedTypeDeclarationKind::Record { fields } => { + total = total + .checked_add( + fields.capacity() + * std::mem::size_of::(), + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + for field in fields { + total = total + .checked_add(field.id.as_str().len()) + .and_then(|bytes| bytes.checked_add(field.name.capacity())) + .and_then(|bytes| bytes.checked_add(hir_type_owned_capacity(&field.ty)?)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + } + crate::hir::ResolvedTypeDeclarationKind::Variant { cases } => { + total = total + .checked_add( + cases.capacity() + * std::mem::size_of::(), + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + for case in cases { + total = total + .checked_add(case.id.as_str().len()) + .and_then(|bytes| bytes.checked_add(case.name.capacity())) + .and_then(|bytes| { + bytes.checked_add( + case.fields.capacity() + * std::mem::size_of::(), + ) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + for field in &case.fields { + total = total + .checked_add(field.id.as_str().len()) + .and_then(|bytes| bytes.checked_add(field.name.capacity())) + .and_then(|bytes| { + bytes.checked_add(hir_type_owned_capacity(&field.ty)?) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + } + } + } + } + for template in &resolved.function_templates { + total = total + .checked_add(template.id.as_str().len()) + .and_then(|bytes| bytes.checked_add(template.result_id.as_str().len())) + .and_then(|bytes| bytes.checked_add(template.name.capacity())) + .and_then(|bytes| bytes.checked_add(hir_type_owned_capacity(&template.return_type)?)) + .and_then(|bytes| { + bytes.checked_add( + template.type_parameters.capacity() + * std::mem::size_of::(), + ) + }) + .and_then(|bytes| { + bytes.checked_add( + template.params.capacity() * std::mem::size_of::(), + ) + }) + .and_then(|bytes| { + bytes.checked_add(template.effects.capacity() * std::mem::size_of::()) + }) + .and_then(|bytes| { + bytes + .checked_add(template.requires.capacity() * std::mem::size_of::()) + }) + .and_then(|bytes| { + bytes.checked_add(template.ensures.capacity() * std::mem::size_of::()) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + for parameter in &template.type_parameters { + total = total + .checked_add(parameter.name.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + for parameter in &template.params { + total = total + .checked_add(parameter.id.as_str().len()) + .and_then(|bytes| bytes.checked_add(parameter.name.capacity())) + .and_then(|bytes| bytes.checked_add(hir_type_owned_capacity(¶meter.ty)?)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + for effect in &template.effects { + total = total + .checked_add(effect.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + for expression in template + .requires + .iter() + .chain(std::iter::once(&template.body)) + .chain(&template.ensures) + { + total = total + .checked_add(hir_expr_owned_capacity(expression)?) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + } + for instance in &resolved.function_instances { + total = total + .checked_add(instance.id.as_str().len()) + .and_then(|bytes| bytes.checked_add(instance.template.as_str().len())) + .and_then(|bytes| { + bytes.checked_add( + instance.type_arguments.capacity() * std::mem::size_of::(), + ) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + for ty in &instance.type_arguments { + total = total + .checked_add( + hir_type_owned_capacity(ty) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + total = total + .checked_add(hir_function_owned_capacity(&instance.function)?) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + Ok(total) +} + +#[cfg(test)] +fn validate_native_rust_expression_budget(resolved: &ResolvedProgram) -> Result<(), Diagnostic> { + let functions = resolved.functions.iter().collect::>(); + validate_native_rust_expression_budget_for_closure(&functions, false) +} + +fn validate_native_rust_expression_budget_for_closure( + functions: &[&ResolvedFunction], + preauthorized: bool, +) -> Result<(), Diagnostic> { + note_hir_post_resolve_phase(1); + let mut pending = Vec::new(); + for function in functions { + pending.extend( + function + .requires + .iter() + .map(|expression| (expression, 1_usize)), + ); + pending.push((&function.body, 1)); + pending.extend( + function + .ensures + .iter() + .map(|expression| (expression, 1_usize)), + ); + } + let mut visited = 0_usize; + while let Some((expression, depth)) = pending.pop() { + note_hir_post_resolve_capacity( + 0, + pending.capacity() * std::mem::size_of::<(&ResolvedExpr, usize)>(), + ); + visited = visited + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if !preauthorized { + debit(std::mem::size_of::<&ResolvedExpr>())?; + } + if visited > MAX_SOURCE_BYTES { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + if depth > MAX_SEMANTIC_EXPRESSION_DEPTH { + return Err(b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + )); + } + let child_depth = depth + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + match &expression.kind { + ResolvedExprKind::Call { args, .. } => { + pending.extend(args.iter().map(|value| (value, child_depth))) + } + ResolvedExprKind::NativeRustImportCall(call) => { + pending.extend(call.args.iter().map(|value| (value, child_depth))) + } + ResolvedExprKind::Unary { value, .. } + | ResolvedExprKind::Try { operand: value, .. } + | ResolvedExprKind::TryOption { operand: value, .. } + | ResolvedExprKind::Project { base: value, .. } => pending.push((value, child_depth)), + ResolvedExprKind::Binary { left, right, .. } => { + pending.push((left, child_depth)); + pending.push((right, child_depth)); + } + ResolvedExprKind::Block { statements, tail } => { + for statement in statements { + let ResolvedStatement::Let { value, .. } = statement; + pending.push((value, child_depth)); + } + pending.push((tail, child_depth)); + } + ResolvedExprKind::If { + condition, + then_branch, + else_branch, + } => { + pending.push((condition, child_depth)); + pending.push((then_branch, child_depth)); + pending.push((else_branch, child_depth)); + } + ResolvedExprKind::ConstructRecord { fields, .. } + | ResolvedExprKind::ConstructVariant { fields, .. } => { + pending.extend(fields.iter().map(|field| (&field.value, child_depth))); + } + ResolvedExprKind::Match { scrutinee, arms } => { + pending.push((scrutinee, child_depth)); + pending.extend(arms.iter().map(|arm| (&arm.value, child_depth))); + } + ResolvedExprKind::UpdateRecord { base, fields, .. } => { + pending.push((base, child_depth)); + pending.extend(fields.iter().map(|field| (&field.value, child_depth))); + } + ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) | ResolvedExprKind::Place(_) => {} + } + } + Ok(()) +} + +fn parameter_json(parameter: &ParameterFact) -> String { + format!( + "{{\"name\":{},\"type\":{},\"mode\":\"value\"}}", + quote_json(¶meter.name), + quote_json(scalar_text(parameter.ty)) + ) +} + +fn result_json(result: ScalarType) -> String { + format!( + "{{\"type\":{},\"out_slot\":{}}}", + quote_json(scalar_text(result)), + result != ScalarType::Unit + ) +} + +struct ExactReplay<'a> { + source: &'a [u8], + position: usize, + failed: bool, +} + +impl<'a> ExactReplay<'a> { + fn new(source: &'a str) -> Self { + Self { + source: source.as_bytes(), + position: 0, + failed: false, + } + } + + fn text(&mut self, expected: &str) { + let end = self.position.checked_add(expected.len()); + let mismatch = end.is_none_or(|end| end > self.source.len()) + || end.is_some_and(|end| &self.source[self.position..end] != expected.as_bytes()); + if self.failed || mismatch { + self.failed = true; + return; + } + self.position = end.unwrap_or(self.position); + } + + fn json(&mut self, value: &str) { + self.text("\""); + for character in value.chars() { + match character { + '\"' => self.text("\\\""), + '\\' => self.text("\\\\"), + '\u{08}' => self.text("\\b"), + '\t' => self.text("\\t"), + '\n' => self.text("\\n"), + '\u{0c}' => self.text("\\f"), + '\r' => self.text("\\r"), + character if character <= '\u{1f}' => { + let code = u32::from(character); + let hex = b"0123456789abcdef"; + let escaped = [ + b'\\', + b'u', + hex[((code >> 12) & 0xf) as usize], + hex[((code >> 8) & 0xf) as usize], + hex[((code >> 4) & 0xf) as usize], + hex[(code & 0xf) as usize], + ]; + self.text(std::str::from_utf8(&escaped).unwrap_or("")); + } + character => { + let mut encoded = [0_u8; 4]; + self.text(character.encode_utf8(&mut encoded)); + } + } + } + self.text("\""); + } + + fn number(&mut self, value: impl std::fmt::Display) { + let rendered = value.to_string(); + #[cfg(test)] + note_post_hir_replay_capacity(rendered.capacity()); + self.text(&rendered); + } + + fn usize_noalloc(&mut self, mut value: usize) { + let mut bytes = [0_u8; 20]; + let mut start = bytes.len(); + loop { + start -= 1; + bytes[start] = b'0' + u8::try_from(value % 10).unwrap_or(0); + value /= 10; + if value == 0 { + break; + } + } + self.text(std::str::from_utf8(&bytes[start..]).unwrap_or("")); + } + + fn raw_digest_json_noalloc(&mut self, bytes: &[u8]) { + const HEX: &[u8; 16] = b"0123456789abcdef"; + self.text("\"sha256:"); + for byte in Sha256::digest(bytes) { + let pair = [HEX[usize::from(byte >> 4)], HEX[usize::from(byte & 0x0f)]]; + self.text(std::str::from_utf8(&pair).unwrap_or("")); + } + self.text("\""); + } + + fn finish(self) -> bool { + !self.failed && self.position == self.source.len() + } +} + +fn replay_limits_exact(replay: &mut ExactReplay<'_>) { + replay.text("{"); + for (index, (name, value)) in LIMIT_ROWS.into_iter().enumerate() { + if index != 0 { + replay.text(","); + } + replay.json(name); + replay.text(":"); + replay.usize_noalloc(value); + } + replay.text("}"); +} + +fn replay_spec_bytes_exact(source: &str, spec: &Spec) -> bool { + let mut replay = ExactReplay::new(source); + replay.text("{\"schema\":"); + replay.json(SPEC_SCHEMA); + replay.text(",\"module\":"); + replay.json(&spec.module); + replay.text(",\"source_revision\":"); + replay.json(&spec.source_revision); + replay.text(",\"target\":{\"triple\":"); + replay.json(&spec.target.triple); + replay.text(",\"pointer_width\":"); + replay.number(spec.target.pointer_width); + replay.text(",\"endian\":"); + replay.json(&spec.target.endian); + replay.text(",\"panic_strategy\":"); + replay.json(&spec.target.panic_strategy); + replay.text(",\"thread_policy\":"); + replay.json(&spec.target.thread_policy); + replay.text("},\"exports\":["); + replay_strings_exact(&mut replay, &spec.exports); + replay.text("],\"imports\":["); + replay_strings_exact(&mut replay, &spec.imports); + replay.text("],\"capabilities\":["); + replay_strings_exact(&mut replay, &spec.capabilities); + replay.text("],\"limits\":"); + replay_limits_exact(&mut replay); + replay.text(",\"nonclaims\":["); + for (index, nonclaim) in NONCLAIMS.iter().enumerate() { + if index != 0 { + replay.text(","); + } + replay.json(nonclaim); + } + replay.text("]}\n"); + replay.finish() +} + +fn replay_parameter_exact(replay: &mut ExactReplay<'_>, parameter: &ParameterFact) { + replay.text("{\"name\":"); + replay.json(¶meter.name); + replay.text(",\"type\":"); + replay.json(scalar_text(parameter.ty)); + replay.text(",\"mode\":\"value\"}"); +} + +fn replay_result_exact(replay: &mut ExactReplay<'_>, result: ScalarType) { + replay.text("{\"type\":"); + replay.json(scalar_text(result)); + replay.text(",\"out_slot\":"); + replay.text(if result == ScalarType::Unit { + "false" + } else { + "true" + }); + replay.text("}"); +} + +fn replay_strings_exact(replay: &mut ExactReplay<'_>, values: &[String]) { + for (index, value) in values.iter().enumerate() { + if index != 0 { + replay.text(","); + } + replay.json(value); + } +} + +fn replay_descriptor_bytes_exact( + source: &str, + spec: &Spec, + hir_digest: &str, + status_domains: &[String], + exports: &[ExportFact], + imports: &[ImportFact], +) -> bool { + let mut replay = ExactReplay::new(source); + replay.text("{\"schema\":"); + replay.json(DESCRIPTOR_SCHEMA); + replay.text(",\"module\":"); + replay.json(&spec.module); + replay.text(",\"source_revision\":"); + replay.json(&spec.source_revision); + replay.text(",\"hir_digest\":"); + replay.json(hir_digest); + replay.text(",\"target\":{\"triple\":"); + replay.json(&spec.target.triple); + replay.text(",\"pointer_width\":"); + replay.number(spec.target.pointer_width); + replay.text(",\"endian\":"); + replay.json(&spec.target.endian); + replay.text(",\"panic_strategy\":"); + replay.json(&spec.target.panic_strategy); + replay.text(",\"thread_policy\":"); + replay.json(&spec.target.thread_policy); + replay.text("},\"status_domains\":[{\"ordinal\":0,\"domain_id\":\"success\"}"); + for (index, domain) in status_domains.iter().enumerate() { + replay.text(",{\"ordinal\":"); + replay.number(index + 1); + replay.text(",\"domain_id\":"); + replay.json(domain); + replay.text("}"); + } + replay.text(r#",{"ordinal":65533,"domain_id":"semaprax.native-rust-semantics.v1"},{"ordinal":65534,"domain_id":"semaprax.native-rust-host.v1"},{"ordinal":65535,"domain_id":"semaprax.native-rust-adapter.v1"}],"abi":{"version":1,"calling_convention":"C","status_word":"u64-domain16-code32-class8-retry1-reserved7","bool":"u8-0-or-1","i64":"signed-two-complement-i64","context":"SPXNRCTX1","imports_table":"SPXNRIMP1","result":"caller-owned-uninitialized-success-only","allocator":"none-across-boundary","unwind":"caught-before-ffi-return","threading":"same-thread","reentrancy":"rejected"},"exports":["#); + for (index, export) in exports.iter().enumerate() { + if index != 0 { + replay.text(","); + } + replay.text("{\"id\":"); + replay.json(&export.id); + replay.text(",\"rust_method\":"); + replay.json(&export.rust_method); + replay.text(",\"c_symbol\":"); + replay.json(&export.c_symbol); + replay.text(",\"parameters\":["); + for (parameter_index, parameter) in export.parameters.iter().enumerate() { + if parameter_index != 0 { + replay.text(","); + } + replay_parameter_exact(&mut replay, parameter); + } + replay.text("],\"result\":"); + replay_result_exact(&mut replay, export.result); + replay.text(",\"effects\":["); + replay_strings_exact(&mut replay, &export.effects); + replay.text("],\"capabilities\":["); + replay_strings_exact(&mut replay, &export.capabilities); + replay.text("],\"required_imports\":["); + replay_strings_exact(&mut replay, &export.required_imports); + replay.text("],\"status_domain_ordinals\":["); + for (ordinal_index, ordinal) in export.status_domain_ordinals.iter().enumerate() { + if ordinal_index != 0 { + replay.text(","); + } + replay.number(ordinal); + } + replay.text("],\"call_contract_digest\":"); + replay.json(&export.call_contract_digest); + replay.text("}"); + } + replay.text("],\"imports\":["); + for (index, import) in imports.iter().enumerate() { + if index != 0 { + replay.text(","); + } + replay.text("{\"id\":"); + replay.json(&import.id); + replay.text(",\"interface\":"); + replay.json(&import.interface); + replay.text(",\"import_key\":"); + replay.json(&import.import_key); + replay.text(",\"rust_method\":"); + replay.json(&import.rust_method); + replay.text(",\"c_field\":"); + replay.json(&import.c_field); + replay.text(",\"parameters\":["); + for (parameter_index, parameter) in import.parameters.iter().enumerate() { + if parameter_index != 0 { + replay.text(","); + } + replay_parameter_exact(&mut replay, parameter); + } + replay.text("],\"result\":"); + replay_result_exact(&mut replay, import.result); + replay.text(",\"effects\":["); + replay_strings_exact(&mut replay, &import.effects); + replay.text("],\"capabilities\":["); + replay_strings_exact(&mut replay, &import.capabilities); + replay.text("],\"failure\":{\"kind\":"); + if let Some(domain) = &import.failure { + replay.text("\"status\",\"domain_id\":"); + replay.json(domain); + } else { + replay.text("\"infallible\""); + } + replay.text("},\"call_contract_digest\":"); + replay.json(&import.call_contract_digest); + replay.text("}"); + } + replay.text("],\"limits\":"); + replay_limits_exact(&mut replay); + replay.text(",\"nonclaims\":["); + for (index, nonclaim) in NONCLAIMS.iter().enumerate() { + if index != 0 { + replay.text(","); + } + replay.json(nonclaim); + } + replay.text("]}\n"); + replay.finish() +} + +fn render_descriptor_with_limit( + spec: &Spec, + hir_digest: &str, + status_domains: &[String], + exports: &[ExportFact], + imports: &[ImportFact], + maximum: usize, +) -> Result { + let mut statuses = vec!["{\"ordinal\":0,\"domain_id\":\"success\"}".to_owned()]; + statuses.extend(status_domains.iter().enumerate().map(|(index, domain)| { + format!( + "{{\"ordinal\":{},\"domain_id\":{}}}", + index + 1, + quote_json(domain) + ) + })); + statuses + .push("{\"ordinal\":65533,\"domain_id\":\"semaprax.native-rust-semantics.v1\"}".to_owned()); + statuses.push("{\"ordinal\":65534,\"domain_id\":\"semaprax.native-rust-host.v1\"}".to_owned()); + statuses + .push("{\"ordinal\":65535,\"domain_id\":\"semaprax.native-rust-adapter.v1\"}".to_owned()); + #[cfg(test)] + let status_scratch = checked_owned_string_vec(&statuses, statuses.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let mut export_row_values = Vec::with_capacity(exports.len()); + for export in exports { + let id = quote_json(&export.id); + let rust_method = quote_json(&export.rust_method); + let c_symbol = quote_json(&export.c_symbol); + let parameter_values = export + .parameters + .iter() + .map(parameter_json) + .collect::>(); + let parameters = parameter_values.join(","); + let effects = render_string_array(&export.effects); + let capabilities = render_string_array(&export.capabilities); + let required_imports = render_string_array(&export.required_imports); + let ordinal_values = export + .status_domain_ordinals + .iter() + .map(u16::to_string) + .collect::>(); + let ordinals = ordinal_values.join(","); + let result = result_json(export.result); + let call_contract_digest = quote_json(&export.call_contract_digest); + let row = format!( + "{{\"id\":{},\"rust_method\":{},\"c_symbol\":{},\"parameters\":[{}],\"result\":{},\"effects\":[{}],\"capabilities\":[{}],\"required_imports\":[{}],\"status_domain_ordinals\":[{}],\"call_contract_digest\":{}}}", + id, + rust_method, + c_symbol, + parameters, + result, + effects, + capabilities, + required_imports, + ordinals, + call_contract_digest + ); + #[cfg(test)] + note_post_hir_render_capacity( + status_scratch + .saturating_add( + checked_owned_string_vec(&export_row_values, export_row_values.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .saturating_add( + checked_owned_string_vec(¶meter_values, parameter_values.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .saturating_add( + checked_owned_string_vec(&ordinal_values, ordinal_values.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .saturating_add(id.capacity()) + .saturating_add(rust_method.capacity()) + .saturating_add(c_symbol.capacity()) + .saturating_add(parameters.capacity()) + .saturating_add(effects.capacity()) + .saturating_add(capabilities.capacity()) + .saturating_add(required_imports.capacity()) + .saturating_add(ordinals.capacity()) + .saturating_add(result.capacity()) + .saturating_add(call_contract_digest.capacity()) + .saturating_add(row.capacity()), + ); + export_row_values.push(row); + } + let export_rows = export_row_values.join(","); + #[cfg(test)] + note_post_hir_render_capacity( + status_scratch + .saturating_add( + checked_owned_string_vec(&export_row_values, export_row_values.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .saturating_add(export_rows.capacity()), + ); + drop(export_row_values); + let mut import_row_values = Vec::with_capacity(imports.len()); + for import in imports { + let id = quote_json(&import.id); + let interface = quote_json(&import.interface); + let import_key = quote_json(&import.import_key); + let rust_method = quote_json(&import.rust_method); + let c_field = quote_json(&import.c_field); + let parameter_values = import + .parameters + .iter() + .map(parameter_json) + .collect::>(); + let parameters = parameter_values.join(","); + let effects = render_string_array(&import.effects); + let capabilities = render_string_array(&import.capabilities); + let failure = import.failure.as_ref().map_or_else( + || "{\"kind\":\"infallible\"}".to_owned(), + |domain| { + format!( + "{{\"kind\":\"status\",\"domain_id\":{}}}", + quote_json(domain) + ) + }, + ); + let result = result_json(import.result); + let call_contract_digest = quote_json(&import.call_contract_digest); + let row = format!( + "{{\"id\":{},\"interface\":{},\"import_key\":{},\"rust_method\":{},\"c_field\":{},\"parameters\":[{}],\"result\":{},\"effects\":[{}],\"capabilities\":[{}],\"failure\":{},\"call_contract_digest\":{}}}", + id, + interface, + import_key, + rust_method, + c_field, + parameters, + result, + effects, + capabilities, + failure, + call_contract_digest + ); + #[cfg(test)] + note_post_hir_render_capacity( + status_scratch + .saturating_add(export_rows.capacity()) + .saturating_add( + checked_owned_string_vec(&import_row_values, import_row_values.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .saturating_add( + checked_owned_string_vec(¶meter_values, parameter_values.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .saturating_add(id.capacity()) + .saturating_add(interface.capacity()) + .saturating_add(import_key.capacity()) + .saturating_add(rust_method.capacity()) + .saturating_add(c_field.capacity()) + .saturating_add(parameters.capacity()) + .saturating_add(effects.capacity()) + .saturating_add(capabilities.capacity()) + .saturating_add(failure.capacity()) + .saturating_add(result.capacity()) + .saturating_add(call_contract_digest.capacity()) + .saturating_add(row.capacity()), + ); + import_row_values.push(row); + } + let import_rows = import_row_values.join(","); + #[cfg(test)] + note_post_hir_render_capacity( + status_scratch + .saturating_add(export_rows.capacity()) + .saturating_add( + checked_owned_string_vec(&import_row_values, import_row_values.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .saturating_add(import_rows.capacity()), + ); + drop(import_row_values); + let schema = quote_json(DESCRIPTOR_SCHEMA); + let module = quote_json(&spec.module); + let source_revision = quote_json(&spec.source_revision); + let hir = quote_json(hir_digest); + let target = target_json(&spec.target); + let status_rows = statuses.join(","); + let limits = limits_json(); + let nonclaims = nonclaims_json(); + #[cfg(test)] + note_post_hir_render_capacity( + checked_owned_string_vec(&statuses, statuses.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))? + .saturating_add(export_rows.capacity()) + .saturating_add(import_rows.capacity()) + .saturating_add(schema.capacity()) + .saturating_add(module.capacity()) + .saturating_add(source_revision.capacity()) + .saturating_add(hir.capacity()) + .saturating_add(target.capacity()) + .saturating_add(status_rows.capacity()) + .saturating_add(limits.capacity()) + .saturating_add(nonclaims.capacity()), + ); + render_exact_artifact("max_descriptor_bytes", maximum, |sink| { + write!( + sink, + "{{\"schema\":{},\"module\":{},\"source_revision\":{},\"hir_digest\":{},\"target\":{},\"status_domains\":[{}],\"abi\":{{\"version\":1,\"calling_convention\":\"C\",\"status_word\":\"u64-domain16-code32-class8-retry1-reserved7\",\"bool\":\"u8-0-or-1\",\"i64\":\"signed-two-complement-i64\",\"context\":\"SPXNRCTX1\",\"imports_table\":\"SPXNRIMP1\",\"result\":\"caller-owned-uninitialized-success-only\",\"allocator\":\"none-across-boundary\",\"unwind\":\"caught-before-ffi-return\",\"threading\":\"same-thread\",\"reentrancy\":\"rejected\"}},\"exports\":[{}],\"imports\":[{}],\"limits\":{},\"nonclaims\":[{}]}}\n", + schema, + module, + source_revision, + hir, + target, + status_rows, + export_rows, + import_rows, + limits, + nonclaims + ) + .map_err(|_| b109("max_descriptor_bytes", MAX_DESCRIPTOR_BYTES)) + }) +} + +fn render_descriptor( + spec: &Spec, + hir_digest: &str, + status_domains: &[String], + exports: &[ExportFact], + imports: &[ImportFact], +) -> Result { + render_descriptor_with_limit( + spec, + hir_digest, + status_domains, + exports, + imports, + MAX_DESCRIPTOR_BYTES, + ) +} + +fn replay_descriptor( + source: &str, + spec: &Spec, + hir_digest: &str, + exports: &[ExportFact], + imports: &[ImportFact], +) -> Result<(), Diagnostic> { + let status_domain_set = imports + .iter() + .filter_map(|import| import.failure.clone()) + .collect::>(); + #[cfg(test)] + let status_domain_set_owned = checked_owned_string_set(&status_domain_set) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + #[cfg(test)] + note_post_hir_replay_capacity(status_domain_set_owned); + let mut status_domains = Vec::with_capacity(status_domain_set.len()); + for domain in status_domain_set { + status_domains.push(domain); + #[cfg(test)] + note_post_hir_replay_capacity( + status_domain_set_owned + .checked_add( + checked_owned_string_vec(&status_domains, status_domains.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ); + } + if !replay_descriptor_bytes_exact(source, spec, hir_digest, &status_domains, exports, imports) { + return Err(b108()); + } + if !source.ends_with('\n') { + return Err(b108()); + } + let value: Value = serde_json::from_str(source).map_err(|_| b108())?; + #[cfg(test)] + let descriptor_dom_owned = checked_json_value_owned(&value) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + #[cfg(test)] + let status_domains_owned = checked_owned_string_vec(&status_domains, status_domains.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + #[cfg(test)] + note_post_hir_replay_capacity( + descriptor_dom_owned + .checked_add(status_domains_owned) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ); + let row = value.as_object().ok_or_else(b108)?; + if row.len() != 11 + || row.get("schema").and_then(Value::as_str) != Some(DESCRIPTOR_SCHEMA) + || row.get("module").and_then(Value::as_str) != Some(&spec.module) + || row.get("source_revision").and_then(Value::as_str) != Some(&spec.source_revision) + || row.get("hir_digest").and_then(Value::as_str) != Some(hir_digest) + || row.get("exports").and_then(Value::as_array).map(Vec::len) != Some(exports.len()) + || row.get("imports").and_then(Value::as_array).map(Vec::len) != Some(imports.len()) + { + return Err(b108()); + } + let target = row + .get("target") + .and_then(Value::as_object) + .ok_or_else(b108)?; + if target.len() != 5 + || target.get("triple").and_then(Value::as_str) != Some(&spec.target.triple) + || target.get("pointer_width").and_then(Value::as_u64) + != Some(u64::from(spec.target.pointer_width)) + || target.get("endian").and_then(Value::as_str) != Some(&spec.target.endian) + || target.get("panic_strategy").and_then(Value::as_str) != Some(&spec.target.panic_strategy) + || target.get("thread_policy").and_then(Value::as_str) != Some(&spec.target.thread_policy) + { + return Err(b108()); + } + let expected_statuses = std::iter::once((0_u64, "success")) + .chain(status_domains.iter().enumerate().map(|(index, domain)| { + ( + u64::try_from(index + 1).unwrap_or(u64::MAX), + domain.as_str(), + ) + })) + .chain([ + (65_533, "semaprax.native-rust-semantics.v1"), + (65_534, "semaprax.native-rust-host.v1"), + (65_535, "semaprax.native-rust-adapter.v1"), + ]) + .collect::>(); + #[cfg(test)] + note_post_hir_replay_capacity( + descriptor_dom_owned + .checked_add(status_domains_owned) + .and_then(|bytes| { + bytes.checked_add( + expected_statuses + .capacity() + .checked_mul(std::mem::size_of::<(u64, &str)>())?, + ) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ); + let statuses = row + .get("status_domains") + .and_then(Value::as_array) + .ok_or_else(b108)?; + if statuses.len() != expected_statuses.len() + || statuses + .iter() + .zip(&expected_statuses) + .any(|(value, expected)| { + value.as_object().is_none_or(|object| { + object.len() != 2 + || object.get("ordinal").and_then(Value::as_u64) != Some(expected.0) + || object.get("domain_id").and_then(Value::as_str) != Some(expected.1) + }) + }) + { + return Err(b108()); + } + let abi = row.get("abi").and_then(Value::as_object).ok_or_else(b108)?; + for (key, expected) in [ + ("calling_convention", "C"), + ("status_word", "u64-domain16-code32-class8-retry1-reserved7"), + ("bool", "u8-0-or-1"), + ("i64", "signed-two-complement-i64"), + ("context", "SPXNRCTX1"), + ("imports_table", "SPXNRIMP1"), + ("result", "caller-owned-uninitialized-success-only"), + ("allocator", "none-across-boundary"), + ("unwind", "caught-before-ffi-return"), + ("threading", "same-thread"), + ("reentrancy", "rejected"), + ] { + if abi.get(key).and_then(Value::as_str) != Some(expected) { + return Err(b108()); + } + } + if abi.len() != 12 || abi.get("version").and_then(Value::as_u64) != Some(1) { + return Err(b108()); + } + validate_descriptor_exports(row.get("exports").ok_or_else(b108)?, exports)?; + validate_descriptor_imports(row.get("imports").ok_or_else(b108)?, imports)?; + let limits = row + .get("limits") + .and_then(Value::as_object) + .ok_or_else(b108)?; + if limits.len() != LIMIT_ROWS.len() + || LIMIT_ROWS.iter().any(|(name, expected)| { + limits.get(*name).and_then(Value::as_u64) != u64::try_from(*expected).ok() + }) + || row + .get("nonclaims") + .and_then(Value::as_array) + .is_none_or(|values| { + values.len() != NONCLAIMS.len() + || values + .iter() + .zip(NONCLAIMS) + .any(|(value, expected)| value.as_str() != Some(*expected)) + }) + { + return Err(b108()); + } + Ok(()) +} + +fn validate_parameter_values(value: &Value, expected: &[ParameterFact]) -> Result<(), Diagnostic> { + let values = value.as_array().ok_or_else(b108)?; + if values.len() != expected.len() { + return Err(b108()); + } + for (value, expected) in values.iter().zip(expected) { + let row = value.as_object().ok_or_else(b108)?; + if row.len() != 3 + || row.get("name").and_then(Value::as_str) != Some(&expected.name) + || row.get("type").and_then(Value::as_str) != Some(scalar_text(expected.ty)) + || row.get("mode").and_then(Value::as_str) != Some("value") + { + return Err(b108()); + } + } + Ok(()) +} + +fn validate_result_value(value: &Value, expected: ScalarType) -> Result<(), Diagnostic> { + let row = value.as_object().ok_or_else(b108)?; + if row.len() != 2 + || row.get("type").and_then(Value::as_str) != Some(scalar_text(expected)) + || row.get("out_slot").and_then(Value::as_bool) != Some(expected != ScalarType::Unit) + { + return Err(b108()); + } + Ok(()) +} + +fn strings_equal(value: Option<&Value>, expected: &[String]) -> bool { + value.and_then(Value::as_array).is_some_and(|values| { + values.len() == expected.len() + && values + .iter() + .zip(expected) + .all(|(value, expected)| value.as_str() == Some(expected)) + }) +} + +fn validate_descriptor_exports(value: &Value, expected: &[ExportFact]) -> Result<(), Diagnostic> { + let rows = value.as_array().ok_or_else(b108)?; + if rows.len() != expected.len() { + return Err(b108()); + } + for (value, expected) in rows.iter().zip(expected) { + let row = value.as_object().ok_or_else(b108)?; + validate_parameter_values( + row.get("parameters").ok_or_else(b108)?, + &expected.parameters, + )?; + validate_result_value(row.get("result").ok_or_else(b108)?, expected.result)?; + if row.len() != 10 + || row.get("id").and_then(Value::as_str) != Some(&expected.id) + || row.get("rust_method").and_then(Value::as_str) != Some(&expected.rust_method) + || row.get("c_symbol").and_then(Value::as_str) != Some(&expected.c_symbol) + || !strings_equal(row.get("effects"), &expected.effects) + || !strings_equal(row.get("capabilities"), &expected.capabilities) + || !strings_equal(row.get("required_imports"), &expected.required_imports) + || row + .get("status_domain_ordinals") + .and_then(Value::as_array) + .is_none_or(|values| { + values.len() != expected.status_domain_ordinals.len() + || values + .iter() + .zip(&expected.status_domain_ordinals) + .any(|(value, expected)| value.as_u64() != Some(u64::from(*expected))) + }) + || row.get("call_contract_digest").and_then(Value::as_str) + != Some(&expected.call_contract_digest) + { + return Err(b108()); + } + } + Ok(()) +} + +fn validate_descriptor_imports(value: &Value, expected: &[ImportFact]) -> Result<(), Diagnostic> { + let rows = value.as_array().ok_or_else(b108)?; + if rows.len() != expected.len() { + return Err(b108()); + } + for (value, expected) in rows.iter().zip(expected) { + let row = value.as_object().ok_or_else(b108)?; + validate_parameter_values( + row.get("parameters").ok_or_else(b108)?, + &expected.parameters, + )?; + validate_result_value(row.get("result").ok_or_else(b108)?, expected.result)?; + let failure = row + .get("failure") + .and_then(Value::as_object) + .ok_or_else(b108)?; + let valid_failure = expected.failure.as_ref().map_or_else( + || { + failure.len() == 1 + && failure.get("kind").and_then(Value::as_str) == Some("infallible") + }, + |domain| { + failure.len() == 2 + && failure.get("kind").and_then(Value::as_str) == Some("status") + && failure.get("domain_id").and_then(Value::as_str) == Some(domain) + }, + ); + if row.len() != 11 + || row.get("id").and_then(Value::as_str) != Some(&expected.id) + || row.get("interface").and_then(Value::as_str) != Some(&expected.interface) + || row.get("import_key").and_then(Value::as_str) != Some(&expected.import_key) + || row.get("rust_method").and_then(Value::as_str) != Some(&expected.rust_method) + || row.get("c_field").and_then(Value::as_str) != Some(&expected.c_field) + || !strings_equal(row.get("effects"), &expected.effects) + || !strings_equal(row.get("capabilities"), &expected.capabilities) + || !valid_failure + || row.get("call_contract_digest").and_then(Value::as_str) + != Some(&expected.call_contract_digest) + { + return Err(b108()); + } + } + Ok(()) +} + +fn c_parameters(parameters: &[ParameterFact]) -> String { + let values = parameters + .iter() + .enumerate() + .map(|(index, parameter)| format!("{} arg_{index}", c_type(parameter.ty))) + .collect::>(); + let joined = values.join(", "); + #[cfg(test)] + note_post_hir_render_capacity( + string_slice_owned_capacity(&values).saturating_add(joined.capacity()), + ); + joined +} + +fn generate_header_with_limit( + exports: &[ExportFact], + imports: &[ImportFact], + maximum: usize, +) -> Result { + let mut import_rows = Vec::with_capacity(imports.len()); + for import in imports { + let params = c_parameters(&import.parameters); + let out = if import.result == ScalarType::Unit { + String::new() + } else { + format!(", {} *result_out", c_type(import.result)) + }; + let row = format!( + " spxnr_status_v1 (*{})(void *userdata{}{}{});", + import.c_field, + if params.is_empty() { "" } else { ", " }, + params, + out + ); + #[cfg(test)] + note_post_hir_render_capacity( + string_slice_owned_capacity(&import_rows) + .saturating_add(params.capacity()) + .saturating_add(out.capacity()) + .saturating_add(row.capacity()), + ); + import_rows.push(row); + } + let mut export_rows = Vec::with_capacity(exports.len()); + for export in exports { + let params = c_parameters(&export.parameters); + let out = if export.result == ScalarType::Unit { + String::new() + } else { + format!(", {} *result_out", c_type(export.result)) + }; + let row = format!( + "spxnr_status_v1 {}(const spxnr_context_v1 *ctx{}{}{});\n", + export.c_symbol, + if params.is_empty() { "" } else { ", " }, + params, + out + ); + #[cfg(test)] + note_post_hir_render_capacity( + string_slice_owned_capacity(&import_rows) + .saturating_add(string_slice_owned_capacity(&export_rows)) + .saturating_add(params.capacity()) + .saturating_add(out.capacity()) + .saturating_add(row.capacity()), + ); + export_rows.push(row); + } + render_exact_artifact("max_generated_header_bytes", maximum, |sink| { + sink.write_str( + "#ifndef SEMAPRAX_NATIVE_RUST_INTEROP_H\n#define SEMAPRAX_NATIVE_RUST_INTEROP_H\n#include \n#include \n#ifdef __cplusplus\nextern \"C\" {\n#endif\ntypedef uint64_t spxnr_status_v1;\ntypedef struct spxnr_imports_v1 spxnr_imports_v1;\ntypedef struct { uint32_t abi_version; uint32_t size; void *userdata; const spxnr_imports_v1 *imports; uint8_t capabilities_digest[32]; uint32_t call_depth; uint32_t reserved; } spxnr_context_v1;\nstruct spxnr_imports_v1 { uint32_t abi_version; uint32_t size;", + ) + .map_err(|_| b109("max_generated_header_bytes", MAX_GENERATED_HEADER_BYTES))?; + for row in &import_rows { + sink.write_str(row) + .map_err(|_| b109("max_generated_header_bytes", MAX_GENERATED_HEADER_BYTES))?; + } + sink.write_str(" };\n") + .map_err(|_| b109("max_generated_header_bytes", MAX_GENERATED_HEADER_BYTES))?; + for row in &export_rows { + sink.write_str(row) + .map_err(|_| b109("max_generated_header_bytes", MAX_GENERATED_HEADER_BYTES))?; + } + sink.write_str("#ifdef __cplusplus\n}\n#endif\n#endif\n") + .map_err(|_| b109("max_generated_header_bytes", MAX_GENERATED_HEADER_BYTES)) + }) +} + +fn generate_header(exports: &[ExportFact], imports: &[ImportFact]) -> Result { + generate_header_with_limit(exports, imports, MAX_GENERATED_HEADER_BYTES) +} + +#[derive(Clone, Copy)] +enum CExpressionMode { + Generate, + Replay, +} + +enum CExpressionFrame<'a> { + Enter(&'a ResolvedExpr), + Unary(crate::ast::UnaryOp), + BinaryLeft(crate::ast::BinaryOp, &'a ResolvedExpr), + BinaryRight(crate::ast::BinaryOp, String), + LazyLeft(crate::ast::BinaryOp, &'a ResolvedExpr), + LazyRight(String), + Block(&'a [ResolvedStatement], usize, &'a ResolvedExpr), + BlockLet(&'a [ResolvedStatement], usize, &'a ResolvedExpr), + IfCondition(&'a ResolvedExpr, &'a ResolvedExpr, ScalarType), + IfThen(&'a ResolvedExpr, Option), + IfElse(Option), + NativeArgs(&'a crate::hir::ResolvedNativeRustImportCall, usize, usize), + CallArgs(&'a str, &'a [ResolvedExpr], &'a ResolvedType, usize, usize), +} + +// Intentionally separate from `CExpressionFrame`: exact replay must not share +// the generator's scheduling state or traversal implementation. +enum ReplayCExpressionFrame<'a> { + Evaluate(&'a ResolvedExpr), + FinishUnary(crate::ast::UnaryOp), + FinishBinaryLeft(crate::ast::BinaryOp, &'a ResolvedExpr), + FinishBinary(crate::ast::BinaryOp, String), + FinishLazyLeft(crate::ast::BinaryOp, &'a ResolvedExpr), + FinishLazy(String), + ContinueBlock(&'a [ResolvedStatement], usize, &'a ResolvedExpr), + FinishBinding(&'a [ResolvedStatement], usize, &'a ResolvedExpr), + FinishCondition(&'a ResolvedExpr, &'a ResolvedExpr, ScalarType), + FinishThen(&'a ResolvedExpr, Option), + FinishElse(Option), + ContinueNative(&'a crate::hir::ResolvedNativeRustImportCall, usize, usize), + ContinueCall(&'a str, &'a [ResolvedExpr], &'a ResolvedType, usize, usize), +} + +/// One fixed backing allocation owns every generated statement byte for one C +/// expression. The final C artifact has a separate reservation; this arena is +/// transient scratch and cannot grow geometrically past the admitted artifact +/// ceiling before the final-size gate observes it. +struct CExpressionLineArena { + bytes: Box<[u8]>, + len: usize, +} + +impl CExpressionLineArena { + fn new() -> Self { + Self { + bytes: vec![0; MAX_GENERATED_C_BYTES].into_boxed_slice(), + len: 0, + } + } + + fn as_str(&self) -> Result<&str, Diagnostic> { + std::str::from_utf8(&self.bytes[..self.len]).map_err(|_| b111()) + } + + fn clear(&mut self) { + self.len = 0; + } + + #[cfg(test)] + fn retained_bytes(&self) -> usize { + self.bytes.len() + } +} + +impl std::fmt::Write for CExpressionLineArena { + fn write_str(&mut self, value: &str) -> std::fmt::Result { + let end = self.len.checked_add(value.len()).ok_or(std::fmt::Error)?; + let destination = self.bytes.get_mut(self.len..end).ok_or(std::fmt::Error)?; + destination.copy_from_slice(value.as_bytes()); + self.len = end; + Ok(()) + } +} + +fn c_expression_hash(mode: CExpressionMode, value: &str) -> String { + match mode { + CExpressionMode::Generate => full_hash(value), + CExpressionMode::Replay => replay_symbol_hash(value), + } +} + +fn c_expression_scalar(mode: CExpressionMode, value: ScalarType) -> &'static str { + match mode { + CExpressionMode::Generate => c_type(value), + CExpressionMode::Replay => replay_c_scalar(value), + } +} + +fn c_expression_resolved_scalar(mode: CExpressionMode, value: &ResolvedType) -> Option { + match mode { + CExpressionMode::Generate => scalar_type(value), + CExpressionMode::Replay => replay_resolved_scalar(value), + } +} + +#[cfg(any())] +fn take_c_lines(lines: &mut Vec) -> String { + let bytes = lines.iter().map(String::len).sum(); + let mut joined = String::with_capacity(bytes); + for line in lines.drain(..) { + joined.push_str(&line); + } + joined +} + +#[cfg(any())] +fn append_c_lines(output: &mut String, lines: &mut Vec) { + for line in lines.drain(..) { + output.push_str(&line); + } +} + +#[cfg(any())] +fn move_root_c_lines(lines: &mut Vec, contexts: &mut [Vec]) { + let mut root = std::mem::take(&mut contexts[0]); + if lines.is_empty() { + std::mem::swap(lines, &mut root); + } else { + lines.append(&mut root); + } +} + +fn c_expression_child(expression: &ResolvedExpr, index: usize) -> Option<&ResolvedExpr> { + match &expression.kind { + ResolvedExprKind::Call { args, .. } => args.get(index), + ResolvedExprKind::NativeRustImportCall(call) => call.args.get(index), + ResolvedExprKind::Unary { value, .. } => (index == 0).then_some(value), + ResolvedExprKind::Binary { left, right, .. } => { + [left.as_ref(), right.as_ref()].get(index).copied() + } + ResolvedExprKind::Block { statements, tail } => statements + .get(index) + .map(|statement| { + let ResolvedStatement::Let { value, .. } = statement; + value + }) + .or_else(|| (index == statements.len()).then_some(tail)), + ResolvedExprKind::If { + condition, + then_branch, + else_branch, + } => [ + condition.as_ref(), + then_branch.as_ref(), + else_branch.as_ref(), + ] + .get(index) + .copied(), + _ => None, + } +} + +fn c_expression_shape(expression: &ResolvedExpr) -> Result<(usize, usize), Diagnostic> { + let mut stack = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + stack[0] = Some((expression, 0usize, 1usize)); + let mut stack_len = 1usize; + let mut nodes = 0usize; + let mut depth = 1usize; + while stack_len > 0 { + let (node, next_child, node_depth) = stack[stack_len - 1].take().ok_or_else(b111)?; + stack_len -= 1; + if next_child == 0 { + nodes = nodes + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + depth = depth.max(node_depth); + } + if let Some(child) = c_expression_child(node, next_child) { + if stack_len + 2 > stack.len() { + return Err(b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + )); + } + stack[stack_len] = Some((node, next_child + 1, node_depth)); + stack[stack_len + 1] = Some((child, 0, node_depth + 1)); + stack_len += 2; + } + } + Ok((nodes, depth)) +} + +fn c_expression_frame_payload(frame: &CExpressionFrame<'_>) -> usize { + match frame { + CExpressionFrame::BinaryRight(_, value) | CExpressionFrame::LazyRight(value) => { + value.capacity() + } + CExpressionFrame::IfThen(_, value) | CExpressionFrame::IfElse(value) => { + value.as_ref().map_or(0, String::capacity) + } + _ => 0, + } +} + +fn c_expression_live_string_payload( + current: &CExpressionFrame<'_>, + frames: &[CExpressionFrame<'_>], + values: &[String], + arguments: &[String], +) -> Option { + frames + .iter() + .try_fold(c_expression_frame_payload(current), |bytes, frame| { + bytes.checked_add(c_expression_frame_payload(frame)) + })? + .checked_add( + values + .iter() + .try_fold(0usize, |bytes, value| bytes.checked_add(value.capacity()))?, + )? + .checked_add( + arguments + .iter() + .try_fold(0usize, |bytes, value| bytes.checked_add(value.capacity()))?, + ) +} + +#[allow(clippy::ptr_arg)] // Exact Vec capacities are part of the scratch proof. +fn note_c_expression_scratch( + mode: CExpressionMode, + current: &CExpressionFrame<'_>, + frames: &Vec>, + values: &Vec, + arguments: &Vec, + lines: &CExpressionLineArena, +) -> Result<(), Diagnostic> { + #[cfg(not(test))] + let _ = mode; + #[cfg(not(test))] + let _ = lines; + let string_payload = c_expression_live_string_payload(current, frames, values, arguments) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if string_payload > MAX_GENERATED_C_BYTES { + return Err(b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES)); + } + #[cfg(test)] + { + let working = frames + .capacity() + .saturating_mul(C_EXPRESSION_FRAME_BYTES) + .saturating_add( + values + .capacity() + .saturating_mul(std::mem::size_of::()), + ) + .saturating_add( + arguments + .capacity() + .saturating_mul(std::mem::size_of::()), + ) + .saturating_add(lines.retained_bytes()) + .saturating_add(string_payload); + match mode { + CExpressionMode::Generate => note_post_hir_render_capacity(working), + CExpressionMode::Replay => note_post_hir_replay_capacity(working), + } + } + Ok(()) +} + +fn write_c_expression_arguments( + lines: &mut CExpressionLineArena, + arguments: &[String], + separator: &str, +) -> Result<(), Diagnostic> { + for (index, argument) in arguments.iter().enumerate() { + if index != 0 { + lines + .write_str(separator) + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + } + lines + .write_str(argument) + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + } + Ok(()) +} + +fn c_expression_linear( + expression: &ResolvedExpr, + imports: &[ImportFact], + temporary_count: &mut usize, + lines: &mut CExpressionLineArena, +) -> Result { + let mode = CExpressionMode::Generate; + let (node_count, depth) = c_expression_shape(expression)?; + let frame_capacity = depth + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let mut frames = Vec::with_capacity(frame_capacity); + let mut values = Vec::::with_capacity(frame_capacity); + let mut arguments = Vec::::with_capacity(node_count); + frames.push(CExpressionFrame::Enter(expression)); + while let Some(frame) = frames.pop() { + note_c_expression_scratch(mode, &frame, &frames, &values, &arguments, lines)?; + match frame { + CExpressionFrame::Enter(expression) => match &expression.kind { + ResolvedExprKind::Int(value) => values.push(if *value == i64::MIN { + "INT64_MIN".to_owned() + } else { + format!("INT64_C({value})") + }), + ResolvedExprKind::Bool(value) => { + values.push(if *value { "UINT8_C(1)" } else { "UINT8_C(0)" }.to_owned()) + } + ResolvedExprKind::Place(place) if place.projections.is_empty() => values.push( + format!("v_{}", c_expression_hash(mode, place.root.as_str())), + ), + ResolvedExprKind::NativeRustImportCall(call) => { + frames.push(CExpressionFrame::NativeArgs(call, 0, arguments.len())); + } + ResolvedExprKind::Unary { op, value } => { + frames.push(CExpressionFrame::Unary(*op)); + frames.push(CExpressionFrame::Enter(value)); + } + ResolvedExprKind::Binary { op, left, right } + if matches!(op, crate::ast::BinaryOp::And | crate::ast::BinaryOp::Or) => + { + frames.push(CExpressionFrame::LazyLeft(*op, right)); + frames.push(CExpressionFrame::Enter(left)); + } + ResolvedExprKind::Binary { op, left, right } => { + frames.push(CExpressionFrame::BinaryLeft(*op, right)); + frames.push(CExpressionFrame::Enter(left)); + } + ResolvedExprKind::Block { statements, tail } => { + frames.push(CExpressionFrame::Block(statements, 0, tail)); + } + ResolvedExprKind::If { + condition, + then_branch, + else_branch, + } => { + let ty = c_expression_resolved_scalar(mode, &expression.ty).ok_or_else(b111)?; + frames.push(CExpressionFrame::IfCondition(then_branch, else_branch, ty)); + frames.push(CExpressionFrame::Enter(condition)); + } + ResolvedExprKind::Call { callee, args, .. } => { + frames.push(CExpressionFrame::CallArgs( + callee.as_str(), + args, + &expression.ty, + 0, + arguments.len(), + )); + } + ResolvedExprKind::ConstructRecord { .. } + | ResolvedExprKind::ConstructVariant { .. } + | ResolvedExprKind::Match { .. } + | ResolvedExprKind::Try { .. } + | ResolvedExprKind::TryOption { .. } + | ResolvedExprKind::UpdateRecord { .. } + | ResolvedExprKind::Project { .. } + | ResolvedExprKind::Place(_) => { + return Err(b107("scalar value signature required")); + } + }, + CExpressionFrame::Unary(op) => { + let value = values.pop().ok_or_else(b111)?; + match op { + crate::ast::UnaryOp::Neg => { + let name = format!("tmp_{}", *temporary_count); + *temporary_count += 1; + write!(lines, "if(({value})==INT64_MIN)return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(1);int64_t {name}=-({value});") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + values.push(name); + } + crate::ast::UnaryOp::Not => values.push(format!("(!({value}))")), + } + } + CExpressionFrame::BinaryLeft(op, right) => { + let left = values.pop().ok_or_else(b111)?; + frames.push(CExpressionFrame::BinaryRight(op, left)); + frames.push(CExpressionFrame::Enter(right)); + } + CExpressionFrame::BinaryRight(op, left) => { + let right = values.pop().ok_or_else(b111)?; + if matches!( + op, + crate::ast::BinaryOp::Add + | crate::ast::BinaryOp::Sub + | crate::ast::BinaryOp::Mul + | crate::ast::BinaryOp::Div + | crate::ast::BinaryOp::Rem + ) { + let name = format!("tmp_{}", *temporary_count); + *temporary_count += 1; + write!(lines, "int64_t {name};") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + match op { + crate::ast::BinaryOp::Add => write!(lines, "if(__builtin_add_overflow({left},{right},&{name}))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(2);"), + crate::ast::BinaryOp::Sub => write!(lines, "if(__builtin_sub_overflow({left},{right},&{name}))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(3);"), + crate::ast::BinaryOp::Mul => write!(lines, "if(__builtin_mul_overflow({left},{right},&{name}))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(4);"), + crate::ast::BinaryOp::Div => write!(lines, "if(({right})==0||(({left})==INT64_MIN&&({right})==-1))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(5);{name}=({left})/({right});"), + crate::ast::BinaryOp::Rem => write!(lines, "if(({right})==0||(({left})==INT64_MIN&&({right})==-1))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(6);{name}=({left})%({right});"), + _ => unreachable!(), + } + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + values.push(name); + } else { + let operator = match op { + crate::ast::BinaryOp::Eq => "==", + crate::ast::BinaryOp::Ne => "!=", + crate::ast::BinaryOp::Lt => "<", + crate::ast::BinaryOp::Le => "<=", + crate::ast::BinaryOp::Gt => ">", + crate::ast::BinaryOp::Ge => ">=", + crate::ast::BinaryOp::And => "&&", + crate::ast::BinaryOp::Or => "||", + _ => unreachable!(), + }; + values.push(format!("(({left}) {operator} ({right}))")); + } + } + CExpressionFrame::LazyLeft(op, right) => { + let left = values.pop().ok_or_else(b111)?; + let name = format!("tmp_{}", *temporary_count); + *temporary_count += 1; + write!( + lines, + "uint8_t {name}=({left})?UINT8_C(1):UINT8_C(0);if({}){{", + if op == crate::ast::BinaryOp::And { + name.clone() + } else { + format!("!{name}") + } + ) + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + frames.push(CExpressionFrame::LazyRight(name)); + frames.push(CExpressionFrame::Enter(right)); + } + CExpressionFrame::LazyRight(name) => { + let right = values.pop().ok_or_else(b111)?; + write!(lines, " {name}=({right})?UINT8_C(1):UINT8_C(0);}}") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + values.push(name); + } + CExpressionFrame::Block(statements, index, tail) => { + if let Some(ResolvedStatement::Let { value, .. }) = statements.get(index) { + frames.push(CExpressionFrame::BlockLet(statements, index, tail)); + frames.push(CExpressionFrame::Enter(value)); + } else { + frames.push(CExpressionFrame::Enter(tail)); + } + } + CExpressionFrame::BlockLet(statements, index, tail) => { + let value = values.pop().ok_or_else(b111)?; + let ResolvedStatement::Let { binding, .. } = &statements[index]; + let ty = c_expression_resolved_scalar(mode, &binding.ty).ok_or_else(b111)?; + if ty != ScalarType::Unit { + write!( + lines, + "{} v_{} = {value};", + c_expression_scalar(mode, ty), + c_expression_hash(mode, binding.id.as_str()) + ) + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + } + frames.push(CExpressionFrame::Block(statements, index + 1, tail)); + } + CExpressionFrame::IfCondition(then_branch, else_branch, ty) => { + let condition = values.pop().ok_or_else(b111)?; + let name = if ty == ScalarType::Unit { + None + } else { + let name = format!("tmp_{}", *temporary_count); + *temporary_count += 1; + write!(lines, "{} {name};", c_expression_scalar(mode, ty)) + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + Some(name) + }; + write!(lines, "if({condition}){{") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + frames.push(CExpressionFrame::IfThen(else_branch, name)); + frames.push(CExpressionFrame::Enter(then_branch)); + } + CExpressionFrame::IfThen(else_branch, name) => { + let then_value = values.pop().ok_or_else(b111)?; + if let Some(name) = &name { + write!(lines, "{name}={then_value};") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + } + lines + .write_str("}else{") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + frames.push(CExpressionFrame::IfElse(name)); + frames.push(CExpressionFrame::Enter(else_branch)); + } + CExpressionFrame::IfElse(name) => { + let else_value = values.pop().ok_or_else(b111)?; + if let Some(name) = name { + write!(lines, "{name}={else_value};}}") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + values.push(name); + } else { + lines + .write_str("}") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + values.push("INT64_C(0)".to_owned()); + } + } + CExpressionFrame::NativeArgs(call, index, start) => { + if index < call.args.len() { + if index > 0 { + arguments.push(values.pop().ok_or_else(b111)?); + } + frames.push(CExpressionFrame::NativeArgs(call, index + 1, start)); + frames.push(CExpressionFrame::Enter(&call.args[index])); + } else { + if !call.args.is_empty() { + arguments.push(values.pop().ok_or_else(b111)?); + } + let import = imports + .iter() + .find(|item| item.id == call.import.as_str()) + .ok_or_else(b111)?; + let name = format!("tmp_{}", *temporary_count); + if import.result != ScalarType::Unit { + *temporary_count += 1; + write!( + lines, + "{} {name};", + c_expression_scalar(mode, import.result) + ) + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + } + write!( + lines, + "status = ctx->imports->{}(ctx->userdata", + import.c_field + ) + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + if start != arguments.len() { + lines + .write_str(", ") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + write_c_expression_arguments(lines, &arguments[start..], ", ")?; + } + if import.result != ScalarType::Unit { + write!(lines, ", &{name}") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + } + write!(lines, "); if (status != 0) {{ if (!spxnr_status_for_{}(status)) return spxnr_adapter(8); return status; }}", import.rust_method) + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + if import.result == ScalarType::Bool { + write!(lines, "if ({name} > UINT8_C(1)) return spxnr_adapter(4);") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + } + arguments.truncate(start); + values.push(if import.result == ScalarType::Unit { + "INT64_C(0)".to_owned() + } else { + name + }); + } + } + CExpressionFrame::CallArgs(callee, source, ty, index, start) => { + if index < source.len() { + if index > 0 { + arguments.push(values.pop().ok_or_else(b111)?); + } + frames.push(CExpressionFrame::CallArgs( + callee, + source, + ty, + index + 1, + start, + )); + frames.push(CExpressionFrame::Enter(&source[index])); + } else { + if !source.is_empty() { + arguments.push(values.pop().ok_or_else(b111)?); + } + if *ty == ResolvedType::Unit { + write!( + lines, + "status=spxnr1_f_{}(ctx", + c_expression_hash(mode, callee) + ) + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + if start != arguments.len() { + lines.write_str(",").map_err(|_| { + b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES) + })?; + write_c_expression_arguments(lines, &arguments[start..], ",")?; + } + lines + .write_str(");if(status!=0)return status;") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + values.push("INT64_C(0)".to_owned()); + } else { + let name = format!("tmp_{}", *temporary_count); + *temporary_count += 1; + write!( + lines, + "{} {name};status=spxnr1_f_{}(ctx", + c_expression_scalar( + mode, + c_expression_resolved_scalar(mode, ty).ok_or_else(b111)? + ), + c_expression_hash(mode, callee) + ) + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + if start != arguments.len() { + lines.write_str(",").map_err(|_| { + b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES) + })?; + write_c_expression_arguments(lines, &arguments[start..], ",")?; + } + write!(lines, ",&{name});if(status!=0)return status;") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + values.push(name); + } + arguments.truncate(start); + } + } + } + } + let terminal = CExpressionFrame::Enter(expression); + note_c_expression_scratch(mode, &terminal, &frames, &values, &arguments, lines)?; + if values.len() != 1 || !arguments.is_empty() { + return Err(b111()); + } + let result = values.pop().ok_or_else(b111)?; + if result.capacity() > MAX_GENERATED_C_BYTES { + return Err(b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES)); + } + Ok(result) +} + +#[cfg(any())] +fn c_context_line_slots(expression: &ResolvedExpr) -> Result { + // A line is owned by exactly one active context. Branch results are + // collapsed to one String before being appended to their parent, and the + // drained child Vec is released immediately. Child contexts therefore do + // not reserve their whole subtree: across all live contexts their logical + // line count is at most 3N. Vec geometric growth is below twice logical + // length, so 6N String slots bounds all context backings simultaneously. + c_expression_shape(expression)? + .0 + .checked_mul(6) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES)) +} + +#[cfg(any())] +fn c_expr_iterative( + expression: &ResolvedExpr, + imports: &[ImportFact], + temporary_count: &mut usize, + mut temporary_names: Option<&mut Vec>, + lines: &mut Vec, + mode: CExpressionMode, +) -> Result { + enum Frame<'a> { + Enter(&'a ResolvedExpr, usize), + Unary(crate::ast::UnaryOp, usize), + BinaryLeft(crate::ast::BinaryOp, &'a ResolvedExpr, usize), + BinaryRight(crate::ast::BinaryOp, String, usize), + LazyLeft(crate::ast::BinaryOp, &'a ResolvedExpr, usize), + LazyRight(crate::ast::BinaryOp, String, String, usize, usize), + Block(&'a [ResolvedStatement], usize, &'a ResolvedExpr, usize), + BlockLet(&'a [ResolvedStatement], usize, &'a ResolvedExpr, usize), + IfCondition(&'a ResolvedExpr, &'a ResolvedExpr, ScalarType, usize), + IfThen(String, &'a ResolvedExpr, Option, usize, usize), + IfElse(String, Option, String, usize, usize, usize), + NativeArgs( + &'a crate::hir::ResolvedNativeRustImportCall, + usize, + Vec, + usize, + ), + CallArgs( + &'a str, + &'a [ResolvedExpr], + &'a ResolvedType, + usize, + Vec, + usize, + ), + } + const _: () = assert!(std::mem::size_of::>() == C_EXPRESSION_FRAME_BYTES); + + let allocate_temporary = + |temporary_count: &mut usize, temporary_names: &mut Option<&mut Vec>| { + let name = format!("tmp_{}", *temporary_count); + *temporary_count += 1; + if let Some(names) = temporary_names.as_deref_mut() { + names.push(name.clone()); + } + name + }; + let (node_count, depth) = c_expression_shape(expression)?; + let line_capacity = node_count + .checked_mul(3) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if lines.capacity() < line_capacity { + lines + .try_reserve_exact(line_capacity - lines.capacity()) + .map_err(|_| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + let frame_capacity = node_count + .checked_mul(2) + .and_then(|slots| slots.checked_add(1)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let mut frames = Vec::with_capacity(frame_capacity); + frames.push(Frame::Enter(expression, 0)); + let mut results = Vec::::with_capacity(depth + 1); + let mut contexts = Vec::with_capacity(node_count + 1); + contexts.push(Vec::::with_capacity(node_count.saturating_mul(3))); + while let Some(frame) = frames.pop() { + #[cfg(test)] + { + let frame_owned = frames + .iter() + .map(|frame| match frame { + Frame::BinaryRight(_, value, _) + | Frame::LazyRight(_, value, _, _, _) + | Frame::IfThen(value, _, _, _, _) + | Frame::IfElse(value, _, _, _, _, _) => value.capacity(), + Frame::NativeArgs(_, _, values, _) | Frame::CallArgs(_, _, _, _, values, _) => { + values.capacity() * std::mem::size_of::() + + values.iter().map(String::capacity).sum::() + } + _ => 0, + }) + .sum::(); + let result_owned = results.capacity() * std::mem::size_of::() + + results.iter().map(String::capacity).sum::(); + let context_owned = contexts.capacity() * std::mem::size_of::>() + + contexts + .iter() + .map(|context| { + context.capacity() * std::mem::size_of::() + + context.iter().map(String::capacity).sum::() + }) + .sum::(); + let caller_lines = lines.capacity() * std::mem::size_of::() + + lines.iter().map(String::capacity).sum::(); + let persistent_temporaries = temporary_names.as_deref().map_or(0, |names| { + names.capacity() * std::mem::size_of::() + + names.iter().map(String::capacity).sum::() + }); + let working = frames.capacity() * std::mem::size_of::>() + + frame_owned + + result_owned + + context_owned + + caller_lines + + persistent_temporaries; + match mode { + CExpressionMode::Generate => note_post_hir_render_capacity(working), + CExpressionMode::Replay => note_post_hir_replay_capacity(working), + } + } + match frame { + Frame::Enter(expression, context) => match &expression.kind { + ResolvedExprKind::Int(value) => results.push(if *value == i64::MIN { + "INT64_MIN".to_owned() + } else { + format!("INT64_C({value})") + }), + ResolvedExprKind::Bool(value) => { + results.push(if *value { "UINT8_C(1)" } else { "UINT8_C(0)" }.to_owned()) + } + ResolvedExprKind::Place(place) if place.projections.is_empty() => results.push( + format!("v_{}", c_expression_hash(mode, place.root.as_str())), + ), + ResolvedExprKind::NativeRustImportCall(call) => { + frames.push(Frame::NativeArgs( + call, + 0, + Vec::with_capacity(call.args.len()), + context, + )); + } + ResolvedExprKind::Unary { op, value } => { + frames.push(Frame::Unary(*op, context)); + frames.push(Frame::Enter(value, context)); + } + ResolvedExprKind::Binary { op, left, right } + if matches!(op, crate::ast::BinaryOp::And | crate::ast::BinaryOp::Or) => + { + frames.push(Frame::LazyLeft(*op, right, context)); + frames.push(Frame::Enter(left, context)); + } + ResolvedExprKind::Binary { op, left, right } => { + frames.push(Frame::BinaryLeft(*op, right, context)); + frames.push(Frame::Enter(left, context)); + } + ResolvedExprKind::Block { statements, tail } => { + frames.push(Frame::Block(statements, 0, tail, context)); + } + ResolvedExprKind::If { + condition, + then_branch, + else_branch, + } => { + let ty = c_expression_resolved_scalar(mode, &expression.ty).ok_or_else(b111)?; + frames.push(Frame::IfCondition(then_branch, else_branch, ty, context)); + frames.push(Frame::Enter(condition, context)); + } + ResolvedExprKind::Call { callee, args, .. } => { + frames.push(Frame::CallArgs( + callee.as_str(), + args, + &expression.ty, + 0, + Vec::with_capacity(args.len()), + context, + )); + } + ResolvedExprKind::ConstructRecord { .. } + | ResolvedExprKind::ConstructVariant { .. } + | ResolvedExprKind::Match { .. } + | ResolvedExprKind::Try { .. } + | ResolvedExprKind::TryOption { .. } + | ResolvedExprKind::UpdateRecord { .. } + | ResolvedExprKind::Project { .. } + | ResolvedExprKind::Place(_) => { + return Err(b107("scalar value signature required")); + } + }, + Frame::Unary(op, context) => { + let value = results.pop().ok_or_else(b111)?; + match op { + crate::ast::UnaryOp::Neg => { + let name = allocate_temporary(temporary_count, &mut temporary_names); + contexts[context].push(format!("if(({value})==INT64_MIN)return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(1);int64_t {name}=-({value});")); + results.push(name); + } + crate::ast::UnaryOp::Not => results.push(format!("(!({value}))")), + } + } + Frame::BinaryLeft(op, right, context) => { + let left = results.pop().ok_or_else(b111)?; + frames.push(Frame::BinaryRight(op, left, context)); + frames.push(Frame::Enter(right, context)); + } + Frame::BinaryRight(op, left, context) => { + let right = results.pop().ok_or_else(b111)?; + if matches!( + op, + crate::ast::BinaryOp::Add + | crate::ast::BinaryOp::Sub + | crate::ast::BinaryOp::Mul + | crate::ast::BinaryOp::Div + | crate::ast::BinaryOp::Rem + ) { + let name = allocate_temporary(temporary_count, &mut temporary_names); + contexts[context].push(format!("int64_t {name};")); + contexts[context].push(match op { + crate::ast::BinaryOp::Add => format!("if(__builtin_add_overflow({left},{right},&{name}))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(2);"), + crate::ast::BinaryOp::Sub => format!("if(__builtin_sub_overflow({left},{right},&{name}))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(3);"), + crate::ast::BinaryOp::Mul => format!("if(__builtin_mul_overflow({left},{right},&{name}))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(4);"), + crate::ast::BinaryOp::Div => format!("if(({right})==0||(({left})==INT64_MIN&&({right})==-1))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(5);{name}=({left})/({right});"), + crate::ast::BinaryOp::Rem => format!("if(({right})==0||(({left})==INT64_MIN&&({right})==-1))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(6);{name}=({left})%({right});"), + _ => unreachable!(), + }); + results.push(name); + } else { + let operator = match op { + crate::ast::BinaryOp::Eq => "==", + crate::ast::BinaryOp::Ne => "!=", + crate::ast::BinaryOp::Lt => "<", + crate::ast::BinaryOp::Le => "<=", + crate::ast::BinaryOp::Gt => ">", + crate::ast::BinaryOp::Ge => ">=", + crate::ast::BinaryOp::And => "&&", + crate::ast::BinaryOp::Or => "||", + _ => unreachable!(), + }; + results.push(format!("(({left}) {operator} ({right}))")); + } + } + Frame::LazyLeft(op, right, context) => { + let left = results.pop().ok_or_else(b111)?; + let name = allocate_temporary(temporary_count, &mut temporary_names); + contexts[context].push(format!("uint8_t {name}=({left})?UINT8_C(1):UINT8_C(0);")); + let branch = contexts.len(); + contexts.push(Vec::new()); + frames.push(Frame::LazyRight(op, name, left, context, branch)); + frames.push(Frame::Enter(right, branch)); + } + Frame::LazyRight(op, name, _left, context, branch) => { + let right = results.pop().ok_or_else(b111)?; + let condition = if op == crate::ast::BinaryOp::And { + name.clone() + } else { + format!("!{name}") + }; + let branch_lines = take_c_lines(&mut contexts[branch]); + contexts[branch] = Vec::new(); + contexts[context].push(format!( + "if({condition}){{{branch_lines} {name}=({right})?UINT8_C(1):UINT8_C(0);}}" + )); + results.push(name); + } + Frame::Block(statements, index, tail, context) => { + if index == statements.len() { + frames.push(Frame::Enter(tail, context)); + } else { + let ResolvedStatement::Let { value, .. } = &statements[index]; + frames.push(Frame::BlockLet(statements, index, tail, context)); + frames.push(Frame::Enter(value, context)); + } + } + Frame::BlockLet(statements, index, tail, context) => { + let value = results.pop().ok_or_else(b111)?; + let ResolvedStatement::Let { binding, .. } = &statements[index]; + let ty = c_expression_resolved_scalar(mode, &binding.ty).ok_or_else(b111)?; + if ty != ScalarType::Unit { + contexts[context].push(format!( + "{} v_{} = {value};", + c_expression_scalar(mode, ty), + c_expression_hash(mode, binding.id.as_str()) + )); + } + frames.push(Frame::Block(statements, index + 1, tail, context)); + } + Frame::IfCondition(then_branch, else_branch, ty, context) => { + let condition = results.pop().ok_or_else(b111)?; + let name = if ty == ScalarType::Unit { + None + } else { + let name = allocate_temporary(temporary_count, &mut temporary_names); + contexts[context].push(format!("{} {name};", c_expression_scalar(mode, ty))); + Some(name) + }; + let then_context = contexts.len(); + contexts.push(Vec::new()); + frames.push(Frame::IfThen( + condition, + else_branch, + name, + context, + then_context, + )); + frames.push(Frame::Enter(then_branch, then_context)); + } + Frame::IfThen(condition, else_branch, name, context, then_context) => { + let then_value = results.pop().ok_or_else(b111)?; + let else_context = contexts.len(); + contexts.push(Vec::new()); + frames.push(Frame::IfElse( + condition, + name, + then_value, + context, + then_context, + else_context, + )); + frames.push(Frame::Enter(else_branch, else_context)); + } + Frame::IfElse(condition, name, then_value, context, then_context, else_context) => { + let else_value = results.pop().ok_or_else(b111)?; + let then_lines = take_c_lines(&mut contexts[then_context]); + let else_lines = take_c_lines(&mut contexts[else_context]); + contexts[then_context] = Vec::new(); + contexts[else_context] = Vec::new(); + if let Some(name) = name { + contexts[context].push(format!("if({condition}){{{then_lines}{name}={then_value};}}else{{{else_lines}{name}={else_value};}}")); + results.push(name); + } else { + contexts[context].push(format!( + "if({condition}){{{then_lines}}}else{{{else_lines}}}" + )); + results.push("INT64_C(0)".to_owned()); + } + } + Frame::NativeArgs(call, index, mut args, context) => { + if index < call.args.len() { + if index > 0 { + args.push(results.pop().ok_or_else(b111)?); + } + frames.push(Frame::NativeArgs(call, index + 1, args, context)); + frames.push(Frame::Enter(&call.args[index], context)); + } else { + if !call.args.is_empty() { + args.push(results.pop().ok_or_else(b111)?); + } + let import = imports + .iter() + .find(|item| item.id == call.import.as_str()) + .ok_or_else(b111)?; + let name = if import.result == ScalarType::Unit { + format!("tmp_{}", *temporary_count) + } else { + allocate_temporary(temporary_count, &mut temporary_names) + }; + if import.result != ScalarType::Unit { + contexts[context].push(format!( + "{} {name};", + c_expression_scalar(mode, import.result) + )); + } + contexts[context].push(format!("status = ctx->imports->{}(ctx->userdata{}{}{}); if (status != 0) {{ if (!spxnr_status_for_{}(status)) return spxnr_adapter(8); return status; }}", import.c_field, if args.is_empty() { "" } else { ", " }, args.join(", "), if import.result == ScalarType::Unit { String::new() } else { format!(", &{name}") }, import.rust_method)); + if import.result == ScalarType::Bool { + contexts[context] + .push(format!("if ({name} > UINT8_C(1)) return spxnr_adapter(4);")); + } + results.push(if import.result == ScalarType::Unit { + "INT64_C(0)".to_owned() + } else { + name + }); + } + } + Frame::CallArgs(callee, call_args, ty, index, mut args, context) => { + if index < call_args.len() { + if index > 0 { + args.push(results.pop().ok_or_else(b111)?); + } + frames.push(Frame::CallArgs( + callee, + call_args, + ty, + index + 1, + args, + context, + )); + frames.push(Frame::Enter(&call_args[index], context)); + } else { + if !call_args.is_empty() { + args.push(results.pop().ok_or_else(b111)?); + } + if *ty == ResolvedType::Unit { + contexts[context].push(format!( + "status=spxnr1_f_{}(ctx{}{});if(status!=0)return status;", + c_expression_hash(mode, callee), + if args.is_empty() { "" } else { ", " }, + args.join(",") + )); + results.push("INT64_C(0)".to_owned()); + } else { + let name = allocate_temporary(temporary_count, &mut temporary_names); + let scalar = c_expression_resolved_scalar(mode, ty).ok_or_else(b111)?; + contexts[context].push(format!("{} {name};status=spxnr1_f_{}(ctx{}{},&{name});if(status!=0)return status;", c_expression_scalar(mode, scalar), c_expression_hash(mode, callee), if args.is_empty() { "" } else { ", " }, args.join(","))); + results.push(name); + } + } + } + } + } + if results.len() != 1 { + return Err(b111()); + } + move_root_c_lines(lines, &mut contexts); + results.pop().ok_or_else(b111) +} + +#[cfg(any())] +fn c_expr( + expression: &ResolvedExpr, + imports: &[ImportFact], + temporaries: &mut Vec, + lines: &mut Vec, +) -> Result { + let mut count = temporaries.len(); + c_expr_iterative( + expression, + imports, + &mut count, + Some(temporaries), + lines, + CExpressionMode::Generate, + ) +} + +fn c_expr( + expression: &ResolvedExpr, + imports: &[ImportFact], + temporary_count: &mut usize, + lines: &mut CExpressionLineArena, +) -> Result { + c_expression_linear(expression, imports, temporary_count, lines) +} + +fn generate_c_into( + output: &mut dyn std::fmt::Write, + spec: &Spec, + closure: &[&ResolvedFunction], + exports: &[ExportFact], + imports: &[ImportFact], +) -> Result<(), Diagnostic> { + let capability_digest = capability_digest(&spec.capabilities); + let capability_hex = capability_digest.strip_prefix("sha256:").ok_or_else(b111)?; + let bytes = (0..64) + .step_by(2) + .map(|index| format!("0x{}", &capability_hex[index..index + 2])) + .collect::>() + .join(","); + write!( + output, + "#include \"semaprax_native_rust_interop.h\"\n#include \n#include \n#include \n#include \nstatic const uint8_t spxnr_capabilities[32] = {{{bytes}}};\nstatic spxnr_status_v1 spxnr_adapter(uint32_t code){{return (((uint64_t)65535)<<48)|(((uint64_t)4)<<32)|code;}}\nstatic spxnr_status_v1 spxnr_validate(const spxnr_context_v1 *ctx){{if(!ctx||((uintptr_t)ctx%_Alignof(spxnr_context_v1))!=0)return spxnr_adapter(1);if(ctx->abi_version!=1||ctx->size!=sizeof(*ctx)||ctx->reserved!=0)return spxnr_adapter(1);if(!ctx->imports||((uintptr_t)ctx->imports%_Alignof(spxnr_imports_v1))!=0)return spxnr_adapter(2);if(ctx->imports->abi_version!=1||ctx->imports->size!=sizeof(*ctx->imports))return spxnr_adapter(2);if(memcmp(ctx->capabilities_digest,spxnr_capabilities,32)!=0)return spxnr_adapter(3);if(ctx->call_depth>=32)return spxnr_adapter(7);return 0;}}\n" + ) + .unwrap(); + output.write_str("static int spxnr_status_canonical(spxnr_status_v1 status){if(status==0)return 1;uint32_t code=(uint32_t)status;uint8_t class_=(uint8_t)(status>>32);uint8_t retry=(uint8_t)((status>>40)&1);uint8_t reserved=(uint8_t)((status>>41)&0x7f);uint16_t domain=(uint16_t)(status>>48);if(code==0||reserved!=0||domain==0)return 0;if(domain==65533)return retry==0&&((class_==1&&code>=1&&code<=6)||(class_==2&&code>=1&&code<=2));").unwrap(); + let domains = imports + .iter() + .filter_map(|import| import.failure.as_ref()) + .collect::>(); + for (index, _) in domains.iter().enumerate() { + write!(output, "if(domain=={})return class_==3;", index + 1).unwrap(); + } + output.write_str("if(domain==65534)return class_==4&&retry==0&&code>=1&&code<=2;if(domain==65535)return class_==4&&retry==0&&code>=1&&code<=8;return 0;}\n").unwrap(); + let domain_ordinals = domains + .iter() + .enumerate() + .map(|(index, domain)| (domain.as_str(), index + 1)) + .collect::>(); + for import in imports { + let custom = import + .failure + .as_deref() + .and_then(|domain| domain_ordinals.get(domain).copied()); + write!( + output, + "static int spxnr_status_for_{}(spxnr_status_v1 status){{if(!spxnr_status_canonical(status))return 0;uint16_t domain=(uint16_t)(status>>48);return domain==65534||domain==65535{};}}\n", + import.rust_method, + custom.map_or_else(String::new, |ordinal| format!("||domain=={ordinal}")) + ) + .unwrap(); + write!(output,"static spxnr_status_v1 spxnr_validate_{}(const spxnr_context_v1 *ctx){{return ctx->imports->{}?0:spxnr_adapter(2);}}\n",import.rust_method,import.c_field).unwrap(); + } + for function in closure { + let parameters = parameter_facts(function)?; + let result = scalar_type(&function.return_type).ok_or_else(b111)?; + let params = c_parameters(¶meters); + write!( + output, + "static spxnr_status_v1 spxnr1_f_{}(const spxnr_context_v1 *ctx{}{}{});\n", + full_hash(function.id.as_str()), + if params.is_empty() { "" } else { ", " }, + params, + if result == ScalarType::Unit { + String::new() + } else { + format!(", {} *result_out", c_type(result)) + } + ) + .unwrap(); + } + for function in closure { + let parameters = parameter_facts(function)?; + let result = scalar_type(&function.return_type).ok_or_else(b111)?; + let params = c_parameters(¶meters); + write!(output,"static spxnr_status_v1 spxnr1_f_{}(const spxnr_context_v1 *ctx{}{}{} ){{spxnr_status_v1 status=0;(void)ctx;",full_hash(function.id.as_str()),if params.is_empty(){""}else{", "},params,if result==ScalarType::Unit{String::new()}else{format!(", {} *result_out",c_type(result))}).unwrap(); + for index in 0..parameters.len() { + write!(output, "(void)arg_{index};").unwrap(); + } + for (index, (parameter, resolved)) in parameters.iter().zip(&function.params).enumerate() { + write!( + output, + "{} v_{}=arg_{};", + c_type(parameter.ty), + full_hash(resolved.id.as_str()), + index + ) + .unwrap(); + } + let mut temporary_count = 0usize; + let mut lines = CExpressionLineArena::new(); + for requirement in &function.requires { + lines.clear(); + let value = c_expr(requirement, imports, &mut temporary_count, &mut lines)?; + output.write_str(lines.as_str()?).unwrap(); + write!( + output, + "if(!({value}))return (((uint64_t)65533)<<48)|(((uint64_t)2)<<32)|UINT32_C(1);" + ) + .unwrap(); + } + lines.clear(); + let value = c_expr(&function.body, imports, &mut temporary_count, &mut lines)?; + output.write_str(lines.as_str()?).unwrap(); + if result != ScalarType::Unit { + write!( + output, + "{} v_{}={value};", + c_type(result), + full_hash(function.result_id.as_str()) + ) + .unwrap(); + } + for guarantee in &function.ensures { + lines.clear(); + let value = c_expr(guarantee, imports, &mut temporary_count, &mut lines)?; + output.write_str(lines.as_str()?).unwrap(); + write!( + output, + "if(!({value}))return (((uint64_t)65533)<<48)|(((uint64_t)2)<<32)|UINT32_C(2);" + ) + .unwrap(); + } + if result != ScalarType::Unit { + write!( + output, + "*result_out=v_{};", + full_hash(function.result_id.as_str()) + ) + .unwrap(); + } + output.write_str("return status;}\n").unwrap(); + } + for export in exports { + let params = c_parameters(&export.parameters); + write!(output, "spxnr_status_v1 {}(const spxnr_context_v1 *ctx{}{}{} ){{spxnr_status_v1 status=spxnr_validate(ctx);if(status!=0)return status;", export.c_symbol, if params.is_empty(){""}else{", "}, params, if export.result==ScalarType::Unit{String::new()}else{format!(", {} *result_out",c_type(export.result))}).unwrap(); + for import in imports { + write!( + output, + "status=spxnr_validate_{}(ctx);if(status!=0)return status;", + import.rust_method + ) + .unwrap(); + } + if export.result != ScalarType::Unit { + write!( + output, + "if(!result_out||((uintptr_t)result_out%_Alignof({}))!=0)return spxnr_adapter(5);", + c_type(export.result) + ) + .unwrap(); + } + for (index, parameter) in export.parameters.iter().enumerate() { + if parameter.ty == ScalarType::Bool { + write!(output, "if(arg_{index}>1)return spxnr_adapter(4);").unwrap(); + } + } + output + .write_str("spxnr_context_v1 local=*ctx;local.call_depth=ctx->call_depth+1;") + .unwrap(); + write!( + output, + "status=spxnr1_f_{}(&local{}{}{});", + full_hash(&export.id), + if export.parameters.is_empty() { + "" + } else { + ", " + }, + (0..export.parameters.len()) + .map(|index| format!("arg_{index}")) + .collect::>() + .join(","), + if export.result == ScalarType::Unit { + String::new() + } else { + ", result_out".to_owned() + } + ) + .unwrap(); + output.write_str("return status;}\n").unwrap(); + } + Ok(()) +} + +fn generate_c( + spec: &Spec, + closure: &[&ResolvedFunction], + exports: &[ExportFact], + imports: &[ImportFact], +) -> Result { + render_exact_artifact("max_generated_c_bytes", MAX_GENERATED_C_BYTES, |sink| { + generate_c_into(sink, spec, closure, exports, imports) + }) +} + +fn capability_digest(capabilities: &[String]) -> String { + let mut hasher = Sha256::new(); + hasher.update(CAPABILITIES_DOMAIN); + for capability in capabilities { + frame(&mut hasher, capability.as_bytes()); + } + format!("sha256:{:x}", hasher.finalize()) +} + +fn rust_parameters(parameters: &[ParameterFact]) -> String { + let values = parameters + .iter() + .enumerate() + .map(|(index, parameter)| format!("arg_{index}: {}", rust_type(parameter.ty))) + .collect::>(); + let joined = values.join(", "); + #[cfg(test)] + note_post_hir_render_capacity( + string_slice_owned_capacity(&values).saturating_add(joined.capacity()), + ); + joined +} + +fn generate_safe_rust_into( + output: &mut dyn std::fmt::Write, + spec: &Spec, + exports: &[ExportFact], + imports: &[ImportFact], +) -> Result<(), Diagnostic> { + output.write_str("mod api{#![forbid(unsafe_code)]\nuse core::num::NonZeroU32;\n#[repr(u8)] #[derive(Clone,Copy,Debug,Eq,PartialEq)] pub enum NativeRustStatusClass{Semantic=1,Contract=2,Import=3,Adapter=4}\npub enum NativeRustImportResult{Success(T),Status{code:NonZeroU32,class:NativeRustStatusClass,retryable:bool},HostFailure}\npub enum NativeRustCallError{Semantic{domain_id:&'static str,code:NonZeroU32,class:NativeRustStatusClass,retryable:bool},HostFailed,HostPanicked,AdapterRejected}\npub struct NativeRustAdmissionError;\n").unwrap(); + output.write_str("pub trait NativeRustImports{").unwrap(); + for import in imports { + write!( + output, + "fn {}(&mut self{}{})->NativeRustImportResult<{}>;", + import.rust_method, + if import.parameters.is_empty() { + "" + } else { + ", " + }, + rust_parameters(&import.parameters), + rust_type(import.result) + ) + .unwrap(); + } + output.write_str("}\n").unwrap(); + let capability_values = spec + .capabilities + .iter() + .map(|value| quote_json(value)) + .collect::>(); + let capabilities = capability_values.join(","); + #[cfg(test)] + note_post_hir_render_capacity( + string_slice_owned_capacity(&capability_values).saturating_add(capabilities.capacity()), + ); + write!( + output, + "const EXPECTED_CAPABILITIES:&[&str]=&[{}];\n", + capabilities + ) + .unwrap(); + output.write_str("pub struct NativeRustCapabilities{digest:[u8;32]} impl NativeRustCapabilities{pub fn new(values:&[&str])->Result{if values!=EXPECTED_CAPABILITIES{return Err(NativeRustAdmissionError)}Ok(Self{digest:super::ffi::capabilities_digest()})}}\n").unwrap(); + output.write_str("struct ActiveGuard<'a>{active:&'a mut bool}impl Drop for ActiveGuard<'_>{fn drop(&mut self){*self.active=false;}}\npub struct NativeRustBridge{host:H,capabilities:NativeRustCapabilities,owner:std::thread::ThreadId,active:bool,calls:u32,_not_send_sync:core::marker::PhantomData<*mut ()>} impl NativeRustBridge{pub fn new(host:H,capabilities:NativeRustCapabilities)->Self{Self{host,capabilities,owner:std::thread::current().id(),active:false,calls:0,_not_send_sync:core::marker::PhantomData}}\n").unwrap(); + for export in exports { + let parameters = rust_parameters(&export.parameters); + let argument_values = (0..export.parameters.len()) + .map(|index| format!("arg_{index}")) + .collect::>(); + let arguments = argument_values.join(", "); + #[cfg(test)] + note_post_hir_render_capacity( + parameters + .capacity() + .saturating_add(string_slice_owned_capacity(&argument_values)) + .saturating_add(arguments.capacity()), + ); + write!(output,"pub fn {}(&mut self{}{})->Result<{},NativeRustCallError>{{if self.owner!=std::thread::current().id()||core::mem::replace(&mut self.active,true){{return Err(NativeRustCallError::AdapterRejected)}}let _active_guard=ActiveGuard{{active:&mut self.active}};super::ffi::{}(&mut self.host,&mut self.calls,self.capabilities.digest{}{})}}\n",export.rust_method,if export.parameters.is_empty(){""}else{", "},parameters,rust_type(export.result),export.rust_method,if export.parameters.is_empty(){""}else{", "},arguments).unwrap(); + } + output + .write_str( + "}\n}\n#[path=\"semaprax_native_rust_interop_ffi.rs\"]mod ffi;\npub use api::*;\n", + ) + .unwrap(); + Ok(()) +} + +fn generate_private_ffi_into( + output: &mut dyn std::fmt::Write, + spec: &Spec, + exports: &[ExportFact], + imports: &[ImportFact], +) -> Result<(), Diagnostic> { + let digest = capability_digest(&spec.capabilities); + let hex = digest.strip_prefix("sha256:").unwrap_or(""); + let byte_values = (0..64) + .step_by(2) + .map(|index| format!("0x{}", &hex[index..index + 2])) + .collect::>(); + let bytes = byte_values.join(","); + let mut import_table_values = Vec::with_capacity(imports.len()); + for import in imports { + let parameter_values = import + .parameters + .iter() + .map(|parameter| match parameter.ty { + ScalarType::I64 => "i64".to_owned(), + ScalarType::Bool => "u8".to_owned(), + ScalarType::Unit => "()".to_owned(), + }) + .collect::>(); + let parameters = parameter_values.join(","); + let result = if import.result == ScalarType::Unit { + String::new() + } else { + format!(", *mut {}", rust_ffi_wire_type(import.result)) + }; + let row = format!( + "{}:unsafe extern \"C\" fn(*mut c_void{}{}{})->u64,", + import.c_field, + if import.parameters.is_empty() { + "" + } else { + ", " + }, + parameters, + result + ); + #[cfg(test)] + note_post_hir_render_capacity( + string_slice_owned_capacity(&byte_values) + .saturating_add(bytes.capacity()) + .saturating_add(string_slice_owned_capacity(&import_table_values)) + .saturating_add(string_slice_owned_capacity(¶meter_values)) + .saturating_add(parameters.capacity()) + .saturating_add(result.capacity()) + .saturating_add(row.capacity()), + ); + import_table_values.push(row); + } + let import_table = import_table_values.join(""); + #[cfg(test)] + note_post_hir_render_capacity( + string_slice_owned_capacity(&byte_values) + .saturating_add(bytes.capacity()) + .saturating_add(string_slice_owned_capacity(&import_table_values)) + .saturating_add(import_table.capacity()), + ); + write!(output, "#![allow(unsafe_code)]\nuse super::api::*;\nuse core::ffi::c_void;\n#[repr(C)]struct Imports{{abi_version:u32,size:u32,{import_table} }}\n#[repr(C)]struct Context{{abi_version:u32,size:u32,userdata:*mut c_void,imports:*const Imports,capabilities_digest:[u8;32],call_depth:u32,reserved:u32}}\nstruct Frame{{host:*mut H,calls:*mut u32}}\npub(super) fn capabilities_digest()->[u8;32]{{[{bytes}]}}\n").unwrap(); + #[cfg(test)] + let ffi_prefix_scratch = digest + .capacity() + .saturating_add(string_slice_owned_capacity(&byte_values)) + .saturating_add(bytes.capacity()) + .saturating_add(string_slice_owned_capacity(&import_table_values)) + .saturating_add(import_table.capacity()); + output.write_str("fn adapter(code:u32)->u64{((65535u64)<<48)|((NativeRustStatusClass::Adapter as u64)<<32)|u64::from(code)}\n").unwrap(); + output.write_str("fn decode_status(status:u64)->NativeRustCallError{let code=(status&0xffff_ffff)as u32;let class=((status>>32)&0xff)as u8;let retryable=((status>>40)&1)!=0;let reserved=(status>>41)&0x7f;let domain=(status>>48)as u16;let Some(code)=core::num::NonZeroU32::new(code)else{return NativeRustCallError::AdapterRejected};let class=match class{1=>NativeRustStatusClass::Semantic,2=>NativeRustStatusClass::Contract,3=>NativeRustStatusClass::Import,4=>NativeRustStatusClass::Adapter,_=>return NativeRustCallError::AdapterRejected};if reserved!=0||domain==0{return NativeRustCallError::AdapterRejected}match domain{65533=>{let valid=!retryable&&match class{NativeRustStatusClass::Semantic=>(1..=6).contains(&code.get()),NativeRustStatusClass::Contract=>(1..=2).contains(&code.get()),_=>false};if !valid{return NativeRustCallError::AdapterRejected}NativeRustCallError::Semantic{domain_id:\"semaprax.native-rust-semantics.v1\",code,class,retryable}},").unwrap(); + let domains = imports + .iter() + .filter_map(|import| import.failure.as_ref()) + .cloned() + .collect::>(); + for (index, domain) in domains.iter().enumerate() { + write!(output,"{}=>{{if class!=NativeRustStatusClass::Import{{return NativeRustCallError::AdapterRejected}}NativeRustCallError::Semantic{{domain_id:{},code,class,retryable}}}},",index+1,quote_json(domain)).unwrap(); + } + output.write_str("65534=>if class==NativeRustStatusClass::Adapter&&!retryable{match code.get(){1=>NativeRustCallError::HostPanicked,2=>NativeRustCallError::HostFailed,_=>NativeRustCallError::AdapterRejected}}else{NativeRustCallError::AdapterRejected},65535=>if class==NativeRustStatusClass::Adapter&&!retryable&&(1..=8).contains(&code.get()){NativeRustCallError::AdapterRejected}else{NativeRustCallError::AdapterRejected},_=>NativeRustCallError::AdapterRejected}}\n").unwrap(); + for import in imports { + let parameter_declaration_values = import + .parameters + .iter() + .enumerate() + .map(|(index, p)| { + format!( + "arg_{index}:{}", + match p.ty { + ScalarType::I64 => "i64", + ScalarType::Bool => "u8", + ScalarType::Unit => "()", + } + ) + }) + .collect::>(); + let parameter_declarations = parameter_declaration_values.join(","); + let result_declaration = if import.result == ScalarType::Unit { + String::new() + } else { + format!( + ", result_out:*mut {}", + match import.result { + ScalarType::I64 => "i64", + ScalarType::Bool => "u8", + ScalarType::Unit => "()", + } + ) + }; + #[cfg(test)] + note_post_hir_render_capacity( + ffi_prefix_scratch + .saturating_add(owned_string_set_owned_capacity(&domains)) + .saturating_add(string_slice_owned_capacity(¶meter_declaration_values)) + .saturating_add(parameter_declarations.capacity()) + .saturating_add(result_declaration.capacity()), + ); + write!(output,"unsafe extern \"C\" fn cb_{}(userdata:*mut c_void{}{}{}) -> u64{{if userdata.is_null(){{return adapter(1);}}",import.rust_method,if import.parameters.is_empty(){""}else{", "},parameter_declarations,result_declaration).unwrap(); + for (index, parameter) in import.parameters.iter().enumerate() { + if parameter.ty == ScalarType::Bool { + write!(output, "if arg_{index}>1{{return adapter(4);}}").unwrap(); + } + } + if import.result != ScalarType::Unit { + write!(output,"if result_out.is_null()||(result_out as usize)%core::mem::align_of::<{}>()!=0{{return adapter(5);}}",rust_type(import.result)).unwrap(); + } + let call_argument_values = import + .parameters + .iter() + .enumerate() + .map(|(index, p)| { + if p.ty == ScalarType::Bool { + format!("arg_{index}!=0") + } else { + format!("arg_{index}") + } + }) + .collect::>(); + let call_arguments = call_argument_values.join(","); + #[cfg(test)] + note_post_hir_render_capacity( + ffi_prefix_scratch + .saturating_add(owned_string_set_owned_capacity(&domains)) + .saturating_add(string_slice_owned_capacity(&call_argument_values)) + .saturating_add(call_arguments.capacity()), + ); + write!(output,"if (userdata as usize)%core::mem::align_of::>()!=0{{return adapter(1);}}let frame=&mut*(userdata as *mut Frame);if frame.host.is_null()||frame.calls.is_null()||*frame.calls>=4096{{return adapter(7);}}*frame.calls+=1;let run=std::panic::catch_unwind(std::panic::AssertUnwindSafe(||{{let host=&mut *frame.host;host.{}({})}}));match run{{Err(payload)=>{{core::mem::forget(payload);((65534u64)<<48)|((NativeRustStatusClass::Adapter as u64)<<32)|1}},Ok(NativeRustImportResult::HostFailure)=>((65534u64)<<48)|((NativeRustStatusClass::Adapter as u64)<<32)|2,",import.rust_method,call_arguments).unwrap(); + let ordinal = import + .failure + .as_ref() + .and_then(|domain| domains.iter().position(|value| value == domain)) + .map(|index| index + 1); + if let Some(ordinal) = ordinal { + write!(output,"Ok(NativeRustImportResult::Status{{code,class,retryable}})=>if class==NativeRustStatusClass::Import{{(({}u64)<<48)|((class as u64)<<32)|((retryable as u64)<<40)|u64::from(code.get())}}else{{adapter(3)}},",ordinal).unwrap(); + } else { + output.write_str("Ok(NativeRustImportResult::Status{code,class,retryable})=>{let _=(code,class,retryable);adapter(3)},").unwrap(); + } + if import.result == ScalarType::Unit { + output + .write_str("Ok(NativeRustImportResult::Success(()))=>0}}}\n") + .unwrap(); + } else { + write!( + output, + "Ok(NativeRustImportResult::Success(value))=>{{*result_out={};0}}", + if import.result == ScalarType::Bool { + "u8::from(value)" + } else { + "value" + } + ) + .unwrap(); + output.write_str("}}\n").unwrap(); + } + } + for export in exports { + let parameter_values = export + .parameters + .iter() + .enumerate() + .map(|(index, parameter)| format!("arg_{index}:{}", rust_ffi_wire_type(parameter.ty))) + .collect::>(); + let parameters = parameter_values.join(","); + let result = if export.result == ScalarType::Unit { + String::new() + } else { + format!(", result_out:*mut {}", rust_ffi_wire_type(export.result)) + }; + #[cfg(test)] + note_post_hir_render_capacity( + ffi_prefix_scratch + .saturating_add(owned_string_set_owned_capacity(&domains)) + .saturating_add(string_slice_owned_capacity(¶meter_values)) + .saturating_add(parameters.capacity()) + .saturating_add(result.capacity()), + ); + write!( + output, + "extern \"C\"{{fn {}(ctx:*const Context{}{}{})->u64;}}\n", + export.c_symbol, + if export.parameters.is_empty() { + "" + } else { + ", " + }, + parameters, + result + ) + .unwrap(); + } + for export in exports { + let result_slot = match export.result { + ScalarType::Unit => String::new(), + ScalarType::I64 => "let mut result=core::mem::MaybeUninit::::uninit();".to_owned(), + ScalarType::Bool => "let mut result=core::mem::MaybeUninit::::uninit();".to_owned(), + }; + let publish = match export.result { + ScalarType::Unit => "Ok(())", + ScalarType::I64 => "Ok(result.assume_init())", + ScalarType::Bool => "let value=result.assume_init();if value>1{return Err(NativeRustCallError::AdapterRejected)}Ok(value!=0)", + }; + let parameters = rust_parameters(&export.parameters); + let callback_values = imports + .iter() + .map(|import| format!("{}:cb_{}::,", import.c_field, import.rust_method)) + .collect::>(); + let callbacks = callback_values.join(""); + let argument_values = export + .parameters + .iter() + .enumerate() + .map(|(index, p)| { + if p.ty == ScalarType::Bool { + format!("u8::from(arg_{index})") + } else { + format!("arg_{index}") + } + }) + .collect::>(); + let arguments = argument_values.join(","); + let result_argument = if export.result == ScalarType::Unit { + String::new() + } else { + ", result.as_mut_ptr()".to_owned() + }; + #[cfg(test)] + note_post_hir_render_capacity( + ffi_prefix_scratch + .saturating_add(owned_string_set_owned_capacity(&domains)) + .saturating_add( + parameters + .capacity() + .saturating_add(string_slice_owned_capacity(&callback_values)) + .saturating_add(callbacks.capacity()) + .saturating_add(result_slot.capacity()) + .saturating_add(string_slice_owned_capacity(&argument_values)) + .saturating_add(arguments.capacity()) + .saturating_add(result_argument.capacity()), + ), + ); + write!(output,"pub(super) fn {}(host:&mut H,calls:&mut u32,digest:[u8;32]{}{})->Result<{},NativeRustCallError>{{unsafe{{if *calls>=4096{{return Err(NativeRustCallError::AdapterRejected)}}*calls+=1;let table=Imports{{abi_version:1,size:core::mem::size_of::() as u32,{}}};let mut frame=Frame{{host:host as *mut H,calls:calls as *mut u32}};let ctx=Context{{abi_version:1,size:core::mem::size_of::() as u32,userdata:&mut frame as *mut Frame as *mut c_void,imports:&table,capabilities_digest:digest,call_depth:0,reserved:0}};{}let status={}(&ctx{}{}{});if status!=0{{return Err(decode_status(status))}}{} }}}}\n",export.rust_method,if export.parameters.is_empty(){""}else{", "},parameters,rust_type(export.result),callbacks,result_slot,export.c_symbol,if export.parameters.is_empty(){""}else{", "},arguments,result_argument,publish).unwrap(); + } + Ok(()) +} + +fn generate_rust_artifacts_with_limit( + spec: &Spec, + exports: &[ExportFact], + imports: &[ImportFact], + maximum: usize, +) -> Result<(String, String), Diagnostic> { + let mut render_safe = + |sink: &mut dyn std::fmt::Write| generate_safe_rust_into(sink, spec, exports, imports); + let mut render_ffi = + |sink: &mut dyn std::fmt::Write| generate_private_ffi_into(sink, spec, exports, imports); + let safe_bytes = count_exact_artifact("max_generated_rust_bytes", maximum, &mut render_safe)?; + let ffi_bytes = count_exact_artifact("max_generated_rust_bytes", maximum, &mut render_ffi)?; + let combined_bytes = safe_bytes + .checked_add(ffi_bytes) + .ok_or_else(|| b109("max_generated_rust_bytes", maximum))?; + if combined_bytes > maximum { + return Err(b109("max_generated_rust_bytes", maximum)); + } + let safe = render_counted_artifact( + "max_generated_rust_bytes", + maximum, + safe_bytes, + &mut render_safe, + )?; + let ffi = render_counted_artifact( + "max_generated_rust_bytes", + maximum, + ffi_bytes, + &mut render_ffi, + )?; + Ok((safe, ffi)) +} + +fn generate_rust_artifacts( + spec: &Spec, + exports: &[ExportFact], + imports: &[ImportFact], +) -> Result<(String, String), Diagnostic> { + generate_rust_artifacts_with_limit(spec, exports, imports, MAX_GENERATED_RUST_BYTES) +} + +fn replay_generated(header: &str, c: &str, rust: &str, ffi: &str) -> Result<(), Diagnostic> { + if !header.starts_with("#ifndef ") + || !header.ends_with("#endif\n") + || !c.starts_with("#include \"semaprax_native_rust_interop.h\"") + || !rust.starts_with("mod api{#![forbid(unsafe_code)]\n") + || rust.contains("unsafe {") + || !ffi.starts_with("#![allow(unsafe_code)]\n") + { + return Err(b111()); + } + Ok(()) +} + +fn replay_header_exact(source: &str, exports: &[ExportFact], imports: &[ImportFact]) -> bool { + let mut replay = ExactReplay::new(source); + replay.text("#ifndef SEMAPRAX_NATIVE_RUST_INTEROP_H\n#define SEMAPRAX_NATIVE_RUST_INTEROP_H\n#include \n#include \n#ifdef __cplusplus\nextern \"C\" {\n#endif\ntypedef uint64_t spxnr_status_v1;\ntypedef struct spxnr_imports_v1 spxnr_imports_v1;\ntypedef struct { uint32_t abi_version; uint32_t size; void *userdata; const spxnr_imports_v1 *imports; uint8_t capabilities_digest[32]; uint32_t call_depth; uint32_t reserved; } spxnr_context_v1;\nstruct spxnr_imports_v1 { uint32_t abi_version; uint32_t size;"); + for import in imports { + replay.text(" spxnr_status_v1 (*"); + replay.text(&import.c_field); + replay.text(")(void *userdata"); + for (index, parameter) in import.parameters.iter().enumerate() { + replay.text(", "); + replay.text(c_type(parameter.ty)); + replay.text(" arg_"); + replay.number(index); + } + if import.result != ScalarType::Unit { + replay.text(", "); + replay.text(c_type(import.result)); + replay.text(" *result_out"); + } + replay.text(");"); + } + replay.text(" };\n"); + for export in exports { + replay.text("spxnr_status_v1 "); + replay.text(&export.c_symbol); + replay.text("(const spxnr_context_v1 *ctx"); + for (index, parameter) in export.parameters.iter().enumerate() { + replay.text(", "); + replay.text(c_type(parameter.ty)); + replay.text(" arg_"); + replay.number(index); + } + if export.result != ScalarType::Unit { + replay.text(", "); + replay.text(c_type(export.result)); + replay.text(" *result_out"); + } + replay.text(");\n"); + } + replay.text("#ifdef __cplusplus\n}\n#endif\n#endif\n"); + replay.finish() +} + +fn replay_rust_scalar(replay: &mut ExactReplay<'_>, ty: ScalarType) { + replay.text(match ty { + ScalarType::I64 => "i64", + ScalarType::Bool => "bool", + ScalarType::Unit => "()", + }); +} + +fn replay_rust_parameters(replay: &mut ExactReplay<'_>, parameters: &[ParameterFact]) { + for (index, parameter) in parameters.iter().enumerate() { + if index != 0 { + replay.text(", "); + } + replay.text("arg_"); + replay.number(index); + replay.text(": "); + replay_rust_scalar(replay, parameter.ty); + } +} + +fn replay_safe_rust_exact( + source: &str, + spec: &Spec, + exports: &[ExportFact], + imports: &[ImportFact], +) -> bool { + let mut replay = ExactReplay::new(source); + replay.text("mod api{#![forbid(unsafe_code)]\nuse core::num::NonZeroU32;\n#[repr(u8)] #[derive(Clone,Copy,Debug,Eq,PartialEq)] pub enum NativeRustStatusClass{Semantic=1,Contract=2,Import=3,Adapter=4}\npub enum NativeRustImportResult{Success(T),Status{code:NonZeroU32,class:NativeRustStatusClass,retryable:bool},HostFailure}\npub enum NativeRustCallError{Semantic{domain_id:&'static str,code:NonZeroU32,class:NativeRustStatusClass,retryable:bool},HostFailed,HostPanicked,AdapterRejected}\npub struct NativeRustAdmissionError;\n"); + replay.text("pub trait NativeRustImports{"); + for import in imports { + replay.text("fn "); + replay.text(&import.rust_method); + replay.text("(&mut self"); + if !import.parameters.is_empty() { + replay.text(", "); + replay_rust_parameters(&mut replay, &import.parameters); + } + replay.text(")->NativeRustImportResult<"); + replay_rust_scalar(&mut replay, import.result); + replay.text(">;"); + } + replay.text("}\nconst EXPECTED_CAPABILITIES:&[&str]=&["); + for (index, capability) in spec.capabilities.iter().enumerate() { + if index != 0 { + replay.text(","); + } + replay.json(capability); + } + replay.text("];\n"); + replay.text("pub struct NativeRustCapabilities{digest:[u8;32]} impl NativeRustCapabilities{pub fn new(values:&[&str])->Result{if values!=EXPECTED_CAPABILITIES{return Err(NativeRustAdmissionError)}Ok(Self{digest:super::ffi::capabilities_digest()})}}\n"); + replay.text("struct ActiveGuard<'a>{active:&'a mut bool}impl Drop for ActiveGuard<'_>{fn drop(&mut self){*self.active=false;}}\npub struct NativeRustBridge{host:H,capabilities:NativeRustCapabilities,owner:std::thread::ThreadId,active:bool,calls:u32,_not_send_sync:core::marker::PhantomData<*mut ()>} impl NativeRustBridge{pub fn new(host:H,capabilities:NativeRustCapabilities)->Self{Self{host,capabilities,owner:std::thread::current().id(),active:false,calls:0,_not_send_sync:core::marker::PhantomData}}\n"); + for export in exports { + replay.text("pub fn "); + replay.text(&export.rust_method); + replay.text("(&mut self"); + if !export.parameters.is_empty() { + replay.text(", "); + replay_rust_parameters(&mut replay, &export.parameters); + } + replay.text(")->Result<"); + replay_rust_scalar(&mut replay, export.result); + replay.text(",NativeRustCallError>{if self.owner!=std::thread::current().id()||core::mem::replace(&mut self.active,true){return Err(NativeRustCallError::AdapterRejected)}let _active_guard=ActiveGuard{active:&mut self.active};super::ffi::"); + replay.text(&export.rust_method); + replay.text("(&mut self.host,&mut self.calls,self.capabilities.digest"); + for index in 0..export.parameters.len() { + replay.text(", arg_"); + replay.number(index); + } + replay.text(")}\n"); + } + replay.text("}\n}\n#[path=\"semaprax_native_rust_interop_ffi.rs\"]mod ffi;\npub use api::*;\n"); + replay.finish() +} + +fn replay_ffi_wire_scalar(replay: &mut ExactReplay<'_>, ty: ScalarType) { + replay.text(match ty { + ScalarType::I64 => "i64", + ScalarType::Bool => "u8", + ScalarType::Unit => "()", + }); +} + +fn replay_private_ffi_exact( + source: &str, + spec: &Spec, + exports: &[ExportFact], + imports: &[ImportFact], +) -> bool { + let mut replay = ExactReplay::new(source); + replay.text("#![allow(unsafe_code)]\nuse super::api::*;\nuse core::ffi::c_void;\n#[repr(C)]struct Imports{abi_version:u32,size:u32,"); + for import in imports { + replay.text(&import.c_field); + replay.text(":unsafe extern \"C\" fn(*mut c_void"); + for (index, parameter) in import.parameters.iter().enumerate() { + replay.text(if index == 0 { ", " } else { "," }); + replay_ffi_wire_scalar(&mut replay, parameter.ty); + } + if import.result != ScalarType::Unit { + replay.text(", *mut "); + replay_ffi_wire_scalar(&mut replay, import.result); + } + replay.text(")->u64,"); + } + replay.text(" }\n#[repr(C)]struct Context{abi_version:u32,size:u32,userdata:*mut c_void,imports:*const Imports,capabilities_digest:[u8;32],call_depth:u32,reserved:u32}\nstruct Frame{host:*mut H,calls:*mut u32}\npub(super) fn capabilities_digest()->[u8;32]{["); + let digest = replay_capabilities_digest(&spec.capabilities); + let Some(hex) = digest.strip_prefix("sha256:") else { + return false; + }; + if hex.len() != 64 { + return false; + } + for index in (0..64).step_by(2) { + if index != 0 { + replay.text(","); + } + replay.text("0x"); + replay.text(&hex[index..index + 2]); + } + replay.text("]}\nfn adapter(code:u32)->u64{((65535u64)<<48)|((NativeRustStatusClass::Adapter as u64)<<32)|u64::from(code)}\n"); + replay.text("fn decode_status(status:u64)->NativeRustCallError{let code=(status&0xffff_ffff)as u32;let class=((status>>32)&0xff)as u8;let retryable=((status>>40)&1)!=0;let reserved=(status>>41)&0x7f;let domain=(status>>48)as u16;let Some(code)=core::num::NonZeroU32::new(code)else{return NativeRustCallError::AdapterRejected};let class=match class{1=>NativeRustStatusClass::Semantic,2=>NativeRustStatusClass::Contract,3=>NativeRustStatusClass::Import,4=>NativeRustStatusClass::Adapter,_=>return NativeRustCallError::AdapterRejected};if reserved!=0||domain==0{return NativeRustCallError::AdapterRejected}match domain{65533=>{let valid=!retryable&&match class{NativeRustStatusClass::Semantic=>(1..=6).contains(&code.get()),NativeRustStatusClass::Contract=>(1..=2).contains(&code.get()),_=>false};if !valid{return NativeRustCallError::AdapterRejected}NativeRustCallError::Semantic{domain_id:\"semaprax.native-rust-semantics.v1\",code,class,retryable}},"); + let domains = imports + .iter() + .filter_map(|import| import.failure.as_ref()) + .cloned() + .collect::>(); + for (index, domain) in domains.iter().enumerate() { + replay.number(index + 1); + replay.text("=>{if class!=NativeRustStatusClass::Import{return NativeRustCallError::AdapterRejected}NativeRustCallError::Semantic{domain_id:"); + replay.json(domain); + replay.text(",code,class,retryable}},"); + } + replay.text("65534=>if class==NativeRustStatusClass::Adapter&&!retryable{match code.get(){1=>NativeRustCallError::HostPanicked,2=>NativeRustCallError::HostFailed,_=>NativeRustCallError::AdapterRejected}}else{NativeRustCallError::AdapterRejected},65535=>if class==NativeRustStatusClass::Adapter&&!retryable&&(1..=8).contains(&code.get()){NativeRustCallError::AdapterRejected}else{NativeRustCallError::AdapterRejected},_=>NativeRustCallError::AdapterRejected}}\n"); + for import in imports { + replay.text("unsafe extern \"C\" fn cb_"); + replay.text(&import.rust_method); + replay.text("(userdata:*mut c_void"); + for (index, parameter) in import.parameters.iter().enumerate() { + replay.text(if index == 0 { ", arg_" } else { ",arg_" }); + replay.number(index); + replay.text(":"); + replay_ffi_wire_scalar(&mut replay, parameter.ty); + } + if import.result != ScalarType::Unit { + replay.text(", result_out:*mut "); + replay_ffi_wire_scalar(&mut replay, import.result); + } + replay.text(") -> u64{if userdata.is_null(){return adapter(1);}"); + for (index, parameter) in import.parameters.iter().enumerate() { + if parameter.ty == ScalarType::Bool { + replay.text("if arg_"); + replay.number(index); + replay.text(">1{return adapter(4);}"); + } + } + if import.result != ScalarType::Unit { + replay.text("if result_out.is_null()||(result_out as usize)%core::mem::align_of::<"); + replay_rust_scalar(&mut replay, import.result); + replay.text(">()!=0{return adapter(5);}"); + } + replay.text("if (userdata as usize)%core::mem::align_of::>()!=0{return adapter(1);}let frame=&mut*(userdata as *mut Frame);if frame.host.is_null()||frame.calls.is_null()||*frame.calls>=4096{return adapter(7);}*frame.calls+=1;let run=std::panic::catch_unwind(std::panic::AssertUnwindSafe(||{let host=&mut *frame.host;host."); + replay.text(&import.rust_method); + replay.text("("); + for (index, parameter) in import.parameters.iter().enumerate() { + if index != 0 { + replay.text(","); + } + replay.text("arg_"); + replay.number(index); + if parameter.ty == ScalarType::Bool { + replay.text("!=0"); + } + } + replay.text(")}));match run{Err(payload)=>{core::mem::forget(payload);((65534u64)<<48)|((NativeRustStatusClass::Adapter as u64)<<32)|1},Ok(NativeRustImportResult::HostFailure)=>((65534u64)<<48)|((NativeRustStatusClass::Adapter as u64)<<32)|2,"); + if let Some(domain) = &import.failure { + let Some(ordinal) = domains.iter().position(|value| value == domain) else { + return false; + }; + replay.text("Ok(NativeRustImportResult::Status{code,class,retryable})=>if class==NativeRustStatusClass::Import{(("); + replay.number(ordinal + 1); + replay.text("u64)<<48)|((class as u64)<<32)|((retryable as u64)<<40)|u64::from(code.get())}else{adapter(3)},"); + } else { + replay.text("Ok(NativeRustImportResult::Status{code,class,retryable})=>{let _=(code,class,retryable);adapter(3)},"); + } + if import.result == ScalarType::Unit { + replay.text("Ok(NativeRustImportResult::Success(()))=>0}}}\n"); + } else { + replay.text("Ok(NativeRustImportResult::Success(value))=>{*result_out="); + if import.result == ScalarType::Bool { + replay.text("u8::from(value)"); + } else { + replay.text("value"); + } + replay.text(";0}}}\n"); + } + } + for export in exports { + replay.text("extern \"C\"{fn "); + replay.text(&export.c_symbol); + replay.text("(ctx:*const Context"); + for (index, parameter) in export.parameters.iter().enumerate() { + replay.text(if index == 0 { ", arg_" } else { ",arg_" }); + replay.number(index); + replay.text(":"); + replay_ffi_wire_scalar(&mut replay, parameter.ty); + } + if export.result != ScalarType::Unit { + replay.text(", result_out:*mut "); + replay_ffi_wire_scalar(&mut replay, export.result); + } + replay.text(")->u64;}\n"); + } + for export in exports { + replay.text("pub(super) fn "); + replay.text(&export.rust_method); + replay.text("(host:&mut H,calls:&mut u32,digest:[u8;32]"); + if !export.parameters.is_empty() { + replay.text(", "); + replay_rust_parameters(&mut replay, &export.parameters); + } + replay.text(")->Result<"); + replay_rust_scalar(&mut replay, export.result); + replay.text(",NativeRustCallError>{unsafe{if *calls>=4096{return Err(NativeRustCallError::AdapterRejected)}*calls+=1;let table=Imports{abi_version:1,size:core::mem::size_of::() as u32,"); + for import in imports { + replay.text(&import.c_field); + replay.text(":cb_"); + replay.text(&import.rust_method); + replay.text("::,"); + } + replay.text("};let mut frame=Frame{host:host as *mut H,calls:calls as *mut u32};let ctx=Context{abi_version:1,size:core::mem::size_of::() as u32,userdata:&mut frame as *mut Frame as *mut c_void,imports:&table,capabilities_digest:digest,call_depth:0,reserved:0};"); + match export.result { + ScalarType::Unit => {} + ScalarType::I64 => { + replay.text("let mut result=core::mem::MaybeUninit::::uninit();") + } + ScalarType::Bool => { + replay.text("let mut result=core::mem::MaybeUninit::::uninit();") + } + } + replay.text("let status="); + replay.text(&export.c_symbol); + replay.text("(&ctx"); + for (index, parameter) in export.parameters.iter().enumerate() { + replay.text(if index == 0 { ", " } else { "," }); + if parameter.ty == ScalarType::Bool { + replay.text("u8::from("); + } + replay.text("arg_"); + replay.number(index); + if parameter.ty == ScalarType::Bool { + replay.text(")"); + } + } + if export.result != ScalarType::Unit { + replay.text(", result.as_mut_ptr()"); + } + replay.text(");if status!=0{return Err(decode_status(status))}"); + match export.result { + ScalarType::Unit => replay.text("Ok(())"), + ScalarType::I64 => replay.text("Ok(result.assume_init())"), + ScalarType::Bool => replay.text("let value=result.assume_init();if value>1{return Err(NativeRustCallError::AdapterRejected)}Ok(value!=0)"), + } + replay.text(" }}\n"); + } + replay.finish() +} + +fn replay_c_scalar(ty: ScalarType) -> &'static str { + match ty { + ScalarType::I64 => "int64_t", + ScalarType::Bool => "uint8_t", + ScalarType::Unit => "void", + } +} + +fn replay_symbol_hash(value: &str) -> String { + let digest = Sha256::digest(value.as_bytes()); + let mut encoded = String::with_capacity(64); + for byte in digest { + write!(encoded, "{byte:02x}").unwrap(); + } + #[cfg(test)] + note_post_hir_replay_capacity(encoded.capacity()); + encoded +} + +fn replay_capabilities_digest(capabilities: &[String]) -> String { + let mut hasher = Sha256::new(); + hasher.update(CAPABILITIES_DOMAIN); + for capability in capabilities { + hasher.update( + u64::try_from(capability.len()) + .unwrap_or(u64::MAX) + .to_be_bytes(), + ); + hasher.update(capability.as_bytes()); + } + let digest = format!("sha256:{:x}", hasher.finalize()); + #[cfg(test)] + note_post_hir_replay_capacity(digest.capacity()); + digest +} + +fn replay_resolved_scalar(ty: &ResolvedType) -> Option { + match ty { + ResolvedType::Unit => Some(ScalarType::Unit), + ResolvedType::I64 => Some(ScalarType::I64), + ResolvedType::Bool => Some(ScalarType::Bool), + _ => None, + } +} + +fn replay_parameter_facts(function: &ResolvedFunction) -> Result, Diagnostic> { + if function.params.len() > MAX_PARAMETERS { + return Err(b109("max_parameters", MAX_PARAMETERS)); + } + function + .params + .iter() + .map(|parameter| { + if parameter.ownership != OwnershipMode::Value + || parameter.name.len() > MAX_IDENTIFIER_BYTES + { + return Err(b107("scalar value signature required")); + } + Ok(ParameterFact { + name: parameter.name.clone(), + ty: replay_resolved_scalar(¶meter.ty) + .filter(|ty| *ty != ScalarType::Unit) + .ok_or_else(|| b107("scalar value signature required"))?, + }) + }) + .collect() +} + +fn replay_c_parameters(parameters: &[ParameterFact]) -> String { + let values = parameters + .iter() + .enumerate() + .map(|(index, parameter)| format!("{} arg_{index}", replay_c_scalar(parameter.ty))) + .collect::>(); + let joined = values.join(", "); + #[cfg(test)] + note_post_hir_replay_capacity( + string_slice_owned_capacity(&values).saturating_add(joined.capacity()), + ); + joined +} + +#[cfg(any())] +fn replay_c_expression( + expression: &ResolvedExpr, + imports: &[ImportFact], + temporary_count: &mut usize, + lines: &mut Vec, +) -> Result { + enum Frame<'a> { + Enter(&'a ResolvedExpr, usize), + Unary(crate::ast::UnaryOp, usize), + BinaryLeft(crate::ast::BinaryOp, &'a ResolvedExpr, usize), + BinaryRight(crate::ast::BinaryOp, String, usize), + LazyLeft(crate::ast::BinaryOp, &'a ResolvedExpr, usize), + LazyRight(crate::ast::BinaryOp, String, usize, usize), + Block(&'a [ResolvedStatement], usize, &'a ResolvedExpr, usize), + BlockLet(&'a [ResolvedStatement], usize, &'a ResolvedExpr, usize), + IfCondition(&'a ResolvedExpr, &'a ResolvedExpr, ScalarType, usize), + IfThen(String, &'a ResolvedExpr, Option, usize, usize), + IfElse(String, Option, String, usize, usize, usize), + NativeArgs( + &'a crate::hir::ResolvedNativeRustImportCall, + usize, + Vec, + usize, + ), + CallArgs( + &'a str, + &'a [ResolvedExpr], + &'a ResolvedType, + usize, + Vec, + usize, + ), + } + const _: () = assert!(std::mem::size_of::>() == C_EXPRESSION_FRAME_BYTES); + let next_temporary = |count: &mut usize| { + let value = format!("tmp_{}", *count); + *count += 1; + value + }; + let (node_count, depth) = c_expression_shape(expression)?; + let line_capacity = node_count + .checked_mul(3) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if lines.capacity() < line_capacity { + lines + .try_reserve_exact(line_capacity - lines.capacity()) + .map_err(|_| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + let frame_capacity = node_count + .checked_mul(2) + .and_then(|slots| slots.checked_add(1)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let mut frames = Vec::with_capacity(frame_capacity); + let mut values = Vec::::with_capacity(depth + 1); + let mut contexts = Vec::>::with_capacity(node_count + 1); + contexts.push(Vec::with_capacity(line_capacity)); + frames.push(Frame::Enter(expression, 0)); + while let Some(frame) = frames.pop() { + #[cfg(test)] + { + let frame_payload = |frame: &Frame<'_>| match frame { + Frame::BinaryRight(_, value, _) + | Frame::LazyRight(_, value, _, _) + | Frame::IfThen(value, _, _, _, _) + | Frame::IfElse(value, _, _, _, _, _) => value.capacity(), + Frame::NativeArgs(_, _, values, _) | Frame::CallArgs(_, _, _, _, values, _) => { + values.capacity() * std::mem::size_of::() + + values.iter().map(String::capacity).sum::() + } + _ => 0, + }; + let frame_owned = frames.iter().map(&frame_payload).sum::(); + let owned = frames.capacity() * std::mem::size_of::>() + + frame_owned + + frame_payload(&frame) + + values.capacity() * std::mem::size_of::() + + values.iter().map(String::capacity).sum::() + + contexts.capacity() * std::mem::size_of::>() + + contexts + .iter() + .map(|context| { + context.capacity() * std::mem::size_of::() + + context.iter().map(String::capacity).sum::() + }) + .sum::() + + lines.capacity() * std::mem::size_of::() + + lines.iter().map(String::capacity).sum::(); + note_post_hir_replay_capacity(owned); + } + match frame { + Frame::Enter(expression, context) => match &expression.kind { + ResolvedExprKind::Int(value) => values.push(if *value == i64::MIN { + "INT64_MIN".to_owned() + } else { + format!("INT64_C({value})") + }), + ResolvedExprKind::Bool(value) => { + values.push(if *value { "UINT8_C(1)" } else { "UINT8_C(0)" }.to_owned()) + } + ResolvedExprKind::Place(place) if place.projections.is_empty() => { + values.push(format!("v_{}", replay_symbol_hash(place.root.as_str()))) + } + ResolvedExprKind::NativeRustImportCall(call) => frames.push(Frame::NativeArgs( + call, + 0, + Vec::with_capacity(call.args.len()), + context, + )), + ResolvedExprKind::Unary { op, value } => { + frames.push(Frame::Unary(*op, context)); + frames.push(Frame::Enter(value, context)); + } + ResolvedExprKind::Binary { op, left, right } + if matches!(op, crate::ast::BinaryOp::And | crate::ast::BinaryOp::Or) => + { + frames.push(Frame::LazyLeft(*op, right, context)); + frames.push(Frame::Enter(left, context)); + } + ResolvedExprKind::Binary { op, left, right } => { + frames.push(Frame::BinaryLeft(*op, right, context)); + frames.push(Frame::Enter(left, context)); + } + ResolvedExprKind::Block { statements, tail } => { + frames.push(Frame::Block(statements, 0, tail, context)); + } + ResolvedExprKind::If { + condition, + then_branch, + else_branch, + } => { + let ty = replay_resolved_scalar(&expression.ty).ok_or_else(b111)?; + frames.push(Frame::IfCondition(then_branch, else_branch, ty, context)); + frames.push(Frame::Enter(condition, context)); + } + ResolvedExprKind::Call { callee, args, .. } => frames.push(Frame::CallArgs( + callee.as_str(), + args, + &expression.ty, + 0, + Vec::with_capacity(args.len()), + context, + )), + _ => return Err(b107("scalar value signature required")), + }, + Frame::Unary(op, context) => { + let value = values.pop().ok_or_else(b111)?; + if op == crate::ast::UnaryOp::Not { + values.push(format!("(!({value}))")); + } else { + let name = next_temporary(temporary_count); + contexts[context].push(format!("if(({value})==INT64_MIN)return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(1);int64_t {name}=-({value});")); + values.push(name); + } + } + Frame::BinaryLeft(op, right, context) => { + let left = values.pop().ok_or_else(b111)?; + frames.push(Frame::BinaryRight(op, left, context)); + frames.push(Frame::Enter(right, context)); + } + Frame::BinaryRight(op, left, context) => { + let right = values.pop().ok_or_else(b111)?; + if matches!( + op, + crate::ast::BinaryOp::Add + | crate::ast::BinaryOp::Sub + | crate::ast::BinaryOp::Mul + | crate::ast::BinaryOp::Div + | crate::ast::BinaryOp::Rem + ) { + let name = next_temporary(temporary_count); + contexts[context].push(format!("int64_t {name};")); + contexts[context].push(match op { + crate::ast::BinaryOp::Add => format!("if(__builtin_add_overflow({left},{right},&{name}))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(2);"), + crate::ast::BinaryOp::Sub => format!("if(__builtin_sub_overflow({left},{right},&{name}))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(3);"), + crate::ast::BinaryOp::Mul => format!("if(__builtin_mul_overflow({left},{right},&{name}))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(4);"), + crate::ast::BinaryOp::Div => format!("if(({right})==0||(({left})==INT64_MIN&&({right})==-1))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(5);{name}=({left})/({right});"), + crate::ast::BinaryOp::Rem => format!("if(({right})==0||(({left})==INT64_MIN&&({right})==-1))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(6);{name}=({left})%({right});"), + _ => unreachable!(), + }); + values.push(name); + } else { + let operator = match op { + crate::ast::BinaryOp::Eq => "==", + crate::ast::BinaryOp::Ne => "!=", + crate::ast::BinaryOp::Lt => "<", + crate::ast::BinaryOp::Le => "<=", + crate::ast::BinaryOp::Gt => ">", + crate::ast::BinaryOp::Ge => ">=", + crate::ast::BinaryOp::And => "&&", + crate::ast::BinaryOp::Or => "||", + _ => unreachable!(), + }; + values.push(format!("(({left}) {operator} ({right}))")); + } + } + Frame::LazyLeft(op, right, context) => { + let left = values.pop().ok_or_else(b111)?; + let name = next_temporary(temporary_count); + contexts[context].push(format!("uint8_t {name}=({left})?UINT8_C(1):UINT8_C(0);")); + let branch = contexts.len(); + contexts.push(Vec::new()); + frames.push(Frame::LazyRight(op, name, context, branch)); + frames.push(Frame::Enter(right, branch)); + } + Frame::LazyRight(op, name, context, branch) => { + let right = values.pop().ok_or_else(b111)?; + let branch_lines = take_c_lines(&mut contexts[branch]); + contexts[branch] = Vec::new(); + let condition = if op == crate::ast::BinaryOp::And { + name.clone() + } else { + format!("!{name}") + }; + contexts[context].push(format!( + "if({condition}){{{branch_lines} {name}=({right})?UINT8_C(1):UINT8_C(0);}}" + )); + values.push(name); + } + Frame::Block(statements, index, tail, context) => { + if let Some(ResolvedStatement::Let { value, .. }) = statements.get(index) { + frames.push(Frame::BlockLet(statements, index, tail, context)); + frames.push(Frame::Enter(value, context)); + } else { + frames.push(Frame::Enter(tail, context)); + } + } + Frame::BlockLet(statements, index, tail, context) => { + let value = values.pop().ok_or_else(b111)?; + let ResolvedStatement::Let { binding, .. } = &statements[index]; + let ty = replay_resolved_scalar(&binding.ty).ok_or_else(b111)?; + if ty != ScalarType::Unit { + contexts[context].push(format!( + "{} v_{} = {value};", + replay_c_scalar(ty), + replay_symbol_hash(binding.id.as_str()) + )); + } + frames.push(Frame::Block(statements, index + 1, tail, context)); + } + Frame::IfCondition(then_branch, else_branch, ty, context) => { + let condition = values.pop().ok_or_else(b111)?; + let name = (ty != ScalarType::Unit).then(|| next_temporary(temporary_count)); + if let Some(name) = &name { + contexts[context].push(format!("{} {name};", replay_c_scalar(ty))); + } + let then_context = contexts.len(); + contexts.push(Vec::new()); + frames.push(Frame::IfThen( + condition, + else_branch, + name, + context, + then_context, + )); + frames.push(Frame::Enter(then_branch, then_context)); + } + Frame::IfThen(condition, else_branch, name, context, then_context) => { + let then_value = values.pop().ok_or_else(b111)?; + let else_context = contexts.len(); + contexts.push(Vec::new()); + frames.push(Frame::IfElse( + condition, + name, + then_value, + context, + then_context, + else_context, + )); + frames.push(Frame::Enter(else_branch, else_context)); + } + Frame::IfElse(condition, name, then_value, context, then_context, else_context) => { + let else_value = values.pop().ok_or_else(b111)?; + let then_lines = take_c_lines(&mut contexts[then_context]); + let else_lines = take_c_lines(&mut contexts[else_context]); + contexts[then_context] = Vec::new(); + contexts[else_context] = Vec::new(); + if let Some(name) = name { + contexts[context].push(format!("if({condition}){{{then_lines}{name}={then_value};}}else{{{else_lines}{name}={else_value};}}")); + values.push(name); + } else { + contexts[context].push(format!( + "if({condition}){{{then_lines}}}else{{{else_lines}}}" + )); + values.push("INT64_C(0)".to_owned()); + } + } + Frame::NativeArgs(call, index, mut args, context) => { + if index < call.args.len() { + if index > 0 { + args.push(values.pop().ok_or_else(b111)?); + } + frames.push(Frame::NativeArgs(call, index + 1, args, context)); + frames.push(Frame::Enter(&call.args[index], context)); + } else { + if !call.args.is_empty() { + args.push(values.pop().ok_or_else(b111)?); + } + let import = imports + .iter() + .find(|item| item.id == call.import.as_str()) + .ok_or_else(b111)?; + let name = if import.result == ScalarType::Unit { + format!("tmp_{}", *temporary_count) + } else { + next_temporary(temporary_count) + }; + if import.result != ScalarType::Unit { + contexts[context] + .push(format!("{} {name};", replay_c_scalar(import.result))); + } + contexts[context].push(format!("status = ctx->imports->{}(ctx->userdata{}{}{}); if (status != 0) {{ if (!spxnr_status_for_{}(status)) return spxnr_adapter(8); return status; }}", import.c_field, if args.is_empty() { "" } else { ", " }, args.join(", "), if import.result == ScalarType::Unit { String::new() } else { format!(", &{name}") }, import.rust_method)); + if import.result == ScalarType::Bool { + contexts[context] + .push(format!("if ({name} > UINT8_C(1)) return spxnr_adapter(4);")); + } + values.push(if import.result == ScalarType::Unit { + "INT64_C(0)".to_owned() + } else { + name + }); + } + } + Frame::CallArgs(callee, args_source, ty, index, mut args, context) => { + if index < args_source.len() { + if index > 0 { + args.push(values.pop().ok_or_else(b111)?); + } + frames.push(Frame::CallArgs( + callee, + args_source, + ty, + index + 1, + args, + context, + )); + frames.push(Frame::Enter(&args_source[index], context)); + } else { + if !args_source.is_empty() { + args.push(values.pop().ok_or_else(b111)?); + } + if *ty == ResolvedType::Unit { + contexts[context].push(format!( + "status=spxnr1_f_{}(ctx{}{});if(status!=0)return status;", + replay_symbol_hash(callee), + if args.is_empty() { "" } else { ", " }, + args.join(",") + )); + values.push("INT64_C(0)".to_owned()); + } else { + let name = next_temporary(temporary_count); + contexts[context].push(format!("{} {name};status=spxnr1_f_{}(ctx{}{},&{name});if(status!=0)return status;", replay_c_scalar(replay_resolved_scalar(ty).ok_or_else(b111)?), replay_symbol_hash(callee), if args.is_empty() { "" } else { ", " }, args.join(","))); + values.push(name); + } + } + } + } + } + if values.len() != 1 { + return Err(b111()); + } + move_root_c_lines(lines, &mut contexts); + values.pop().ok_or_else(b111) +} + +fn replay_c_expression_child(expression: &ResolvedExpr, index: usize) -> Option<&ResolvedExpr> { + match &expression.kind { + ResolvedExprKind::Call { args, .. } => args.get(index), + ResolvedExprKind::NativeRustImportCall(call) => call.args.get(index), + ResolvedExprKind::Unary { value, .. } => (index == 0).then_some(value), + ResolvedExprKind::Binary { left, right, .. } => { + [left.as_ref(), right.as_ref()].get(index).copied() + } + ResolvedExprKind::Block { statements, tail } => statements + .get(index) + .map(|statement| { + let ResolvedStatement::Let { value, .. } = statement; + value + }) + .or_else(|| (index == statements.len()).then_some(tail)), + ResolvedExprKind::If { + condition, + then_branch, + else_branch, + } => [ + condition.as_ref(), + then_branch.as_ref(), + else_branch.as_ref(), + ] + .get(index) + .copied(), + _ => None, + } +} + +fn replay_c_expression_shape(expression: &ResolvedExpr) -> Result<(usize, usize), Diagnostic> { + let mut pending = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + pending[0] = Some((expression, 0usize, 1usize)); + let mut pending_len = 1usize; + let mut nodes = 0usize; + let mut maximum_depth = 1usize; + while pending_len != 0 { + let (node, child_index, node_depth) = pending[pending_len - 1].take().ok_or_else(b111)?; + pending_len -= 1; + if child_index == 0 { + nodes = nodes + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + maximum_depth = maximum_depth.max(node_depth); + } + if let Some(child) = replay_c_expression_child(node, child_index) { + if pending_len + 2 > pending.len() { + return Err(b109( + "max_semantic_expression_depth", + MAX_SEMANTIC_EXPRESSION_DEPTH, + )); + } + pending[pending_len] = Some((node, child_index + 1, node_depth)); + pending[pending_len + 1] = Some((child, 0, node_depth + 1)); + pending_len += 2; + } + } + Ok((nodes, maximum_depth)) +} + +fn replay_c_frame_payload(frame: &ReplayCExpressionFrame<'_>) -> usize { + match frame { + ReplayCExpressionFrame::FinishBinary(_, value) + | ReplayCExpressionFrame::FinishLazy(value) => value.capacity(), + ReplayCExpressionFrame::FinishThen(_, value) + | ReplayCExpressionFrame::FinishElse(value) => value.as_ref().map_or(0, String::capacity), + _ => 0, + } +} + +#[allow(clippy::ptr_arg)] // Exact Vec capacities are part of the scratch proof. +fn note_replay_c_expression_scratch( + current: &ReplayCExpressionFrame<'_>, + frames: &Vec>, + values: &Vec, + arguments: &Vec, + lines: &CExpressionLineArena, +) -> Result<(), Diagnostic> { + #[cfg(not(test))] + let _ = lines; + let mut string_payload = replay_c_frame_payload(current); + for frame in frames { + string_payload = string_payload + .checked_add(replay_c_frame_payload(frame)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + for value in values.iter().chain(arguments) { + string_payload = string_payload + .checked_add(value.capacity()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + } + if string_payload > MAX_GENERATED_C_BYTES { + return Err(b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES)); + } + #[cfg(test)] + note_post_hir_replay_capacity( + frames + .capacity() + .saturating_mul(REPLAY_C_EXPRESSION_FRAME_BYTES) + .saturating_add( + values + .capacity() + .saturating_mul(std::mem::size_of::()), + ) + .saturating_add( + arguments + .capacity() + .saturating_mul(std::mem::size_of::()), + ) + .saturating_add(lines.retained_bytes()) + .saturating_add(string_payload), + ); + Ok(()) +} + +fn replay_write_c_arguments( + lines: &mut CExpressionLineArena, + arguments: &[String], + separator: &str, +) -> Result<(), Diagnostic> { + for (index, argument) in arguments.iter().enumerate() { + if index > 0 { + lines + .write_str(separator) + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + } + lines + .write_str(argument) + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + } + Ok(()) +} + +fn replay_c_expression_linear_independent( + expression: &ResolvedExpr, + imports: &[ImportFact], + temporary_count: &mut usize, + lines: &mut CExpressionLineArena, +) -> Result { + let (node_count, depth) = replay_c_expression_shape(expression)?; + let capacity = depth + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let mut frames = Vec::with_capacity(capacity); + let mut values = Vec::::with_capacity(capacity); + let mut arguments = Vec::::with_capacity(node_count); + frames.push(ReplayCExpressionFrame::Evaluate(expression)); + while let Some(frame) = frames.pop() { + note_replay_c_expression_scratch(&frame, &frames, &values, &arguments, lines)?; + match frame { + ReplayCExpressionFrame::Evaluate(expression) => match &expression.kind { + ResolvedExprKind::Int(value) => values.push(if *value == i64::MIN { + "INT64_MIN".to_owned() + } else { + format!("INT64_C({value})") + }), + ResolvedExprKind::Bool(value) => { + values.push(if *value { "UINT8_C(1)" } else { "UINT8_C(0)" }.to_owned()) + } + ResolvedExprKind::Place(place) if place.projections.is_empty() => { + values.push(format!("v_{}", replay_symbol_hash(place.root.as_str()))) + } + ResolvedExprKind::NativeRustImportCall(call) => { + frames.push(ReplayCExpressionFrame::ContinueNative( + call, + 0, + arguments.len(), + )); + } + ResolvedExprKind::Unary { op, value } => { + frames.push(ReplayCExpressionFrame::FinishUnary(*op)); + frames.push(ReplayCExpressionFrame::Evaluate(value)); + } + ResolvedExprKind::Binary { op, left, right } + if matches!(op, crate::ast::BinaryOp::And | crate::ast::BinaryOp::Or) => + { + frames.push(ReplayCExpressionFrame::FinishLazyLeft(*op, right)); + frames.push(ReplayCExpressionFrame::Evaluate(left)); + } + ResolvedExprKind::Binary { op, left, right } => { + frames.push(ReplayCExpressionFrame::FinishBinaryLeft(*op, right)); + frames.push(ReplayCExpressionFrame::Evaluate(left)); + } + ResolvedExprKind::Block { statements, tail } => { + frames.push(ReplayCExpressionFrame::ContinueBlock(statements, 0, tail)); + } + ResolvedExprKind::If { + condition, + then_branch, + else_branch, + } => { + let ty = replay_resolved_scalar(&expression.ty).ok_or_else(b111)?; + frames.push(ReplayCExpressionFrame::FinishCondition( + then_branch, + else_branch, + ty, + )); + frames.push(ReplayCExpressionFrame::Evaluate(condition)); + } + ResolvedExprKind::Call { callee, args, .. } => { + frames.push(ReplayCExpressionFrame::ContinueCall( + callee.as_str(), + args, + &expression.ty, + 0, + arguments.len(), + )); + } + _ => return Err(b107("scalar value signature required")), + }, + ReplayCExpressionFrame::FinishUnary(op) => { + let value = values.pop().ok_or_else(b111)?; + if op == crate::ast::UnaryOp::Not { + values.push(format!("(!({value}))")); + } else { + let name = format!("tmp_{}", *temporary_count); + *temporary_count += 1; + write!(lines, "if(({value})==INT64_MIN)return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(1);int64_t {name}=-({value});") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + values.push(name); + } + } + ReplayCExpressionFrame::FinishBinaryLeft(op, right) => { + let left = values.pop().ok_or_else(b111)?; + frames.push(ReplayCExpressionFrame::FinishBinary(op, left)); + frames.push(ReplayCExpressionFrame::Evaluate(right)); + } + ReplayCExpressionFrame::FinishBinary(op, left) => { + let right = values.pop().ok_or_else(b111)?; + if matches!( + op, + crate::ast::BinaryOp::Add + | crate::ast::BinaryOp::Sub + | crate::ast::BinaryOp::Mul + | crate::ast::BinaryOp::Div + | crate::ast::BinaryOp::Rem + ) { + let name = format!("tmp_{}", *temporary_count); + *temporary_count += 1; + write!(lines, "int64_t {name};") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + match op { + crate::ast::BinaryOp::Add => write!(lines, "if(__builtin_add_overflow({left},{right},&{name}))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(2);"), + crate::ast::BinaryOp::Sub => write!(lines, "if(__builtin_sub_overflow({left},{right},&{name}))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(3);"), + crate::ast::BinaryOp::Mul => write!(lines, "if(__builtin_mul_overflow({left},{right},&{name}))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(4);"), + crate::ast::BinaryOp::Div => write!(lines, "if(({right})==0||(({left})==INT64_MIN&&({right})==-1))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(5);{name}=({left})/({right});"), + crate::ast::BinaryOp::Rem => write!(lines, "if(({right})==0||(({left})==INT64_MIN&&({right})==-1))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(6);{name}=({left})%({right});"), + _ => unreachable!(), + } + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + values.push(name); + } else { + let operator = match op { + crate::ast::BinaryOp::Eq => "==", + crate::ast::BinaryOp::Ne => "!=", + crate::ast::BinaryOp::Lt => "<", + crate::ast::BinaryOp::Le => "<=", + crate::ast::BinaryOp::Gt => ">", + crate::ast::BinaryOp::Ge => ">=", + crate::ast::BinaryOp::And => "&&", + crate::ast::BinaryOp::Or => "||", + _ => unreachable!(), + }; + values.push(format!("(({left}) {operator} ({right}))")); + } + } + ReplayCExpressionFrame::FinishLazyLeft(op, right) => { + let left = values.pop().ok_or_else(b111)?; + let name = format!("tmp_{}", *temporary_count); + *temporary_count += 1; + write!( + lines, + "uint8_t {name}=({left})?UINT8_C(1):UINT8_C(0);if({}){{", + if op == crate::ast::BinaryOp::And { + name.clone() + } else { + format!("!{name}") + } + ) + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + frames.push(ReplayCExpressionFrame::FinishLazy(name)); + frames.push(ReplayCExpressionFrame::Evaluate(right)); + } + ReplayCExpressionFrame::FinishLazy(name) => { + let right = values.pop().ok_or_else(b111)?; + write!(lines, " {name}=({right})?UINT8_C(1):UINT8_C(0);}}") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + values.push(name); + } + ReplayCExpressionFrame::ContinueBlock(statements, index, tail) => { + if let Some(ResolvedStatement::Let { value, .. }) = statements.get(index) { + frames.push(ReplayCExpressionFrame::FinishBinding( + statements, index, tail, + )); + frames.push(ReplayCExpressionFrame::Evaluate(value)); + } else { + frames.push(ReplayCExpressionFrame::Evaluate(tail)); + } + } + ReplayCExpressionFrame::FinishBinding(statements, index, tail) => { + let value = values.pop().ok_or_else(b111)?; + let ResolvedStatement::Let { binding, .. } = &statements[index]; + let ty = replay_resolved_scalar(&binding.ty).ok_or_else(b111)?; + if ty != ScalarType::Unit { + write!( + lines, + "{} v_{} = {value};", + replay_c_scalar(ty), + replay_symbol_hash(binding.id.as_str()) + ) + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + } + frames.push(ReplayCExpressionFrame::ContinueBlock( + statements, + index + 1, + tail, + )); + } + ReplayCExpressionFrame::FinishCondition(then_branch, else_branch, ty) => { + let condition = values.pop().ok_or_else(b111)?; + let name = if ty == ScalarType::Unit { + None + } else { + let name = format!("tmp_{}", *temporary_count); + *temporary_count += 1; + write!(lines, "{} {name};", replay_c_scalar(ty)) + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + Some(name) + }; + write!(lines, "if({condition}){{") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + frames.push(ReplayCExpressionFrame::FinishThen(else_branch, name)); + frames.push(ReplayCExpressionFrame::Evaluate(then_branch)); + } + ReplayCExpressionFrame::FinishThen(else_branch, name) => { + let then_value = values.pop().ok_or_else(b111)?; + if let Some(name) = &name { + write!(lines, "{name}={then_value};") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + } + lines + .write_str("}else{") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + frames.push(ReplayCExpressionFrame::FinishElse(name)); + frames.push(ReplayCExpressionFrame::Evaluate(else_branch)); + } + ReplayCExpressionFrame::FinishElse(name) => { + let else_value = values.pop().ok_or_else(b111)?; + if let Some(name) = name { + write!(lines, "{name}={else_value};}}") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + values.push(name); + } else { + lines + .write_str("}") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + values.push("INT64_C(0)".to_owned()); + } + } + ReplayCExpressionFrame::ContinueNative(call, index, start) => { + if index < call.args.len() { + if index > 0 { + arguments.push(values.pop().ok_or_else(b111)?); + } + frames.push(ReplayCExpressionFrame::ContinueNative( + call, + index + 1, + start, + )); + frames.push(ReplayCExpressionFrame::Evaluate(&call.args[index])); + } else { + if !call.args.is_empty() { + arguments.push(values.pop().ok_or_else(b111)?); + } + let import = imports + .iter() + .find(|item| item.id == call.import.as_str()) + .ok_or_else(b111)?; + let name = format!("tmp_{}", *temporary_count); + if import.result != ScalarType::Unit { + *temporary_count += 1; + write!(lines, "{} {name};", replay_c_scalar(import.result)) + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + } + write!( + lines, + "status = ctx->imports->{}(ctx->userdata", + import.c_field + ) + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + if start < arguments.len() { + lines + .write_str(", ") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + replay_write_c_arguments(lines, &arguments[start..], ", ")?; + } + if import.result != ScalarType::Unit { + write!(lines, ", &{name}") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + } + write!(lines, "); if (status != 0) {{ if (!spxnr_status_for_{}(status)) return spxnr_adapter(8); return status; }}", import.rust_method) + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + if import.result == ScalarType::Bool { + write!(lines, "if ({name} > UINT8_C(1)) return spxnr_adapter(4);") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + } + arguments.truncate(start); + values.push(if import.result == ScalarType::Unit { + "INT64_C(0)".to_owned() + } else { + name + }); + } + } + ReplayCExpressionFrame::ContinueCall(callee, source, ty, index, start) => { + if index < source.len() { + if index > 0 { + arguments.push(values.pop().ok_or_else(b111)?); + } + frames.push(ReplayCExpressionFrame::ContinueCall( + callee, + source, + ty, + index + 1, + start, + )); + frames.push(ReplayCExpressionFrame::Evaluate(&source[index])); + } else { + if !source.is_empty() { + arguments.push(values.pop().ok_or_else(b111)?); + } + if *ty == ResolvedType::Unit { + write!(lines, "status=spxnr1_f_{}(ctx", replay_symbol_hash(callee)) + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + if start < arguments.len() { + lines.write_str(",").map_err(|_| { + b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES) + })?; + replay_write_c_arguments(lines, &arguments[start..], ",")?; + } + lines + .write_str(");if(status!=0)return status;") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + values.push("INT64_C(0)".to_owned()); + } else { + let name = format!("tmp_{}", *temporary_count); + *temporary_count += 1; + write!( + lines, + "{} {name};status=spxnr1_f_{}(ctx", + replay_c_scalar(replay_resolved_scalar(ty).ok_or_else(b111)?), + replay_symbol_hash(callee) + ) + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + if start < arguments.len() { + lines.write_str(",").map_err(|_| { + b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES) + })?; + replay_write_c_arguments(lines, &arguments[start..], ",")?; + } + write!(lines, ",&{name});if(status!=0)return status;") + .map_err(|_| b109("max_generated_c_bytes", MAX_GENERATED_C_BYTES))?; + values.push(name); + } + arguments.truncate(start); + } + } + } + } + let terminal = ReplayCExpressionFrame::Evaluate(expression); + note_replay_c_expression_scratch(&terminal, &frames, &values, &arguments, lines)?; + if values.len() != 1 || !arguments.is_empty() { + return Err(b111()); + } + values.pop().ok_or_else(b111) +} + +fn replay_c_expression( + expression: &ResolvedExpr, + imports: &[ImportFact], + temporary_count: &mut usize, + lines: &mut CExpressionLineArena, +) -> Result { + replay_c_expression_linear_independent(expression, imports, temporary_count, lines) +} + +// Kept out of every build: the iterative generator above is the sole replay +// evaluator. This source reference makes authored formatting changes easy to +// audit while preventing a recursive production route from reappearing. +#[cfg(any())] +fn replay_c_expression_recursive_reference( + expression: &ResolvedExpr, + imports: &[ImportFact], + temporary_count: &mut usize, + lines: &mut Vec, +) -> Result { + match &expression.kind { + ResolvedExprKind::Int(value) => Ok(if *value == i64::MIN { + "INT64_MIN".to_owned() + } else { + format!("INT64_C({value})") + }), + ResolvedExprKind::Bool(value) => Ok(if *value { + "UINT8_C(1)".to_owned() + } else { + "UINT8_C(0)".to_owned() + }), + ResolvedExprKind::Place(place) if place.projections.is_empty() => { + Ok(format!("v_{}", replay_symbol_hash(place.root.as_str()))) + } + ResolvedExprKind::NativeRustImportCall(call) => { + let import = imports + .iter() + .find(|item| item.id == call.import.as_str()) + .ok_or_else(b111)?; + let args = call + .args + .iter() + .map(|arg| replay_c_expression(arg, imports, temporary_count, lines)) + .collect::, _>>()?; + let name = format!("tmp_{}", *temporary_count); + if import.result != ScalarType::Unit { + lines.push(format!("{} {name};", replay_c_scalar(import.result))); + *temporary_count += 1; + } + lines.push(format!( + "status = ctx->imports->{}(ctx->userdata{}{}{}); if (status != 0) {{ if (!spxnr_status_for_{}(status)) return spxnr_adapter(8); return status; }}", + import.c_field, + if args.is_empty() { "" } else { ", " }, + args.join(", "), + if import.result == ScalarType::Unit { + String::new() + } else { + format!(", &{name}") + }, + import.rust_method, + )); + if import.result == ScalarType::Bool { + lines.push(format!("if ({name} > UINT8_C(1)) return spxnr_adapter(4);")); + } + Ok(if import.result == ScalarType::Unit { + "INT64_C(0)".to_owned() + } else { + name + }) + } + ResolvedExprKind::Unary { op, value } => { + let value = replay_c_expression(value, imports, temporary_count, lines)?; + match op { + crate::ast::UnaryOp::Neg => { + let name = format!("tmp_{}", *temporary_count); + *temporary_count += 1; + lines.push(format!("if(({value})==INT64_MIN)return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(1);int64_t {name}=-({value});")); + Ok(name) + } + crate::ast::UnaryOp::Not => Ok(format!("(!({value}))")), + } + } + ResolvedExprKind::Binary { + op: crate::ast::BinaryOp::And | crate::ast::BinaryOp::Or, + left, + right, + } => { + let left = replay_c_expression(left, imports, temporary_count, lines)?; + let name = format!("tmp_{}", *temporary_count); + *temporary_count += 1; + lines.push(format!("uint8_t {name}=({left})?UINT8_C(1):UINT8_C(0);")); + let mut branch_lines = Vec::new(); + let right = replay_c_expression(right, imports, temporary_count, &mut branch_lines)?; + let condition = if matches!( + expression.kind, + ResolvedExprKind::Binary { + op: crate::ast::BinaryOp::And, + .. + } + ) { + name.clone() + } else { + format!("!{name}") + }; + lines.push(format!( + "if({condition}){{{} {name}=({right})?UINT8_C(1):UINT8_C(0);}}", + branch_lines.join("") + )); + Ok(name) + } + ResolvedExprKind::Binary { op, left, right } => { + let left = replay_c_expression(left, imports, temporary_count, lines)?; + let right = replay_c_expression(right, imports, temporary_count, lines)?; + if matches!( + op, + crate::ast::BinaryOp::Add + | crate::ast::BinaryOp::Sub + | crate::ast::BinaryOp::Mul + | crate::ast::BinaryOp::Div + | crate::ast::BinaryOp::Rem + ) { + let name = format!("tmp_{}", *temporary_count); + *temporary_count += 1; + lines.push(format!("int64_t {name};")); + lines.push(match op { + crate::ast::BinaryOp::Add => format!("if(__builtin_add_overflow({left},{right},&{name}))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(2);"), + crate::ast::BinaryOp::Sub => format!("if(__builtin_sub_overflow({left},{right},&{name}))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(3);"), + crate::ast::BinaryOp::Mul => format!("if(__builtin_mul_overflow({left},{right},&{name}))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(4);"), + crate::ast::BinaryOp::Div => format!("if(({right})==0||(({left})==INT64_MIN&&({right})==-1))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(5);{name}=({left})/({right});"), + crate::ast::BinaryOp::Rem => format!("if(({right})==0||(({left})==INT64_MIN&&({right})==-1))return (((uint64_t)65533)<<48)|(((uint64_t)1)<<32)|UINT32_C(6);{name}=({left})%({right});"), + _ => unreachable!(), + }); + return Ok(name); + } + let operator = match op { + crate::ast::BinaryOp::Add => "+", + crate::ast::BinaryOp::Sub => "-", + crate::ast::BinaryOp::Mul => "*", + crate::ast::BinaryOp::Div => "/", + crate::ast::BinaryOp::Rem => "%", + crate::ast::BinaryOp::Eq => "==", + crate::ast::BinaryOp::Ne => "!=", + crate::ast::BinaryOp::Lt => "<", + crate::ast::BinaryOp::Le => "<=", + crate::ast::BinaryOp::Gt => ">", + crate::ast::BinaryOp::Ge => ">=", + crate::ast::BinaryOp::And => "&&", + crate::ast::BinaryOp::Or => "||", + }; + Ok(format!("(({left}) {operator} ({right}))")) + } + ResolvedExprKind::Block { statements, tail } => { + for statement in statements { + let ResolvedStatement::Let { binding, value, .. } = statement; + let value = replay_c_expression(value, imports, temporary_count, lines)?; + let ty = replay_resolved_scalar(&binding.ty).ok_or_else(b111)?; + if ty != ScalarType::Unit { + lines.push(format!( + "{} v_{} = {value};", + replay_c_scalar(ty), + replay_symbol_hash(binding.id.as_str()) + )); + } + } + replay_c_expression(tail, imports, temporary_count, lines) + } + ResolvedExprKind::If { + condition, + then_branch, + else_branch, + } => { + let condition = replay_c_expression(condition, imports, temporary_count, lines)?; + if replay_resolved_scalar(&expression.ty) == Some(ScalarType::Unit) { + let mut then_lines = Vec::new(); + let _ = + replay_c_expression(then_branch, imports, temporary_count, &mut then_lines)?; + let mut else_lines = Vec::new(); + let _ = + replay_c_expression(else_branch, imports, temporary_count, &mut else_lines)?; + lines.push(format!( + "if({condition}){{{}}}else{{{}}}", + then_lines.join(""), + else_lines.join("") + )); + return Ok("INT64_C(0)".to_owned()); + } + let name = format!("tmp_{}", *temporary_count); + *temporary_count += 1; + lines.push(format!( + "{} {name};", + replay_c_scalar(replay_resolved_scalar(&expression.ty).ok_or_else(b111)?) + )); + let mut then_lines = Vec::new(); + let then_value = + replay_c_expression(then_branch, imports, temporary_count, &mut then_lines)?; + let mut else_lines = Vec::new(); + let else_value = + replay_c_expression(else_branch, imports, temporary_count, &mut else_lines)?; + lines.push(format!( + "if({condition}){{{}{name}={then_value};}}else{{{}{name}={else_value};}}", + then_lines.join(""), + else_lines.join("") + )); + Ok(name) + } + ResolvedExprKind::Call { callee, args, .. } => { + let args = args + .iter() + .map(|arg| replay_c_expression(arg, imports, temporary_count, lines)) + .collect::, _>>()?; + if expression.ty == ResolvedType::Unit { + lines.push(format!( + "status=spxnr1_f_{}(ctx{}{});if(status!=0)return status;", + replay_symbol_hash(callee.as_str()), + if args.is_empty() { "" } else { ", " }, + args.join(",") + )); + return Ok("INT64_C(0)".to_owned()); + } + let name = format!("tmp_{}", *temporary_count); + *temporary_count += 1; + lines.push(format!( + "{} {name};status=spxnr1_f_{}(ctx{}{},&{name});if(status!=0)return status;", + replay_c_scalar(replay_resolved_scalar(&expression.ty).ok_or_else(b111)?), + replay_symbol_hash(callee.as_str()), + if args.is_empty() { "" } else { ", " }, + args.join(",") + )); + Ok(name) + } + ResolvedExprKind::ConstructRecord { .. } + | ResolvedExprKind::ConstructVariant { .. } + | ResolvedExprKind::Match { .. } + | ResolvedExprKind::Try { .. } + | ResolvedExprKind::TryOption { .. } + | ResolvedExprKind::UpdateRecord { .. } + | ResolvedExprKind::Project { .. } + | ResolvedExprKind::Place(_) => Err(b107("scalar value signature required")), + } +} + +fn replay_c_exact( + source: &str, + spec: &Spec, + closure: &[&ResolvedFunction], + exports: &[ExportFact], + imports: &[ImportFact], +) -> Result { + let mut replay = ExactReplay::new(source); + replay.text("#include \"semaprax_native_rust_interop.h\"\n#include \n#include \n#include \n#include \nstatic const uint8_t spxnr_capabilities[32] = {"); + let digest = replay_capabilities_digest(&spec.capabilities); + let hex = digest.strip_prefix("sha256:").ok_or_else(b111)?; + if hex.len() != 64 { + return Err(b111()); + } + for index in (0..64).step_by(2) { + if index != 0 { + replay.text(","); + } + replay.text("0x"); + replay.text(&hex[index..index + 2]); + } + replay.text("};\nstatic spxnr_status_v1 spxnr_adapter(uint32_t code){return (((uint64_t)65535)<<48)|(((uint64_t)4)<<32)|code;}\nstatic spxnr_status_v1 spxnr_validate(const spxnr_context_v1 *ctx){if(!ctx||((uintptr_t)ctx%_Alignof(spxnr_context_v1))!=0)return spxnr_adapter(1);if(ctx->abi_version!=1||ctx->size!=sizeof(*ctx)||ctx->reserved!=0)return spxnr_adapter(1);if(!ctx->imports||((uintptr_t)ctx->imports%_Alignof(spxnr_imports_v1))!=0)return spxnr_adapter(2);if(ctx->imports->abi_version!=1||ctx->imports->size!=sizeof(*ctx->imports))return spxnr_adapter(2);if(memcmp(ctx->capabilities_digest,spxnr_capabilities,32)!=0)return spxnr_adapter(3);if(ctx->call_depth>=32)return spxnr_adapter(7);return 0;}\n"); + replay.text("static int spxnr_status_canonical(spxnr_status_v1 status){if(status==0)return 1;uint32_t code=(uint32_t)status;uint8_t class_=(uint8_t)(status>>32);uint8_t retry=(uint8_t)((status>>40)&1);uint8_t reserved=(uint8_t)((status>>41)&0x7f);uint16_t domain=(uint16_t)(status>>48);if(code==0||reserved!=0||domain==0)return 0;if(domain==65533)return retry==0&&((class_==1&&code>=1&&code<=6)||(class_==2&&code>=1&&code<=2));"); + let domains = imports + .iter() + .filter_map(|import| import.failure.as_ref()) + .collect::>(); + for (index, _) in domains.iter().enumerate() { + replay.text("if(domain=="); + replay.number(index + 1); + replay.text(")return class_==3;"); + } + replay.text("if(domain==65534)return class_==4&&retry==0&&code>=1&&code<=2;if(domain==65535)return class_==4&&retry==0&&code>=1&&code<=8;return 0;}\n"); + let ordinals = domains + .iter() + .enumerate() + .map(|(index, domain)| (domain.as_str(), index + 1)) + .collect::>(); + for import in imports { + replay.text("static int spxnr_status_for_"); + replay.text(&import.rust_method); + replay.text("(spxnr_status_v1 status){if(!spxnr_status_canonical(status))return 0;uint16_t domain=(uint16_t)(status>>48);return domain==65534||domain==65535"); + if let Some(ordinal) = import + .failure + .as_deref() + .and_then(|domain| ordinals.get(domain).copied()) + { + replay.text("||domain=="); + replay.number(ordinal); + } + replay.text(";}\nstatic spxnr_status_v1 spxnr_validate_"); + replay.text(&import.rust_method); + replay.text("(const spxnr_context_v1 *ctx){return ctx->imports->"); + replay.text(&import.c_field); + replay.text("?0:spxnr_adapter(2);}\n"); + } + for function in closure { + let parameters = replay_parameter_facts(function)?; + let result = replay_resolved_scalar(&function.return_type).ok_or_else(b111)?; + replay.text("static spxnr_status_v1 spxnr1_f_"); + replay.text(&replay_symbol_hash(function.id.as_str())); + replay.text("(const spxnr_context_v1 *ctx"); + if !parameters.is_empty() { + replay.text(", "); + replay.text(&replay_c_parameters(¶meters)); + } + if result != ScalarType::Unit { + replay.text(", "); + replay.text(replay_c_scalar(result)); + replay.text(" *result_out"); + } + replay.text(");\n"); + } + for function in closure { + let parameters = replay_parameter_facts(function)?; + let result = replay_resolved_scalar(&function.return_type).ok_or_else(b111)?; + replay.text("static spxnr_status_v1 spxnr1_f_"); + replay.text(&replay_symbol_hash(function.id.as_str())); + replay.text("(const spxnr_context_v1 *ctx"); + if !parameters.is_empty() { + replay.text(", "); + replay.text(&replay_c_parameters(¶meters)); + } + if result != ScalarType::Unit { + replay.text(", "); + replay.text(replay_c_scalar(result)); + replay.text(" *result_out"); + } + replay.text(" ){spxnr_status_v1 status=0;(void)ctx;"); + for index in 0..parameters.len() { + replay.text("(void)arg_"); + replay.number(index); + replay.text(";"); + } + for (index, (parameter, resolved)) in parameters.iter().zip(&function.params).enumerate() { + replay.text(replay_c_scalar(parameter.ty)); + replay.text(" v_"); + replay.text(&replay_symbol_hash(resolved.id.as_str())); + replay.text("=arg_"); + replay.number(index); + replay.text(";"); + } + let mut temporary_count = 0; + let mut lines = CExpressionLineArena::new(); + for requirement in &function.requires { + lines.clear(); + let value = + replay_c_expression(requirement, imports, &mut temporary_count, &mut lines)?; + replay.text(lines.as_str()?); + replay.text("if(!("); + replay.text(&value); + replay.text("))return (((uint64_t)65533)<<48)|(((uint64_t)2)<<32)|UINT32_C(1);"); + } + lines.clear(); + let value = replay_c_expression(&function.body, imports, &mut temporary_count, &mut lines)?; + replay.text(lines.as_str()?); + if result != ScalarType::Unit { + replay.text(replay_c_scalar(result)); + replay.text(" v_"); + replay.text(&replay_symbol_hash(function.result_id.as_str())); + replay.text("="); + replay.text(&value); + replay.text(";"); + } + for guarantee in &function.ensures { + lines.clear(); + let value = replay_c_expression(guarantee, imports, &mut temporary_count, &mut lines)?; + replay.text(lines.as_str()?); + replay.text("if(!("); + replay.text(&value); + replay.text("))return (((uint64_t)65533)<<48)|(((uint64_t)2)<<32)|UINT32_C(2);"); + } + if result != ScalarType::Unit { + replay.text("*result_out=v_"); + replay.text(&replay_symbol_hash(function.result_id.as_str())); + replay.text(";"); + } + replay.text("return status;}\n"); + } + for export in exports { + replay.text("spxnr_status_v1 "); + replay.text(&export.c_symbol); + replay.text("(const spxnr_context_v1 *ctx"); + if !export.parameters.is_empty() { + replay.text(", "); + replay.text(&replay_c_parameters(&export.parameters)); + } + if export.result != ScalarType::Unit { + replay.text(", "); + replay.text(replay_c_scalar(export.result)); + replay.text(" *result_out"); + } + replay.text(" ){spxnr_status_v1 status=spxnr_validate(ctx);if(status!=0)return status;"); + for import in imports { + replay.text("status=spxnr_validate_"); + replay.text(&import.rust_method); + replay.text("(ctx);if(status!=0)return status;"); + } + if export.result != ScalarType::Unit { + replay.text("if(!result_out||((uintptr_t)result_out%_Alignof("); + replay.text(replay_c_scalar(export.result)); + replay.text("))!=0)return spxnr_adapter(5);"); + } + for (index, parameter) in export.parameters.iter().enumerate() { + if parameter.ty == ScalarType::Bool { + replay.text("if(arg_"); + replay.number(index); + replay.text(">1)return spxnr_adapter(4);"); + } + } + replay.text( + "spxnr_context_v1 local=*ctx;local.call_depth=ctx->call_depth+1;status=spxnr1_f_", + ); + replay.text(&replay_symbol_hash(&export.id)); + replay.text("(&local"); + for index in 0..export.parameters.len() { + replay.text(if index == 0 { ", " } else { "," }); + replay.text("arg_"); + replay.number(index); + } + if export.result != ScalarType::Unit { + replay.text(", result_out"); + } + replay.text(");return status;}\n"); + } + Ok(replay.finish()) +} + +#[allow(clippy::too_many_arguments)] +fn replay_generated_exact( + spec: &Spec, + closure: &[&ResolvedFunction], + exports: &[ExportFact], + imports: &[ImportFact], + header: &str, + c: &str, + rust: &str, + ffi: &str, +) -> Result<(), Diagnostic> { + if !replay_header_exact(header, exports, imports) { + return Err(b111()); + } + if !replay_safe_rust_exact(rust, spec, exports, imports) + || !replay_private_ffi_exact(ffi, spec, exports, imports) + || !replay_c_exact(c, spec, closure, exports, imports)? + { + return Err(b111()); + } + replay_generated(header, c, rust, ffi) +} + +#[cfg(test)] +struct TestTool { + path: PathBuf, +} + +#[cfg(test)] +fn same_file_metadata(left: &std::fs::Metadata, right: &std::fs::Metadata) -> bool { + if left.len() != right.len() + || left.is_file() != right.is_file() + || left.modified().ok() != right.modified().ok() + { + return false; + } + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt as _; + left.dev() == right.dev() && left.ino() == right.ino() + } + #[cfg(not(unix))] + { + true + } +} + +#[cfg(test)] +fn configured_tool(variable: &str) -> Result { + if let Some(value) = std::env::var_os(variable) { + let path = std::fs::canonicalize(value).map_err(|_| b110())?; + return Ok(TestTool { path }); + } + let name = if variable == "RUSTC" { + if cfg!(windows) { + "rustc.exe" + } else { + "rustc" + } + } else if cfg!(windows) { + "clang.exe" + } else { + "clang" + }; + if let Some(paths) = std::env::var_os("PATH") { + for directory in std::env::split_paths(&paths) { + let candidate = directory.join(name); + let Ok(path) = std::fs::canonicalize(candidate) else { + continue; + }; + let Ok(metadata) = std::fs::symlink_metadata(&path) else { + continue; + }; + if metadata.is_file() && !metadata.file_type().is_symlink() { + return Ok(TestTool { path }); + } + } + } + Err(b110()) +} + +#[cfg(test)] +fn bind_test_tool_environment(command: &mut std::process::Command) { + #[cfg(target_os = "linux")] + command.args(["-C", "link-arg=-fuse-ld=/usr/bin/ld"]); + #[cfg(windows)] + for variable in ["INCLUDE", "LIB"] { + if let Some(value) = std::env::var_os(variable) { + command.env(variable, value); + } + } + #[cfg(not(any(target_os = "linux", windows)))] + let _ = command; +} + +struct RustcVersion { + storage: String, + boundaries: [usize; 5], +} + +impl RustcVersion { + fn prepared() -> Result { + let storage = String::with_capacity(PHASE_B_TOOL_VERSION_CAPACITY); + if storage.capacity() != PHASE_B_TOOL_VERSION_CAPACITY { + return Err(PhaseBLocalError::BuilderBudget); + } + Ok(Self { + storage, + boundaries: [0; 5], + }) + } + + fn capacity(&self) -> usize { + self.storage.capacity() + } + + fn field(&self, index: usize) -> &str { + &self.storage[self.boundaries[index]..self.boundaries[index + 1]] + } + + fn release(&self) -> &str { + self.field(0) + } + + fn commit_hash(&self) -> &str { + self.field(1) + } + + fn host(&self) -> &str { + self.field(2) + } + + fn llvm_version(&self) -> &str { + self.field(3) + } + + fn store(&mut self, values: [&str; 4]) -> Result<(), PhaseBLocalError> { + if self.capacity() != PHASE_B_TOOL_VERSION_CAPACITY + || !self.storage.is_empty() + || self.boundaries != [0; 5] + { + return Err(PhaseBLocalError::BuilderBudget); + } + let total = values + .iter() + .try_fold(0usize, |total, value| total.checked_add(value.len())); + if total.is_none_or(|total| total > self.capacity()) { + return Err(PhaseBLocalError::Unsupported); + } + for (index, value) in values.into_iter().enumerate() { + self.storage.push_str(value); + self.boundaries[index + 1] = self.storage.len(); + } + if self.capacity() != PHASE_B_TOOL_VERSION_CAPACITY { + return Err(PhaseBLocalError::BuilderBudget); + } + Ok(()) + } + + #[cfg(test)] + fn from_fields(values: [&str; 4]) -> Self { + let mut version = Self::prepared().unwrap(); + version.store(values).unwrap(); + version + } +} + +struct FrozenToolEnvironment { + clang: Option, + rustc: Option, + path: Option, + sanitizer: Option, + include: Option, + libraries: Option, + budget: TemporaryBudget, +} + +struct AuthorizedProcessArena { + arena: Option, + budget: Option, +} + +impl AuthorizedProcessArena { + fn new(arena: platform::PreparedProcessArena, budget: TemporaryBudget) -> Self { + Self { + arena: Some(arena), + budget: Some(budget), + } + } + + fn arena(&self) -> Result<&platform::PreparedProcessArena, PhaseBLocalError> { + self.arena.as_ref().ok_or(PhaseBLocalError::BuilderBudget) + } + + fn arena_mut(&mut self) -> Result<&mut platform::PreparedProcessArena, PhaseBLocalError> { + self.arena.as_mut().ok_or(PhaseBLocalError::BuilderBudget) + } + + fn authorized_capacity(&self) -> Result { + self.budget + .as_ref() + .map(TemporaryBudget::maximum) + .ok_or(PhaseBLocalError::BuilderBudget) + } +} + +impl Drop for AuthorizedProcessArena { + fn drop(&mut self) { + if let Some(arena) = self.arena.take() { + drop(arena); + #[cfg(test)] + { + PHASE_B_PROCESS_ARENA_DROPS.with(|drops| drops.set(drops.get() + 1)); + note_phase_b_process_arena_drop(1); + } + } + if let Some(budget) = self.budget.take() { + drop(budget); + #[cfg(test)] + { + PHASE_B_PROCESS_ARENA_BUDGET_DROPS.with(|drops| drops.set(drops.get() + 1)); + note_phase_b_process_arena_drop(2); + } + } + } +} + +struct PreparedToolchainPlan { + environment: FrozenToolEnvironment, + path_budget: TemporaryBudget, + discovery_output_budget: TemporaryBudget, + direct_sysroot_output_budget: TemporaryBudget, + rustc_output_budget: TemporaryBudget, + clang_output_budget: TemporaryBudget, + command_budget: TemporaryBudget, + clang_resolver: platform::PreparedToolResolver, + discovery_resolver: platform::PreparedToolResolver, + direct_resolver: platform::PreparedToolResolver, + direct_recheck_resolver: platform::PreparedToolResolver, + discovery_invocation: platform::PreparedSysrootInvocation, + direct_sysroot_invocation: platform::PreparedSysrootInvocation, + rustc_invocation: platform::PreparedRustcVersionInvocation, + clang_invocation: platform::PreparedVersionInvocation, + process_arena: AuthorizedProcessArena, + rustc_version: RustcVersion, +} + +struct ToolchainFacts { + rustc: platform::HeldDirectRustc, + clang: platform::HeldTool, + process_arena: Option, + rustc_version: RustcVersion, + clang_version: String, +} + +fn freeze_tool_environment() -> Result { + #[cfg(test)] + let invalid_tool_environment = PHASE_B_INVALID_TOOL_ENV_INJECTION.with(std::cell::Cell::get); + #[cfg(not(test))] + let invalid_tool_environment = false; + let clang = if invalid_tool_environment { + Some(OsString::from("__semaprax_missing_clang__")) + } else { + std::env::var_os("CLANG") + }; + let rustc = if invalid_tool_environment { + Some(OsString::from("__semaprax_missing_rustc__")) + } else { + std::env::var_os("RUSTC") + }; + let path = std::env::var_os("PATH"); + let sanitizer = std::env::var_os("SEMAPRAX_REQUIRE_NATIVE_RUST_INTEROP_SANITIZERS"); + let include = if cfg!(windows) { + std::env::var_os("INCLUDE") + } else { + None + }; + let libraries = if cfg!(windows) { + std::env::var_os("LIB") + } else { + None + }; + let capacity = [&clang, &rustc, &path, &sanitizer, &include, &libraries] + .into_iter() + .try_fold(0usize, |total, value| { + total.checked_add(value.as_ref().map_or(0, OsString::capacity)) + }) + .ok_or(PhaseBLocalError::BuilderBudget)?; + let budget = reserve_phase_b(capacity)?; + Ok(FrozenToolEnvironment { + clang, + rustc, + path, + sanitizer, + include, + libraries, + budget, + }) +} + +fn prepare_toolchain_plan() -> Result { + let environment = freeze_tool_environment()?; + let clang_name = if cfg!(windows) { "clang.exe" } else { "clang" }; + let rustc_name = if cfg!(windows) { "rustc.exe" } else { "rustc" }; + let path_budget = reserve_phase_b( + PHASE_B_TOOL_RESOLVER_CAPACITY + .checked_mul(4) + .ok_or(PhaseBLocalError::BuilderBudget)?, + )?; + let discovery_output_budget = reserve_phase_b(PHASE_B_TOOL_VERSION_CAPACITY)?; + let direct_sysroot_output_budget = reserve_phase_b(PHASE_B_TOOL_VERSION_CAPACITY)?; + let rustc_output_budget = reserve_phase_b(PHASE_B_TOOL_VERSION_CAPACITY)?; + let clang_output_budget = reserve_phase_b(PHASE_B_TOOL_VERSION_CAPACITY)?; + let command_budget = reserve_phase_b( + PHASE_B_VERSION_COMMAND_CAPACITY + .checked_mul(4) + .ok_or(PhaseBLocalError::BuilderBudget)?, + )?; + let clang_resolver = platform::prepare_tool_resolver(clang_name, PHASE_B_TOOL_PATH_CAPACITY) + .map_err(|_| PhaseBLocalError::BuilderBudget)?; + let discovery_resolver = + platform::prepare_tool_resolver(rustc_name, PHASE_B_TOOL_PATH_CAPACITY) + .map_err(|_| PhaseBLocalError::BuilderBudget)?; + let direct_resolver = platform::prepare_tool_resolver(rustc_name, PHASE_B_TOOL_PATH_CAPACITY) + .map_err(|_| PhaseBLocalError::BuilderBudget)?; + let direct_recheck_resolver = + platform::prepare_tool_resolver(rustc_name, PHASE_B_TOOL_PATH_CAPACITY) + .map_err(|_| PhaseBLocalError::BuilderBudget)?; + let resolver_owned = platform::prepared_tool_resolver_owned_capacity(&clang_resolver) + .checked_add(platform::prepared_tool_resolver_owned_capacity( + &discovery_resolver, + )) + .and_then(|total| { + total.checked_add(platform::prepared_tool_resolver_owned_capacity( + &direct_resolver, + )) + }) + .and_then(|total| { + total.checked_add(platform::prepared_tool_resolver_owned_capacity( + &direct_recheck_resolver, + )) + }) + .ok_or(PhaseBLocalError::BuilderBudget)?; + if resolver_owned > path_budget.maximum() { + return Err(PhaseBLocalError::BuilderBudget); + } + let discovery_invocation = platform::prepare_sysroot_invocation(PHASE_B_TOOL_VERSION_CAPACITY) + .map_err(|_| PhaseBLocalError::BuilderBudget)?; + let direct_sysroot_invocation = + platform::prepare_sysroot_invocation(PHASE_B_TOOL_VERSION_CAPACITY) + .map_err(|_| PhaseBLocalError::BuilderBudget)?; + let rustc_invocation = + platform::prepare_rustc_version_invocation(PHASE_B_TOOL_VERSION_CAPACITY) + .map_err(|_| PhaseBLocalError::BuilderBudget)?; + let clang_invocation = + platform::prepare_version_invocation("--version", PHASE_B_TOOL_VERSION_CAPACITY) + .map_err(|_| PhaseBLocalError::BuilderBudget)?; + let command_owned = platform::prepared_sysroot_owned_capacity(&discovery_invocation) + .checked_sub(PHASE_B_TOOL_VERSION_CAPACITY) + .and_then(|discovery| { + platform::prepared_sysroot_owned_capacity(&direct_sysroot_invocation) + .checked_sub(PHASE_B_TOOL_VERSION_CAPACITY) + .and_then(|direct| discovery.checked_add(direct)) + }) + .and_then(|total| { + platform::prepared_rustc_version_owned_capacity(&rustc_invocation) + .checked_sub(PHASE_B_TOOL_VERSION_CAPACITY) + .and_then(|rustc| total.checked_add(rustc)) + }) + .and_then(|total| { + platform::prepared_version_owned_capacity(&clang_invocation) + .checked_sub(PHASE_B_TOOL_VERSION_CAPACITY) + .and_then(|clang| total.checked_add(clang)) + }) + .ok_or(PhaseBLocalError::BuilderBudget)?; + if command_owned > command_budget.maximum() { + return Err(PhaseBLocalError::BuilderBudget); + } + let persistent = PHASE_B_TOOL_VERSION_CAPACITY; + let persistent_budget = reserve_phase_b(persistent)?; + let rustc_version = RustcVersion::prepared()?; + if rustc_version.capacity() != PHASE_B_TOOL_VERSION_CAPACITY { + return Err(PhaseBLocalError::BuilderBudget); + } + retain_phase_b(persistent_budget, rustc_version.capacity())?; + let process_arena = prepare_process_arena_authorized( + environment.include.as_deref(), + environment.libraries.as_deref(), + )?; + Ok(PreparedToolchainPlan { + environment, + path_budget, + discovery_output_budget, + direct_sysroot_output_budget, + rustc_output_budget, + clang_output_budget, + command_budget, + clang_resolver, + discovery_resolver, + direct_resolver, + direct_recheck_resolver, + discovery_invocation, + direct_sysroot_invocation, + rustc_invocation, + clang_invocation, + process_arena, + rustc_version, + }) +} + +fn authenticate_toolchain( + plan: PreparedToolchainPlan, + target: &Target, + cwd: &platform::HeldDirectory, +) -> Result { + let PreparedToolchainPlan { + environment, + path_budget, + discovery_output_budget, + direct_sysroot_output_budget, + rustc_output_budget, + clang_output_budget, + command_budget, + clang_resolver, + discovery_resolver, + direct_resolver, + direct_recheck_resolver, + discovery_invocation, + direct_sysroot_invocation, + rustc_invocation, + clang_invocation, + mut process_arena, + mut rustc_version, + } = plan; + let FrozenToolEnvironment { + clang: configured_clang, + rustc: configured_rustc, + path, + sanitizer, + include, + libraries, + budget: environment_budget, + } = environment; + match sanitizer { + None => {} + Some(value) if value == "1" && cfg!(target_os = "linux") => {} + Some(_) => return Err(PhaseBLocalError::Unsupported), + } + let clang = platform::resolve_and_hold_tool_prepared( + clang_resolver, + configured_clang.as_deref(), + path.as_deref(), + ) + .map_err(|_| PhaseBLocalError::Unsupported)?; + #[cfg(test)] + PHASE_B_TOOL_HOLDS.with(|count| count.set(count.get().saturating_add(1))); + let configured_rustc = configured_rustc.ok_or(PhaseBLocalError::Unsupported)?; + let discovery = + platform::hold_rustc_discovery_prepared(discovery_resolver, configured_rustc.as_os_str()) + .map_err(|_| PhaseBLocalError::Unsupported)?; + #[cfg(test)] + PHASE_B_TOOL_HOLDS.with(|count| count.set(count.get().saturating_add(1))); + drop(configured_clang); + drop(configured_rustc); + drop(include); + drop(libraries); + #[cfg(test)] + PHASE_B_TOOL_PROCESSES.with(|count| count.set(count.get().saturating_add(1))); + let discovery_sysroot = platform::rustc_discovery_output_prepared( + &discovery, + cwd, + discovery_invocation, + process_arena.arena_mut()?, + ) + .map_err(|_| PhaseBLocalError::Unsupported)?; + if discovery_sysroot.capacity() != PHASE_B_TOOL_VERSION_CAPACITY { + return Err(PhaseBLocalError::BuilderBudget); + } + let rustc = platform::hold_direct_rustc_prepared(direct_resolver, discovery_sysroot.bytes()) + .map_err(|_| PhaseBLocalError::Unsupported)?; + #[cfg(test)] + PHASE_B_TOOL_HOLDS.with(|count| count.set(count.get().saturating_add(1))); + drop(discovery); + drop(discovery_sysroot); + drop(discovery_output_budget); + #[cfg(test)] + PHASE_B_TOOL_PROCESSES.with(|count| count.set(count.get().saturating_add(1))); + let direct_sysroot = platform::direct_rustc_output_prepared( + &rustc, + cwd, + direct_sysroot_invocation, + process_arena.arena_mut()?, + ) + .map_err(|_| PhaseBLocalError::Unsupported)?; + if direct_sysroot.capacity() != PHASE_B_TOOL_VERSION_CAPACITY { + return Err(PhaseBLocalError::BuilderBudget); + } + platform::direct_rustc_reproduces_sysroot( + &rustc, + direct_recheck_resolver, + direct_sysroot.bytes(), + ) + .map_err(|_| PhaseBLocalError::Unsupported)?; + #[cfg(test)] + if PHASE_B_DIRECT_SYSROOT_MISMATCH_INJECTION.with(std::cell::Cell::get) { + return Err(PhaseBLocalError::Unsupported); + } + drop(direct_sysroot); + drop(direct_sysroot_output_budget); + drop(path); + drop(environment_budget); + retain_phase_b(path_budget, platform::tool_path_capacity(&clang))?; + #[cfg(test)] + PHASE_B_TOOL_PROCESSES.with(|count| count.set(count.get().saturating_add(1))); + let rustc_text = platform::direct_rustc_version_prepared( + &rustc, + cwd, + rustc_invocation, + process_arena.arena_mut()?, + ) + .map_err(|_| PhaseBLocalError::Unsupported)?; + if rustc_text.capacity() != PHASE_B_TOOL_VERSION_CAPACITY { + return Err(PhaseBLocalError::BuilderBudget); + } + let rustc_bytes = rustc_text.into_bytes(); + let rustc_text = std::str::from_utf8(&rustc_bytes) + .map_err(|_| PhaseBLocalError::Unsupported)? + .trim(); + parse_rustc_version(rustc_text, &mut rustc_version)?; + drop(rustc_bytes); + drop(rustc_output_budget); + if rustc_version.host() != target.triple { + return Err(PhaseBLocalError::Unsupported); + } + #[cfg(test)] + PHASE_B_TOOL_PROCESSES.with(|count| count.set(count.get().saturating_add(1))); + let clang_text = + platform::tool_version_prepared(&clang, cwd, clang_invocation, process_arena.arena_mut()?) + .map_err(|_| PhaseBLocalError::Unsupported)?; + if clang_text.capacity() != PHASE_B_TOOL_VERSION_CAPACITY { + return Err(PhaseBLocalError::BuilderBudget); + } + let mut clang_version = + String::from_utf8(clang_text.into_bytes()).map_err(|_| PhaseBLocalError::Unsupported)?; + let trimmed = clang_version.trim(); + let start = trimmed.as_ptr() as usize - clang_version.as_ptr() as usize; + let end = start + trimmed.len(); + clang_version.truncate(end); + clang_version.drain(..start); + if clang_version.capacity() != PHASE_B_TOOL_VERSION_CAPACITY { + return Err(PhaseBLocalError::BuilderBudget); + } + retain_phase_b(clang_output_budget, clang_version.capacity())?; + drop(command_budget); + if clang_version.is_empty() { + return Err(PhaseBLocalError::Unsupported); + } + if platform::prepared_process_arena_remaining(process_arena.arena()?) + != PHASE_B_PROCESS_INVOCATIONS - 4 + { + return Err(PhaseBLocalError::BuilderBudget); + } + Ok(ToolchainFacts { + rustc, + clang, + process_arena: Some(process_arena), + rustc_version, + clang_version, + }) +} + +fn planned_sanitizers(plan: &PreparedToolchainPlan) -> bool { + cfg!(target_os = "linux") + && plan.environment.sanitizer.as_deref() == Some(std::ffi::OsStr::new("1")) +} + +fn parse_rustc_version(source: &str, output: &mut RustcVersion) -> Result<(), PhaseBLocalError> { + if output.capacity() != PHASE_B_TOOL_VERSION_CAPACITY + || !output.storage.is_empty() + || output.boundaries != [0; 5] + { + return Err(PhaseBLocalError::BuilderBudget); + } + let mut lines = source.lines(); + let header = lines + .next() + .filter(|line| line.starts_with("rustc ") && line.len() > 6) + .ok_or(PhaseBLocalError::Unsupported)?; + let mut values = [None; 4]; + let mut binary_seen = false; + let mut date_seen = false; + for line in lines { + let (key, value) = line.split_once(": ").ok_or(PhaseBLocalError::Unsupported)?; + let slot = match key { + "release" => Some(0), + "commit-hash" => Some(1), + "host" => Some(2), + "LLVM version" => Some(3), + "binary" if !binary_seen => { + binary_seen = true; + None + } + "commit-date" if !date_seen => { + date_seen = true; + None + } + _ => return Err(PhaseBLocalError::Unsupported), + }; + if let Some(slot) = slot { + if values[slot].replace(value).is_some() { + return Err(PhaseBLocalError::Unsupported); + } + } + } + let [Some(release), Some(commit_hash), Some(host), Some(llvm_version)] = values else { + return Err(PhaseBLocalError::Unsupported); + }; + if release.is_empty() + || !header.contains(release) + || commit_hash.len() < 7 + || !commit_hash.bytes().all(|byte| byte.is_ascii_hexdigit()) + || host.is_empty() + || llvm_version.is_empty() + || [release, commit_hash, host, llvm_version] + .iter() + .any(|value| value.len() > PHASE_B_TOOL_VERSION_CAPACITY) + { + return Err(PhaseBLocalError::Unsupported); + } + output.store([release, commit_hash, host, llvm_version]) +} + +/// Canonical manifest row order is a wire contract. The platform object is +/// always the third row; no caller may infer or sort this order dynamically. +const fn canonical_manifest_file_names() -> [&'static str; 6] { + [ + "descriptor.json", + "module.c", + if cfg!(windows) { + "module.obj" + } else { + "module.o" + }, + "semaprax_native_rust_interop.h", + "semaprax_native_rust_interop.rs", + "semaprax_native_rust_interop_ffi.rs", + ] +} + +fn write_raw_digest_json(output: &mut impl std::fmt::Write, bytes: &[u8]) -> std::fmt::Result { + const HEX: &[u8; 16] = b"0123456789abcdef"; + output.write_str("\"sha256:")?; + for byte in Sha256::digest(bytes) { + let pair = [HEX[usize::from(byte >> 4)], HEX[usize::from(byte & 0x0f)]]; + output.write_str(std::str::from_utf8(&pair).map_err(|_| std::fmt::Error)?)?; + } + output.write_char('"') +} + +fn write_usize_decimal(output: &mut impl std::fmt::Write, mut value: usize) -> std::fmt::Result { + let mut bytes = [0_u8; 20]; + let mut start = bytes.len(); + loop { + start -= 1; + bytes[start] = b'0' + u8::try_from(value % 10).map_err(|_| std::fmt::Error)?; + value /= 10; + if value == 0 { + break; + } + } + output.write_str(std::str::from_utf8(&bytes[start..]).map_err(|_| std::fmt::Error)?) +} + +fn write_manifest_file_row( + output: &mut impl std::fmt::Write, + path: &str, + bytes: &[u8], +) -> std::fmt::Result { + output.write_str("{\"path\":")?; + write_json_string(output, path)?; + output.write_str(",\"sha256\":")?; + write_raw_digest_json(output, bytes)?; + output.write_str(",\"bytes\":")?; + write_usize_decimal(output, bytes.len())?; + output.write_char('}') +} + +fn write_manifest( + output: &mut impl std::fmt::Write, + prepared: &PreparedNativeRustInterop, + files: &[(&str, &[u8])], + clang_path: &str, + clang_version: &str, + rustc: &RustcVersion, + target: &str, +) -> std::fmt::Result { + output.write_str("{\"schema\":")?; + write_json_string(output, BUNDLE_SCHEMA)?; + output.write_str(",\"descriptor\":{\"schema\":")?; + write_json_string(output, DESCRIPTOR_SCHEMA)?; + output.write_str(",\"digest\":")?; + write_json_string(output, &prepared.descriptor_digest)?; + output.write_str(",\"bytes\":")?; + write_usize_decimal(output, prepared.descriptor.len())?; + output.write_str("},\"files\":[")?; + for (index, (path, bytes)) in files.iter().enumerate() { + if index != 0 { + output.write_char(',')?; + } + write_manifest_file_row(output, path, bytes)?; + } + output.write_str("],\"toolchain\":{\"rustc_release\":")?; + write_json_string(output, rustc.release())?; + output.write_str(",\"rustc_commit_hash\":")?; + write_json_string(output, rustc.commit_hash())?; + output.write_str(",\"host\":")?; + write_json_string(output, rustc.host())?; + output.write_str(",\"llvm_version\":")?; + write_json_string(output, rustc.llvm_version())?; + output.write_str(",\"clang_path\":")?; + write_json_string(output, clang_path)?; + output.write_str(",\"clang_version\":")?; + write_json_string(output, clang_version)?; + output.write_str(",\"target\":")?; + write_json_string(output, target)?; + output.write_str("},\"limits\":")?; + write_limits_json(output)?; + output.write_str(",\"nonclaims\":[")?; + for (index, nonclaim) in NONCLAIMS.iter().enumerate() { + if index != 0 { + output.write_char(',')?; + } + write_json_string(output, nonclaim)?; + } + output.write_str("]}\n") +} + +#[cfg(test)] +fn render_manifest( + prepared: &PreparedNativeRustInterop, + files: &[(&str, &[u8])], + clang_path: &str, + clang_version: &str, + rustc: &RustcVersion, + target: &str, +) -> String { + let mut count = CountingSink { + bytes: 0, + maximum: MAX_MANIFEST_BYTES, + overflowed: false, + }; + write_manifest( + &mut count, + prepared, + files, + clang_path, + clang_version, + rustc, + target, + ) + .expect("manifest count cannot fail"); + assert!(!count.overflowed); + let mut output = String::with_capacity(count.bytes); + write_manifest( + &mut output, + prepared, + files, + clang_path, + clang_version, + rustc, + target, + ) + .expect("String writing cannot fail"); + assert_eq!(output.capacity(), count.bytes); + output +} + +fn replay_manifest_bytes_exact( + source: &str, + prepared: &PreparedNativeRustInterop, + files: &[(&str, &[u8])], + clang_path: &str, + clang_version: &str, + rustc: &RustcVersion, + target: &str, +) -> bool { + let mut exact = ExactReplay::new(source); + exact.text("{\"schema\":"); + exact.json(BUNDLE_SCHEMA); + exact.text(",\"descriptor\":{\"schema\":"); + exact.json(DESCRIPTOR_SCHEMA); + exact.text(",\"digest\":"); + exact.json(&prepared.descriptor_digest); + exact.text(",\"bytes\":"); + exact.usize_noalloc(prepared.descriptor.len()); + exact.text("},\"files\":["); + for (index, (path, bytes)) in files.iter().enumerate() { + if index != 0 { + exact.text(","); + } + exact.text("{\"path\":"); + exact.json(path); + exact.text(",\"sha256\":"); + exact.raw_digest_json_noalloc(bytes); + exact.text(",\"bytes\":"); + exact.usize_noalloc(bytes.len()); + exact.text("}"); + } + exact.text("],\"toolchain\":{\"rustc_release\":"); + exact.json(rustc.release()); + exact.text(",\"rustc_commit_hash\":"); + exact.json(rustc.commit_hash()); + exact.text(",\"host\":"); + exact.json(rustc.host()); + exact.text(",\"llvm_version\":"); + exact.json(rustc.llvm_version()); + exact.text(",\"clang_path\":"); + exact.json(clang_path); + exact.text(",\"clang_version\":"); + exact.json(clang_version); + exact.text(",\"target\":"); + exact.json(target); + exact.text("},\"limits\":"); + replay_limits_exact(&mut exact); + exact.text(",\"nonclaims\":["); + for (index, nonclaim) in NONCLAIMS.iter().enumerate() { + if index != 0 { + exact.text(","); + } + exact.json(nonclaim); + } + exact.text("]}\n"); + exact.finish() +} + +/// Independently consumes the fixed manifest JSON grammar without a DOM or +/// decoded-string allocation. The exact replay above binds canonical bytes; +/// this cursor separately validates decoded values and the complete member, +/// type, cardinality, depth, and trailing-byte shape. +struct ManifestCursor<'a> { + source: &'a str, + offset: usize, + work: usize, + maximum_work: usize, +} + +impl<'a> ManifestCursor<'a> { + fn new(source: &'a str) -> Result { + Ok(Self { + source, + offset: 0, + work: 0, + maximum_work: source + .len() + .checked_mul(2) + .ok_or(PhaseBLocalError::Replay)?, + }) + } + + fn bytes(&self) -> &'a [u8] { + self.source.as_bytes() + } + + fn advance(&mut self, bytes: usize) -> Result<(), PhaseBLocalError> { + self.offset = self + .offset + .checked_add(bytes) + .ok_or(PhaseBLocalError::Replay)?; + self.work = self + .work + .checked_add(bytes) + .ok_or(PhaseBLocalError::Replay)?; + if self.offset > self.source.len() || self.work > self.maximum_work { + return Err(PhaseBLocalError::Replay); + } + Ok(()) + } + + fn expect(&mut self, expected: &[u8]) -> Result<(), PhaseBLocalError> { + let end = self + .offset + .checked_add(expected.len()) + .ok_or(PhaseBLocalError::Replay)?; + if self.bytes().get(self.offset..end) != Some(expected) { + return Err(PhaseBLocalError::Replay); + } + self.advance(expected.len()) + } + + fn hex_quad(&mut self) -> Result { + let mut value = 0_u16; + for _ in 0..4 { + let byte = *self + .bytes() + .get(self.offset) + .ok_or(PhaseBLocalError::Replay)?; + self.advance(1)?; + let digit = match byte { + b'0'..=b'9' => u16::from(byte - b'0'), + b'a'..=b'f' => u16::from(byte - b'a') + 10, + b'A'..=b'F' => u16::from(byte - b'A') + 10, + _ => return Err(PhaseBLocalError::Replay), + }; + value = value + .checked_mul(16) + .and_then(|value| value.checked_add(digit)) + .ok_or(PhaseBLocalError::Replay)?; + } + Ok(value) + } + + fn json_character(&mut self) -> Result { + let byte = *self + .bytes() + .get(self.offset) + .ok_or(PhaseBLocalError::Replay)?; + if byte == b'"' || byte < 0x20 { + return Err(PhaseBLocalError::Replay); + } + if byte != b'\\' { + let character = self + .source + .get(self.offset..) + .ok_or(PhaseBLocalError::Replay)? + .chars() + .next() + .ok_or(PhaseBLocalError::Replay)?; + self.advance(character.len_utf8())?; + return Ok(character); + } + self.advance(1)?; + let escape = *self + .bytes() + .get(self.offset) + .ok_or(PhaseBLocalError::Replay)?; + self.advance(1)?; + match escape { + b'"' => Ok('"'), + b'\\' => Ok('\\'), + b'/' => Ok('/'), + b'b' => Ok('\u{08}'), + b'f' => Ok('\u{0c}'), + b'n' => Ok('\n'), + b'r' => Ok('\r'), + b't' => Ok('\t'), + b'u' => { + let first = self.hex_quad()?; + let scalar = if (0xd800..=0xdbff).contains(&first) { + self.expect(b"\\u")?; + let second = self.hex_quad()?; + if !(0xdc00..=0xdfff).contains(&second) { + return Err(PhaseBLocalError::Replay); + } + 0x1_0000 + ((u32::from(first) - 0xd800) << 10) + (u32::from(second) - 0xdc00) + } else if (0xdc00..=0xdfff).contains(&first) { + return Err(PhaseBLocalError::Replay); + } else { + u32::from(first) + }; + char::from_u32(scalar).ok_or(PhaseBLocalError::Replay) + } + _ => Err(PhaseBLocalError::Replay), + } + } + + fn string_eq(&mut self, expected: &str) -> Result<(), PhaseBLocalError> { + self.expect(b"\"")?; + for expected in expected.chars() { + if self.json_character()? != expected { + return Err(PhaseBLocalError::Replay); + } + } + self.expect(b"\"") + } + + fn usize_eq(&mut self, expected: usize) -> Result<(), PhaseBLocalError> { + let first = *self + .bytes() + .get(self.offset) + .ok_or(PhaseBLocalError::Replay)?; + if !first.is_ascii_digit() { + return Err(PhaseBLocalError::Replay); + } + let mut value = 0_usize; + let mut digits = 0_usize; + while let Some(byte @ b'0'..=b'9') = self.bytes().get(self.offset).copied() { + if digits == 1 && first == b'0' { + return Err(PhaseBLocalError::Replay); + } + value = value + .checked_mul(10) + .and_then(|value| value.checked_add(usize::from(byte - b'0'))) + .ok_or(PhaseBLocalError::Replay)?; + self.advance(1)?; + digits += 1; + } + if value == expected { + Ok(()) + } else { + Err(PhaseBLocalError::Replay) + } + } + + fn raw_digest_eq(&mut self, bytes: &[u8]) -> Result<(), PhaseBLocalError> { + const HEX: &[u8; 16] = b"0123456789abcdef"; + self.expect(b"\"sha256:")?; + for byte in Sha256::digest(bytes) { + self.expect(&[HEX[usize::from(byte >> 4)], HEX[usize::from(byte & 0x0f)]])?; + } + self.expect(b"\"") + } + + fn finish(self) -> Result { + if self.offset == self.source.len() && self.work <= self.maximum_work { + Ok(self.work) + } else { + Err(PhaseBLocalError::Replay) + } + } +} + +fn replay_manifest_semantic( + source: &str, + prepared: &PreparedNativeRustInterop, + files: &[(&str, &[u8]); 6], + clang_path: &str, + clang_version: &str, + rustc: &RustcVersion, + target: &str, +) -> Result { + let mut cursor = ManifestCursor::new(source)?; + cursor.expect(b"{\"schema\":")?; + cursor.string_eq(BUNDLE_SCHEMA)?; + cursor.expect(b",\"descriptor\":{\"schema\":")?; + cursor.string_eq(DESCRIPTOR_SCHEMA)?; + cursor.expect(b",\"digest\":")?; + cursor.string_eq(&prepared.descriptor_digest)?; + cursor.expect(b",\"bytes\":")?; + cursor.usize_eq(prepared.descriptor.len())?; + cursor.expect(b"},\"files\":[")?; + for (index, (path, bytes)) in files.iter().enumerate() { + if index != 0 { + cursor.expect(b",")?; + } + cursor.expect(b"{\"path\":")?; + cursor.string_eq(path)?; + cursor.expect(b",\"sha256\":")?; + cursor.raw_digest_eq(bytes)?; + cursor.expect(b",\"bytes\":")?; + cursor.usize_eq(bytes.len())?; + cursor.expect(b"}")?; + } + cursor.expect(b"],\"toolchain\":{\"rustc_release\":")?; + cursor.string_eq(rustc.release())?; + cursor.expect(b",\"rustc_commit_hash\":")?; + cursor.string_eq(rustc.commit_hash())?; + cursor.expect(b",\"host\":")?; + cursor.string_eq(rustc.host())?; + cursor.expect(b",\"llvm_version\":")?; + cursor.string_eq(rustc.llvm_version())?; + cursor.expect(b",\"clang_path\":")?; + cursor.string_eq(clang_path)?; + cursor.expect(b",\"clang_version\":")?; + cursor.string_eq(clang_version)?; + cursor.expect(b",\"target\":")?; + cursor.string_eq(target)?; + cursor.expect(b"},\"limits\":{")?; + for (index, (name, value)) in LIMIT_ROWS.iter().enumerate() { + if index != 0 { + cursor.expect(b",")?; + } + cursor.string_eq(name)?; + cursor.expect(b":")?; + cursor.usize_eq(*value)?; + } + cursor.expect(b"},\"nonclaims\":[")?; + for (index, nonclaim) in NONCLAIMS.iter().enumerate() { + if index != 0 { + cursor.expect(b",")?; + } + cursor.string_eq(nonclaim)?; + } + cursor.expect(b"]}\n")?; + cursor.finish() +} + +fn replay_manifest( + source: &str, + prepared: &PreparedNativeRustInterop, + files: &[(&str, &[u8]); 6], + tools: &ToolchainFacts, +) -> Result<(), PhaseBLocalError> { + if !replay_manifest_bytes_exact( + source, + prepared, + files, + platform::tool_path(&tools.clang), + &tools.clang_version, + &tools.rustc_version, + &prepared.target.triple, + ) { + return Err(PhaseBLocalError::Replay); + } + replay_manifest_semantic( + source, + prepared, + files, + platform::tool_path(&tools.clang), + &tools.clang_version, + &tools.rustc_version, + &prepared.target.triple, + ) + .map(|_| ()) +} + +fn render_rust_harness( + output: &mut impl std::fmt::Write, + prepared: &PreparedNativeRustInterop, +) -> std::fmt::Result { + output.write_str( + "#[path=\"semaprax_native_rust_interop.rs\"]mod semaprax_native_rust_interop;\nuse semaprax_native_rust_interop::*;\nstruct Host;\nimpl NativeRustImports for Host{\n", + )?; + for import in &prepared.imports { + write!( + output, + "fn {}(&mut self{}", + import.rust_method, + if import.parameters.is_empty() { + "" + } else { + ", " + }, + )?; + for (index, parameter) in import.parameters.iter().enumerate() { + if index != 0 { + output.write_str(", ")?; + } + write!(output, "_arg_{index}: {}", rust_type(parameter.ty))?; + } + write!( + output, + ")->NativeRustImportResult<{}>{{{} }}\n", + rust_type(import.result), + match import.result { + ScalarType::Unit => "NativeRustImportResult::Success(())", + ScalarType::Bool => "NativeRustImportResult::Success(false)", + ScalarType::I64 => "NativeRustImportResult::Success(0)", + } + )?; + } + output.write_str("}\n#[no_mangle]pub extern \"C\" fn spxnr1_rust_harness_run()->i32{let code=core::num::NonZeroU32::new(1).unwrap();let _=NativeRustImportResult::<()>::Status{code,class:NativeRustStatusClass::Import,retryable:false};let _=NativeRustImportResult::<()>::HostFailure;let probe=NativeRustCallError::Semantic{domain_id:\"semaprax.native-rust-semantics.v1\",code,class:NativeRustStatusClass::Semantic,retryable:false};if let NativeRustCallError::Semantic{domain_id,code,class,retryable}=probe{let _=(domain_id,code,class,retryable);}let caps=match NativeRustCapabilities::new(&[")?; + let mut previous = None; + let mut first = true; + loop { + let mut selected = None; + for capability in prepared + .imports + .iter() + .flat_map(|import| &import.capabilities) + .map(String::as_str) + { + if previous.is_none_or(|prior| capability > prior) + && selected.is_none_or(|current| capability < current) + { + selected = Some(capability); + } + } + let Some(capability) = selected else { + break; + }; + if !first { + output.write_char(',')?; + } + write_json_string(output, capability)?; + previous = Some(capability); + first = false; + } + output.write_str( + "]){Ok(value)=>value,Err(_)=>return 2};let mut bridge=NativeRustBridge::new(Host,caps);", + )?; + for export in &prepared.exports { + write!(output, "let _closed_result=bridge.{}(", export.rust_method)?; + for (index, parameter) in export.parameters.iter().enumerate() { + if index != 0 { + output.write_char(',')?; + } + output.write_str(match parameter.ty { + ScalarType::I64 => "0", + ScalarType::Bool => "false", + ScalarType::Unit => "()", + })?; + } + output.write_str(");")?; + } + output.write_str("0}\n") +} + +#[derive(Default)] +struct HarnessCount { + length: usize, +} + +impl std::fmt::Write for HarnessCount { + fn write_str(&mut self, value: &str) -> std::fmt::Result { + self.length = self + .length + .checked_add(value.len()) + .ok_or(std::fmt::Error)?; + Ok(()) + } +} + +fn prepare_rust_harness( + prepared: &PreparedNativeRustInterop, +) -> Result<(String, TemporaryBudget), PhaseBLocalError> { + let mut count = HarnessCount::default(); + render_rust_harness(&mut count, prepared).map_err(|_| PhaseBLocalError::BuilderBudget)?; + let budget = reserve_phase_b(count.length)?; + let mut output = String::with_capacity(count.length); + if output.capacity() != count.length { + return Err(PhaseBLocalError::BuilderBudget); + } + render_rust_harness(&mut output, prepared).map_err(|_| PhaseBLocalError::BuilderBudget)?; + if output.len() != count.length || output.capacity() != count.length { + return Err(PhaseBLocalError::BuilderBudget); + } + Ok((output, budget)) +} + +const PHASE_B_INVOCATION_ARGUMENT_CAPACITY: usize = 16_384; + +struct PreparedBuildInvocations { + c_o0: (platform::PreparedCCompileInvocation, TemporaryBudget), + c_o2: (platform::PreparedCCompileInvocation, TemporaryBudget), + rust: (platform::PreparedRustCompileInvocation, TemporaryBudget), + c_main: (platform::PreparedCCompileInvocation, TemporaryBudget), + link_o0: (platform::PreparedLinkInvocation, TemporaryBudget), + run_o0: (platform::PreparedRunInvocation, TemporaryBudget), + link_o2: (platform::PreparedLinkInvocation, TemporaryBudget), + run_o2: (platform::PreparedRunInvocation, TemporaryBudget), +} + +fn prepare_invocation( + maximum: usize, + prepare: impl FnOnce() -> Result, + capacity: impl FnOnce(&T) -> usize, +) -> Result<(T, TemporaryBudget), PhaseBLocalError> { + let budget = reserve_phase_b(maximum)?; + let invocation = prepare().map_err(|error| match error { + platform::Error::OutputLimit => PhaseBLocalError::BuilderBudget, + platform::Error::Invalid + | platform::Error::Unsupported + | platform::Error::Exists + | platform::Error::Changed + | platform::Error::Spawn + | platform::Error::Exit => PhaseBLocalError::Unsupported, + })?; + if capacity(&invocation) > budget.maximum() { + return Err(PhaseBLocalError::BuilderBudget); + } + #[cfg(test)] + PHASE_B_BUILD_INVOCATION_PLANS.with(|count| count.set(count.get().saturating_add(1))); + Ok((invocation, budget)) +} + +fn consume_invocation(plan: (T, TemporaryBudget)) -> (T, TemporaryBudget) { + #[cfg(test)] + PHASE_B_BUILD_INVOCATION_CONSUMPTIONS.with(|count| count.set(count.get().saturating_add(1))); + plan +} + +fn prepare_build_invocations( + prepared: &PreparedNativeRustInterop, + sanitizers: bool, +) -> Result { + let c_maximum = MAX_GENERATED_C_BYTES + .checked_add(PHASE_B_INVOCATION_ARGUMENT_CAPACITY) + .ok_or(PhaseBLocalError::BuilderBudget)?; + let command_maximum = PHASE_B_INVOCATION_ARGUMENT_CAPACITY; + let staticlib_name = if cfg!(windows) { + "semaprax_bridge.lib" + } else { + "libsemaprax_bridge.a" + }; + let link_o0_name = if cfg!(windows) { + "__semaprax_native_rust_link_O0.exe" + } else { + "__semaprax_native_rust_link_O0" + }; + let link_o2_name = if cfg!(windows) { + "__semaprax_native_rust_link_O2.exe" + } else { + "__semaprax_native_rust_link_O2" + }; + Ok(PreparedBuildInvocations { + c_o0: prepare_invocation( + c_maximum, + || { + platform::prepare_c_compile_invocation( + &prepared.target.triple, + "module.c".as_ref(), + 0, + sanitizers, + MAX_GENERATED_C_BYTES, + ) + }, + platform::prepared_c_compile_owned_capacity, + )?, + c_o2: prepare_invocation( + c_maximum, + || { + platform::prepare_c_compile_invocation( + &prepared.target.triple, + "module.c".as_ref(), + 2, + sanitizers, + MAX_GENERATED_C_BYTES, + ) + }, + platform::prepared_c_compile_owned_capacity, + )?, + rust: prepare_invocation( + command_maximum, + || { + platform::prepare_rust_compile_invocation( + &prepared.target.triple, + "__semaprax_native_rust_link.rs".as_ref(), + staticlib_name.as_ref(), + ) + }, + platform::prepared_rust_compile_owned_capacity, + )?, + c_main: prepare_invocation( + c_maximum, + || { + platform::prepare_c_compile_invocation( + &prepared.target.triple, + "__semaprax_native_rust_main.c".as_ref(), + 2, + sanitizers, + MAX_GENERATED_C_BYTES, + ) + }, + platform::prepared_c_compile_owned_capacity, + )?, + link_o0: prepare_invocation( + command_maximum, + || { + platform::prepare_link_invocation( + &prepared.target.triple, + "__semaprax_native_rust_main.o".as_ref(), + "module_O0.o".as_ref(), + staticlib_name.as_ref(), + link_o0_name.as_ref(), + sanitizers, + ) + }, + platform::prepared_link_owned_capacity, + )?, + run_o0: prepare_invocation( + command_maximum, + platform::prepare_run_invocation, + platform::prepared_run_owned_capacity, + )?, + link_o2: prepare_invocation( + command_maximum, + || { + platform::prepare_link_invocation( + &prepared.target.triple, + "__semaprax_native_rust_main.o".as_ref(), + "module_O2.o".as_ref(), + staticlib_name.as_ref(), + link_o2_name.as_ref(), + sanitizers, + ) + }, + platform::prepared_link_owned_capacity, + )?, + run_o2: prepare_invocation( + command_maximum, + platform::prepare_run_invocation, + platform::prepared_run_owned_capacity, + )?, + }) +} + +/// Private phase-B static bundle construction. The output directory is +/// create-new and never merged with existing content. +const PHASE_B_PUBLICATION_MESSAGE: &str = "Native Rust Interop output publication failed"; +const PHASE_B_COMPILE_MESSAGE: &str = "Native Rust Interop Clang compilation failed"; +const PHASE_B_LINK_MESSAGE: &str = "Native Rust Interop Rust compilation or link failed"; +const PHASE_B_UNSUPPORTED_MESSAGE: &str = "Native Rust Interop target or toolchain is unsupported"; +const PHASE_B_REPLAY_MESSAGE: &str = "Native Rust Interop generated artifact replay failed"; +const PHASE_B_BUILDER_BUDGET_MESSAGE: &str = + "Native Rust Interop max_builder_bytes exceeds 33554432"; +const PHASE_B_MANIFEST_BUDGET_MESSAGE: &str = + "Native Rust Interop max_manifest_bytes exceeds 1048576"; +const PHASE_B_TOOL_VERSION_CAPACITY: usize = 65_536; +const PHASE_B_TOOL_PATH_CAPACITY: usize = 32_768; +const PHASE_B_VERSION_COMMAND_CAPACITY: usize = 256; +const PHASE_B_TOOL_RESOLVER_CAPACITY: usize = PHASE_B_TOOL_PATH_CAPACITY * 7 + 256; +const PHASE_B_PROCESS_INVOCATIONS: usize = 12; +#[cfg(windows)] +const PHASE_B_PROCESS_ARENA_MAX_CAPACITY: usize = 1_245_188; +#[cfg(unix)] +const PHASE_B_PROCESS_ARENA_MAX_CAPACITY: usize = 0; + +fn prepare_process_arena_authorized( + include: Option<&OsStr>, + libraries: Option<&OsStr>, +) -> Result { + if cfg!(windows) && (include.is_none() || libraries.is_none()) { + return Err(PhaseBLocalError::Unsupported); + } + let plan = platform::prepare_process_arena_plan_with_environment( + PHASE_B_PROCESS_INVOCATIONS, + include, + libraries, + ) + .map_err(|error| match error { + platform::Error::OutputLimit => PhaseBLocalError::BuilderBudget, + platform::Error::Invalid + | platform::Error::Unsupported + | platform::Error::Exists + | platform::Error::Changed + | platform::Error::Spawn + | platform::Error::Exit => PhaseBLocalError::Unsupported, + })?; + let required = platform::prepared_process_arena_plan_capacity(&plan); + if required > PHASE_B_PROCESS_ARENA_MAX_CAPACITY { + return Err(PhaseBLocalError::BuilderBudget); + } + let budget = reserve_phase_b(required)?; + let arena = platform::materialize_process_arena_with_environment(plan, include, libraries) + .map_err(|error| match error { + platform::Error::OutputLimit => PhaseBLocalError::BuilderBudget, + platform::Error::Invalid + | platform::Error::Unsupported + | platform::Error::Exists + | platform::Error::Changed + | platform::Error::Spawn + | platform::Error::Exit => PhaseBLocalError::Unsupported, + })?; + if platform::prepared_process_arena_owned_capacity(&arena) != required { + return Err(PhaseBLocalError::BuilderBudget); + } + Ok(AuthorizedProcessArena::new(arena, budget)) +} + +#[cfg(test)] +fn note_phase_b_process_arena_drop(value: u8) { + PHASE_B_PROCESS_ARENA_DROP_ORDER.with(|order| { + PHASE_B_PROCESS_ARENA_DROP_ORDER_LENGTH.with(|length| { + let index = length.get(); + if index < 2 { + let mut values = order.get(); + values[index] = value; + order.set(values); + length.set(index + 1); + } + }); + }); +} + +#[cfg(test)] +fn reset_phase_b_process_arena_drop_observer() { + PHASE_B_PROCESS_ARENA_DROPS.with(|drops| drops.set(0)); + PHASE_B_PROCESS_ARENA_BUDGET_DROPS.with(|drops| drops.set(0)); + PHASE_B_PROCESS_ARENA_DROP_ORDER.with(|order| order.set([0; 2])); + PHASE_B_PROCESS_ARENA_DROP_ORDER_LENGTH.with(|length| length.set(0)); +} + +#[cfg(test)] +fn reset_phase_b_error_materialization_observer() { + PHASE_B_EFFECT_STARTED.with(|started| started.set(false)); + PHASE_B_POST_EFFECT_ERROR_MATERIALIZATIONS.with(|count| count.set(0)); +} + +#[cfg(test)] +fn reset_phase_b_native_stage_arena_observer() { + PHASE_B_NATIVE_STAGE_ARENA_ALLOCATIONS.with(|count| count.set(0)); + PHASE_B_NATIVE_STAGE_ARENA_SETS.with(|count| count.set(0)); + PHASE_B_NATIVE_STAGE_ARENA_CONSUMPTIONS.with(|count| count.set(0)); +} + +#[cfg(test)] +fn reset_phase_b_object_authority_observer() { + assert!(!PHASE_B_OBJECT_AUTHORITY_LIVE.with(std::cell::Cell::get)); + let prior_length = PHASE_B_OBJECT_DROP_ORDER_LENGTH.with(std::cell::Cell::get); + assert!(prior_length == 0 || prior_length == 2); + PHASE_B_OBJECT_AUTHORITY_TRANSFERS.with(|count| count.set(0)); + PHASE_B_OBJECT_AUTHORITY_DROPS.with(|count| count.set(0)); + PHASE_B_OBJECT_AUTHORITY_MANIFEST_OBSERVATIONS.with(|count| count.set(0)); + PHASE_B_OBJECT_AUTHORITY_PUBLISH_OBSERVATIONS.with(|count| count.set(0)); + PHASE_B_OBJECT_BYTES_DROPS.with(|count| count.set(0)); + PHASE_B_OBJECT_DROP_ORDER.with(|order| order.set([0; 2])); + PHASE_B_OBJECT_DROP_ORDER_LENGTH.with(|length| length.set(0)); +} + +#[cfg(test)] +fn assert_phase_b_object_drop_order(expected: usize) { + assert_eq!( + PHASE_B_OBJECT_BYTES_DROPS.with(std::cell::Cell::get), + expected + ); + assert_eq!( + PHASE_B_OBJECT_AUTHORITY_DROPS.with(std::cell::Cell::get), + expected + ); + assert_eq!( + PHASE_B_OBJECT_DROP_ORDER_LENGTH.with(std::cell::Cell::get), + expected.saturating_mul(2), + ); + assert_eq!( + PHASE_B_OBJECT_DROP_ORDER.with(std::cell::Cell::get), + if expected == 0 { [0, 0] } else { [1, 2] }, + ); + assert!(!PHASE_B_OBJECT_AUTHORITY_LIVE.with(std::cell::Cell::get)); +} + +#[cfg(test)] +fn reset_phase_b_manifest_authority_observer() { + assert!(!PHASE_B_MANIFEST_AUTHORITY_LIVE.with(std::cell::Cell::get)); + let prior_length = PHASE_B_MANIFEST_DROP_ORDER_LENGTH.with(std::cell::Cell::get); + assert!(prior_length == 0 || prior_length == 2); + PHASE_B_MANIFEST_PLAN_CAPACITY.with(|capacity| capacity.set(MAX_MANIFEST_BYTES)); + PHASE_B_MANIFEST_ARENA_ALLOCATIONS.with(|count| count.set(0)); + PHASE_B_MANIFEST_ARENA_GROWTHS.with(|count| count.set(0)); + PHASE_B_MANIFEST_AUTHORITY_TRANSFERS.with(|count| count.set(0)); + PHASE_B_MANIFEST_AUTHORITY_DROPS.with(|count| count.set(0)); + PHASE_B_MANIFEST_BYTES_DROPS.with(|count| count.set(0)); + PHASE_B_MANIFEST_DROP_ORDER.with(|order| order.set([0; 2])); + PHASE_B_MANIFEST_DROP_ORDER_LENGTH.with(|length| length.set(0)); +} + +#[cfg(test)] +fn assert_phase_b_manifest_drop_order(expected: usize) { + assert_eq!( + PHASE_B_MANIFEST_BYTES_DROPS.with(std::cell::Cell::get), + expected + ); + assert_eq!( + PHASE_B_MANIFEST_AUTHORITY_DROPS.with(std::cell::Cell::get), + expected + ); + assert_eq!( + PHASE_B_MANIFEST_DROP_ORDER_LENGTH.with(std::cell::Cell::get), + expected.saturating_mul(2) + ); + assert_eq!( + PHASE_B_MANIFEST_DROP_ORDER.with(std::cell::Cell::get), + if expected == 0 { [0, 0] } else { [1, 2] } + ); + assert!(!PHASE_B_MANIFEST_AUTHORITY_LIVE.with(std::cell::Cell::get)); +} + +#[cfg(not(test))] +fn reset_phase_b_error_materialization_observer() {} + +#[cfg(test)] +fn mark_phase_b_effect_started() { + PHASE_B_EFFECT_STARTED.with(|started| started.set(true)); +} + +#[cfg(not(test))] +fn mark_phase_b_effect_started() {} + +#[cfg(test)] +fn observe_phase_b_error_materialization() { + if PHASE_B_EFFECT_STARTED.with(std::cell::Cell::get) { + PHASE_B_POST_EFFECT_ERROR_MATERIALIZATIONS.with(|count| { + count.set(count.get().saturating_add(1)); + }); + } +} + +#[cfg(not(test))] +fn observe_phase_b_error_materialization() {} + +#[allow( + clippy::vec_init_then_push, + reason = "the one-element public diagnostic carrier requires an observed exact capacity" +)] +fn diagnostic_vector(error: Diagnostic) -> Vec { + observe_phase_b_error_materialization(); + let mut errors = Vec::with_capacity(1); + errors.push(error); + errors +} + +struct BundleBuildSuccess { + facts: NativeRustInteropBundleFacts, + overflow: Vec, +} + +enum BundleBuildError { + Diagnostic(Diagnostic), + Prepared { + selected: Vec, + overflow: Option>, + }, +} + +impl From for BundleBuildError { + fn from(error: Diagnostic) -> Self { + Self::Diagnostic(error) + } +} + +impl BundleBuildError { + fn into_diagnostics(self, overflowed: bool) -> Vec { + match self { + Self::Diagnostic(error) => { + if overflowed { + diagnostic_vector(b109("max_builder_bytes", MAX_BUILDER_BYTES)) + } else { + diagnostic_vector(error) + } + } + Self::Prepared { selected, overflow } => { + if overflowed { + overflow.unwrap_or(selected) + } else { + selected + } + } + } + } +} + +struct StickyDiagnosticCarrier { + errors: Option>, +} + +impl StickyDiagnosticCarrier { + #[allow( + clippy::vec_init_then_push, + reason = "the pre-effect sticky diagnostic carrier requires an observed exact capacity" + )] + fn prepare(code: &'static str, message: &'static str) -> Result { + let maximum = message + .len() + .checked_add(std::mem::size_of::()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let authority = reserve_temporary_exact(maximum)?; + observe_phase_b_error_materialization(); + let diagnostic = Diagnostic::io(code, message); + let mut errors = Vec::with_capacity(1); + errors.push(diagnostic); + let retained = errors[0] + .message + .capacity() + .checked_add( + errors + .capacity() + .checked_mul(std::mem::size_of::()) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?, + ) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + if errors.capacity() != 1 || retained > maximum { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + authority.retain(retained)?; + Ok(Self { + errors: Some(errors), + }) + } + + fn take(&mut self) -> Vec { + self.errors + .take() + .expect("sticky phase-B diagnostic is consumed once") + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum PhaseBLocalError { + BuilderBudget, + ManifestBudget, + Unsupported, + Replay, + Compile, + Link, + Publication, +} + +struct PreparedManifestPlan { + file_names: [&'static str; 6], + manifest: AuthorizedManifest, +} + +impl PreparedManifestPlan { + fn prepare(object_name: &'static str) -> Result { + let file_names = canonical_manifest_file_names(); + if object_name != file_names[2] { + return Err(PhaseBLocalError::BuilderBudget); + } + #[cfg(test)] + let capacity = PHASE_B_MANIFEST_PLAN_CAPACITY.with(std::cell::Cell::get); + #[cfg(not(test))] + let capacity = MAX_MANIFEST_BYTES; + let authority = reserve_phase_b(capacity)?; + let arena = String::with_capacity(capacity); + #[cfg(test)] + PHASE_B_MANIFEST_ARENA_ALLOCATIONS.with(|count| count.set(count.get().saturating_add(1))); + if arena.capacity() != MAX_MANIFEST_BYTES { + return Err(PhaseBLocalError::ManifestBudget); + } + Ok(Self { + file_names, + manifest: AuthorizedManifest::new(arena, authority)?, + }) + } + + #[allow(clippy::too_many_arguments, reason = "manifest inputs remain explicit")] + fn render( + mut self, + prepared: &PreparedNativeRustInterop, + files: &[(&str, &[u8]); 6], + clang_path: &str, + clang_version: &str, + rustc: &RustcVersion, + target: &str, + ) -> Result { + if files + .iter() + .zip(self.file_names) + .any(|((actual, _), expected)| *actual != expected) + || self.manifest.manifest.bytes.capacity() != MAX_MANIFEST_BYTES + { + return Err(PhaseBLocalError::BuilderBudget); + } + self.manifest.check()?; + let mut count = CountingSink { + bytes: 0, + maximum: MAX_MANIFEST_BYTES, + overflowed: false, + }; + write_manifest( + &mut count, + prepared, + files, + clang_path, + clang_version, + rustc, + target, + ) + .map_err(|_| PhaseBLocalError::ManifestBudget)?; + #[cfg(test)] + if PHASE_B_OVERSIZE_MANIFEST_INJECTION.with(std::cell::Cell::get) { + count.overflowed = true; + } + if count.overflowed { + return Err(PhaseBLocalError::ManifestBudget); + } + write_manifest( + &mut self.manifest.manifest.bytes, + prepared, + files, + clang_path, + clang_version, + rustc, + target, + ) + .map_err(|_| PhaseBLocalError::ManifestBudget)?; + #[cfg(test)] + if self.manifest.manifest.bytes.capacity() != MAX_MANIFEST_BYTES { + PHASE_B_MANIFEST_ARENA_GROWTHS.with(|count| count.set(count.get().saturating_add(1))); + } + if self.manifest.manifest.bytes.len() != count.bytes + || self.manifest.manifest.bytes.capacity() != MAX_MANIFEST_BYTES + { + return Err(PhaseBLocalError::ManifestBudget); + } + self.manifest.check()?; + Ok(self.manifest) + } +} + +impl PhaseBLocalError { + const fn index(self) -> usize { + match self { + Self::BuilderBudget => 0, + Self::ManifestBudget => 1, + Self::Unsupported => 2, + Self::Replay => 3, + Self::Compile => 4, + Self::Link => 5, + Self::Publication => 6, + } + } + + const fn diagnostic(self) -> (&'static str, &'static str) { + match self { + Self::BuilderBudget => ("SPX-B109", PHASE_B_BUILDER_BUDGET_MESSAGE), + Self::ManifestBudget => ("SPX-B109", PHASE_B_MANIFEST_BUDGET_MESSAGE), + Self::Unsupported => ("SPX-B110", PHASE_B_UNSUPPORTED_MESSAGE), + Self::Replay => ("SPX-B111", PHASE_B_REPLAY_MESSAGE), + Self::Compile => ("SPX-I230", PHASE_B_COMPILE_MESSAGE), + Self::Link => ("SPX-I231", PHASE_B_LINK_MESSAGE), + Self::Publication => ("SPX-I232", PHASE_B_PUBLICATION_MESSAGE), + } + } +} + +fn debit_phase_b(bytes: usize) -> Result<(), PhaseBLocalError> { + if crate::bounded_output::reserve_active(bytes) { + Ok(()) + } else { + Err(PhaseBLocalError::BuilderBudget) + } +} + +fn reserve_phase_b(maximum: usize) -> Result { + let remaining = crate::bounded_output::remaining_active().unwrap_or(MAX_BUILDER_BYTES); + if maximum > remaining { + return Err(PhaseBLocalError::BuilderBudget); + } + debit_phase_b(maximum)?; + Ok(TemporaryBudget { reserved: maximum }) +} + +fn shrink_phase_b(authority: &mut TemporaryBudget, actual: usize) -> Result<(), PhaseBLocalError> { + if actual > authority.reserved { + return Err(PhaseBLocalError::BuilderBudget); + } + crate::bounded_output::release_active(authority.reserved - actual); + authority.reserved = actual; + Ok(()) +} + +fn retain_phase_b(mut authority: TemporaryBudget, actual: usize) -> Result<(), PhaseBLocalError> { + if actual > authority.reserved { + return Err(PhaseBLocalError::BuilderBudget); + } + crate::bounded_output::release_active(authority.reserved - actual); + authority.reserved = 0; + Ok(()) +} + +struct PhaseBErrorCarriers { + carriers: [StickyDiagnosticCarrier; 7], +} + +fn finish_bounded_bundle( + result: Result, + overflowed: bool, +) -> Result> { + match result { + Ok(success) if overflowed => Err(success.overflow), + Ok(success) => Ok(success.facts), + Err(error) => Err(error.into_diagnostics(overflowed)), + } +} + +impl PhaseBErrorCarriers { + fn prepare() -> Result { + let kinds = [ + PhaseBLocalError::BuilderBudget, + PhaseBLocalError::ManifestBudget, + PhaseBLocalError::Unsupported, + PhaseBLocalError::Replay, + PhaseBLocalError::Compile, + PhaseBLocalError::Link, + PhaseBLocalError::Publication, + ]; + let carriers = kinds.map(|kind| { + let (code, message) = kind.diagnostic(); + StickyDiagnosticCarrier::prepare(code, message) + }); + let [Ok(builder), Ok(manifest), Ok(unsupported), Ok(replay), Ok(compile), Ok(link), Ok(publication)] = + carriers + else { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + }; + let carriers = [ + builder, + manifest, + unsupported, + replay, + compile, + link, + publication, + ]; + #[cfg(test)] + PHASE_B_PREPARED_CARRIER_IDENTITIES.with(|identities| { + identities.set(std::array::from_fn(|index| { + carriers[index].errors.as_ref().expect("prepared")[0] + .message + .as_ptr() as usize + })); + }); + Ok(Self { carriers }) + } + + fn take(&mut self, kind: PhaseBLocalError) -> Vec { + self.carriers[kind.index()].take() + } + + fn error(&mut self, kind: PhaseBLocalError) -> BundleBuildError { + let selected = self.take(kind); + let overflow = if kind == PhaseBLocalError::BuilderBudget { + None + } else { + Some(self.take(PhaseBLocalError::BuilderBudget)) + }; + BundleBuildError::Prepared { selected, overflow } + } +} + +pub(crate) fn build_native_rust_interop_bundle( + program: &Program, + spec_bytes: &[u8], + output: &Path, +) -> Result> { + reset_phase_b_error_materialization_observer(); + let (result, overflowed) = crate::bounded_output::with_limit(MAX_BUILDER_BYTES, || { + let mut hook = |_, _: &Path, _: &Path, _: &Path| {}; + build_native_rust_interop_bundle_bounded(program, spec_bytes, output, &mut hook) + }); + finish_bounded_bundle(result, overflowed) +} + +#[cfg(test)] +fn build_native_rust_interop_bundle_with_test_limit( + program: &Program, + spec_bytes: &[u8], + output: &Path, + limit: usize, +) -> Result> { + assert!(limit <= MAX_BUILDER_BYTES); + reset_phase_b_error_materialization_observer(); + let (result, overflowed) = crate::bounded_output::with_limit(limit, || { + let mut hook = |_, _: &Path, _: &Path, _: &Path| {}; + build_native_rust_interop_bundle_bounded(program, spec_bytes, output, &mut hook) + }); + finish_bounded_bundle(result, overflowed) +} + +#[cfg(test)] +#[allow(clippy::enum_variant_names)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum NativeRustBuildPoint { + BeforeClang, + BeforeRustLink, + BeforeExecutableAuthentication, + BeforeExecute, + BeforeObjectRead, + BeforeManifestPublish, + BeforeBundlePublish, +} + +type PublishDiscardInventory = platform::PreparedDiscardInventory<7>; +type RunDiscardInventory = platform::PreparedDiscardInventory<10>; + +fn native_relative_name_capacity(name: &OsStr) -> Result { + let bytes = name + .to_str() + .filter(|name| name.is_ascii()) + .ok_or_else(platform_publication_error)? + .len(); + if cfg!(windows) { + bytes + .checked_mul(2) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES)) + } else { + bytes + .checked_add(1) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES)) + } +} + +fn prepare_discard_inventory( + names: [&'static OsStr; N], +) -> Result, Diagnostic> { + let retained = names.iter().try_fold(0usize, |bytes, name| { + bytes + .checked_add(native_relative_name_capacity(name)?) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES)) + })?; + let authority = reserve_temporary_exact(retained)?; + let inventory = platform::prepare_discard_inventory_bounded(names, retained) + .map_err(|_| platform_publication_error())?; + if platform::prepared_discard_inventory_owned_capacity(&inventory) != retained { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + authority.retain(retained)?; + Ok(inventory) +} + +fn prepare_publish_discard_inventory() -> Result { + prepare_discard_inventory([ + OsStr::new("descriptor.json"), + OsStr::new("module.c"), + OsStr::new("semaprax_native_rust_interop.h"), + OsStr::new("semaprax_native_rust_interop.rs"), + OsStr::new("semaprax_native_rust_interop_ffi.rs"), + OsStr::new(if cfg!(windows) { + "module.obj" + } else { + "module.o" + }), + OsStr::new("semaprax.native-rust-interop.json"), + ]) +} + +fn prepare_run_discard_inventory() -> Result { + prepare_discard_inventory([ + OsStr::new("module_O0.o"), + OsStr::new("__semaprax_native_rust_link.rs"), + OsStr::new("semaprax_native_rust_interop.rs"), + OsStr::new("semaprax_native_rust_interop_ffi.rs"), + OsStr::new("module_O2.o"), + OsStr::new(if cfg!(windows) { + "semaprax_bridge.lib" + } else { + "libsemaprax_bridge.a" + }), + OsStr::new("__semaprax_native_rust_main.c"), + OsStr::new("__semaprax_native_rust_main.o"), + OsStr::new(if cfg!(windows) { + "__semaprax_native_rust_link_O0.exe" + } else { + "__semaprax_native_rust_link_O0" + }), + OsStr::new(if cfg!(windows) { + "__semaprax_native_rust_link_O2.exe" + } else { + "__semaprax_native_rust_link_O2" + }), + ]) +} + +struct PreparedLinkCopies { + safe_rust: (platform::PreparedLinkOrCopy, TemporaryBudget), + private_ffi: (platform::PreparedLinkOrCopy, TemporaryBudget), + optimized_object: (platform::PreparedLinkOrCopy, TemporaryBudget), +} + +fn prepare_link_copy( + source: &platform::PreparedDiscardInventory, + source_name: &'static str, + destination: &platform::PreparedDiscardInventory, + destination_name: &'static str, +) -> Result<(platform::PreparedLinkOrCopy, TemporaryBudget), PhaseBLocalError> { + let required = platform::link_or_copy_required_capacity( + source, + source_name, + destination, + destination_name, + ) + .map_err(|_| PhaseBLocalError::BuilderBudget)?; + let budget = reserve_phase_b(required)?; + let prepared = + platform::prepare_link_or_copy(source, source_name, destination, destination_name) + .map_err(|_| PhaseBLocalError::BuilderBudget)?; + if platform::prepared_link_or_copy_owned_capacity(&prepared) != required { + return Err(PhaseBLocalError::BuilderBudget); + } + #[cfg(test)] + PHASE_B_LINK_COPY_PLANS.with(|count| count.set(count.get().saturating_add(1))); + Ok((prepared, budget)) +} + +fn prepare_link_copies( + publish: &PublishDiscardInventory, + run: &RunDiscardInventory, + object_name: &'static str, +) -> Result { + let prepared = PreparedLinkCopies { + safe_rust: prepare_link_copy( + publish, + "semaprax_native_rust_interop.rs", + run, + "semaprax_native_rust_interop.rs", + )?, + private_ffi: prepare_link_copy( + publish, + "semaprax_native_rust_interop_ffi.rs", + run, + "semaprax_native_rust_interop_ffi.rs", + )?, + optimized_object: prepare_link_copy(publish, object_name, run, "module_O2.o")?, + }; + #[cfg(all(test, debug_assertions))] + let prepared = { + let mut prepared = prepared; + if PHASE_B_LINK_COPY_FAIL_BEFORE_AUTHENTICATION.with(std::cell::Cell::get) { + platform::inject_link_or_copy_failure_before_authentication(&mut prepared.safe_rust.0); + } + prepared + }; + Ok(prepared) +} + +fn consume_link_copy( + plan: (platform::PreparedLinkOrCopy, TemporaryBudget), + source: &platform::PreparedDiscardInventory, + destination_directory: &HeldStage, + destination: &mut platform::PreparedDiscardInventory, + source_bytes: &[u8], +) -> Result<(), PhaseBLocalError> { + let (prepared, budget) = plan; + #[cfg(test)] + PHASE_B_LINK_COPY_CONSUMPTIONS.with(|count| count.set(count.get().saturating_add(1))); + let result = platform::link_or_copy_new_prepared( + prepared, + source, + destination_directory.authority.held(), + destination, + source_bytes, + ) + .map_err(|_| PhaseBLocalError::Publication); + drop(budget); + result +} + +fn prepare_publish_inventory_exact( + publish: &PublishDiscardInventory, +) -> Result<(platform::PreparedInventoryExact<7>, TemporaryBudget), PhaseBLocalError> { + let required = platform::inventory_exact_required_capacity(publish) + .map_err(|_| PhaseBLocalError::BuilderBudget)?; + let budget = reserve_phase_b(required)?; + let prepared = + platform::prepare_inventory_exact(publish).map_err(|_| PhaseBLocalError::BuilderBudget)?; + if platform::prepared_inventory_exact_owned_capacity(&prepared) != required + || platform::prepared_inventory_exact_remaining(&prepared) != 2 + { + return Err(PhaseBLocalError::BuilderBudget); + } + #[cfg(test)] + PHASE_B_INVENTORY_EXACT_PLANS.with(|count| count.set(count.get().saturating_add(1))); + Ok((prepared, budget)) +} + +fn scan_publish_inventory_exact( + prepared: &mut platform::PreparedInventoryExact<7>, + stage: &HeldStage, + publish: &PublishDiscardInventory, +) -> Result<(), PhaseBLocalError> { + #[cfg(test)] + PHASE_B_INVENTORY_EXACT_SCANS.with(|count| count.set(count.get().saturating_add(1))); + platform::inventory_exact_prepared(prepared, stage.authority.held(), publish) + .map_err(|_| PhaseBLocalError::Publication) +} + +fn prepare_final_publish( + output: &Path, +) -> Result<(platform::PreparedPublishDirectory, TemporaryBudget), PhaseBLocalError> { + let output_name = output.file_name().ok_or(PhaseBLocalError::Publication)?; + let required = platform::publish_directory_required_capacity(output_name) + .map_err(|_| PhaseBLocalError::Publication)?; + let budget = reserve_phase_b(required)?; + let prepared = platform::prepare_publish_directory(output_name) + .map_err(|_| PhaseBLocalError::Publication)?; + if platform::prepared_publish_directory_owned_capacity(&prepared) != required + || platform::prepared_publish_directory_remaining(&prepared) != 1 + { + return Err(PhaseBLocalError::BuilderBudget); + } + #[cfg(test)] + PHASE_B_PUBLISH_PLANS.with(|count| count.set(count.get().saturating_add(1))); + Ok((prepared, budget)) +} + +#[cfg(not(test))] +#[allow(clippy::enum_variant_names)] +#[derive(Clone, Copy)] +enum NativeRustBuildPoint { + BeforeClang, + BeforeRustLink, + BeforeExecutableAuthentication, + BeforeExecute, + BeforeObjectRead, + BeforeManifestPublish, + BeforeBundlePublish, +} + +#[cfg(test)] +fn build_native_rust_interop_bundle_with_hook( + program: &Program, + spec_bytes: &[u8], + output: &Path, + mut hook: impl FnMut(NativeRustBuildPoint, &Path, &Path, &Path), +) -> Result> { + reset_phase_b_error_materialization_observer(); + let (result, overflowed) = crate::bounded_output::with_limit(MAX_BUILDER_BYTES, || { + build_native_rust_interop_bundle_bounded(program, spec_bytes, output, &mut hook) + }); + finish_bounded_bundle(result, overflowed) +} + +fn build_native_rust_interop_bundle_bounded( + program: &Program, + spec_bytes: &[u8], + output: &Path, + hook: &mut dyn FnMut(NativeRustBuildPoint, &Path, &Path, &Path), +) -> Result { + let prepared = prepare_native_rust_interop_bounded(program, spec_bytes)?; + let object_name: &'static str = if cfg!(windows) { + "module.obj" + } else { + "module.o" + }; + let parent = output.parent().ok_or_else(platform_publication_error)?; + let mut pending_facts = PendingBundleFacts::new(output, object_name)?; + let publish_slot = StageSlot::new(parent, &prepared.descriptor_digest, "publish")?; + let run_slot = StageSlot::new(parent, &prepared.descriptor_digest, "run")?; + let mut publish_files = prepare_publish_discard_inventory()?; + let mut run_files = prepare_run_discard_inventory()?; + #[cfg(all(test, debug_assertions))] + run_files.inject_discard_failure_after_delete( + PHASE_B_DISCARD_FAILURE_AFTER_DELETE.with(std::cell::Cell::get), + ); + let parent_capacity = parent.as_os_str().as_encoded_bytes().len(); + let parent_budget = reserve_temporary_exact(parent_capacity)?; + let parent_path = exact_path_copy(parent, parent_capacity)?; + parent_budget.retain(parent_capacity)?; + let mut carriers = PhaseBErrorCarriers::prepare()?; + let toolchain_plan = match prepare_toolchain_plan() { + Ok(plan) => plan, + Err(error) => return Err(carriers.error(error)), + }; + let harness_plan = match prepare_rust_harness(&prepared) { + Ok(plan) => plan, + Err(error) => return Err(carriers.error(error)), + }; + let build_invocations = + match prepare_build_invocations(&prepared, planned_sanitizers(&toolchain_plan)) { + Ok(plan) => plan, + Err(error) => return Err(carriers.error(error)), + }; + let manifest_plan = match PreparedManifestPlan::prepare(object_name) { + Ok(plan) => plan, + Err(error) => return Err(carriers.error(error)), + }; + let link_copies = match prepare_link_copies(&publish_files, &run_files, object_name) { + Ok(plan) => plan, + Err(error) => return Err(carriers.error(error)), + }; + let inventory_exact = match prepare_publish_inventory_exact(&publish_files) { + Ok(plan) => plan, + Err(error) => return Err(carriers.error(error)), + }; + let mut final_publish = match prepare_final_publish(output) { + Ok(plan) => plan, + Err(error) => return Err(carriers.error(error)), + }; + + mark_phase_b_effect_started(); + #[cfg(test)] + PHASE_B_OUTPUT_PROBES.with(|count| count.set(count.get().saturating_add(1))); + if output.exists() { + return Err(carriers.error(PhaseBLocalError::Publication)); + } + let parent_authority = match hold_stage(parent_path) { + Ok(parent) => parent, + Err(error) => return Err(carriers.error(error)), + }; + if parent_authority.recheck_local().is_err() { + return Err(carriers.error(PhaseBLocalError::Publication)); + } + let stage = match create_stage(&parent_authority, publish_slot, &publish_files) { + Ok(stage) => stage, + Err(error) => return Err(carriers.error(error)), + }; + let run_stage = match parent_authority + .recheck_local() + .and_then(|()| create_stage(&parent_authority, run_slot, &run_files)) + { + Ok(run_stage) => run_stage, + Err(error) => { + let _ = discard_run_stage(&parent_authority, &stage, &publish_files); + return Err(carriers.error(error)); + } + }; + let build = (|| { + #[cfg(test)] + if let Some(error) = PHASE_B_LOCAL_FAILURE_INJECTION.with(std::cell::Cell::get) { + return Err(error); + } + let mut tools = + authenticate_toolchain(toolchain_plan, &prepared.target, run_stage.authority.held())?; + build_stage_platform( + &prepared, + &mut tools, + &stage, + &run_stage, + harness_plan, + build_invocations, + link_copies, + inventory_exact, + manifest_plan, + output, + hook, + &mut run_files, + &mut publish_files, + ) + })(); + let cleanup = discard_run_stage(&parent_authority, &run_stage, &run_files); + let mut facts = match (build, cleanup) { + (Err(error), _) => { + let _ = discard_run_stage(&parent_authority, &stage, &publish_files); + return Err(carriers.error(error)); + } + (Ok(_), Err(error)) => { + let _ = discard_run_stage(&parent_authority, &stage, &publish_files); + return Err(carriers.error(error)); + } + (Ok(facts), Ok(())) => facts, + }; + if let Err(error) = facts.observe_object_authority_for_manifest() { + let _ = discard_run_stage(&parent_authority, &stage, &publish_files); + return Err(carriers.error(error)); + } + if let Err(error) = pending_facts.bind_manifest_digest(facts.manifest.as_bytes()) { + let _ = discard_run_stage(&parent_authority, &stage, &publish_files); + return Err(carriers.error(error)); + } + let bundle_facts = pending_facts.finish(); + let publication: Result = (|| { + parent_authority.recheck_local()?; + stage.recheck_local()?; + hook( + NativeRustBuildPoint::BeforeBundlePublish, + &stage.path, + &run_stage.path, + output, + ); + publish_stage_platform( + &parent_authority, + &stage, + output, + &prepared, + &mut facts, + &publish_files, + &mut final_publish.0, + )?; + Ok(bundle_facts) + })(); + if publication.is_err() { + let _ = discard_run_stage(&parent_authority, &stage, &publish_files); + } + match publication { + Ok(facts) => Ok(BundleBuildSuccess { + facts, + overflow: carriers.take(PhaseBLocalError::BuilderBudget), + }), + Err(error) => Err(carriers.error(error)), + } +} + +struct HeldStage { + path: PathBuf, + authority: crate::workspace::AuthenticatedDirectory, + discard_name: Option, +} + +struct StageSlot { + purpose: &'static str, + digest_prefix: [u8; 16], + name: String, + path: PathBuf, + path_capacity: usize, + native_name: platform::PreparedStageName, +} + +impl StageSlot { + fn new(parent: &Path, digest: &str, purpose: &'static str) -> Result { + let digest = Sha256::digest(digest.as_bytes()); + let mut digest_prefix = [0_u8; 16]; + const HEX: &[u8; 16] = b"0123456789abcdef"; + for (index, byte) in digest.iter().take(8).copied().enumerate() { + digest_prefix[index * 2] = HEX[usize::from(byte >> 4)]; + digest_prefix[index * 2 + 1] = HEX[usize::from(byte & 0x0f)]; + } + let parent_bytes = parent.as_os_str().as_encoded_bytes().len(); + let path_capacity = parent_bytes + .checked_add(1) + .and_then(|bytes| bytes.checked_add(PHASE_B_STAGE_NAME_CAPACITY)) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let retained = PHASE_B_STAGE_NAME_CAPACITY + .checked_add(path_capacity) + .and_then(|bytes| { + bytes.checked_add(if cfg!(windows) { + PHASE_B_STAGE_NAME_CAPACITY.checked_mul(2)? + } else { + PHASE_B_STAGE_NAME_CAPACITY.checked_add(1)? + }) + }) + .ok_or_else(|| b109("max_builder_bytes", MAX_BUILDER_BYTES))?; + let authority = reserve_temporary_exact(retained)?; + let name = String::with_capacity(PHASE_B_STAGE_NAME_CAPACITY); + let path = PathBuf::with_capacity(path_capacity); + let native_name = platform::prepare_stage_name_arena(PHASE_B_STAGE_NAME_CAPACITY) + .map_err(|_| platform_publication_error())?; + #[cfg(test)] + PHASE_B_NATIVE_STAGE_ARENA_ALLOCATIONS.with(|count| { + count.set(count.get().saturating_add(1)); + }); + if name.capacity() != PHASE_B_STAGE_NAME_CAPACITY || path.capacity() != path_capacity { + return Err(b109("max_builder_bytes", MAX_BUILDER_BYTES)); + } + authority.retain(retained)?; + Ok(Self { + purpose, + digest_prefix, + name, + path, + path_capacity, + native_name, + }) + } + + fn prepare(&mut self, parent: &Path, nonce: u32) -> Result<(), PhaseBLocalError> { + self.name.clear(); + write!( + self.name, + ".semaprax-native-rust-interop-{}-{}-{}-{nonce}", + self.purpose, + std::process::id(), + std::str::from_utf8(&self.digest_prefix).map_err(|_| PhaseBLocalError::Publication)?, + ) + .map_err(|_| PhaseBLocalError::Publication)?; + if self.name.capacity() != PHASE_B_STAGE_NAME_CAPACITY { + return Err(PhaseBLocalError::Publication); + } + self.path.clear(); + self.path.push(parent); + self.path.push(&self.name); + if self.path.capacity() != self.path_capacity { + return Err(PhaseBLocalError::Publication); + } + self.native_name + .set(self.name.as_ref()) + .map_err(|_| PhaseBLocalError::Publication)?; + #[cfg(test)] + PHASE_B_NATIVE_STAGE_ARENA_SETS.with(|count| { + count.set(count.get().saturating_add(1)); + }); + Ok(()) + } +} + +impl HeldStage { + fn recheck_local(&self) -> Result<(), PhaseBLocalError> { + self.authority + .recheck() + .map_err(|_| PhaseBLocalError::Publication)?; + if !self.authority.same_directory_path(&self.path) { + return Err(PhaseBLocalError::Publication); + } + Ok(()) + } + + fn recheck(&self) -> Result<(), Diagnostic> { + self.recheck_local() + .map_err(|_| platform_publication_error()) + } +} + +fn hold_stage(path: PathBuf) -> Result { + let authority = crate::workspace::authenticate_directory_held(&path) + .map_err(|_| PhaseBLocalError::Publication)?; + Ok(HeldStage { + path, + authority, + discard_name: None, + }) +} + +fn create_stage( + parent: &HeldStage, + mut slot: StageSlot, + inventory: &platform::PreparedDiscardInventory, +) -> Result { + for nonce in 0_u32..1024 { + slot.prepare(&parent.path, nonce)?; + #[cfg(test)] + PHASE_B_NATIVE_STAGE_ARENA_CONSUMPTIONS.with(|count| { + count.set(count.get().saturating_add(1)); + }); + match platform::create_directory_new_prepared( + parent.authority.held(), + &slot.native_name, + 0o700, + ) { + Ok(held) => { + #[cfg(test)] + let authentication_path = match CREATE_AUTH_DISAGREEMENT.with(std::cell::Cell::get) + { + None => None, + Some(CreateAuthDisagreement::Clean) => Some(parent.path.clone()), + Some(CreateAuthDisagreement::Substituted) => { + let displaced = parent.path.join("auth-displaced"); + std::fs::rename(&slot.path, &displaced) + .map_err(|_| PhaseBLocalError::Publication)?; + std::fs::create_dir(&slot.path) + .map_err(|_| PhaseBLocalError::Publication)?; + std::fs::write(slot.path.join("foreign-sentinel"), b"foreign") + .map_err(|_| PhaseBLocalError::Publication)?; + Some(slot.path.clone()) + } + }; + #[cfg(test)] + let authentication_path = authentication_path.as_deref().unwrap_or(&slot.path); + #[cfg(not(test))] + let authentication_path = &slot.path; + let authority = match crate::workspace::authenticate_created_directory( + authentication_path, + held, + ) { + Ok(authority) => authority, + Err(crate::workspace::CreatedDirectoryAuthenticationError::Disagreement( + raw_child, + )) => { + #[cfg(test)] + CREATE_AUTH_DISCARD_ATTEMPTS.with(|attempts| { + attempts.set(attempts.get().saturating_add(1)); + }); + let _ = platform::discard_owned_stage_prepared( + parent.authority.held(), + &raw_child, + &slot.native_name, + inventory, + ); + return Err(PhaseBLocalError::Publication); + } + }; + return Ok(HeldStage { + path: slot.path, + authority, + discard_name: Some(slot.native_name), + }); + } + Err(platform::Error::Exists) => {} + Err(_) => break, + } + } + Err(PhaseBLocalError::Publication) +} + +#[cfg(test)] +fn exact_inventory(directory: &Path, expected: &BTreeSet<&str>) -> Result<(), Diagnostic> { + let metadata = std::fs::symlink_metadata(directory) + .map_err(|_| Diagnostic::io("SPX-I232", "Native Rust Interop output publication failed"))?; + if !metadata.is_dir() || metadata.file_type().is_symlink() { + return Err(Diagnostic::io( + "SPX-I232", + "Native Rust Interop output publication failed", + )); + } + let mut actual = BTreeSet::new(); + for entry in std::fs::read_dir(directory) + .map_err(|_| Diagnostic::io("SPX-I232", "Native Rust Interop output publication failed"))? + { + if actual.len() >= expected.len() { + return Err(Diagnostic::io( + "SPX-I232", + "Native Rust Interop output publication failed", + )); + } + let entry = entry.map_err(|_| { + Diagnostic::io("SPX-I232", "Native Rust Interop output publication failed") + })?; + let name = entry.file_name().into_string().map_err(|_| { + Diagnostic::io("SPX-I232", "Native Rust Interop output publication failed") + })?; + if !expected.contains(name.as_str()) || !crate::bounded_output::reserve_active(name.len()) { + return Err(Diagnostic::io( + "SPX-I232", + "Native Rust Interop output publication failed", + )); + } + let metadata = std::fs::symlink_metadata(entry.path()).map_err(|_| { + Diagnostic::io("SPX-I232", "Native Rust Interop output publication failed") + })?; + if !metadata.is_file() || metadata.file_type().is_symlink() { + return Err(Diagnostic::io( + "SPX-I232", + "Native Rust Interop output publication failed", + )); + } + if !actual.insert(name) { + return Err(Diagnostic::io( + "SPX-I232", + "Native Rust Interop output publication failed", + )); + } + } + if actual.iter().map(String::as_str).collect::>() != *expected { + return Err(Diagnostic::io( + "SPX-I232", + "Native Rust Interop output publication failed", + )); + } + Ok(()) +} + +#[cfg(test)] +fn match_regular_file(path: &Path, expected: &[u8]) -> Result<(), Diagnostic> { + let before_path = std::fs::symlink_metadata(path) + .map_err(|_| Diagnostic::io("SPX-I232", "Native Rust Interop output publication failed"))?; + if !before_path.is_file() || before_path.file_type().is_symlink() { + return Err(Diagnostic::io( + "SPX-I232", + "Native Rust Interop output publication failed", + )); + } + let mut options = std::fs::OpenOptions::new(); + options.read(true).write(false); + #[cfg(all(unix, target_os = "macos"))] + { + use std::os::unix::fs::OpenOptionsExt as _; + options.custom_flags(0x0000_0100); + } + #[cfg(all(unix, not(target_os = "macos")))] + { + use std::os::unix::fs::OpenOptionsExt as _; + options.custom_flags(0x0002_0000); + } + #[cfg(windows)] + { + use std::os::windows::fs::OpenOptionsExt as _; + options.custom_flags(0x0020_0000); + } + let mut file = options + .open(path) + .map_err(|_| Diagnostic::io("SPX-I232", "Native Rust Interop output publication failed"))?; + let before = file + .metadata() + .map_err(|_| Diagnostic::io("SPX-I232", "Native Rust Interop output publication failed"))?; + if !before.is_file() + || !same_file_metadata(&before_path, &before) + || before.len() != u64::try_from(expected.len()).unwrap_or(u64::MAX) + { + return Err(Diagnostic::io( + "SPX-I232", + "Native Rust Interop output publication failed", + )); + } + let mut offset = 0_usize; + let mut buffer = [0_u8; 8192]; + loop { + let length = std::io::Read::read(&mut file, &mut buffer).map_err(|_| { + Diagnostic::io("SPX-I232", "Native Rust Interop output publication failed") + })?; + if length == 0 { + break; + } + let end = offset.checked_add(length).ok_or_else(|| { + Diagnostic::io("SPX-I232", "Native Rust Interop output publication failed") + })?; + if expected.get(offset..end) != Some(&buffer[..length]) { + return Err(Diagnostic::io( + "SPX-I232", + "Native Rust Interop output publication failed", + )); + } + offset = end; + } + let after = file + .metadata() + .map_err(|_| Diagnostic::io("SPX-I232", "Native Rust Interop output publication failed"))?; + if offset != expected.len() || !same_file_metadata(&before, &after) { + return Err(Diagnostic::io( + "SPX-I232", + "Native Rust Interop output publication failed", + )); + } + Ok(()) +} + +struct ObjectAuthority { + budget: TemporaryBudget, +} + +impl ObjectAuthority { + fn new(mut budget: TemporaryBudget, object_capacity: usize) -> Result { + shrink_phase_b(&mut budget, object_capacity)?; + if budget.maximum() != object_capacity { + return Err(PhaseBLocalError::BuilderBudget); + } + #[cfg(test)] + { + if PHASE_B_OBJECT_AUTHORITY_LIVE.with(|live| live.replace(true)) { + return Err(PhaseBLocalError::BuilderBudget); + } + // The detailed order trace is per authorized object. Aggregate + // transfer/drop counters intentionally remain cumulative so tests + // can also prove that repeated builder invocations release once. + PHASE_B_OBJECT_DROP_ORDER.with(|order| order.set([0, 0])); + PHASE_B_OBJECT_DROP_ORDER_LENGTH.with(|length| length.set(0)); + PHASE_B_OBJECT_AUTHORITY_TRANSFERS + .with(|count| count.set(count.get().saturating_add(1))); + } + Ok(Self { budget }) + } + + fn check(&self, object: &[u8], object_capacity: usize) -> Result<(), PhaseBLocalError> { + if self.budget.maximum() != object_capacity || object.len() > object_capacity { + return Err(PhaseBLocalError::BuilderBudget); + } + Ok(()) + } +} + +impl Drop for ObjectAuthority { + fn drop(&mut self) { + #[cfg(test)] + { + let index = PHASE_B_OBJECT_DROP_ORDER_LENGTH.with(std::cell::Cell::get); + assert_eq!(index, 1, "object bytes must drop before their authority"); + PHASE_B_OBJECT_DROP_ORDER.with(|order| { + let mut values = order.get(); + assert_eq!(values[0], 1); + values[index] = 2; + order.set(values); + }); + PHASE_B_OBJECT_DROP_ORDER_LENGTH.with(|length| length.set(index + 1)); + assert!(PHASE_B_OBJECT_AUTHORITY_LIVE.with(|live| live.replace(false))); + PHASE_B_OBJECT_AUTHORITY_DROPS.with(|count| count.set(count.get().saturating_add(1))); + } + } +} + +struct ObjectDropGuard; + +impl Drop for ObjectDropGuard { + fn drop(&mut self) { + #[cfg(test)] + { + assert!(PHASE_B_OBJECT_AUTHORITY_LIVE.with(std::cell::Cell::get)); + let index = PHASE_B_OBJECT_DROP_ORDER_LENGTH.with(std::cell::Cell::get); + assert_eq!(index, 0); + PHASE_B_OBJECT_DROP_ORDER.with(|order| { + let mut values = order.get(); + values[index] = 1; + order.set(values); + }); + PHASE_B_OBJECT_DROP_ORDER_LENGTH.with(|length| length.set(index + 1)); + PHASE_B_OBJECT_BYTES_DROPS.with(|count| count.set(count.get().saturating_add(1))); + } + } +} + +struct ObjectBytes { + bytes: Vec, + drop_guard: ObjectDropGuard, +} + +struct AuthorizedObject { + object: ObjectBytes, + authority: ObjectAuthority, +} + +impl AuthorizedObject { + fn new(bytes: Vec, budget: TemporaryBudget) -> Result { + let authority = ObjectAuthority::new(budget, bytes.capacity())?; + Ok(Self { + object: ObjectBytes { + bytes, + drop_guard: ObjectDropGuard, + }, + authority, + }) + } + + fn as_slice(&self) -> &[u8] { + &self.object.bytes + } + + fn check(&self) -> Result<(), PhaseBLocalError> { + let _ = &self.object.drop_guard; + self.authority + .check(self.as_slice(), self.object.bytes.capacity()) + } +} + +struct ManifestAuthority { + budget: TemporaryBudget, +} + +impl ManifestAuthority { + fn check(&self, manifest: &String) -> Result<(), PhaseBLocalError> { + if self.budget.maximum() != manifest.capacity() + || manifest.len() > self.budget.maximum() + || manifest.capacity() != MAX_MANIFEST_BYTES + { + return Err(PhaseBLocalError::BuilderBudget); + } + Ok(()) + } +} + +impl Drop for ManifestAuthority { + fn drop(&mut self) { + #[cfg(test)] + { + let index = PHASE_B_MANIFEST_DROP_ORDER_LENGTH.with(std::cell::Cell::get); + assert_eq!(index, 1, "manifest bytes must drop before their authority"); + PHASE_B_MANIFEST_DROP_ORDER.with(|order| { + let mut values = order.get(); + assert_eq!(values[0], 1); + values[index] = 2; + order.set(values); + }); + PHASE_B_MANIFEST_DROP_ORDER_LENGTH.with(|length| length.set(index + 1)); + assert!(PHASE_B_MANIFEST_AUTHORITY_LIVE.with(|live| live.replace(false))); + PHASE_B_MANIFEST_AUTHORITY_DROPS.with(|count| count.set(count.get().saturating_add(1))); + } + } +} + +struct ManifestDropGuard; + +impl Drop for ManifestDropGuard { + fn drop(&mut self) { + #[cfg(test)] + { + assert!(PHASE_B_MANIFEST_AUTHORITY_LIVE.with(std::cell::Cell::get)); + let index = PHASE_B_MANIFEST_DROP_ORDER_LENGTH.with(std::cell::Cell::get); + assert_eq!(index, 0); + PHASE_B_MANIFEST_DROP_ORDER.with(|order| { + let mut values = order.get(); + values[index] = 1; + order.set(values); + }); + PHASE_B_MANIFEST_DROP_ORDER_LENGTH.with(|length| length.set(index + 1)); + PHASE_B_MANIFEST_BYTES_DROPS.with(|count| count.set(count.get().saturating_add(1))); + } + } +} + +struct ManifestBytes { + bytes: String, + drop_guard: ManifestDropGuard, +} + +struct AuthorizedManifest { + manifest: ManifestBytes, + authority: ManifestAuthority, +} + +impl AuthorizedManifest { + fn new(bytes: String, budget: TemporaryBudget) -> Result { + if bytes.capacity() != budget.maximum() || bytes.capacity() != MAX_MANIFEST_BYTES { + return Err(PhaseBLocalError::BuilderBudget); + } + #[cfg(test)] + { + if PHASE_B_MANIFEST_AUTHORITY_LIVE.with(|live| live.replace(true)) { + return Err(PhaseBLocalError::BuilderBudget); + } + PHASE_B_MANIFEST_DROP_ORDER.with(|order| order.set([0, 0])); + PHASE_B_MANIFEST_DROP_ORDER_LENGTH.with(|length| length.set(0)); + PHASE_B_MANIFEST_AUTHORITY_TRANSFERS + .with(|count| count.set(count.get().saturating_add(1))); + } + Ok(Self { + manifest: ManifestBytes { + bytes, + drop_guard: ManifestDropGuard, + }, + authority: ManifestAuthority { budget }, + }) + } + + fn as_str(&self) -> &str { + &self.manifest.bytes + } + + fn as_bytes(&self) -> &[u8] { + self.as_str().as_bytes() + } + + fn check(&self) -> Result<(), PhaseBLocalError> { + let _ = &self.manifest.drop_guard; + self.authority.check(&self.manifest.bytes) + } +} + +struct BuildStageFacts { + object_name: &'static str, + object: AuthorizedObject, + manifest: AuthorizedManifest, + inventory_exact: (platform::PreparedInventoryExact<7>, TemporaryBudget), +} + +impl BuildStageFacts { + fn observe_object_authority_for_manifest(&self) -> Result<(), PhaseBLocalError> { + self.object.check()?; + self.manifest.check()?; + #[cfg(test)] + { + if !PHASE_B_OBJECT_AUTHORITY_LIVE.with(std::cell::Cell::get) { + return Err(PhaseBLocalError::BuilderBudget); + } + PHASE_B_OBJECT_AUTHORITY_MANIFEST_OBSERVATIONS + .with(|count| count.set(count.get().saturating_add(1))); + } + Ok(()) + } + + fn observe_object_authority_for_publish(&self) -> Result<(), PhaseBLocalError> { + self.object.check()?; + self.manifest.check()?; + #[cfg(test)] + { + if !PHASE_B_OBJECT_AUTHORITY_LIVE.with(std::cell::Cell::get) { + return Err(PhaseBLocalError::BuilderBudget); + } + PHASE_B_OBJECT_AUTHORITY_PUBLISH_OBSERVATIONS + .with(|count| count.set(count.get().saturating_add(1))); + } + Ok(()) + } +} + +fn platform_publication_error() -> Diagnostic { + observe_phase_b_error_materialization(); + Diagnostic::io("SPX-I232", PHASE_B_PUBLICATION_MESSAGE) +} + +fn platform_compile_error() -> Diagnostic { + Diagnostic::io("SPX-I230", "Native Rust Interop Clang compilation failed") +} + +fn platform_link_error() -> Diagnostic { + Diagnostic::io( + "SPX-I231", + "Native Rust Interop Rust compilation or link failed", + ) +} + +fn map_link_error(_error: platform::Error) -> Diagnostic { + #[cfg(test)] + eprintln!("native rust platform link error: {_error:?}"); + platform_link_error() +} + +fn write_platform_file( + directory: &HeldStage, + files: &mut platform::PreparedDiscardInventory, + name: &str, + bytes: &[u8], +) -> Result<(), PhaseBLocalError> { + files + .validate_next(name) + .map_err(|_| PhaseBLocalError::Publication)?; + directory.recheck_local()?; + platform::write_file_new_prepared(directory.authority.held(), files, name, bytes, 0o600) + .map_err(|_| PhaseBLocalError::Publication) +} + +fn discard_run_stage( + parent: &HeldStage, + stage: &HeldStage, + files: &platform::PreparedDiscardInventory, +) -> Result<(), PhaseBLocalError> { + #[cfg(test)] + PHASE_B_DISCARD_ATTEMPTS.with(|attempts| { + attempts.set(attempts.get().saturating_add(1)); + }); + let stage_name = stage + .discard_name + .as_ref() + .ok_or(PhaseBLocalError::Publication)?; + platform::discard_owned_stage_prepared( + parent.authority.held(), + stage.authority.held(), + stage_name, + files, + ) + .map_err(|_| PhaseBLocalError::Publication) +} + +fn track_run_file( + files: &mut platform::PreparedDiscardInventory, + name: &str, + file: platform::HeldRegularFile, +) -> Result<(), PhaseBLocalError> { + files + .attach(name, file) + .map_err(|_| PhaseBLocalError::Publication) +} + +fn recheck_tracked_files( + files: &platform::PreparedDiscardInventory, + required: &[&str], +) -> Result<(), PhaseBLocalError> { + files + .recheck(required) + .map_err(|_| PhaseBLocalError::Publication) +} + +const REQUIRED_NATIVE_RUST_SANITIZER_FLAGS: [&str; 2] = + ["-fsanitize=address,undefined", "-fno-sanitize-recover=all"]; + +fn sanitizer_mode() -> Result { + match std::env::var_os("SEMAPRAX_REQUIRE_NATIVE_RUST_INTEROP_SANITIZERS") { + None => Ok(false), + Some(value) if value == "1" && cfg!(target_os = "linux") => { + let _ = REQUIRED_NATIVE_RUST_SANITIZER_FLAGS; + Ok(true) + } + Some(_) => Err(PhaseBLocalError::Link), + } +} + +#[allow( + clippy::too_many_arguments, + reason = "held publish and run inventories stay explicit" +)] +fn build_stage_platform( + prepared: &PreparedNativeRustInterop, + tools: &mut ToolchainFacts, + stage: &HeldStage, + run_stage: &HeldStage, + harness_plan: (String, TemporaryBudget), + build_invocations: PreparedBuildInvocations, + link_copies: PreparedLinkCopies, + mut inventory_exact: (platform::PreparedInventoryExact<7>, TemporaryBudget), + manifest_plan: PreparedManifestPlan, + output: &Path, + hook: &mut dyn FnMut(NativeRustBuildPoint, &Path, &Path, &Path), + run_files: &mut RunDiscardInventory, + publish_files: &mut PublishDiscardInventory, +) -> Result { + let PreparedBuildInvocations { + c_o0, + c_o2, + rust, + c_main, + link_o0, + run_o0, + link_o2, + run_o2, + } = build_invocations; + let PreparedLinkCopies { + safe_rust, + private_ffi, + optimized_object, + } = link_copies; + for (name, bytes) in [ + ("descriptor.json", prepared.descriptor.as_bytes()), + ("module.c", prepared.generated_c.as_bytes()), + ( + "semaprax_native_rust_interop.h", + prepared.generated_header.as_bytes(), + ), + ( + "semaprax_native_rust_interop.rs", + prepared.generated_rust.as_bytes(), + ), + ( + "semaprax_native_rust_interop_ffi.rs", + prepared.private_ffi_source.as_bytes(), + ), + ] { + write_platform_file(stage, publish_files, name, bytes)?; + } + let object_name = if cfg!(windows) { + "module.obj" + } else { + "module.o" + }; + hook( + NativeRustBuildPoint::BeforeClang, + &stage.path, + &run_stage.path, + output, + ); + stage.recheck_local()?; + recheck_tracked_files( + publish_files, + &["module.c", "semaprax_native_rust_interop.h"], + )?; + let mut retained_object = None; + for (optimization, invocation) in [(0_u8, c_o0), (2_u8, c_o2)] { + let (invocation, invocation_budget) = consume_invocation(invocation); + let output = platform::compile_c_tool_prepared( + &tools.clang, + stage.authority.held(), + invocation, + tools + .process_arena + .as_mut() + .ok_or(PhaseBLocalError::BuilderBudget)? + .arena_mut()?, + ) + .map_err(|_| PhaseBLocalError::Compile)?; + let object = output.into_bytes(); + if optimization == 0 { + let name = "module_O0.o"; + write_platform_file(run_stage, run_files, name, &object)?; + drop(object); + drop(invocation_budget); + } else { + retained_object = Some((object, invocation_budget)); + } + } + let (object, object_invocation_budget) = retained_object.ok_or(PhaseBLocalError::Compile)?; + let object = AuthorizedObject::new(object, object_invocation_budget)?; + object.check()?; + write_platform_file(stage, publish_files, object_name, object.as_slice())?; + + let (harness, harness_budget) = harness_plan; + write_platform_file( + run_stage, + run_files, + "__semaprax_native_rust_link.rs", + harness.as_bytes(), + )?; + drop(harness); + drop(harness_budget); + consume_link_copy( + safe_rust, + publish_files, + run_stage, + run_files, + prepared.generated_rust.as_bytes(), + )?; + consume_link_copy( + private_ffi, + publish_files, + run_stage, + run_files, + prepared.private_ffi_source.as_bytes(), + )?; + consume_link_copy( + optimized_object, + publish_files, + run_stage, + run_files, + object.as_slice(), + )?; + + let staticlib_name = if cfg!(windows) { + "semaprax_bridge.lib" + } else { + "libsemaprax_bridge.a" + }; + hook( + NativeRustBuildPoint::BeforeRustLink, + &stage.path, + &run_stage.path, + output, + ); + recheck_tracked_files( + run_files, + &[ + "__semaprax_native_rust_link.rs", + "semaprax_native_rust_interop.rs", + "semaprax_native_rust_interop_ffi.rs", + "module_O0.o", + "module_O2.o", + ], + )?; + let (rust, rust_invocation_budget) = consume_invocation(rust); + let staticlib = platform::compile_rust_tool_prepared( + &tools.rustc, + run_stage.authority.held(), + rust, + tools + .process_arena + .as_mut() + .ok_or(PhaseBLocalError::BuilderBudget)? + .arena_mut()?, + ) + .map_err(|error| { + #[cfg(test)] + eprintln!("compile staticlib: {error:?}"); + let _ = error; + PhaseBLocalError::Link + })?; + drop(rust_invocation_budget); + track_run_file(run_files, staticlib_name, staticlib)?; + + let c_harness = "extern int spxnr1_rust_harness_run(void);int main(void){return spxnr1_rust_harness_run();}\n"; + write_platform_file( + run_stage, + run_files, + "__semaprax_native_rust_main.c", + c_harness.as_bytes(), + )?; + let (c_main, c_main_invocation_budget) = consume_invocation(c_main); + let harness_object = platform::compile_c_tool_prepared( + &tools.clang, + run_stage.authority.held(), + c_main, + tools + .process_arena + .as_mut() + .ok_or(PhaseBLocalError::BuilderBudget)? + .arena_mut()?, + ) + .map_err(|_| PhaseBLocalError::Compile)?; + write_platform_file( + run_stage, + run_files, + "__semaprax_native_rust_main.o", + harness_object.bytes(), + )?; + drop(harness_object); + drop(c_main_invocation_budget); + for (optimization, link_invocation, run_invocation) in + [(0_u8, link_o0, run_o0), (2_u8, link_o2, run_o2)] + { + let c_object = if optimization == 0 { + "module_O0.o" + } else { + "module_O2.o" + }; + let executable_name = if cfg!(windows) { + if optimization == 0 { + "__semaprax_native_rust_link_O0.exe" + } else { + "__semaprax_native_rust_link_O2.exe" + } + } else if optimization == 0 { + "__semaprax_native_rust_link_O0" + } else { + "__semaprax_native_rust_link_O2" + }; + hook( + NativeRustBuildPoint::BeforeExecutableAuthentication, + &stage.path, + &run_stage.path, + output, + ); + recheck_tracked_files( + run_files, + &["__semaprax_native_rust_main.o", c_object, staticlib_name], + )?; + let (link_invocation, link_invocation_budget) = consume_invocation(link_invocation); + let executable = platform::link_tool_prepared( + &tools.clang, + run_stage.authority.held(), + link_invocation, + tools + .process_arena + .as_mut() + .ok_or(PhaseBLocalError::BuilderBudget)? + .arena_mut()?, + ) + .map_err(|_| PhaseBLocalError::Link)?; + drop(link_invocation_budget); + let executable_file = platform::executable_regular_file(&executable) + .map_err(|_| PhaseBLocalError::Publication)?; + track_run_file(run_files, executable_name, executable_file)?; + hook( + NativeRustBuildPoint::BeforeExecute, + &stage.path, + &run_stage.path, + output, + ); + let (run_invocation, run_invocation_budget) = consume_invocation(run_invocation); + platform::execute_tool_prepared( + &executable, + run_stage.authority.held(), + run_invocation, + tools + .process_arena + .as_mut() + .ok_or(PhaseBLocalError::BuilderBudget)? + .arena_mut()?, + ) + .map_err(|_| PhaseBLocalError::Link)?; + drop(run_invocation_budget); + } + let process_arena = tools + .process_arena + .take() + .ok_or(PhaseBLocalError::BuilderBudget)?; + if platform::prepared_process_arena_remaining(process_arena.arena()?) != 0 { + return Err(PhaseBLocalError::BuilderBudget); + } + let process_arena_capacity = process_arena.authorized_capacity()?; + if platform::prepared_process_arena_owned_capacity(process_arena.arena()?) + != process_arena_capacity + { + return Err(PhaseBLocalError::BuilderBudget); + } + drop(process_arena); + hook( + NativeRustBuildPoint::BeforeObjectRead, + &stage.path, + &run_stage.path, + output, + ); + recheck_tracked_files(publish_files, &[object_name])?; + + let manifest_file_names = canonical_manifest_file_names(); + let files = [ + (manifest_file_names[0], prepared.descriptor.as_bytes()), + (manifest_file_names[1], prepared.generated_c.as_bytes()), + (manifest_file_names[2], object.as_slice()), + (manifest_file_names[3], prepared.generated_header.as_bytes()), + (manifest_file_names[4], prepared.generated_rust.as_bytes()), + ( + manifest_file_names[5], + prepared.private_ffi_source.as_bytes(), + ), + ]; + let manifest = manifest_plan.render( + prepared, + &files, + platform::tool_path(&tools.clang), + &tools.clang_version, + &tools.rustc_version, + &prepared.target.triple, + )?; + replay_manifest(manifest.as_str(), prepared, &files, tools)?; + hook( + NativeRustBuildPoint::BeforeManifestPublish, + &stage.path, + &run_stage.path, + output, + ); + recheck_tracked_files( + publish_files, + &[ + "descriptor.json", + "module.c", + object_name, + "semaprax_native_rust_interop.h", + "semaprax_native_rust_interop.rs", + "semaprax_native_rust_interop_ffi.rs", + ], + )?; + write_platform_file( + stage, + publish_files, + "semaprax.native-rust-interop.json", + manifest.as_bytes(), + )?; + scan_publish_inventory_exact(&mut inventory_exact.0, stage, publish_files)?; + Ok(BuildStageFacts { + object_name, + object, + manifest, + inventory_exact, + }) +} + +fn publish_stage_platform( + parent: &HeldStage, + stage: &HeldStage, + output: &Path, + prepared: &PreparedNativeRustInterop, + facts: &mut BuildStageFacts, + publish_files: &PublishDiscardInventory, + final_publish: &mut platform::PreparedPublishDirectory, +) -> Result<(), PhaseBLocalError> { + facts.observe_object_authority_for_publish()?; + parent.recheck_local()?; + stage.recheck_local()?; + let mut comparison_scratch = [0_u8; platform::FILE_COMPARE_SCRATCH_BYTES]; + for (name, expected) in [ + ("descriptor.json", prepared.descriptor.as_bytes()), + ("module.c", prepared.generated_c.as_bytes()), + ( + "semaprax.native-rust-interop.json", + facts.manifest.as_bytes(), + ), + ( + "semaprax_native_rust_interop.h", + prepared.generated_header.as_bytes(), + ), + ( + "semaprax_native_rust_interop.rs", + prepared.generated_rust.as_bytes(), + ), + ( + "semaprax_native_rust_interop_ffi.rs", + prepared.private_ffi_source.as_bytes(), + ), + (facts.object_name, facts.object.as_slice()), + ] { + let held = publish_files + .file(name) + .map_err(|_| PhaseBLocalError::Publication)?; + if !platform::compare_exact(held, expected, &mut comparison_scratch) + .map_err(|_| PhaseBLocalError::Publication)? + { + return Err(PhaseBLocalError::Publication); + } + } + scan_publish_inventory_exact(&mut facts.inventory_exact.0, stage, publish_files)?; + let output_name = output.file_name().ok_or(PhaseBLocalError::Publication)?; + let stage_name = stage + .discard_name + .as_ref() + .ok_or(PhaseBLocalError::Publication)?; + #[cfg(all(test, debug_assertions))] + if let Some(point) = + PHASE_B_PUBLISH_FAILURE.with(|point| (point.get() != 0).then(|| point.get())) + { + platform::inject_publish_directory_failure(final_publish, point) + .map_err(|_| PhaseBLocalError::Publication)?; + } + let publication = platform::publish_directory_new_prepared( + final_publish, + parent.authority.held(), + stage.authority.held(), + stage_name, + output_name, + ); + #[cfg(test)] + if platform::prepared_publish_directory_remaining(final_publish) == 0 { + PHASE_B_PUBLISH_CONSUMPTIONS.with(|count| count.set(count.get().saturating_add(1))); + } + publication.map_err(|_| PhaseBLocalError::Publication)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::path::Path; + use std::process::Command; + + const SOURCE: &str = r#"module interop.fixture; + +permit { host.math } + +@id("host.math") +interface HostMath + permits { host.math } +{ + @id("host.add") + import rust fn host_add(left: i64, right: i64) -> i64 + effects { host.math } + failure status "host.math.v1"; +} + +@id("interop.add") +fn add(left: i64, right: i64) -> i64 + uses { host.math } +{ + host_add(left, right) + right +} + +@id("interop.main") +fn main() -> i64 +{ + 0 +} +"#; + + fn fixture() -> (Program, String) { + let program = crate::parse(SOURCE, Path::new("native-rust-interop.spx")).unwrap(); + let source = crate::format::canonical(&program); + let spec = Spec { + module: program.module.clone(), + source_revision: domain_digest(SOURCE_DOMAIN, source.as_bytes()), + target: current_target().unwrap(), + exports: vec!["interop.add".to_owned()], + imports: vec!["host.add".to_owned()], + capabilities: vec!["host.math".to_owned()], + }; + let canonical = render_spec(&spec); + (program, canonical) + } + + #[derive(Default)] + struct ObservedCleanupProof { + slot_payload_bytes: usize, + call_argument_slot_payload_bytes: usize, + shape_identity_bytes: usize, + shape_field_capacity_entries: usize, + flag_lifecycle_bytes: usize, + flag_projection_bytes: usize, + flag_projection_capacity_entries: usize, + place_storage_bytes: usize, + place_projection_bytes: usize, + place_projection_capacity_entries: usize, + finalizer_storage_bytes: usize, + finalizer_projection_bytes: usize, + finalizer_projection_capacity_entries: usize, + finalizer_lifecycle_bytes: usize, + inventory_slot_capacity_entries: usize, + inventory_flag_capacity_entries: usize, + inventory_entry_capacity_entries: usize, + plan_slot_capacity_entries: usize, + plan_entry_capacity_entries: usize, + finalizer_capacity_entries: usize, + block_capacity_entries: usize, + edge_capacity_entries: usize, + region_capacity_entries: usize, + exit_capacity_entries: usize, + status_capacity_entries: usize, + transition_capacity_entries: usize, + branch_edge_capacity_entries: usize, + region_slot_capacity_entries: usize, + exit_region_capacity_entries: usize, + status_case_capacity_entries: usize, + } + + fn observed_type_bytes(ty: &ResolvedType) -> usize { + match ty { + ResolvedType::Unit | ResolvedType::I64 | ResolvedType::Bool => 0, + ResolvedType::TypeParameter { owner, .. } => owner.as_str().len(), + ResolvedType::Nominal { + declaration, + arguments, + } => { + declaration.as_str().len() + + arguments.capacity() * std::mem::size_of::() + + arguments.iter().map(observed_type_bytes).sum::() + } + } + } + + fn observe_shape( + root: &semaprax::cleanup::FieldLivenessShape, + observed: &mut ObservedCleanupProof, + ) -> Option<()> { + let mut pending = vec![root]; + while let Some(shape) = pending.pop() { + match shape { + semaprax::cleanup::FieldLivenessShape::NoDrop => {} + semaprax::cleanup::FieldLivenessShape::Leaf { lifecycle, .. } => { + observed.shape_identity_bytes += lifecycle.as_str().len(); + } + semaprax::cleanup::FieldLivenessShape::Record { + declaration, + fields, + } => { + observed.shape_identity_bytes += declaration.as_str().len(); + observed.shape_field_capacity_entries += fields.capacity(); + for field in fields { + observed.shape_identity_bytes += field.field.as_str().len(); + pending.push(&field.shape); + } + } + _ => return None, + } + } + Some(()) + } + + fn observed_storage_bytes(storage: &semaprax::cleanup_plan::StorageId) -> usize { + match storage { + semaprax::cleanup_plan::StorageId::Value(value) => value.as_str().len(), + semaprax::cleanup_plan::StorageId::Temporary(expression) => expression.as_str().len(), + semaprax::cleanup_plan::StorageId::CallArgument { + call, + value_expression, + .. + } => call.as_str().len() + value_expression.as_str().len(), + semaprax::cleanup_plan::StorageId::ProvisionalResult => 0, + } + } + + fn observe_place( + place: &semaprax::cleanup_plan::CleanupPlace, + finalizer: bool, + observed: &mut ObservedCleanupProof, + ) { + let storage = observed_storage_bytes(&place.storage); + let projection_bytes = place + .projections + .iter() + .map(|projection| projection.as_str().len()) + .sum::(); + if finalizer { + observed.finalizer_storage_bytes += storage; + observed.finalizer_projection_bytes += projection_bytes; + observed.finalizer_projection_capacity_entries += place.projections.capacity(); + } else { + observed.place_storage_bytes += storage; + observed.place_projection_bytes += projection_bytes; + observed.place_projection_capacity_entries += place.projections.capacity(); + } + } + + fn observe_cleanup_function( + function: &ResolvedFunction, + observed: &mut ObservedCleanupProof, + ) -> Option<()> { + use semaprax::cleanup::CleanupStorageOrigin; + use semaprax::cleanup_plan::{ + CleanupResultSource, CleanupTerminator, CleanupTransition, ExitContinuation, + StatusProducer, + }; + + observed.inventory_slot_capacity_entries += function.cleanup.slots.capacity(); + observed.inventory_flag_capacity_entries += function.cleanup.flags.capacity(); + observed.inventory_entry_capacity_entries += function + .cleanup + .entry_state + .live_owned_parameters + .capacity(); + for slot in &function.cleanup.slots { + observed.slot_payload_bytes += match &slot.origin { + CleanupStorageOrigin::Parameter { value, .. } + | CleanupStorageOrigin::Binding { value } + | CleanupStorageOrigin::ProvisionalResult { value } => value.as_str().len(), + CleanupStorageOrigin::Temporary { expression } => expression.as_str().len(), + _ => return None, + }; + observed.slot_payload_bytes += observed_type_bytes(&slot.ty); + observe_shape(&slot.shape, observed)?; + } + for flag in &function.cleanup.flags { + observed.flag_lifecycle_bytes += flag.lifecycle.as_str().len(); + observed.flag_projection_bytes += flag + .place + .projections + .iter() + .map(|projection| projection.as_str().len()) + .sum::(); + observed.flag_projection_capacity_entries += flag.place.projections.capacity(); + } + + let plan = &function.cleanup_plan; + observed.plan_slot_capacity_entries += plan.slots.capacity(); + observed.plan_entry_capacity_entries += plan.entry_state.live_owned_parameters.capacity(); + observed.block_capacity_entries += plan.blocks.capacity(); + observed.edge_capacity_entries += plan.edges.capacity(); + observed.region_capacity_entries += plan.regions.capacity(); + observed.exit_capacity_entries += plan.exits.capacity(); + observed.status_capacity_entries += plan.status_sources.capacity(); + for slot in &plan.slots { + let payload = observed_storage_bytes(&slot.storage) + observed_type_bytes(&slot.ty); + if matches!( + slot.storage, + semaprax::cleanup_plan::StorageId::CallArgument { .. } + ) { + observed.call_argument_slot_payload_bytes += payload; + } else { + observed.slot_payload_bytes += payload; + } + observe_shape(&slot.field_liveness_shape, observed)?; + } + for place in &plan.entry_state.live_owned_parameters { + observe_place(place, false, observed); + } + for status in &plan.status_sources { + if let StatusProducer::CheckedArithmetic { + normalized_cases, .. + } = &status.producer + { + observed.status_case_capacity_entries += normalized_cases.capacity(); + } + } + for block in &plan.blocks { + observed.transition_capacity_entries += block.transitions.capacity(); + for transition in &block.transitions { + match transition { + CleanupTransition::Initialize { destination, .. } => { + observe_place(destination, false, observed); + } + CleanupTransition::Transfer { + source, + destination, + .. + } => { + observe_place(source, false, observed); + observe_place(destination, false, observed); + } + CleanupTransition::CallCommit { arguments, .. } => { + for argument in arguments { + observe_place(&argument.source, false, observed); + } + } + CleanupTransition::SelectFailure { .. } + | CleanupTransition::StageCopyResult { .. } => {} + } + } + if let CleanupTerminator::Branch(edges) = &block.terminator { + observed.branch_edge_capacity_entries += edges.capacity(); + } + } + for region in &plan.regions { + observed.region_slot_capacity_entries += region.slots.capacity(); + observed.place_storage_bytes += region + .slots + .iter() + .map(observed_storage_bytes) + .sum::(); + } + for exit in &plan.exits { + observed.exit_region_capacity_entries += exit.leaves_regions.capacity(); + observed.finalizer_capacity_entries += exit.finalize_in_order.capacity(); + for action in &exit.finalize_in_order { + observe_place(&action.source, true, observed); + observed.finalizer_lifecycle_bytes += action.lifecycle_id.as_str().len(); + } + if let ExitContinuation::CommitResult { + source: CleanupResultSource::Owned { storage }, + } = &exit.continuation + { + observe_place(storage, false, observed); + } + } + Some(()) + } + + #[test] + fn build_race_hooks_reject_each_pre_effect_mutation_and_preserve_foreign_bytes() { + use std::io::Write as _; + + let points = [ + NativeRustBuildPoint::BeforeClang, + NativeRustBuildPoint::BeforeRustLink, + NativeRustBuildPoint::BeforeExecutableAuthentication, + NativeRustBuildPoint::BeforeExecute, + NativeRustBuildPoint::BeforeObjectRead, + NativeRustBuildPoint::BeforeManifestPublish, + NativeRustBuildPoint::BeforeBundlePublish, + ]; + let complete_order = [ + NativeRustBuildPoint::BeforeClang, + NativeRustBuildPoint::BeforeRustLink, + NativeRustBuildPoint::BeforeExecutableAuthentication, + NativeRustBuildPoint::BeforeExecute, + NativeRustBuildPoint::BeforeExecutableAuthentication, + NativeRustBuildPoint::BeforeExecute, + NativeRustBuildPoint::BeforeObjectRead, + NativeRustBuildPoint::BeforeManifestPublish, + NativeRustBuildPoint::BeforeBundlePublish, + ]; + let (program, spec) = fixture(); + for (index, selected) in points.into_iter().enumerate() { + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-race-hook-{}-{index}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + let output = root.join("bundle"); + let mut fired = false; + let mut observed = Vec::with_capacity(complete_order.len()); + PHASE_B_DISCARD_ATTEMPTS.with(|attempts| attempts.set(0)); + PHASE_B_INVENTORY_EXACT_PLANS.with(|count| count.set(0)); + PHASE_B_INVENTORY_EXACT_SCANS.with(|count| count.set(0)); + PHASE_B_PUBLISH_PLANS.with(|count| count.set(0)); + PHASE_B_PUBLISH_CONSUMPTIONS.with(|count| count.set(0)); + reset_phase_b_object_authority_observer(); + reset_phase_b_manifest_authority_observer(); + let result = build_native_rust_interop_bundle_with_hook( + &program, + spec.as_bytes(), + &output, + |point, publish, run, final_output| { + observed.push(point); + if point != selected { + return; + } + assert!(!fired, "hook {selected:?} fired more than once"); + fired = true; + match point { + NativeRustBuildPoint::BeforeClang => { + append_hostile(&publish.join("module.c")); + } + NativeRustBuildPoint::BeforeRustLink => { + append_hostile(&run.join("__semaprax_native_rust_link.rs")); + } + NativeRustBuildPoint::BeforeExecutableAuthentication => { + append_hostile(&run.join("__semaprax_native_rust_main.o")); + } + NativeRustBuildPoint::BeforeExecute => { + append_hostile(&run.join(if cfg!(windows) { + "__semaprax_native_rust_link_O0.exe" + } else { + "__semaprax_native_rust_link_O0" + })); + } + NativeRustBuildPoint::BeforeObjectRead => { + append_hostile(&publish.join(if cfg!(windows) { + "module.obj" + } else { + "module.o" + })); + } + NativeRustBuildPoint::BeforeManifestPublish => { + std::fs::write(publish.join("foreign-sentinel"), b"foreign").unwrap(); + } + NativeRustBuildPoint::BeforeBundlePublish => { + std::fs::create_dir(final_output).unwrap(); + std::fs::write(final_output.join("foreign-sentinel"), b"foreign") + .unwrap(); + } + } + }, + ); + assert!(fired, "hook {selected:?} was not reached"); + let selected_index = complete_order + .iter() + .position(|point| *point == selected) + .unwrap(); + assert_eq!( + observed, + complete_order[..=selected_index], + "hook {selected:?} allowed a later action or skipped an earlier action", + ); + let error = match result { + Ok(_) => panic!("hostile hook {selected:?} unexpectedly published"), + Err(error) => error, + }; + assert_eq!(error.len(), 1); + assert_eq!( + error[0].code, + if selected == NativeRustBuildPoint::BeforeExecute { + "SPX-I231" + } else { + "SPX-I232" + } + ); + let carrier = if selected == NativeRustBuildPoint::BeforeExecute { + PhaseBLocalError::Link + } else { + PhaseBLocalError::Publication + }; + assert_eq!( + error[0].message.as_ptr() as usize, + PHASE_B_PREPARED_CARRIER_IDENTITIES.with(std::cell::Cell::get)[carrier.index()], + "hook {selected:?} did not return its pre-effect carrier", + ); + assert_eq!( + PHASE_B_POST_EFFECT_ERROR_MATERIALIZATIONS.with(std::cell::Cell::get), + 0, + "hook {selected:?} materialized an error after effects", + ); + assert_eq!( + PHASE_B_DISCARD_ATTEMPTS.with(std::cell::Cell::get), + 2, + "hook {selected:?} did not attempt run and publish settlement exactly once", + ); + assert_eq!(PHASE_B_INVENTORY_EXACT_PLANS.with(std::cell::Cell::get), 1); + assert_eq!( + PHASE_B_INVENTORY_EXACT_SCANS.with(std::cell::Cell::get), + match selected { + NativeRustBuildPoint::BeforeManifestPublish => 1, + NativeRustBuildPoint::BeforeBundlePublish => 2, + _ => 0, + }, + "hook {selected:?} crossed an unexpected exact-inventory scan boundary", + ); + assert_eq!(PHASE_B_PUBLISH_PLANS.with(std::cell::Cell::get), 1); + assert_eq!( + PHASE_B_PUBLISH_CONSUMPTIONS.with(std::cell::Cell::get), + usize::from(selected == NativeRustBuildPoint::BeforeBundlePublish), + "hook {selected:?} consumed final publication unexpectedly", + ); + let expected_transfer = usize::from(selected != NativeRustBuildPoint::BeforeClang); + assert_eq!( + PHASE_B_OBJECT_AUTHORITY_TRANSFERS.with(std::cell::Cell::get), + expected_transfer, + "hook {selected:?} transferred the O2 authority at the wrong boundary", + ); + assert_eq!( + PHASE_B_OBJECT_AUTHORITY_DROPS.with(std::cell::Cell::get), + expected_transfer, + "hook {selected:?} did not release the O2 authority exactly once", + ); + let expected_outer_observation = + usize::from(selected == NativeRustBuildPoint::BeforeBundlePublish); + assert_eq!( + PHASE_B_OBJECT_AUTHORITY_MANIFEST_OBSERVATIONS.with(std::cell::Cell::get), + expected_outer_observation, + ); + assert_eq!( + PHASE_B_OBJECT_AUTHORITY_PUBLISH_OBSERVATIONS.with(std::cell::Cell::get), + expected_outer_observation, + ); + assert_phase_b_object_drop_order(expected_transfer); + assert_eq!( + PHASE_B_MANIFEST_ARENA_ALLOCATIONS.with(std::cell::Cell::get), + 1 + ); + assert_eq!(PHASE_B_MANIFEST_ARENA_GROWTHS.with(std::cell::Cell::get), 0); + assert_eq!( + PHASE_B_MANIFEST_AUTHORITY_TRANSFERS.with(std::cell::Cell::get), + 1 + ); + assert_phase_b_manifest_drop_order(1); + if selected == NativeRustBuildPoint::BeforeBundlePublish { + assert_eq!( + std::fs::read(output.join("foreign-sentinel")).unwrap(), + b"foreign" + ); + } else { + assert!(!output.exists()); + } + if selected == NativeRustBuildPoint::BeforeManifestPublish { + let sentinel = std::fs::read_dir(&root) + .unwrap() + .filter_map(Result::ok) + .map(|entry| entry.path().join("foreign-sentinel")) + .find(|path| path.is_file()) + .unwrap(); + assert_eq!(std::fs::read(sentinel).unwrap(), b"foreign"); + } + let stages = std::fs::read_dir(&root) + .unwrap() + .filter_map(Result::ok) + .filter(|entry| { + entry + .file_name() + .to_string_lossy() + .starts_with(".semaprax-native-rust-interop-") + }) + .map(|entry| entry.path()) + .collect::>(); + match selected { + NativeRustBuildPoint::BeforeManifestPublish => { + assert_eq!( + stages.len(), + 1, + "foreign inventory uncertainty must leave one inert stage" + ); + } + NativeRustBuildPoint::BeforeClang + | NativeRustBuildPoint::BeforeRustLink + | NativeRustBuildPoint::BeforeExecutableAuthentication + | NativeRustBuildPoint::BeforeExecute => { + assert_eq!(stages.len(), 1, "mutated held bytes must leave one inert stage rather than deleting uncertain data"); + } + NativeRustBuildPoint::BeforeObjectRead => { + let expected = if cfg!(target_os = "linux") { 2 } else { 1 }; + assert_eq!( + stages.len(), + expected, + "mutating the hard-linked Linux object must preserve both uncertain stages" + ); + } + NativeRustBuildPoint::BeforeBundlePublish => { + assert!(stages.is_empty(), "owned publish stage must settle when only the foreign final output conflicts"); + } + } + std::fs::remove_dir_all(&root).unwrap(); + } + + fn append_hostile(path: &Path) { + std::fs::OpenOptions::new() + .append(true) + .open(path) + .unwrap() + .write_all(b"hostile") + .unwrap(); + } + } + + #[cfg(debug_assertions)] + #[test] + fn phase_b_prepared_publish_open_information_and_rename_failures_are_sticky() { + let (program, spec) = fixture(); + for point in [1_u8, 2, 4] { + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-publish-failure-{}-{point}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + let output = root.join("bundle"); + PHASE_B_PUBLISH_FAILURE.with(|selected| selected.set(point)); + PHASE_B_DISCARD_ATTEMPTS.with(|count| count.set(0)); + PHASE_B_PUBLISH_PLANS.with(|count| count.set(0)); + PHASE_B_PUBLISH_CONSUMPTIONS.with(|count| count.set(0)); + let mut reached_publish = false; + let result = build_native_rust_interop_bundle_with_hook( + &program, + spec.as_bytes(), + &output, + |boundary, _, _, _| { + if boundary == NativeRustBuildPoint::BeforeBundlePublish { + reached_publish = true; + } + }, + ); + PHASE_B_PUBLISH_FAILURE.with(|selected| selected.set(0)); + assert!(reached_publish); + let diagnostics = match result { + Ok(_) => panic!("injected publish failure {point} unexpectedly succeeded"), + Err(diagnostics) => diagnostics, + }; + assert_eq!(diagnostics.len(), 1); + assert_eq!(diagnostics[0].code, "SPX-I232"); + assert_eq!( + diagnostics[0].message.as_ptr() as usize, + PHASE_B_PREPARED_CARRIER_IDENTITIES.with(std::cell::Cell::get) + [PhaseBLocalError::Publication.index()] + ); + assert_eq!( + PHASE_B_POST_EFFECT_ERROR_MATERIALIZATIONS.with(std::cell::Cell::get), + 0 + ); + assert_eq!(PHASE_B_DISCARD_ATTEMPTS.with(std::cell::Cell::get), 2); + assert_eq!(PHASE_B_PUBLISH_PLANS.with(std::cell::Cell::get), 1); + assert_eq!(PHASE_B_PUBLISH_CONSUMPTIONS.with(std::cell::Cell::get), 1); + assert!(!output.exists()); + assert!( + std::fs::read_dir(&root) + .unwrap() + .filter_map(Result::ok) + .all(|entry| !entry + .file_name() + .to_string_lossy() + .starts_with(".semaprax-native-rust-interop-")), + "owned stages must settle after injected publish failure {point}" + ); + std::fs::remove_dir_all(root).unwrap(); + } + } + + #[cfg(debug_assertions)] + #[test] + fn phase_b_prepared_publish_close_failure_child() { + let Ok(root) = std::env::var("SEMAPRAX_PUBLISH_CLOSE_FAILURE_ROOT") else { + return; + }; + let root = Path::new(&root); + let (program, spec) = fixture(); + PHASE_B_PUBLISH_FAILURE.with(|selected| selected.set(3)); + let _ = build_native_rust_interop_bundle_with_hook( + &program, + spec.as_bytes(), + &root.join("bundle"), + |_, _, _, _| {}, + ); + std::fs::write(root.join("later-action"), b"must not exist").unwrap(); + } + + #[cfg(debug_assertions)] + #[test] + fn phase_b_prepared_publish_close_uncertainty_is_fail_stop() { + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-publish-close-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + let status = Command::new(std::env::current_exe().unwrap()) + .arg("--exact") + .arg("implementation::tests::phase_b_prepared_publish_close_failure_child") + .arg("--nocapture") + .env("SEMAPRAX_PUBLISH_CLOSE_FAILURE_ROOT", &root) + .status() + .unwrap(); + assert!(!status.success()); + assert!(!root.join("later-action").exists()); + std::fs::remove_dir_all(root).unwrap(); + } + + fn prepare_source( + source: &str, + exports: &[&str], + imports: &[&str], + ) -> Result> { + let program = crate::parse(source, Path::new("native-rust-unit-builder.spx")) + .map_err(|diagnostic| vec![diagnostic])?; + hir::resolve(&program)?; + let canonical = crate::format::canonical(&program); + let spec = Spec { + module: program.module.clone(), + source_revision: domain_digest(SOURCE_DOMAIN, canonical.as_bytes()), + target: current_target().unwrap(), + exports: exports.iter().map(|value| (*value).to_owned()).collect(), + imports: imports.iter().map(|value| (*value).to_owned()).collect(), + capabilities: Vec::new(), + }; + prepare_native_rust_interop(&program, render_spec(&spec).as_bytes()) + } + + #[test] + fn native_unit_import_is_exact_direct_unused_let_and_resolved_identity_scoped() { + const UNIT_SOURCE: &str = r#"module interop.unit; + +@id("host.unit") +interface HostUnit + permits { } +{ + @id("host.unit.ping") + import rust fn ping(value: i64) -> unit + effects { } + failure infallible; +} + +@id("interop.unit.selected") +fn selected(value: i64) -> i64 +{ + let acknowledged = ping(value); + let outcome = value + 1; + outcome +} + +@id("interop.unit.unselected") +fn unselected(value: i64) -> i64 +{ + let acknowledged = ping(value); + let outcome = 7; + outcome +} + +@id("interop.unit.main") +fn main() -> i64 +{ + 0 +} +"#; + let prepared = prepare_source(UNIT_SOURCE, &["interop.unit.selected"], &["host.unit.ping"]) + .unwrap_or_else(|errors| panic!("unit prepare: {errors:?}")); + assert_eq!(prepared.exports.len(), 1); + assert_eq!(prepared.imports.len(), 1); + assert!(prepared.imports[0].result == ScalarType::Unit); + + for hostile in [ + UNIT_SOURCE.replacen( + " let outcome = value + 1;\n outcome", + " let outcome = value + 1;\n acknowledged", + 1, + ), + UNIT_SOURCE.replacen( + " let outcome = value + 1;\n outcome", + " let outcome = selected(acknowledged);\n outcome", + 1, + ), + UNIT_SOURCE.replacen( + " let acknowledged = ping(value);", + " let acknowledged = { ping(value) };", + 1, + ), + UNIT_SOURCE.replacen( + " let acknowledged = ping(value);\n let outcome = value + 1;", + " let acknowledged = 0;\n let outcome = if ping(value) { 1 } else { 2 };", + 1, + ), + UNIT_SOURCE.replacen( + " let acknowledged = ping(value);\n let outcome = value + 1;", + " let acknowledged = 0;\n let outcome = if true { ping(value) } else { ping(value) };", + 1, + ), + UNIT_SOURCE + .replacen( + "@id(\"interop.unit.selected\")", + "@id(\"interop.unit.helper\")\nfn helper(value: i64) -> unit\n{\n ping(value)\n}\n\n@id(\"interop.unit.selected\")", + 1, + ) + .replacen( + " let acknowledged = ping(value);", + " let acknowledged = helper(value);", + 1, + ), + ] { + let errors = + match prepare_source(&hostile, &["interop.unit.selected"], &["host.unit.ping"]) { + Ok(_) => panic!("hostile Unit use was accepted"), + Err(errors) => errors, + }; + assert_eq!(errors.len(), 1, "{errors:?}"); + assert_eq!(errors[0].code, "SPX-B107"); + assert_eq!( + errors[0].message, + "Native Rust Interop declaration set is unsupported: scalar value signature required" + ); + } + + let unit_export = UNIT_SOURCE.replacen( + "fn selected(value: i64) -> i64\n{\n let acknowledged = ping(value);\n let outcome = value + 1;\n outcome", + "fn selected(value: i64) -> unit\n{\n let acknowledged = ping(value);\n acknowledged", + 1, + ); + let errors = match prepare_source( + &unit_export, + &["interop.unit.selected"], + &["host.unit.ping"], + ) { + Ok(_) => panic!("Unit export was accepted"), + Err(errors) => errors, + }; + assert_eq!(errors.len(), 1, "{errors:?}"); + assert_eq!(errors[0].code, "SPX-B107"); + assert_eq!( + errors[0].message, + "Native Rust Interop declaration set is unsupported: scalar value signature required" + ); + } + + #[test] + fn multi_export_contract_binds_global_capabilities_and_import_table_exactly() { + assert_eq!( + replay_symbol_hash("interop.add"), + "ee967df46a76c68f1e8650d38ddb6886c897b34a82c4ea48ed3f70788e911326" + ); + assert_eq!( + replay_capabilities_digest(&["host.math".to_owned()]), + "sha256:d510605f56f47934126eeac931a6b363d7da36f492af90bb36ff573b00fb7d84" + ); + let source = r#"module interop.disjoint; + +permit { cap.a, cap.b } + +@id("host.a") +interface HostA permits { cap.a } { + @id("host.a.call") + import rust fn call_a(value: i64) -> i64 + effects { cap.a } + failure infallible; +} + +@id("host.b") +interface HostB permits { cap.b } { + @id("host.b.call") + import rust fn call_b(value: i64) -> i64 + effects { cap.b } + failure infallible; +} + +@id("export.a") +fn export_a(value: i64) -> i64 uses { cap.a } { call_a(value) } + +@id("export.b") +fn export_b(value: i64) -> i64 uses { cap.b } { call_b(value) } + +@id("interop.disjoint.main") +fn main() -> i64 { 0 } +"#; + let program = crate::parse(source, Path::new("native-rust-disjoint.spx")).unwrap(); + let canonical = crate::format::canonical(&program); + let spec = Spec { + module: program.module.clone(), + source_revision: domain_digest(SOURCE_DOMAIN, canonical.as_bytes()), + target: current_target().unwrap(), + exports: vec!["export.a".to_owned(), "export.b".to_owned()], + imports: vec!["host.a.call".to_owned(), "host.b.call".to_owned()], + capabilities: vec!["cap.a".to_owned(), "cap.b".to_owned()], + }; + let prepared = + prepare_native_rust_interop(&program, render_spec(&spec).as_bytes()).unwrap(); + for export in &prepared.exports { + assert_eq!(export.capabilities, spec.capabilities); + assert_eq!(export.required_imports, spec.imports); + } + assert_eq!( + prepared + .descriptor + .matches("\"required_imports\":[\"host.a.call\",\"host.b.call\"]") + .count(), + 2 + ); + assert!(prepared + .generated_rust + .contains("const EXPECTED_CAPABILITIES:&[&str]=&[\"cap.a\",\"cap.b\"]")); + assert!(prepared.generated_c.contains("spxnr_validate_import_")); + + let first = call_digest( + "export", + "delimiter.test", + &[], + ScalarType::I64, + &[], + &[], + &["a,b".to_owned(), "c".to_owned()], + &[("a,b".to_owned(), "sha256:first".to_owned())], + "status", + 0, + &spec.target, + ) + .unwrap(); + let second = call_digest( + "export", + "delimiter.test", + &[], + ScalarType::I64, + &[], + &[], + &["a".to_owned(), "b,c".to_owned()], + &[("a".to_owned(), "sha256:first".to_owned())], + "status", + 0, + &spec.target, + ) + .unwrap(); + assert_ne!(first, second); + + let import_i64 = call_digest( + "import", + "same.id", + &[ParameterFact { + name: "value".to_owned(), + ty: ScalarType::I64, + }], + ScalarType::I64, + &[], + &[], + &[], + &[], + "infallible", + 0, + &spec.target, + ) + .unwrap(); + let import_bool = call_digest( + "import", + "same.id", + &[ParameterFact { + name: "value".to_owned(), + ty: ScalarType::Bool, + }], + ScalarType::I64, + &[], + &[], + &[], + &[], + "infallible", + 0, + &spec.target, + ) + .unwrap(); + assert_ne!(import_i64, import_bool); + let export_for_i64 = call_digest( + "export", + "export.same", + &[], + ScalarType::I64, + &[], + &[], + &["same.id".to_owned()], + &[("same.id".to_owned(), import_i64)], + "status", + 0, + &spec.target, + ) + .unwrap(); + let export_for_bool = call_digest( + "export", + "export.same", + &[], + ScalarType::I64, + &[], + &[], + &["same.id".to_owned()], + &[("same.id".to_owned(), import_bool)], + "status", + 0, + &spec.target, + ) + .unwrap(); + assert_ne!(export_for_i64, export_for_bool); + } + + #[test] + fn private_a_is_canonical_and_pure() { + let (program, spec) = fixture(); + let prepared = prepare_native_rust_interop(&program, spec.as_bytes()).unwrap(); + assert_eq!(prepared.canonical_spec, spec); + assert!(prepared.descriptor.ends_with('\n')); + assert!(prepared.generated_c.contains("spxnr1_i_")); + assert!(prepared.generated_header.contains("spxnr_context_v1")); + assert!(prepared + .generated_rust + .starts_with("mod api{#![forbid(unsafe_code)]")); + assert!(prepared + .private_ffi_source + .starts_with("#![allow(unsafe_code)]")); + } + + #[test] + fn source_descriptor_and_generated_views_reconstruct_from_authenticated_facts() { + let (program, spec_source) = fixture(); + let prepared = prepare_native_rust_interop(&program, spec_source.as_bytes()).unwrap(); + let spec = parse_spec(&program, spec_source.as_bytes()).unwrap(); + let status_domains = prepared + .imports + .iter() + .filter_map(|import| import.failure.clone()) + .collect::>() + .into_iter() + .collect::>(); + let reconstructed = render_descriptor( + &spec, + &prepared.hir_digest, + &status_domains, + &prepared.exports, + &prepared.imports, + ) + .unwrap(); + assert_eq!(reconstructed, prepared.descriptor); + assert!(reconstructed.contains( + "\"status_domains\":[{\"ordinal\":0,\"domain_id\":\"success\"},{\"ordinal\":1,\"domain_id\":\"host.math.v1\"},{\"ordinal\":65533,\"domain_id\":\"semaprax.native-rust-semantics.v1\"},{\"ordinal\":65534,\"domain_id\":\"semaprax.native-rust-host.v1\"},{\"ordinal\":65535,\"domain_id\":\"semaprax.native-rust-adapter.v1\"}]" + )); + assert!(reconstructed.contains("\"status_domain_ordinals\":[1,65533,65534,65535]")); + assert_eq!( + domain_digest(DESCRIPTOR_DIGEST_DOMAIN, reconstructed.as_bytes()), + prepared.descriptor_digest + ); + assert_eq!( + domain_digest(SOURCE_DOMAIN, crate::format::canonical(&program).as_bytes()), + prepared.source_revision + ); + replay_descriptor( + &reconstructed, + &spec, + &prepared.hir_digest, + &prepared.exports, + &prepared.imports, + ) + .unwrap(); + replay_generated( + &prepared.generated_header, + &prepared.generated_c, + &prepared.generated_rust, + &prepared.private_ffi_source, + ) + .unwrap(); + + let changed_source = SOURCE.replacen("host_add(left, right)", "host_add(right, left)", 1); + let changed = crate::parse( + &changed_source, + Path::new("native-rust-interop-changed.spx"), + ) + .unwrap(); + let stale = match prepare_native_rust_interop(&changed, spec_source.as_bytes()) { + Ok(_) => panic!("stale source binding was accepted"), + Err(error) => error, + }; + assert_eq!(stale.len(), 1); + assert_eq!(stale[0].code, "SPX-B107"); + assert_eq!( + stale[0].message, + "Native Rust Interop declaration set is unsupported: selected identity missing" + ); + + let mut changed_spec = spec; + changed_spec.source_revision = + domain_digest(SOURCE_DOMAIN, crate::format::canonical(&changed).as_bytes()); + let changed_prepared = + prepare_native_rust_interop(&changed, render_spec(&changed_spec).as_bytes()).unwrap(); + assert_ne!(changed_prepared.source_revision, prepared.source_revision); + assert_ne!(changed_prepared.hir_digest, prepared.hir_digest); + assert_ne!( + changed_prepared.descriptor_digest, + prepared.descriptor_digest + ); + assert_ne!(changed_prepared.generated_c, prepared.generated_c); + } + + #[test] + fn descriptor_and_generated_source_replay_reject_every_bound_family() { + let (program, spec_source) = fixture(); + let prepared = prepare_native_rust_interop(&program, spec_source.as_bytes()).unwrap(); + let spec = parse_spec(&program, spec_source.as_bytes()).unwrap(); + let resolved = hir::resolve(&program).unwrap(); + let (closure, _) = selected_closure(&resolved, &spec.exports).unwrap(); + + let descriptor_mutations = [ + prepared.descriptor.replacen( + "\"module\":\"interop.fixture\"", + "\"module\":\"interop.forgery\"", + 1, + ), + prepared + .descriptor + .replacen(&prepared.source_revision, "sha256:forged-source", 1), + prepared + .descriptor + .replacen(&prepared.hir_digest, "sha256:forged-hir", 1), + prepared + .descriptor + .replacen("\"pointer_width\":64", "\"pointer_width\":32", 1), + prepared + .descriptor + .replacen("\"ordinal\":65533", "\"ordinal\":65532", 1), + prepared.descriptor.replacen( + "\"calling_convention\":\"C\"", + "\"calling_convention\":\"X\"", + 1, + ), + prepared + .descriptor + .replacen("\"id\":\"interop.add\"", "\"id\":\"interop.bad\"", 1), + prepared + .descriptor + .replacen("\"id\":\"host.add\"", "\"id\":\"host.bad\"", 1), + prepared + .descriptor + .replacen("\"max_exports\":32", "\"max_exports\":31", 1), + prepared.descriptor.replacen( + "no_resource_owned_borrow_shared_or_aggregate_abi", + "xo_resource_owned_borrow_shared_or_aggregate_abi", + 1, + ), + prepared.descriptor.trim_end().to_owned(), + ]; + for (index, mutation) in descriptor_mutations.into_iter().enumerate() { + let error = replay_descriptor( + &mutation, + &spec, + &prepared.hir_digest, + &prepared.exports, + &prepared.imports, + ) + .unwrap_err(); + assert_eq!(error.code, "SPX-B108", "mutation {index}"); + assert_eq!( + error.message, + "Native Rust Interop descriptor disagrees with validated source and HIR", + "mutation {index}" + ); + } + + let generated_mutations = [ + ( + prepared.generated_header.replacen("#ifndef", "#ifndez", 1), + prepared.generated_c.clone(), + prepared.generated_rust.clone(), + prepared.private_ffi_source.clone(), + ), + ( + prepared.generated_header.clone(), + prepared.generated_c.replacen("#include", "#includx", 1), + prepared.generated_rust.clone(), + prepared.private_ffi_source.clone(), + ), + ( + prepared.generated_header.clone(), + prepared.generated_c.clone(), + prepared.generated_rust.replacen("forbid", "forbia", 1), + prepared.private_ffi_source.clone(), + ), + ( + prepared.generated_header.clone(), + prepared.generated_c.clone(), + prepared.generated_rust.clone(), + prepared.private_ffi_source.replacen("allow", "allox", 1), + ), + ]; + for (index, (header, c, rust, ffi)) in generated_mutations.into_iter().enumerate() { + let error = replay_generated_exact( + &spec, + &closure, + &prepared.exports, + &prepared.imports, + &header, + &c, + &rust, + &ffi, + ) + .unwrap_err(); + assert_eq!(error.code, "SPX-B111", "generated mutation {index}"); + assert_eq!( + error.message, "Native Rust Interop generated artifact replay failed", + "generated mutation {index}" + ); + } + } + + fn each_byte_edit(label: &str, value: &str, mut reject: impl FnMut(&str) -> bool) { + let bytes = value.as_bytes(); + for index in 0..bytes.len() { + let mut mutation = bytes.to_vec(); + mutation[index] = match mutation[index] { + b'x' => b'y', + byte if byte.is_ascii() => b'x', + _ => continue, + }; + let Ok(mutation) = String::from_utf8(mutation) else { + continue; + }; + assert!( + reject(&mutation), + "{label} substitution at byte {index} was accepted" + ); + let mut deletion = bytes.to_vec(); + deletion.remove(index); + assert!( + reject(std::str::from_utf8(&deletion).unwrap()), + "{label} deletion at byte {index} was accepted" + ); + } + for index in 0..=bytes.len() { + let mut insertion = bytes.to_vec(); + insertion.insert(index, b'x'); + assert!( + reject(std::str::from_utf8(&insertion).unwrap()), + "{label} insertion at byte {index} was accepted" + ); + } + for index in 0..bytes.len() { + assert!( + reject(std::str::from_utf8(&bytes[..index]).unwrap()), + "{label} truncation at byte {index} was accepted" + ); + } + } + + #[test] + fn exact_replayers_reject_every_generated_and_descriptor_byte_substitution() { + let (program, spec_source) = fixture(); + let prepared = prepare_native_rust_interop(&program, spec_source.as_bytes()).unwrap(); + let spec = parse_spec(&program, spec_source.as_bytes()).unwrap(); + let resolved = hir::resolve(&program).unwrap(); + let (closure, _) = selected_closure(&resolved, &spec.exports).unwrap(); + + each_byte_edit("spec", &spec_source, |mutation| { + !replay_spec_bytes_exact(mutation, &spec) + }); + + each_byte_edit("descriptor", &prepared.descriptor, |mutation| { + replay_descriptor( + mutation, + &spec, + &prepared.hir_digest, + &prepared.exports, + &prepared.imports, + ) + .is_err() + }); + + let artifacts = [ + (0, prepared.generated_header.as_str()), + (1, prepared.generated_c.as_str()), + (2, prepared.generated_rust.as_str()), + (3, prepared.private_ffi_source.as_str()), + ]; + for (selected, artifact) in artifacts { + each_byte_edit("generated", artifact, |mutation| { + let mut values = [ + prepared.generated_header.as_str(), + prepared.generated_c.as_str(), + prepared.generated_rust.as_str(), + prepared.private_ffi_source.as_str(), + ]; + values[selected] = mutation; + replay_generated_exact( + &spec, + &closure, + &prepared.exports, + &prepared.imports, + values[0], + values[1], + values[2], + values[3], + ) + .is_err() + }); + } + + let files = [("descriptor.json", prepared.descriptor.as_bytes())]; + let rustc = RustcVersion::from_fields([ + "1.0.0", + "0123456789abcdef", + &prepared.target.triple, + "20.0.0", + ]); + let manifest = render_manifest( + &prepared, + &files, + "/held/clang", + "clang version 20.0.0", + &rustc, + &prepared.target.triple, + ); + each_byte_edit("manifest", &manifest, |mutation| { + !replay_manifest_bytes_exact( + mutation, + &prepared, + &files, + "/held/clang", + "clang version 20.0.0", + &rustc, + &prepared.target.triple, + ) + }); + } + + #[test] + fn manifest_fixed_names_and_streaming_cursor_work_are_exact() { + assert_eq!( + canonical_manifest_file_names(), + [ + "descriptor.json", + "module.c", + if cfg!(windows) { + "module.obj" + } else { + "module.o" + }, + "semaprax_native_rust_interop.h", + "semaprax_native_rust_interop.rs", + "semaprax_native_rust_interop_ffi.rs", + ] + ); + + let assert_linear = |encoded: &str, decoded: &str| { + let mut cursor = ManifestCursor::new(encoded).unwrap(); + cursor.string_eq(decoded).unwrap(); + let work = cursor.finish().unwrap(); + assert_eq!(work, encoded.len()); + assert!(work <= encoded.len().checked_mul(2).unwrap()); + }; + { + let decoded = "a".repeat(MAX_MANIFEST_BYTES - 2); + let mut encoded = String::with_capacity(MAX_MANIFEST_BYTES); + encoded.push('"'); + encoded.push_str(&decoded); + encoded.push('"'); + assert_linear(&encoded, &decoded); + } + { + let decoded = "é".repeat((MAX_MANIFEST_BYTES - 2) / 2); + let mut encoded = String::with_capacity(MAX_MANIFEST_BYTES); + encoded.push('"'); + encoded.push_str(&decoded); + encoded.push('"'); + assert_linear(&encoded, &decoded); + } + { + let characters = (MAX_MANIFEST_BYTES - 2) / 6; + let decoded = "a".repeat(characters); + let mut encoded = String::with_capacity(characters * 6 + 2); + encoded.push('"'); + for _ in 0..characters { + encoded.push_str("\\u0061"); + } + encoded.push('"'); + assert_linear(&encoded, &decoded); + } + + for malformed in [ + "\"\\ud800\"", + "\"\\udc00\"", + "\"\\ud800\\u0000\"", + "\"\\x\"", + ] { + let mut cursor = ManifestCursor::new(malformed).unwrap(); + assert!(cursor.string_eq("x").is_err()); + } + let mut leading_zero = ManifestCursor::new("01").unwrap(); + assert!(leading_zero.usize_eq(1).is_err()); + let overflow = "9".repeat(usize::BITS as usize + 2); + let mut overflow = ManifestCursor::new(&overflow).unwrap(); + assert!(overflow.usize_eq(usize::MAX).is_err()); + } + + #[test] + fn six_output_artifact_known_answer_vectors_are_frozen() { + fn independent_sha256(bytes: &[u8]) -> String { + format!("sha256:{:x}", Sha256::digest(bytes)) + } + + fn independent_domain_sha256(domain: &[u8], bytes: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(domain); + hasher.update(bytes); + format!("sha256:{:x}", hasher.finalize()) + } + + fn assert_raw_kat(name: &str, bytes: &[u8], length: usize, digest: &str) { + assert_eq!(bytes.len(), length, "{name} byte length changed"); + assert_eq!( + independent_sha256(bytes), + digest, + "{name} raw SHA-256 changed" + ); + } + + with_test_target( + Target { + triple: "x86_64-unknown-linux-gnu".to_owned(), + pointer_width: 64, + endian: "little".to_owned(), + panic_strategy: "unwind".to_owned(), + thread_policy: "same_thread".to_owned(), + }, + || { + let (program, spec_source) = fixture(); + let prepared = + prepare_native_rust_interop(&program, spec_source.as_bytes()).unwrap(); + let object = b"semaprax-native-rust-interop-kat-object-v1"; + let files = [ + ("descriptor.json", prepared.descriptor.as_bytes()), + ("module.c", prepared.generated_c.as_bytes()), + ("module.o", object.as_slice()), + ( + "semaprax_native_rust_interop.h", + prepared.generated_header.as_bytes(), + ), + ( + "semaprax_native_rust_interop.rs", + prepared.generated_rust.as_bytes(), + ), + ( + "semaprax_native_rust_interop_ffi.rs", + prepared.private_ffi_source.as_bytes(), + ), + ]; + let rustc = RustcVersion::from_fields([ + "1.88.0", + "0123456789abcdef", + &prepared.target.triple, + "20.1.0", + ]); + let manifest = render_manifest( + &prepared, + &files, + "/authenticated/clang", + "clang version 20.1.0", + &rustc, + &prepared.target.triple, + ); + assert_raw_kat( + "descriptor.json", + prepared.descriptor.as_bytes(), + 4_498, + "sha256:603c609409a2e35ee524481aa2225c6f0c6557dbff7d9650df8057daebcf173c", + ); + assert_raw_kat( + "semaprax.native-rust-interop.json", + manifest.as_bytes(), + 3_493, + "sha256:e8652276a9ea4489c5758aaa1e24456c9b3be96538384a125c8f41fb714b73ca", + ); + assert_raw_kat( + "semaprax_native_rust_interop.h", + prepared.generated_header.as_bytes(), + 870, + "sha256:3ebdf5567d93b9e24ccdea5a0bb76d83b7bdcc44721e2a65846f83f1c92ace3b", + ); + assert_raw_kat( + "module.c", + prepared.generated_c.as_bytes(), + 4_124, + "sha256:1f1640553fe746b0c2baef87b76ce1013ee2ac9f8ee1bc9209dae6b4ccbb3e61", + ); + assert_raw_kat( + "semaprax_native_rust_interop.rs", + prepared.generated_rust.as_bytes(), + 2_100, + "sha256:b75eb57f911ea274cd1ae5fb1a4b789f58008613d027c94d904c90d3085e2d62", + ); + assert_raw_kat( + "semaprax_native_rust_interop_ffi.rs", + prepared.private_ffi_source.as_bytes(), + 4_719, + "sha256:f317bef66a0ac44ba4ba89862ae645383f7d48668277f6a8fa559ada8fc4ff9a", + ); + + let descriptor_domain = + "sha256:d10e85e8fefed377df137ac22791099a702b460ed31ea3c65a6061b222e0c7ba"; + assert_eq!(prepared.descriptor_digest, descriptor_domain); + assert_eq!( + independent_domain_sha256( + DESCRIPTOR_DIGEST_DOMAIN, + prepared.descriptor.as_bytes() + ), + descriptor_domain + ); + assert_eq!( + independent_domain_sha256(BUNDLE_DIGEST_DOMAIN, manifest.as_bytes()), + "sha256:4fbab384e26a272eb02166bc02aeb59f03cabfc92d5d547854b124d7eaf813bf" + ); + assert!(replay_manifest_bytes_exact( + &manifest, + &prepared, + &files, + "/authenticated/clang", + "clang version 20.1.0", + &rustc, + &prepared.target.triple, + )); + assert!(replay_manifest_semantic( + &manifest, + &prepared, + &files, + "/authenticated/clang", + "clang version 20.1.0", + &rustc, + &prepared.target.triple, + ) + .is_ok()); + + let escaped = + manifest.replacen("clang version 20.1.0", "clang\\u0020version 20.1.0", 1); + assert!(replay_manifest_semantic( + &escaped, + &prepared, + &files, + "/authenticated/clang", + "clang version 20.1.0", + &rustc, + &prepared.target.triple, + ) + .is_ok()); + assert!(!replay_manifest_bytes_exact( + &escaped, + &prepared, + &files, + "/authenticated/clang", + "clang version 20.1.0", + &rustc, + &prepared.target.triple, + )); + + let malformed = [ + manifest.replacen("{\"schema\":", "{\"schema\":\"duplicate\",\"schema\":", 1), + manifest.replacen("{\"schema\":", "{\"unknown\":0,\"schema\":", 1), + manifest.replacen("{\"schema\":", "{\"missing_schema\":", 1), + manifest.replacen("\"bytes\":4498", "\"bytes\":\"4498\"", 1), + manifest.replacen("\"descriptor\":{", "\"descriptor\":[{", 1), + format!("{manifest}trailing"), + ]; + for hostile in malformed { + assert!(replay_manifest_semantic( + &hostile, + &prepared, + &files, + "/authenticated/clang", + "clang version 20.1.0", + &rustc, + &prepared.target.triple, + ) + .is_err()); + } + }, + ); + } + + #[test] + fn cumulative_builder_limit_is_exact_and_cannot_be_widened() { + let (program, spec) = fixture(); + let (mut low, mut high) = (0_usize, MAX_BUILDER_BYTES); + while low < high { + let middle = low + (high - low) / 2; + if prepare_native_rust_interop_with_test_limit(&program, spec.as_bytes(), middle) + .is_ok() + { + high = middle; + } else { + low = middle + 1; + } + } + let minimum = low; + assert!(minimum > 0 && minimum <= MAX_BUILDER_BYTES); + let prepared = + prepare_native_rust_interop_with_test_limit(&program, spec.as_bytes(), minimum) + .unwrap(); + assert_eq!(prepared.canonical_spec, spec); + let error = match prepare_native_rust_interop_with_test_limit( + &program, + spec.as_bytes(), + minimum - 1, + ) { + Ok(_) => panic!("one-under builder limit was accepted"), + Err(error) => error, + }; + assert_eq!(error.len(), 1); + assert_eq!(error[0].code, "SPX-B109"); + assert_eq!( + error[0].message, + "Native Rust Interop max_builder_bytes exceeds 33554432" + ); + + let widened = std::panic::catch_unwind(|| { + let _ = prepare_native_rust_interop_with_test_limit( + &program, + spec.as_bytes(), + MAX_BUILDER_BYTES + 1, + ); + }); + assert!(widened.is_err()); + } + + #[test] + fn full_bundle_builder_limit_is_cumulative_exact_and_cannot_be_widened() { + let (program, spec) = fixture(); + let probe = |limit: usize, nonce: usize| { + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-builder-probe-{}-{nonce}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + let result = build_native_rust_interop_bundle_with_test_limit( + &program, + spec.as_bytes(), + &root.join("bundle"), + limit, + ); + std::fs::remove_dir_all(&root).unwrap(); + result.map(|_| ()) + }; + + let (mut low, mut high, mut nonce) = (0_usize, MAX_BUILDER_BYTES, 0_usize); + while low < high { + let middle = low + (high - low) / 2; + nonce += 1; + match probe(middle, nonce) { + Ok(()) => high = middle, + Err(error) => { + assert_eq!(error.len(), 1); + assert_eq!(error[0].code, "SPX-B109"); + assert_eq!( + error[0].message, + "Native Rust Interop max_builder_bytes exceeds 33554432" + ); + low = middle + 1; + } + } + } + let minimum = low; + assert!(minimum > 0 && minimum <= MAX_BUILDER_BYTES); + probe(minimum, nonce + 1).unwrap(); + let error = probe(minimum - 1, nonce + 2).unwrap_err(); + assert_eq!(error.len(), 1); + assert_eq!(error[0].code, "SPX-B109"); + assert_eq!( + error[0].message, + "Native Rust Interop max_builder_bytes exceeds 33554432" + ); + + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-builder-widen-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + let widened = std::panic::catch_unwind(|| { + let _ = build_native_rust_interop_bundle_with_test_limit( + &program, + spec.as_bytes(), + &root.join("bundle"), + MAX_BUILDER_BYTES + 1, + ); + }); + std::fs::remove_dir_all(&root).unwrap(); + assert!(widened.is_err()); + } + + #[test] + fn phase_b_local_paths_digest_and_stage_names_are_frozen_before_effects() { + let output = Path::new("phase-b-bundle"); + let mut pending = PendingBundleFacts::new(output, "module.o").unwrap(); + pending.bind_manifest_digest(b"manifest\n").unwrap(); + let facts = pending.finish(); + assert_eq!(facts.output_directory, output); + assert_eq!(facts.object_path, output.join("module.o")); + assert_eq!(facts.descriptor_path, output.join("descriptor.json")); + assert_eq!( + facts.manifest_path, + output.join("semaprax.native-rust-interop.json") + ); + assert_eq!( + facts.manifest_digest, + domain_digest(BUNDLE_DIGEST_DOMAIN, b"manifest\n") + ); + + let parent = Path::new("phase-b-parent"); + let descriptor = "sha256:phase-b-descriptor"; + let mut slot = StageSlot::new(parent, descriptor, "publish").unwrap(); + let name_capacity = slot.name.capacity(); + let path_capacity = slot.path.capacity(); + slot.prepare(parent, 0).unwrap(); + assert_eq!( + slot.name, + format!( + ".semaprax-native-rust-interop-publish-{}-{}-0", + std::process::id(), + &full_hash(descriptor)[..16] + ) + ); + assert_eq!(slot.path, parent.join(&slot.name)); + slot.prepare(parent, 1023).unwrap(); + assert!(slot.name.ends_with("-1023")); + assert_eq!(slot.name.capacity(), name_capacity); + assert_eq!(slot.path.capacity(), path_capacity); + } + + #[test] + fn phase_b_rejects_non_component_output_before_build_hooks() { + let (program, spec) = fixture(); + let mut hooks = 0usize; + let error = match build_native_rust_interop_bundle_with_hook( + &program, + spec.as_bytes(), + Path::new("/"), + |_, _, _, _| hooks += 1, + ) { + Ok(_) => panic!("non-component output unexpectedly reached Phase B"), + Err(error) => error, + }; + assert_eq!(hooks, 0); + assert_eq!(error.len(), 1); + assert_eq!(error[0].code, "SPX-I232"); + assert_eq!( + error[0].message, + "Native Rust Interop output publication failed" + ); + } + + #[test] + fn phase_b_output_exists_precedes_invalid_tool_environment_without_tool_activity() { + let (program, spec) = fixture(); + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-output-precedes-tools-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + let output = root.join("bundle"); + std::fs::create_dir(&output).unwrap(); + std::fs::write(output.join("sentinel"), b"foreign").unwrap(); + PHASE_B_OUTPUT_PROBES.with(|count| count.set(0)); + PHASE_B_TOOL_HOLDS.with(|count| count.set(0)); + PHASE_B_TOOL_PROCESSES.with(|count| count.set(0)); + PHASE_B_INVALID_TOOL_ENV_INJECTION.with(|injection| injection.set(true)); + let mut hooks = 0usize; + let result = build_native_rust_interop_bundle_with_hook( + &program, + spec.as_bytes(), + &output, + |_, _, _, _| hooks += 1, + ); + PHASE_B_INVALID_TOOL_ENV_INJECTION.with(|injection| injection.set(false)); + let error = match result { + Ok(_) => panic!("existing output unexpectedly allowed the invalid tool environment"), + Err(error) => error, + }; + assert_eq!(error.len(), 1); + assert_eq!(error[0].code, "SPX-I232"); + assert_eq!(error[0].message, PHASE_B_PUBLICATION_MESSAGE); + assert_eq!( + error[0].message.as_ptr() as usize, + PHASE_B_PREPARED_CARRIER_IDENTITIES.with(std::cell::Cell::get) + [PhaseBLocalError::Publication.index()], + ); + assert_eq!(PHASE_B_OUTPUT_PROBES.with(std::cell::Cell::get), 1); + assert_eq!(PHASE_B_TOOL_HOLDS.with(std::cell::Cell::get), 0); + assert_eq!(PHASE_B_TOOL_PROCESSES.with(std::cell::Cell::get), 0); + assert_eq!(hooks, 0); + assert_eq!(std::fs::read(output.join("sentinel")).unwrap(), b"foreign"); + std::fs::remove_dir_all(&root).unwrap(); + } + + #[test] + fn phase_b_invalid_frozen_tool_environment_fails_after_stages_before_hold_or_spawn() { + let (program, spec) = fixture(); + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-invalid-frozen-tools-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + PHASE_B_DISCARD_ATTEMPTS.with(|attempts| attempts.set(0)); + PHASE_B_OUTPUT_PROBES.with(|count| count.set(0)); + PHASE_B_TOOL_HOLDS.with(|count| count.set(0)); + PHASE_B_TOOL_PROCESSES.with(|count| count.set(0)); + PHASE_B_INVALID_TOOL_ENV_INJECTION.with(|injection| injection.set(true)); + let mut hooks = 0usize; + let result = build_native_rust_interop_bundle_with_hook( + &program, + spec.as_bytes(), + &root.join("bundle"), + |_, _, _, _| hooks += 1, + ); + PHASE_B_INVALID_TOOL_ENV_INJECTION.with(|injection| injection.set(false)); + let error = match result { + Ok(_) => panic!("invalid frozen tool environment unexpectedly authenticated"), + Err(error) => error, + }; + assert_eq!(error.len(), 1); + assert_eq!(error[0].code, "SPX-B110"); + assert_eq!(error[0].message, PHASE_B_UNSUPPORTED_MESSAGE); + assert_eq!( + error[0].message.as_ptr() as usize, + PHASE_B_PREPARED_CARRIER_IDENTITIES.with(std::cell::Cell::get) + [PhaseBLocalError::Unsupported.index()], + ); + assert_eq!(PHASE_B_OUTPUT_PROBES.with(std::cell::Cell::get), 1); + assert_eq!(PHASE_B_TOOL_HOLDS.with(std::cell::Cell::get), 0); + assert_eq!(PHASE_B_TOOL_PROCESSES.with(std::cell::Cell::get), 0); + assert_eq!(PHASE_B_DISCARD_ATTEMPTS.with(std::cell::Cell::get), 2); + assert_eq!(hooks, 0); + assert_eq!(std::fs::read_dir(&root).unwrap().count(), 0); + std::fs::remove_dir_all(&root).unwrap(); + } + + #[test] + fn phase_b_direct_rustc_fixed_point_mismatch_is_b110_before_artifact_processes() { + let (program, spec) = fixture(); + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-direct-fixed-point-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + PHASE_B_DISCARD_ATTEMPTS.with(|attempts| attempts.set(0)); + PHASE_B_TOOL_PROCESSES.with(|count| count.set(0)); + PHASE_B_BUILD_INVOCATION_CONSUMPTIONS.with(|count| count.set(0)); + PHASE_B_DIRECT_SYSROOT_MISMATCH_INJECTION.with(|injection| injection.set(true)); + let mut hooks = 0usize; + let result = build_native_rust_interop_bundle_with_hook( + &program, + spec.as_bytes(), + &root.join("bundle"), + |_, _, _, _| hooks += 1, + ); + PHASE_B_DIRECT_SYSROOT_MISMATCH_INJECTION.with(|injection| injection.set(false)); + let error = match result { + Ok(_) => panic!("mismatched direct rustc sysroot unexpectedly admitted"), + Err(error) => error, + }; + assert_eq!(error.len(), 1); + assert_eq!(error[0].code, "SPX-B110"); + assert_eq!(error[0].message, PHASE_B_UNSUPPORTED_MESSAGE); + assert_eq!(PHASE_B_TOOL_PROCESSES.with(std::cell::Cell::get), 2); + assert_eq!( + PHASE_B_BUILD_INVOCATION_CONSUMPTIONS.with(std::cell::Cell::get), + 0 + ); + assert_eq!(PHASE_B_DISCARD_ATTEMPTS.with(std::cell::Cell::get), 2); + assert_eq!(hooks, 0); + assert_eq!(std::fs::read_dir(&root).unwrap().count(), 0); + std::fs::remove_dir_all(&root).unwrap(); + } + + #[test] + fn phase_b_fixed_rustc_version_parser_is_no_growth_at_representative_and_maximum() { + for source in [ + String::from( + "rustc 1.88.0 (012345678 2026-01-01)\nbinary: rustc\ncommit-hash: 0123456789abcdef\ncommit-date: 2026-01-01\nhost: x86_64-unknown-linux-gnu\nrelease: 1.88.0\nLLVM version: 20.1.0", + ), + format!( + "rustc 1.88.0 (012345678 2026-01-01)\nbinary: rustc\ncommit-hash: 0123456789abcdef\ncommit-date: 2026-01-01\nhost: x86_64-unknown-linux-gnu\nrelease: 1.88.0\nLLVM version: {}", + "1".repeat(PHASE_B_TOOL_VERSION_CAPACITY - 200) + ), + ] { + assert!(source.len() <= PHASE_B_TOOL_VERSION_CAPACITY); + let mut parsed = RustcVersion::prepared().unwrap(); + let capacity = parsed.capacity(); + parse_rustc_version(&source, &mut parsed).unwrap(); + assert_eq!(parsed.capacity(), capacity); + assert_eq!(parsed.release(), "1.88.0"); + assert_eq!(parsed.commit_hash(), "0123456789abcdef"); + assert_eq!(parsed.host(), "x86_64-unknown-linux-gnu"); + } + + let exact_first = "r".repeat(PHASE_B_TOOL_VERSION_CAPACITY - 3); + let mut exact = RustcVersion::prepared().unwrap(); + let exact_pointer = exact.storage.as_ptr(); + exact.store([&exact_first, "c", "h", "l"]).unwrap(); + assert_eq!(exact.storage.len(), PHASE_B_TOOL_VERSION_CAPACITY); + assert_eq!(exact.storage.capacity(), PHASE_B_TOOL_VERSION_CAPACITY); + assert_eq!(exact.storage.as_ptr(), exact_pointer); + + let overflow_first = "r".repeat(PHASE_B_TOOL_VERSION_CAPACITY - 2); + let mut overflow = RustcVersion::prepared().unwrap(); + let overflow_pointer = overflow.storage.as_ptr(); + assert_eq!( + overflow.store([&overflow_first, "c", "h", "l"]), + Err(PhaseBLocalError::Unsupported), + ); + assert!(overflow.storage.is_empty()); + assert_eq!(overflow.boundaries, [0; 5]); + assert_eq!(overflow.storage.capacity(), PHASE_B_TOOL_VERSION_CAPACITY); + assert_eq!(overflow.storage.as_ptr(), overflow_pointer); + + for invalid in [ + "rustc 1.88.0\nrelease: 1.88.0\nrelease: 1.88.0\ncommit-hash: 0123456\nhost: h\nLLVM version: 1", + "rustc 1.88.0\nrelease: 1.88.0\ncommit-hash: 0123456\nhost: h\nunknown: value\nLLVM version: 1", + "rustc 1.88.0\nrelease: 1.88.0\ncommit-hash: 0123456\nhost: h", + ] { + let mut parsed = RustcVersion::prepared().unwrap(); + assert_eq!( + parse_rustc_version(invalid, &mut parsed), + Err(PhaseBLocalError::Unsupported), + ); + } + } + + #[test] + fn phase_b_process_arena_reservation_precedes_materialization_source_contract() { + let source = include_str!("implementation.rs"); + let start = source.find("fn prepare_process_arena_authorized(").unwrap(); + let end = source[start..] + .find("#[cfg(test)]\nfn reset_phase_b_error_materialization_observer") + .map(|offset| start + offset) + .unwrap(); + let helper = &source[start..end]; + let plan = helper + .find("platform::prepare_process_arena_plan_with_environment(") + .unwrap(); + let required = helper + .find("platform::prepared_process_arena_plan_capacity(&plan)") + .unwrap(); + let reserve = helper.find("reserve_phase_b(required)?").unwrap(); + let allocate = helper + .find("platform::materialize_process_arena_with_environment(") + .unwrap(); + assert!(plan < required && required < reserve && reserve < allocate); + assert!(helper.contains("required > PHASE_B_PROCESS_ARENA_MAX_CAPACITY")); + assert!( + helper.contains("platform::prepared_process_arena_owned_capacity(&arena) != required") + ); + + let wrapper_start = source.find("struct AuthorizedProcessArena {").unwrap(); + let wrapper_end = source[wrapper_start..] + .find("struct PreparedToolchainPlan {") + .map(|offset| wrapper_start + offset) + .unwrap(); + let wrapper = &source[wrapper_start..wrapper_end]; + assert!(wrapper.find("arena:").unwrap() < wrapper.find("budget:").unwrap()); + assert!(wrapper.find("drop(arena)").unwrap() < wrapper.find("drop(budget)").unwrap()); + assert!( + !source[source.find("struct PreparedToolchainPlan {").unwrap()..] + .split("struct ToolchainFacts {") + .next() + .unwrap() + .contains("process_arena_budget") + ); + } + + #[test] + fn phase_b_process_arena_drops_bytes_before_authority_on_early_plan_failure() { + reset_phase_b_process_arena_drop_observer(); + let (plan, overflowed) = + crate::bounded_output::with_limit(MAX_BUILDER_BYTES, prepare_toolchain_plan); + assert!(!overflowed); + let plan = plan.unwrap(); + assert_eq!(PHASE_B_PROCESS_ARENA_DROPS.with(std::cell::Cell::get), 0); + assert_eq!( + PHASE_B_PROCESS_ARENA_BUDGET_DROPS.with(std::cell::Cell::get), + 0 + ); + drop(plan); + assert_eq!(PHASE_B_PROCESS_ARENA_DROPS.with(std::cell::Cell::get), 1); + assert_eq!( + PHASE_B_PROCESS_ARENA_BUDGET_DROPS.with(std::cell::Cell::get), + 1 + ); + assert_eq!( + PHASE_B_PROCESS_ARENA_DROP_ORDER.with(std::cell::Cell::get), + [1, 2] + ); + assert_eq!( + PHASE_B_PROCESS_ARENA_DROP_ORDER_LENGTH.with(std::cell::Cell::get), + 2 + ); + } + + #[cfg(windows)] + #[test] + fn phase_b_process_arena_exact_and_one_less_is_zero_effect() { + let include = OsStr::new(r"C:\sdk\include"); + let libraries = OsStr::new(r"C:\sdk\lib"); + let sizing = platform::prepare_process_arena_plan_with_environment( + PHASE_B_PROCESS_INVOCATIONS, + Some(include), + Some(libraries), + ) + .unwrap(); + let required = platform::prepared_process_arena_plan_capacity(&sizing); + assert!(required > 0 && required <= PHASE_B_PROCESS_ARENA_MAX_CAPACITY); + + let (exact, overflowed, used) = crate::bounded_output::with_limit_usage(required, || { + prepare_process_arena_authorized(Some(include), Some(libraries)) + }); + assert!(!overflowed); + let arena = exact.unwrap(); + assert_eq!(used, required); + assert_eq!(arena.authorized_capacity().unwrap(), required); + assert_eq!( + platform::prepared_process_arena_owned_capacity(arena.arena().unwrap()), + required + ); + assert_eq!( + platform::prepared_process_arena_remaining(arena.arena().unwrap()), + PHASE_B_PROCESS_INVOCATIONS + ); + drop(arena); + + PHASE_B_OUTPUT_PROBES.with(|count| count.set(0)); + PHASE_B_TOOL_HOLDS.with(|count| count.set(0)); + PHASE_B_TOOL_PROCESSES.with(|count| count.set(0)); + reset_phase_b_error_materialization_observer(); + let (one_less, overflowed) = crate::bounded_output::with_limit(required - 1, || { + prepare_process_arena_authorized(Some(include), Some(libraries)) + }); + assert!(!overflowed); + assert!(matches!(one_less, Err(PhaseBLocalError::BuilderBudget))); + assert_eq!(PHASE_B_OUTPUT_PROBES.with(std::cell::Cell::get), 0); + assert_eq!(PHASE_B_TOOL_HOLDS.with(std::cell::Cell::get), 0); + assert_eq!(PHASE_B_TOOL_PROCESSES.with(std::cell::Cell::get), 0); + assert_eq!( + PHASE_B_POST_EFFECT_ERROR_MATERIALIZATIONS.with(std::cell::Cell::get), + 0 + ); + assert!(!PHASE_B_EFFECT_STARTED.with(std::cell::Cell::get)); + } + + #[test] + fn phase_b_harness_is_exact_capacity_at_representative_and_maximum_and_one_less_is_pre_effect() + { + let (program, spec) = fixture(); + let prepared = prepare_native_rust_interop(&program, spec.as_bytes()).unwrap(); + let (representative, representative_budget) = prepare_rust_harness(&prepared).unwrap(); + assert_eq!(representative.len(), representative.capacity()); + assert_eq!(representative.len(), representative_budget.maximum()); + drop((representative, representative_budget)); + + let mut maximum = prepared; + let mut import = maximum.imports[0].clone(); + import.parameters = (0..MAX_PARAMETERS) + .map(|index| ParameterFact { + name: format!("p{index}"), + ty: ScalarType::I64, + }) + .collect(); + import.capabilities = (0..MAX_EFFECTS) + .map(|index| format!("capability.{index:02}")) + .collect(); + maximum.imports = vec![import; MAX_IMPORTS]; + let mut export = maximum.exports[0].clone(); + export.parameters = (0..MAX_PARAMETERS) + .map(|index| ParameterFact { + name: format!("p{index}"), + ty: ScalarType::I64, + }) + .collect(); + maximum.exports = vec![export; MAX_EXPORTS]; + let (harness, overflowed, exact) = + crate::bounded_output::with_limit_usage(MAX_BUILDER_BYTES, || { + prepare_rust_harness(&maximum) + }); + assert!(!overflowed); + let (harness, budget) = harness.unwrap(); + assert_eq!(harness.len(), harness.capacity()); + assert_eq!(harness.len(), budget.maximum()); + assert_eq!(exact, harness.len()); + drop((harness, budget)); + + PHASE_B_OUTPUT_PROBES.with(|count| count.set(0)); + PHASE_B_TOOL_HOLDS.with(|count| count.set(0)); + PHASE_B_TOOL_PROCESSES.with(|count| count.set(0)); + let (one_less, overflowed) = + crate::bounded_output::with_limit(exact - 1, || prepare_rust_harness(&maximum)); + assert!(!overflowed); + assert!(matches!(one_less, Err(PhaseBLocalError::BuilderBudget))); + assert_eq!(PHASE_B_OUTPUT_PROBES.with(std::cell::Cell::get), 0); + assert_eq!(PHASE_B_TOOL_HOLDS.with(std::cell::Cell::get), 0); + assert_eq!(PHASE_B_TOOL_PROCESSES.with(std::cell::Cell::get), 0); + } + + #[test] + fn phase_b_prepared_invocations_admit_linux_underscore_and_reject_other_punctuation_as_b110() { + let mut prepared = with_test_target( + Target { + triple: "x86_64-unknown-linux-gnu".to_owned(), + pointer_width: 64, + endian: "little".to_owned(), + panic_strategy: "unwind".to_owned(), + thread_policy: "same_thread".to_owned(), + }, + || { + let (program, spec) = fixture(); + prepare_native_rust_interop(&program, spec.as_bytes()).unwrap() + }, + ); + PHASE_B_BUILD_INVOCATION_PLANS.with(|count| count.set(0)); + let plans = prepare_build_invocations(&prepared, false).unwrap(); + assert_eq!(PHASE_B_BUILD_INVOCATION_PLANS.with(std::cell::Cell::get), 8); + drop(plans); + + prepared.target.triple = "x86_64-unknown/linux-gnu".to_owned(); + let error = match prepare_build_invocations(&prepared, false) { + Ok(_) => panic!("noncanonical target punctuation was admitted"), + Err(error) => error, + }; + assert_eq!(error, PhaseBLocalError::Unsupported); + } + + #[test] + fn phase_b_all_eight_build_invocations_are_prepared_bounded_and_consumed_once() { + let (program, spec) = fixture(); + let prepared = prepare_native_rust_interop(&program, spec.as_bytes()).unwrap(); + PHASE_B_BUILD_INVOCATION_PLANS.with(|count| count.set(0)); + PHASE_B_BUILD_INVOCATION_CONSUMPTIONS.with(|count| count.set(0)); + PHASE_B_LINK_COPY_PLANS.with(|count| count.set(0)); + PHASE_B_LINK_COPY_CONSUMPTIONS.with(|count| count.set(0)); + PHASE_B_INVENTORY_EXACT_PLANS.with(|count| count.set(0)); + PHASE_B_INVENTORY_EXACT_SCANS.with(|count| count.set(0)); + PHASE_B_PUBLISH_PLANS.with(|count| count.set(0)); + PHASE_B_PUBLISH_CONSUMPTIONS.with(|count| count.set(0)); + reset_phase_b_object_authority_observer(); + reset_phase_b_manifest_authority_observer(); + let plans = prepare_build_invocations(&prepared, false).unwrap(); + assert_eq!(PHASE_B_BUILD_INVOCATION_PLANS.with(std::cell::Cell::get), 8); + assert!( + platform::prepared_c_compile_owned_capacity(&plans.c_o0.0) <= plans.c_o0.1.maximum() + ); + assert!( + platform::prepared_c_compile_owned_capacity(&plans.c_o2.0) <= plans.c_o2.1.maximum() + ); + assert!( + platform::prepared_rust_compile_owned_capacity(&plans.rust.0) <= plans.rust.1.maximum() + ); + assert!( + platform::prepared_c_compile_owned_capacity(&plans.c_main.0) + <= plans.c_main.1.maximum() + ); + assert!( + platform::prepared_link_owned_capacity(&plans.link_o0.0) <= plans.link_o0.1.maximum() + ); + assert!(platform::prepared_run_owned_capacity(&plans.run_o0.0) <= plans.run_o0.1.maximum()); + assert!( + platform::prepared_link_owned_capacity(&plans.link_o2.0) <= plans.link_o2.1.maximum() + ); + assert!(platform::prepared_run_owned_capacity(&plans.run_o2.0) <= plans.run_o2.1.maximum()); + drop(plans); + + let publish = prepare_publish_discard_inventory().unwrap(); + let run = prepare_run_discard_inventory().unwrap(); + let copies = prepare_link_copies( + &publish, + &run, + if cfg!(windows) { + "module.obj" + } else { + "module.o" + }, + ) + .unwrap(); + assert_eq!(PHASE_B_LINK_COPY_PLANS.with(std::cell::Cell::get), 3); + for (prepared, budget) in [ + &copies.safe_rust, + &copies.private_ffi, + &copies.optimized_object, + ] { + assert_eq!( + platform::prepared_link_or_copy_owned_capacity(prepared), + budget.maximum() + ); + } + drop((copies, publish, run)); + + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-prepared-build-plans-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + PHASE_B_BUILD_INVOCATION_PLANS.with(|count| count.set(0)); + PHASE_B_BUILD_INVOCATION_CONSUMPTIONS.with(|count| count.set(0)); + PHASE_B_LINK_COPY_PLANS.with(|count| count.set(0)); + PHASE_B_LINK_COPY_CONSUMPTIONS.with(|count| count.set(0)); + build_native_rust_interop_bundle_with_hook( + &program, + spec.as_bytes(), + &root.join("bundle"), + |_, _, _, _| {}, + ) + .unwrap(); + assert_eq!(PHASE_B_BUILD_INVOCATION_PLANS.with(std::cell::Cell::get), 8); + assert_eq!( + PHASE_B_BUILD_INVOCATION_CONSUMPTIONS.with(std::cell::Cell::get), + 8 + ); + assert_eq!(PHASE_B_LINK_COPY_PLANS.with(std::cell::Cell::get), 3); + assert_eq!(PHASE_B_LINK_COPY_CONSUMPTIONS.with(std::cell::Cell::get), 3); + assert_eq!(PHASE_B_INVENTORY_EXACT_PLANS.with(std::cell::Cell::get), 1); + assert_eq!(PHASE_B_INVENTORY_EXACT_SCANS.with(std::cell::Cell::get), 2); + assert_eq!(PHASE_B_PUBLISH_PLANS.with(std::cell::Cell::get), 1); + assert_eq!(PHASE_B_PUBLISH_CONSUMPTIONS.with(std::cell::Cell::get), 1); + assert_eq!( + PHASE_B_POST_EFFECT_ERROR_MATERIALIZATIONS.with(std::cell::Cell::get), + 0 + ); + assert_eq!( + PHASE_B_OBJECT_AUTHORITY_TRANSFERS.with(std::cell::Cell::get), + 1 + ); + assert_eq!(PHASE_B_OBJECT_AUTHORITY_DROPS.with(std::cell::Cell::get), 1); + assert_eq!( + PHASE_B_OBJECT_AUTHORITY_MANIFEST_OBSERVATIONS.with(std::cell::Cell::get), + 1 + ); + assert_eq!( + PHASE_B_OBJECT_AUTHORITY_PUBLISH_OBSERVATIONS.with(std::cell::Cell::get), + 1 + ); + assert_phase_b_object_drop_order(1); + assert_eq!( + PHASE_B_MANIFEST_ARENA_ALLOCATIONS.with(std::cell::Cell::get), + 1 + ); + assert_eq!(PHASE_B_MANIFEST_ARENA_GROWTHS.with(std::cell::Cell::get), 0); + assert_eq!( + PHASE_B_MANIFEST_AUTHORITY_TRANSFERS.with(std::cell::Cell::get), + 1 + ); + assert_phase_b_manifest_drop_order(1); + std::fs::remove_dir_all(&root).unwrap(); + } + + #[cfg(unix)] + #[test] + fn phase_b_prepared_tool_resolver_covers_path_positions_symlink_and_capacity_minus_one() { + use std::os::unix::fs::symlink; + + let real = configured_tool("CLANG").unwrap().path; + let canonical = std::fs::canonicalize(&real).unwrap(); + let canonical_text = canonical.to_str().unwrap(); + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-prepared-tool-resolver-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + let directories = [root.join("first"), root.join("middle"), root.join("last")]; + for directory in &directories { + std::fs::create_dir(directory).unwrap(); + } + + for position in 0..directories.len() { + let link = directories[position].join("clang"); + symlink(&real, &link).unwrap(); + let paths = std::env::join_paths(&directories).unwrap(); + let resolver = + platform::prepare_tool_resolver("clang", PHASE_B_TOOL_PATH_CAPACITY).unwrap(); + assert!( + platform::prepared_tool_resolver_owned_capacity(&resolver) + <= PHASE_B_TOOL_RESOLVER_CAPACITY + ); + reset_phase_b_error_materialization_observer(); + mark_phase_b_effect_started(); + let held = + platform::resolve_and_hold_tool_prepared(resolver, None, Some(paths.as_os_str())) + .unwrap(); + assert_eq!(platform::tool_path(&held), canonical_text); + assert_eq!( + platform::tool_path_capacity(&held), + PHASE_B_TOOL_PATH_CAPACITY + ); + assert_eq!( + PHASE_B_POST_EFFECT_ERROR_MATERIALIZATIONS.with(std::cell::Cell::get), + 0, + ); + std::fs::remove_file(link).unwrap(); + } + + let configured_link = root.join("configured-clang"); + symlink(&real, &configured_link).unwrap(); + let resolver = + platform::prepare_tool_resolver("clang", PHASE_B_TOOL_PATH_CAPACITY).unwrap(); + reset_phase_b_error_materialization_observer(); + mark_phase_b_effect_started(); + let held = platform::resolve_and_hold_tool_prepared( + resolver, + Some(configured_link.as_os_str()), + None, + ) + .unwrap(); + assert_eq!(platform::tool_path(&held), canonical_text); + assert_eq!( + PHASE_B_POST_EFFECT_ERROR_MATERIALIZATIONS.with(std::cell::Cell::get), + 0, + ); + + let missing_paths = std::env::join_paths(&directories).unwrap(); + let resolver = + platform::prepare_tool_resolver("clang", PHASE_B_TOOL_PATH_CAPACITY).unwrap(); + reset_phase_b_error_materialization_observer(); + mark_phase_b_effect_started(); + assert!(platform::resolve_and_hold_tool_prepared( + resolver, + None, + Some(missing_paths.as_os_str()), + ) + .is_err()); + assert_eq!( + PHASE_B_POST_EFFECT_ERROR_MATERIALIZATIONS.with(std::cell::Cell::get), + 0, + ); + + let exact = canonical.as_os_str().as_encoded_bytes().len() + 1; + let resolver = platform::prepare_tool_resolver("clang", exact).unwrap(); + let held = + platform::resolve_and_hold_tool_prepared(resolver, Some(canonical.as_os_str()), None) + .unwrap(); + assert_eq!(platform::tool_path(&held), canonical_text); + assert_eq!(platform::tool_path_capacity(&held), exact); + let resolver = platform::prepare_tool_resolver("clang", exact - 1).unwrap(); + reset_phase_b_error_materialization_observer(); + mark_phase_b_effect_started(); + assert!(platform::resolve_and_hold_tool_prepared( + resolver, + Some(canonical.as_os_str()), + None, + ) + .is_err()); + assert_eq!( + PHASE_B_POST_EFFECT_ERROR_MATERIALIZATIONS.with(std::cell::Cell::get), + 0, + ); + std::fs::remove_dir_all(&root).unwrap(); + } + + #[test] + fn phase_b_discard_inventory_capacity_minus_one_rejects_without_effects() { + let ((publish, run), overflowed, exact) = + crate::bounded_output::with_limit_usage(MAX_BUILDER_BYTES, || { + ( + prepare_publish_discard_inventory(), + prepare_run_discard_inventory(), + ) + }); + assert!(!overflowed); + let publish = publish.unwrap(); + let run = run.unwrap(); + assert_eq!((publish.capacity(), publish.attached()), (7, 0)); + assert_eq!((run.capacity(), run.attached()), (10, 0)); + assert!(exact > 0); + + let ((publish, run), overflowed) = crate::bounded_output::with_limit(exact - 1, || { + ( + prepare_publish_discard_inventory(), + prepare_run_discard_inventory(), + ) + }); + assert!(!overflowed); + assert!(publish.is_ok()); + let error = match run { + Ok(_) => panic!("capacity-minus-one admitted the run discard plan"), + Err(error) => error, + }; + assert_eq!(error.code, "SPX-B109"); + assert_eq!( + error.message, + "Native Rust Interop max_builder_bytes exceeds 33554432" + ); + } + + #[test] + fn phase_b_created_directory_auth_disagreement_attempts_one_discard_and_stops() { + for mode in [ + CreateAuthDisagreement::Clean, + CreateAuthDisagreement::Substituted, + ] { + let (program, spec) = fixture(); + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-auth-disagreement-{}-{mode:?}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + CREATE_AUTH_DISCARD_ATTEMPTS.with(|attempts| attempts.set(0)); + CREATE_AUTH_DISAGREEMENT.with(|injection| injection.set(Some(mode))); + let mut hooks = 0usize; + let result = build_native_rust_interop_bundle_with_hook( + &program, + spec.as_bytes(), + &root.join("bundle"), + |_, _, _, _| hooks += 1, + ); + CREATE_AUTH_DISAGREEMENT.with(|injection| injection.set(None)); + let error = match result { + Ok(_) => panic!("created-directory authentication disagreement was accepted"), + Err(error) => error, + }; + assert_eq!(error.len(), 1); + assert_eq!(error[0].code, "SPX-I232"); + assert_eq!(CREATE_AUTH_DISCARD_ATTEMPTS.with(std::cell::Cell::get), 1); + assert_eq!( + PHASE_B_POST_EFFECT_ERROR_MATERIALIZATIONS.with(std::cell::Cell::get), + 0, + "authentication disagreement materialized its sticky diagnostic after effects", + ); + assert_eq!(hooks, 0, "later build action followed sticky I232"); + assert!(!root.join("bundle").exists()); + match mode { + CreateAuthDisagreement::Clean => { + assert_eq!(std::fs::read_dir(&root).unwrap().count(), 0); + } + CreateAuthDisagreement::Substituted => { + assert!(root.join("auth-displaced").is_dir()); + let substitute = std::fs::read_dir(&root) + .unwrap() + .map(Result::unwrap) + .find(|entry| { + entry + .file_name() + .to_string_lossy() + .starts_with(".semaprax-native-rust-interop-publish-") + }) + .expect("substituted stage remains inert"); + assert_eq!( + std::fs::read(substitute.path().join("foreign-sentinel")).unwrap(), + b"foreign" + ); + } + } + std::fs::remove_dir_all(&root).unwrap(); + } + } + + #[test] + fn phase_b_all_local_failures_move_exact_prebuilt_carrier_and_settle_without_later_action() { + let kinds = [ + PhaseBLocalError::BuilderBudget, + PhaseBLocalError::ManifestBudget, + PhaseBLocalError::Unsupported, + PhaseBLocalError::Replay, + PhaseBLocalError::Compile, + PhaseBLocalError::Link, + PhaseBLocalError::Publication, + ]; + for (index, kind) in kinds.into_iter().enumerate() { + let (program, spec) = fixture(); + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-local-carrier-{}-{index}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + PHASE_B_DISCARD_ATTEMPTS.with(|attempts| attempts.set(0)); + PHASE_B_BUILD_INVOCATION_PLANS.with(|count| count.set(0)); + PHASE_B_BUILD_INVOCATION_CONSUMPTIONS.with(|count| count.set(0)); + PHASE_B_LOCAL_FAILURE_INJECTION.with(|injection| injection.set(Some(kind))); + let mut hooks = 0usize; + let result = build_native_rust_interop_bundle_with_hook( + &program, + spec.as_bytes(), + &root.join("bundle"), + |_, _, _, _| hooks += 1, + ); + PHASE_B_LOCAL_FAILURE_INJECTION.with(|injection| injection.set(None)); + let error = match result { + Ok(_) => panic!("{kind:?} failure injection unexpectedly succeeded"), + Err(error) => error, + }; + let (code, message) = kind.diagnostic(); + assert_eq!(error.len(), 1); + assert_eq!(error[0].code, code); + assert_eq!(error[0].message, message); + assert_eq!( + error[0].message.as_ptr() as usize, + PHASE_B_PREPARED_CARRIER_IDENTITIES.with(std::cell::Cell::get)[kind.index()], + "{kind:?} did not move its pre-effect String allocation", + ); + assert_eq!( + PHASE_B_POST_EFFECT_ERROR_MATERIALIZATIONS.with(std::cell::Cell::get), + 0, + "{kind:?} materialized a Diagnostic, String, or Vec after effects", + ); + assert_eq!(PHASE_B_DISCARD_ATTEMPTS.with(std::cell::Cell::get), 2); + assert_eq!(PHASE_B_BUILD_INVOCATION_PLANS.with(std::cell::Cell::get), 8); + assert_eq!( + PHASE_B_BUILD_INVOCATION_CONSUMPTIONS.with(std::cell::Cell::get), + 0, + ); + assert_eq!(hooks, 0, "{kind:?} allowed a later build action"); + assert!(!root.join("bundle").exists()); + assert_eq!(std::fs::read_dir(&root).unwrap().count(), 0); + std::fs::remove_dir_all(&root).unwrap(); + } + } + + #[test] + fn phase_b_real_oversize_manifest_uses_exact_manifest_carrier_and_settles() { + let (program, spec) = fixture(); + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-oversize-manifest-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + PHASE_B_DISCARD_ATTEMPTS.with(|attempts| attempts.set(0)); + reset_phase_b_object_authority_observer(); + reset_phase_b_manifest_authority_observer(); + PHASE_B_OVERSIZE_MANIFEST_INJECTION.with(|injection| injection.set(true)); + let mut observed = Vec::new(); + let result = build_native_rust_interop_bundle_with_hook( + &program, + spec.as_bytes(), + &root.join("bundle"), + |point, _, _, _| observed.push(point), + ); + PHASE_B_OVERSIZE_MANIFEST_INJECTION.with(|injection| injection.set(false)); + let error = match result { + Ok(_) => panic!("oversize manifest unexpectedly published"), + Err(error) => error, + }; + assert_eq!(error.len(), 1); + assert_eq!(error[0].code, "SPX-B109"); + assert_eq!(error[0].message, PHASE_B_MANIFEST_BUDGET_MESSAGE); + assert_eq!( + error[0].message.as_ptr() as usize, + PHASE_B_PREPARED_CARRIER_IDENTITIES.with(std::cell::Cell::get) + [PhaseBLocalError::ManifestBudget.index()], + ); + assert_eq!( + PHASE_B_POST_EFFECT_ERROR_MATERIALIZATIONS.with(std::cell::Cell::get), + 0, + ); + assert_eq!(PHASE_B_DISCARD_ATTEMPTS.with(std::cell::Cell::get), 2); + assert_eq!( + PHASE_B_OBJECT_AUTHORITY_TRANSFERS.with(std::cell::Cell::get), + 1 + ); + assert_eq!(PHASE_B_OBJECT_AUTHORITY_DROPS.with(std::cell::Cell::get), 1); + assert_eq!( + PHASE_B_OBJECT_AUTHORITY_MANIFEST_OBSERVATIONS.with(std::cell::Cell::get), + 0 + ); + assert_eq!( + PHASE_B_OBJECT_AUTHORITY_PUBLISH_OBSERVATIONS.with(std::cell::Cell::get), + 0 + ); + assert_phase_b_object_drop_order(1); + assert_eq!( + PHASE_B_MANIFEST_ARENA_ALLOCATIONS.with(std::cell::Cell::get), + 1 + ); + assert_eq!(PHASE_B_MANIFEST_ARENA_GROWTHS.with(std::cell::Cell::get), 0); + assert_eq!( + PHASE_B_MANIFEST_AUTHORITY_TRANSFERS.with(std::cell::Cell::get), + 1 + ); + assert_phase_b_manifest_drop_order(1); + assert_eq!( + observed, + [ + NativeRustBuildPoint::BeforeClang, + NativeRustBuildPoint::BeforeRustLink, + NativeRustBuildPoint::BeforeExecutableAuthentication, + NativeRustBuildPoint::BeforeExecute, + NativeRustBuildPoint::BeforeExecutableAuthentication, + NativeRustBuildPoint::BeforeExecute, + NativeRustBuildPoint::BeforeObjectRead, + ], + "oversize manifest allowed a later manifest-write or publish hook", + ); + assert!(!root.join("bundle").exists()); + assert_eq!(std::fs::read_dir(&root).unwrap().count(), 0); + std::fs::remove_dir_all(&root).unwrap(); + } + + #[test] + fn phase_b_manifest_capacity_minus_one_fails_before_any_effect() { + let (program, spec) = fixture(); + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-manifest-capacity-minus-one-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + reset_phase_b_manifest_authority_observer(); + PHASE_B_OUTPUT_PROBES.with(|count| count.set(0)); + PHASE_B_TOOL_HOLDS.with(|count| count.set(0)); + PHASE_B_TOOL_PROCESSES.with(|count| count.set(0)); + PHASE_B_BUILD_INVOCATION_CONSUMPTIONS.with(|count| count.set(0)); + PHASE_B_DISCARD_ATTEMPTS.with(|count| count.set(0)); + PHASE_B_MANIFEST_PLAN_CAPACITY.with(|capacity| capacity.set(MAX_MANIFEST_BYTES - 1)); + let mut hooks = 0_usize; + let result = build_native_rust_interop_bundle_with_hook( + &program, + spec.as_bytes(), + &root.join("bundle"), + |_, _, _, _| hooks += 1, + ); + PHASE_B_MANIFEST_PLAN_CAPACITY.with(|capacity| capacity.set(MAX_MANIFEST_BYTES)); + let error = match result { + Ok(_) => panic!("capacity-minus-one manifest plan unexpectedly reached effects"), + Err(error) => error, + }; + assert_eq!(error.len(), 1); + assert_eq!(error[0].code, "SPX-B109"); + assert_eq!(error[0].message, PHASE_B_MANIFEST_BUDGET_MESSAGE); + assert_eq!( + error[0].message.as_ptr() as usize, + PHASE_B_PREPARED_CARRIER_IDENTITIES.with(std::cell::Cell::get) + [PhaseBLocalError::ManifestBudget.index()] + ); + assert_eq!(hooks, 0); + assert_eq!(PHASE_B_OUTPUT_PROBES.with(std::cell::Cell::get), 0); + assert_eq!(PHASE_B_TOOL_HOLDS.with(std::cell::Cell::get), 0); + assert_eq!(PHASE_B_TOOL_PROCESSES.with(std::cell::Cell::get), 0); + assert_eq!( + PHASE_B_BUILD_INVOCATION_CONSUMPTIONS.with(std::cell::Cell::get), + 0 + ); + assert_eq!(PHASE_B_DISCARD_ATTEMPTS.with(std::cell::Cell::get), 0); + assert_eq!( + PHASE_B_POST_EFFECT_ERROR_MATERIALIZATIONS.with(std::cell::Cell::get), + 0 + ); + assert_eq!( + PHASE_B_MANIFEST_ARENA_ALLOCATIONS.with(std::cell::Cell::get), + 1 + ); + assert_eq!( + PHASE_B_MANIFEST_AUTHORITY_TRANSFERS.with(std::cell::Cell::get), + 0 + ); + assert_phase_b_manifest_drop_order(0); + assert!(!root.join("bundle").exists()); + assert_eq!(std::fs::read_dir(&root).unwrap().count(), 0); + std::fs::remove_dir_all(&root).unwrap(); + } + + #[cfg(debug_assertions)] + #[test] + fn phase_b_primary_failure_is_sticky_over_mid_cleanup_failure_without_materialization() { + let (program, spec) = fixture(); + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-sticky-cleanup-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + PHASE_B_DISCARD_ATTEMPTS.with(|attempts| attempts.set(0)); + PHASE_B_DISCARD_FAILURE_AFTER_DELETE.with(|failure| failure.set(Some(0))); + PHASE_B_LOCAL_FAILURE_INJECTION + .with(|injection| injection.set(Some(PhaseBLocalError::Compile))); + let mut hooks = 0usize; + let result = build_native_rust_interop_bundle_with_hook( + &program, + spec.as_bytes(), + &root.join("bundle"), + |_, _, _, _| hooks += 1, + ); + PHASE_B_LOCAL_FAILURE_INJECTION.with(|injection| injection.set(None)); + PHASE_B_DISCARD_FAILURE_AFTER_DELETE.with(|failure| failure.set(None)); + let error = match result { + Ok(_) => panic!("compile failure injection unexpectedly succeeded"), + Err(error) => error, + }; + assert_eq!(error.len(), 1); + assert_eq!(error[0].code, "SPX-I230"); + assert_eq!(error[0].message, PHASE_B_COMPILE_MESSAGE); + assert_eq!( + error[0].message.as_ptr() as usize, + PHASE_B_PREPARED_CARRIER_IDENTITIES.with(std::cell::Cell::get) + [PhaseBLocalError::Compile.index()], + ); + assert_eq!( + PHASE_B_POST_EFFECT_ERROR_MATERIALIZATIONS.with(std::cell::Cell::get), + 0, + ); + assert_eq!(PHASE_B_DISCARD_ATTEMPTS.with(std::cell::Cell::get), 2); + assert_eq!(hooks, 0); + assert!(!root.join("bundle").exists()); + assert_eq!( + std::fs::read_dir(&root) + .unwrap() + .filter_map(Result::ok) + .filter(|entry| entry + .file_name() + .to_string_lossy() + .starts_with(".semaprax-native-rust-interop-run-")) + .count(), + 1, + "failed run-stage cleanup must leave one inert owned residue", + ); + std::fs::remove_dir_all(&root).unwrap(); + } + + #[test] + fn phase_b_fixed_discard_plans_remove_every_partial_prefix_without_growth() { + fn exercise(label: &str, names: [&'static str; N]) { + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-prefix-{label}-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + let parent = platform::hold_directory(&root).unwrap(); + for prefix in 0..=N { + let stage_name = format!("s{prefix}"); + let prepared_stage = platform::prepare_stage_name(stage_name.as_ref()).unwrap(); + let stage = + platform::create_directory_new(&parent, stage_name.as_ref(), 0o700).unwrap(); + let os_names = names.map(OsStr::new); + let mut inventory = platform::prepare_discard_inventory(os_names).unwrap(); + let native_capacity = + platform::prepared_discard_inventory_owned_capacity(&inventory); + assert_eq!(inventory.capacity(), N); + for (index, name) in names.iter().take(prefix).enumerate() { + platform::write_file_new_prepared( + &stage, + &mut inventory, + name, + b"owned", + 0o600, + ) + .unwrap(); + assert_eq!(inventory.attached(), index + 1); + assert_eq!(inventory.capacity(), N); + assert_eq!( + platform::prepared_discard_inventory_owned_capacity(&inventory), + native_capacity + ); + } + assert_eq!(inventory.attached(), prefix); + platform::discard_owned_stage_prepared( + &parent, + &stage, + &prepared_stage, + &inventory, + ) + .unwrap(); + // Windows finalizes the delete disposition only after the + // last authenticated directory handle closes. + drop(stage); + assert!(!root.join(stage_name).exists()); + } + std::fs::remove_dir_all(&root).unwrap(); + } + + exercise("publish", ["p0", "p1", "p2", "p3", "p4", "p5", "p6"]); + exercise( + "run", + ["r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9"], + ); + } + + #[test] + fn phase_b_prepared_file_names_reject_invalid_duplicate_and_wrong_order_before_create() { + for invalid in ["", ".", "..", "slash/name", "nul\0name"] { + assert!(platform::prepare_discard_inventory([OsStr::new(invalid)]).is_err()); + } + assert!(platform::prepare_discard_inventory([ + OsStr::new("duplicate"), + OsStr::new("duplicate") + ]) + .is_err()); + let bounded_names = [OsStr::new("first"), OsStr::new("second")]; + let bounded = platform::prepare_discard_inventory(bounded_names).unwrap(); + let exact_native = platform::prepared_discard_inventory_owned_capacity(&bounded); + assert!(exact_native > 0); + drop(bounded); + assert!(platform::prepare_discard_inventory_bounded(bounded_names, exact_native).is_ok()); + assert!( + platform::prepare_discard_inventory_bounded(bounded_names, exact_native - 1).is_err() + ); + #[cfg(windows)] + { + for invalid in ["back\\slash", "CON", "com1.txt", "trailing.", "trailing "] { + assert!(platform::prepare_discard_inventory([OsStr::new(invalid)]).is_err()); + } + assert!( + platform::prepare_discard_inventory([OsStr::new("Case"), OsStr::new("case")]) + .is_err() + ); + } + + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-prepared-file-order-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + let directory = platform::hold_directory(&root).unwrap(); + let mut inventory = + platform::prepare_discard_inventory([OsStr::new("first"), OsStr::new("second")]) + .unwrap(); + reset_phase_b_error_materialization_observer(); + assert!(platform::write_file_new_prepared( + &directory, + &mut inventory, + "second", + b"must-not-exist", + 0o600, + ) + .is_err()); + assert!(platform::hold_regular_file_prepared(&directory, &inventory, "second").is_err()); + assert!(platform::hold_regular_file_prepared(&directory, &inventory, "unknown").is_err()); + assert_eq!(inventory.attached(), 0); + assert_eq!(std::fs::read_dir(&root).unwrap().count(), 0); + platform::write_file_new_prepared(&directory, &mut inventory, "first", b"first", 0o600) + .unwrap(); + assert!(platform::write_file_new_prepared( + &directory, + &mut inventory, + "first", + b"duplicate", + 0o600, + ) + .is_err()); + assert!(platform::hold_regular_file_prepared(&directory, &inventory, "second").is_err()); + assert_eq!(inventory.attached(), 1); + assert!(!root.join("second").exists()); + platform::write_file_new_prepared(&directory, &mut inventory, "second", b"second", 0o600) + .unwrap(); + let held = platform::hold_regular_file_prepared(&directory, &inventory, "second").unwrap(); + platform::recheck_regular_file(&held).unwrap(); + drop(held); + std::fs::rename(root.join("second"), root.join("tracked-original")).unwrap(); + std::fs::write(root.join("second"), b"foreign").unwrap(); + assert!(platform::hold_regular_file_prepared(&directory, &inventory, "second").is_err()); + assert_eq!( + platform::read_exact(inventory.file("second").unwrap(), b"second".len()).unwrap(), + b"second" + ); + assert_eq!(std::fs::read(root.join("second")).unwrap(), b"foreign"); + assert_eq!( + PHASE_B_POST_EFFECT_ERROR_MATERIALIZATIONS.with(std::cell::Cell::get), + 0 + ); + drop(inventory); + drop(directory); + std::fs::remove_dir_all(&root).unwrap(); + } + + #[test] + fn phase_b_prepared_link_copy_binds_tracked_source_and_exact_next_destination() { + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-prepared-link-copy-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + let directory = platform::hold_directory(&root).unwrap(); + let mut source = platform::prepare_discard_inventory([OsStr::new("source")]).unwrap(); + let mut destination = + platform::prepare_discard_inventory([OsStr::new("first"), OsStr::new("copy")]).unwrap(); + let future = + platform::prepare_link_or_copy(&source, "source", &destination, "copy").unwrap(); + let unattached = + platform::prepare_link_or_copy(&source, "source", &destination, "copy").unwrap(); + let substituted = + platform::prepare_link_or_copy(&source, "source", &destination, "copy").unwrap(); + let duplicate = + platform::prepare_link_or_copy(&source, "source", &destination, "copy").unwrap(); + assert!(platform::prepare_link_or_copy(&source, "unknown", &destination, "copy").is_err()); + assert!( + platform::prepare_link_or_copy(&source, "source", &destination, "unknown").is_err() + ); + + reset_phase_b_error_materialization_observer(); + assert!(platform::link_or_copy_new_prepared( + unattached, + &source, + &directory, + &mut destination, + b"original", + ) + .is_err()); + assert!(platform::link_or_copy_new_prepared( + future, + &source, + &directory, + &mut destination, + b"original", + ) + .is_err()); + assert_eq!(destination.attached(), 0); + assert_eq!(std::fs::read_dir(&root).unwrap().count(), 0); + + platform::write_file_new_prepared(&directory, &mut source, "source", b"original", 0o600) + .unwrap(); + platform::write_file_new_prepared(&directory, &mut destination, "first", b"first", 0o600) + .unwrap(); + std::fs::rename(root.join("source"), root.join("tracked-source")).unwrap(); + std::fs::write(root.join("source"), b"foreign").unwrap(); + platform::link_or_copy_new_prepared( + substituted, + &source, + &directory, + &mut destination, + b"original", + ) + .unwrap(); + assert_eq!(destination.attached(), 2); + assert_eq!( + platform::read_exact(destination.file("copy").unwrap(), b"original".len()).unwrap(), + b"original" + ); + assert_eq!(std::fs::read(root.join("source")).unwrap(), b"foreign"); + assert!(platform::link_or_copy_new_prepared( + duplicate, + &source, + &directory, + &mut destination, + b"original", + ) + .is_err()); + assert_eq!(destination.attached(), 2); + + let mut exists_destination = platform::prepare_discard_inventory([ + OsStr::new("exists-first"), + OsStr::new("exists-copy"), + ]) + .unwrap(); + let exists = + platform::prepare_link_or_copy(&source, "source", &exists_destination, "exists-copy") + .unwrap(); + platform::write_file_new_prepared( + &directory, + &mut exists_destination, + "exists-first", + b"first", + 0o600, + ) + .unwrap(); + std::fs::write(root.join("exists-copy"), b"foreign-destination").unwrap(); + assert!(platform::link_or_copy_new_prepared( + exists, + &source, + &directory, + &mut exists_destination, + b"original", + ) + .is_err()); + assert_eq!(exists_destination.attached(), 1); + assert_eq!( + std::fs::read(root.join("exists-copy")).unwrap(), + b"foreign-destination" + ); + assert_eq!( + PHASE_B_POST_EFFECT_ERROR_MATERIALIZATIONS.with(std::cell::Cell::get), + 0 + ); + drop((exists_destination, destination, source, directory)); + std::fs::remove_dir_all(&root).unwrap(); + } + + #[cfg(debug_assertions)] + #[test] + fn phase_b_post_link_pre_auth_failure_is_sticky_and_preserves_inert_stage() { + let (program, spec) = fixture(); + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-post-link-pre-auth-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + let output = root.join("bundle"); + let mut run_stage = None; + let mut hooks = 0_usize; + let mut unexpected_hook = false; + PHASE_B_LINK_COPY_PLANS.with(|count| count.set(0)); + PHASE_B_LINK_COPY_CONSUMPTIONS.with(|count| count.set(0)); + PHASE_B_DISCARD_ATTEMPTS.with(|count| count.set(0)); + PHASE_B_LINK_COPY_FAIL_BEFORE_AUTHENTICATION.with(|enabled| enabled.set(true)); + let result = build_native_rust_interop_bundle_with_hook( + &program, + spec.as_bytes(), + &output, + |point, _, run, _| { + hooks += 1; + unexpected_hook |= point != NativeRustBuildPoint::BeforeClang; + if point == NativeRustBuildPoint::BeforeClang { + run_stage = Some(run.to_path_buf()); + std::fs::write(run.join("foreign-sentinel"), b"foreign").unwrap(); + } + }, + ); + PHASE_B_LINK_COPY_FAIL_BEFORE_AUTHENTICATION.with(|enabled| enabled.set(false)); + let error = match result { + Ok(_) => panic!("post-link authentication failure unexpectedly succeeded"), + Err(error) => error, + }; + assert_eq!(error.len(), 1); + assert_eq!(error[0].code, "SPX-I232"); + assert_eq!(error[0].message, PHASE_B_PUBLICATION_MESSAGE); + assert_eq!(hooks, 1); + assert!(!unexpected_hook); + assert_eq!(PHASE_B_LINK_COPY_PLANS.with(std::cell::Cell::get), 3); + assert_eq!(PHASE_B_LINK_COPY_CONSUMPTIONS.with(std::cell::Cell::get), 1); + assert_eq!(PHASE_B_DISCARD_ATTEMPTS.with(std::cell::Cell::get), 2); + assert_eq!( + PHASE_B_POST_EFFECT_ERROR_MATERIALIZATIONS.with(std::cell::Cell::get), + 0 + ); + let run_stage = run_stage.expect("BeforeClang records the run stage"); + assert!(run_stage.is_dir()); + assert_eq!( + std::fs::read(run_stage.join("foreign-sentinel")).unwrap(), + b"foreign" + ); + assert!(run_stage.join("semaprax_native_rust_interop.rs").is_file()); + assert!(!output.exists()); + std::fs::remove_dir_all(&root).unwrap(); + } + + #[test] + fn phase_b_link_copy_capacity_minus_one_is_pre_effect() { + let publish = prepare_publish_discard_inventory().unwrap(); + let run = prepare_run_discard_inventory().unwrap(); + let required = platform::link_or_copy_required_capacity( + &publish, + "semaprax_native_rust_interop.rs", + &run, + "semaprax_native_rust_interop.rs", + ) + .unwrap(); + assert!(required > 0); + + PHASE_B_OUTPUT_PROBES.with(|count| count.set(0)); + PHASE_B_TOOL_HOLDS.with(|count| count.set(0)); + PHASE_B_TOOL_PROCESSES.with(|count| count.set(0)); + PHASE_B_LINK_COPY_PLANS.with(|count| count.set(0)); + let (plan, overflowed) = crate::bounded_output::with_limit(required, || { + prepare_link_copy( + &publish, + "semaprax_native_rust_interop.rs", + &run, + "semaprax_native_rust_interop.rs", + ) + }); + assert!(!overflowed); + let (prepared, budget) = plan.unwrap(); + assert_eq!( + platform::prepared_link_or_copy_owned_capacity(&prepared), + required + ); + assert_eq!(budget.maximum(), required); + drop((prepared, budget)); + + PHASE_B_LINK_COPY_PLANS.with(|count| count.set(0)); + let (one_less, overflowed) = crate::bounded_output::with_limit(required - 1, || { + prepare_link_copy( + &publish, + "semaprax_native_rust_interop.rs", + &run, + "semaprax_native_rust_interop.rs", + ) + }); + assert!(!overflowed); + assert!(matches!(one_less, Err(PhaseBLocalError::BuilderBudget))); + assert_eq!(PHASE_B_LINK_COPY_PLANS.with(std::cell::Cell::get), 0); + assert_eq!(PHASE_B_OUTPUT_PROBES.with(std::cell::Cell::get), 0); + assert_eq!(PHASE_B_TOOL_HOLDS.with(std::cell::Cell::get), 0); + assert_eq!(PHASE_B_TOOL_PROCESSES.with(std::cell::Cell::get), 0); + assert_eq!( + PHASE_B_POST_EFFECT_ERROR_MATERIALIZATIONS.with(std::cell::Cell::get), + 0 + ); + } + + #[test] + fn phase_b_inventory_exact_plan_is_exact_capacity_and_one_less_is_pre_effect() { + let publish = prepare_publish_discard_inventory().unwrap(); + let required = platform::inventory_exact_required_capacity(&publish).unwrap(); + assert!(required > 0); + PHASE_B_OUTPUT_PROBES.with(|count| count.set(0)); + PHASE_B_TOOL_HOLDS.with(|count| count.set(0)); + PHASE_B_TOOL_PROCESSES.with(|count| count.set(0)); + PHASE_B_INVENTORY_EXACT_PLANS.with(|count| count.set(0)); + let (exact, overflowed) = crate::bounded_output::with_limit(required, || { + prepare_publish_inventory_exact(&publish) + }); + assert!(!overflowed); + let (prepared, budget) = exact.unwrap(); + assert_eq!( + platform::prepared_inventory_exact_owned_capacity(&prepared), + required + ); + assert_eq!(budget.maximum(), required); + assert_eq!(platform::prepared_inventory_exact_remaining(&prepared), 2); + drop((prepared, budget)); + + PHASE_B_INVENTORY_EXACT_PLANS.with(|count| count.set(0)); + let (one_less, overflowed) = crate::bounded_output::with_limit(required - 1, || { + prepare_publish_inventory_exact(&publish) + }); + assert!(!overflowed); + assert!(matches!(one_less, Err(PhaseBLocalError::BuilderBudget))); + assert_eq!(PHASE_B_INVENTORY_EXACT_PLANS.with(std::cell::Cell::get), 0); + assert_eq!(PHASE_B_OUTPUT_PROBES.with(std::cell::Cell::get), 0); + assert_eq!(PHASE_B_TOOL_HOLDS.with(std::cell::Cell::get), 0); + assert_eq!(PHASE_B_TOOL_PROCESSES.with(std::cell::Cell::get), 0); + assert_eq!( + PHASE_B_POST_EFFECT_ERROR_MATERIALIZATIONS.with(std::cell::Cell::get), + 0 + ); + } + + #[test] + fn phase_b_final_publish_plan_is_exact_capacity_and_one_less_is_pre_effect() { + let output = Path::new("bundle"); + let required = platform::publish_directory_required_capacity(OsStr::new("bundle")).unwrap(); + assert!(required > 0); + PHASE_B_EFFECT_STARTED.with(|started| started.set(false)); + PHASE_B_OUTPUT_PROBES.with(|count| count.set(0)); + PHASE_B_TOOL_HOLDS.with(|count| count.set(0)); + PHASE_B_TOOL_PROCESSES.with(|count| count.set(0)); + PHASE_B_PUBLISH_PLANS.with(|count| count.set(0)); + let (exact, overflowed) = + crate::bounded_output::with_limit(required, || prepare_final_publish(output)); + assert!(!overflowed); + let (prepared, budget) = exact.unwrap(); + assert_eq!( + platform::prepared_publish_directory_owned_capacity(&prepared), + required + ); + assert_eq!(platform::prepared_publish_directory_remaining(&prepared), 1); + assert_eq!(budget.maximum(), required); + assert_eq!(PHASE_B_PUBLISH_PLANS.with(std::cell::Cell::get), 1); + drop((prepared, budget)); + + PHASE_B_PUBLISH_PLANS.with(|count| count.set(0)); + let (one_less, overflowed) = + crate::bounded_output::with_limit(required - 1, || prepare_final_publish(output)); + assert!(!overflowed); + assert!(matches!(one_less, Err(PhaseBLocalError::BuilderBudget))); + assert_eq!(PHASE_B_PUBLISH_PLANS.with(std::cell::Cell::get), 0); + assert!(!PHASE_B_EFFECT_STARTED.with(std::cell::Cell::get)); + assert_eq!(PHASE_B_OUTPUT_PROBES.with(std::cell::Cell::get), 0); + assert_eq!(PHASE_B_TOOL_HOLDS.with(std::cell::Cell::get), 0); + assert_eq!(PHASE_B_TOOL_PROCESSES.with(std::cell::Cell::get), 0); + } + + #[test] + fn phase_b_final_comparisons_precede_scan_and_allocate_no_file_buffers() { + let source = include_str!("implementation.rs"); + let start = source.find("fn publish_stage_platform").unwrap(); + let end = source[start..] + .find("#[cfg(test)]\nmod tests") + .map(|offset| start + offset) + .unwrap(); + let publish = &source[start..end]; + let comparison = publish.find("platform::compare_exact").unwrap(); + let scan = publish.find("scan_publish_inventory_exact").unwrap(); + let rename = publish + .find("platform::publish_directory_new_prepared") + .unwrap(); + assert!(comparison < scan && scan < rename); + assert!(publish.contains("platform::FILE_COMPARE_SCRATCH_BYTES")); + assert!(publish.contains(".discard_name")); + assert!(!publish.contains("platform::read_exact")); + assert!(!publish.contains("debit_phase_b")); + assert!(!publish.contains("try_clone")); + } + + #[cfg(unix)] + #[test] + fn phase_b_inventory_exact_rejects_every_missing_and_substituted_slot_and_extra() { + const NAMES: [&str; 7] = [ + "descriptor.json", + "module.c", + "semaprax_native_rust_interop.h", + "semaprax_native_rust_interop.rs", + "semaprax_native_rust_interop_ffi.rs", + "module.o", + "semaprax.native-rust-interop.json", + ]; + + fn fixture( + root: &Path, + ) -> ( + platform::HeldDirectory, + PublishDiscardInventory, + platform::PreparedInventoryExact<7>, + ) { + std::fs::create_dir(root).unwrap(); + let directory = platform::hold_directory(root).unwrap(); + let mut inventory = prepare_publish_discard_inventory().unwrap(); + let prepared = platform::prepare_inventory_exact(&inventory).unwrap(); + for (index, name) in NAMES.iter().enumerate() { + platform::write_file_new_prepared( + &directory, + &mut inventory, + name, + &[u8::try_from(index).unwrap()], + 0o600, + ) + .unwrap(); + } + (directory, inventory, prepared) + } + + let base = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-inventory-exact-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&base); + std::fs::create_dir(&base).unwrap(); + + for (index, name) in NAMES.iter().enumerate() { + let root = base.join(format!("missing-{index}")); + let (directory, inventory, mut prepared) = fixture(&root); + std::fs::remove_file(root.join(name)).unwrap(); + assert!( + platform::inventory_exact_prepared(&mut prepared, &directory, &inventory).is_err() + ); + drop((prepared, inventory, directory)); + std::fs::remove_dir_all(&root).unwrap(); + } + + for (index, name) in NAMES.iter().enumerate() { + let root = base.join(format!("substituted-{index}")); + let (directory, inventory, mut prepared) = fixture(&root); + std::fs::rename(root.join(name), root.join(format!("tracked-{index}"))).unwrap(); + std::fs::write(root.join(name), b"foreign").unwrap(); + assert!( + platform::inventory_exact_prepared(&mut prepared, &directory, &inventory).is_err() + ); + assert_eq!(std::fs::read(root.join(name)).unwrap(), b"foreign"); + drop((prepared, inventory, directory)); + std::fs::remove_dir_all(&root).unwrap(); + } + + let root = base.join("extra"); + let (directory, inventory, mut prepared) = fixture(&root); + std::fs::write(root.join("foreign-extra"), b"foreign").unwrap(); + assert!(platform::inventory_exact_prepared(&mut prepared, &directory, &inventory).is_err()); + drop((prepared, inventory, directory)); + std::fs::remove_dir_all(&root).unwrap(); + + #[cfg(target_os = "linux")] + { + let root = base.join("invalid-encoding"); + let (directory, inventory, mut prepared) = fixture(&root); + use std::os::unix::ffi::OsStringExt as _; + std::fs::write( + root.join(std::ffi::OsString::from_vec(vec![0xff])), + b"foreign", + ) + .unwrap(); + assert!( + platform::inventory_exact_prepared(&mut prepared, &directory, &inventory).is_err() + ); + drop((prepared, inventory, directory)); + std::fs::remove_dir_all(&root).unwrap(); + } + std::fs::remove_dir_all(&base).unwrap(); + } + + #[cfg(unix)] + #[test] + fn phase_b_inventory_exact_is_bound_to_one_inventory_and_exactly_two_scans() { + const NAMES: [&str; 7] = [ + "descriptor.json", + "module.c", + "semaprax_native_rust_interop.h", + "semaprax_native_rust_interop.rs", + "semaprax_native_rust_interop_ffi.rs", + "module.o", + "semaprax.native-rust-interop.json", + ]; + let base = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-inventory-binding-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&base); + std::fs::create_dir(&base).unwrap(); + let mut fixtures = Vec::with_capacity(2); + for suffix in ["a", "b"] { + let root = base.join(suffix); + std::fs::create_dir(&root).unwrap(); + let directory = platform::hold_directory(&root).unwrap(); + let mut inventory = prepare_publish_discard_inventory().unwrap(); + for (index, name) in NAMES.iter().enumerate() { + platform::write_file_new_prepared( + &directory, + &mut inventory, + name, + &[u8::try_from(index).unwrap()], + 0o600, + ) + .unwrap(); + } + fixtures.push((root, directory, inventory)); + } + let mut prepared = platform::prepare_inventory_exact(&fixtures[0].2).unwrap(); + assert!( + platform::inventory_exact_prepared(&mut prepared, &fixtures[1].1, &fixtures[1].2) + .is_err() + ); + assert_eq!(platform::prepared_inventory_exact_remaining(&prepared), 2); + assert!( + platform::inventory_exact_prepared(&mut prepared, &fixtures[0].1, &fixtures[0].2) + .is_ok() + ); + assert_eq!(platform::prepared_inventory_exact_remaining(&prepared), 1); + assert!( + platform::inventory_exact_prepared(&mut prepared, &fixtures[0].1, &fixtures[0].2) + .is_ok() + ); + assert_eq!(platform::prepared_inventory_exact_remaining(&prepared), 0); + assert!( + platform::inventory_exact_prepared(&mut prepared, &fixtures[0].1, &fixtures[0].2) + .is_err() + ); + let hardlink_root = base.join("hardlink-second-directory"); + std::fs::create_dir(&hardlink_root).unwrap(); + for name in NAMES { + std::fs::hard_link(fixtures[0].0.join(name), hardlink_root.join(name)).unwrap(); + } + let hardlink_directory = platform::hold_directory(&hardlink_root).unwrap(); + let mut hardlink_prepared = platform::prepare_inventory_exact(&fixtures[0].2).unwrap(); + assert!(platform::inventory_exact_prepared( + &mut hardlink_prepared, + &fixtures[0].1, + &fixtures[0].2 + ) + .is_ok()); + assert_eq!( + platform::prepared_inventory_exact_remaining(&hardlink_prepared), + 1 + ); + assert!(platform::inventory_exact_prepared( + &mut hardlink_prepared, + &hardlink_directory, + &fixtures[0].2 + ) + .is_err()); + assert_eq!( + platform::prepared_inventory_exact_remaining(&hardlink_prepared), + 1 + ); + let mut consumed_failure = platform::prepare_inventory_exact(&fixtures[1].2).unwrap(); + assert!(platform::inventory_exact_prepared( + &mut consumed_failure, + &fixtures[1].1, + &fixtures[1].2 + ) + .is_ok()); + assert_eq!( + platform::prepared_inventory_exact_remaining(&consumed_failure), + 1 + ); + for name in NAMES { + std::fs::remove_file(fixtures[1].0.join(name)).unwrap(); + } + assert!(platform::inventory_exact_prepared( + &mut consumed_failure, + &fixtures[1].1, + &fixtures[1].2 + ) + .is_err()); + assert_eq!( + platform::prepared_inventory_exact_remaining(&consumed_failure), + 0 + ); + drop(( + consumed_failure, + hardlink_prepared, + hardlink_directory, + prepared, + fixtures, + )); + std::fs::remove_dir_all(&base).unwrap(); + } + + #[test] + fn phase_b_nonce_exists_retries_reuse_native_stage_name_arena_without_materialization() { + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-nonce-retry-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + let parent = hold_stage(root.clone()).unwrap(); + let digest = "sha256:nonce-retry"; + reset_phase_b_native_stage_arena_observer(); + let mut collision = StageSlot::new(&root, digest, "retry").unwrap(); + let native_capacity = collision.native_name.capacity(); + assert_eq!( + PHASE_B_NATIVE_STAGE_ARENA_ALLOCATIONS.with(std::cell::Cell::get), + 1, + ); + collision.prepare(&root, 0).unwrap(); + assert_eq!(collision.native_name.capacity(), native_capacity); + std::fs::create_dir(&collision.path).unwrap(); + let inventory = platform::prepare_discard_inventory([]).unwrap(); + let slot = StageSlot::new(&root, digest, "retry").unwrap(); + assert_eq!(slot.native_name.capacity(), native_capacity); + assert_eq!( + PHASE_B_NATIVE_STAGE_ARENA_ALLOCATIONS.with(std::cell::Cell::get), + 2, + ); + assert_eq!( + PHASE_B_NATIVE_STAGE_ARENA_SETS.with(std::cell::Cell::get), + 1, + ); + assert_eq!( + PHASE_B_NATIVE_STAGE_ARENA_CONSUMPTIONS.with(std::cell::Cell::get), + 0, + ); + reset_phase_b_error_materialization_observer(); + mark_phase_b_effect_started(); + let stage = create_stage(&parent, slot, &inventory).unwrap(); + assert!(stage + .path + .file_name() + .unwrap() + .to_string_lossy() + .ends_with("-1")); + assert_eq!( + PHASE_B_POST_EFFECT_ERROR_MATERIALIZATIONS.with(std::cell::Cell::get), + 0, + ); + assert_eq!( + PHASE_B_NATIVE_STAGE_ARENA_ALLOCATIONS.with(std::cell::Cell::get), + 2, + "nonce retries allocated a new native name after effects", + ); + assert_eq!( + PHASE_B_NATIVE_STAGE_ARENA_SETS.with(std::cell::Cell::get), + 3, + "the collision probe plus nonce zero and nonce one must set one arena", + ); + assert_eq!( + PHASE_B_NATIVE_STAGE_ARENA_CONSUMPTIONS.with(std::cell::Cell::get), + 2, + "both create attempts must consume the prepared native arena", + ); + assert_eq!( + stage.discard_name.as_ref().unwrap().capacity(), + native_capacity, + ); + discard_run_stage(&parent, &stage, &inventory).unwrap(); + drop(stage); + drop(parent); + std::fs::remove_dir_all(&root).unwrap(); + } + + #[cfg(debug_assertions)] + #[test] + fn phase_b_every_mid_discard_failure_moves_prebuilt_i232_without_materialization() { + const NAMES: [&str; 3] = ["first", "second", "third"]; + for failure_after_delete in 0..=NAMES.len() { + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-mid-discard-{}-{failure_after_delete}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + let parent = hold_stage(root.clone()).unwrap(); + let mut inventory = platform::prepare_discard_inventory(NAMES.map(OsStr::new)).unwrap(); + let slot = StageSlot::new(&root, "sha256:mid-discard", "discard").unwrap(); + let stage = create_stage(&parent, slot, &inventory).unwrap(); + for name in NAMES { + platform::write_file_new_prepared( + stage.authority.held(), + &mut inventory, + name, + b"owned", + 0o600, + ) + .unwrap(); + } + inventory.inject_discard_failure_after_delete(Some(failure_after_delete)); + reset_phase_b_error_materialization_observer(); + let mut carriers = PhaseBErrorCarriers::prepare().unwrap(); + mark_phase_b_effect_started(); + assert!(discard_run_stage(&parent, &stage, &inventory).is_err()); + let error = carriers.take(PhaseBLocalError::Publication); + assert_eq!(error.len(), 1); + assert_eq!(error[0].code, "SPX-I232"); + assert_eq!(error[0].message, PHASE_B_PUBLICATION_MESSAGE); + assert_eq!( + PHASE_B_POST_EFFECT_ERROR_MATERIALIZATIONS.with(std::cell::Cell::get), + 0, + "delete boundary {failure_after_delete} materialized an error after effects", + ); + drop(error); + drop(inventory); + drop(stage); + drop(parent); + std::fs::remove_dir_all(&root).unwrap(); + } + } + + #[test] + fn noncanonical_spec_is_b106_before_target_admission() { + let (program, spec) = fixture(); + let noncanonical = spec.replacen("{\"schema\"", "{ \"schema\"", 1); + let error = match prepare_native_rust_interop(&program, noncanonical.as_bytes()) { + Ok(_) => panic!("noncanonical spec was accepted"), + Err(error) => error, + }; + assert_eq!(error[0].code, "SPX-B106"); + } + + #[test] + fn specification_parser_is_canonical_bounded_and_intent_bound() { + fn assert_spec_error(program: &Program, source: &[u8], code: &str, message: &str) { + let error = match parse_spec(program, source) { + Ok(_) => panic!("hostile specification was accepted"), + Err(error) => error, + }; + assert_eq!(error.code, code); + assert_eq!(error.message, message); + } + + let (program, canonical) = fixture(); + parse_spec(&program, canonical.as_bytes()).unwrap(); + let b106_message = + "Native Rust Interop specification is not canonical semaprax.native-rust-interop-spec.v1 JSON"; + let schema_prefix = format!("\"schema\":{},", quote_json(SPEC_SCHEMA)); + let malformed = [ + format!(" \n{canonical}"), + canonical.trim_end().to_owned(), + canonical.replace('\n', "\r\n"), + format!("\u{feff}{canonical}"), + canonical.replacen(&schema_prefix, "", 1), + canonical.replacen( + &schema_prefix, + &format!("{schema_prefix}{schema_prefix}"), + 1, + ), + canonical.replacen(&schema_prefix, &format!("{schema_prefix}\"extra\":0,"), 1), + canonical.replacen( + &format!("\"schema\":{}", quote_json(SPEC_SCHEMA)), + "\"schema\":1", + 1, + ), + canonical.replacen( + "\"exports\":[\"interop.add\"]", + "\"exports\":[\"interop.add\",\"interop.add\"]", + 1, + ), + canonical.replacen("\"max_exports\":32", "\"max_exports\":31", 1), + canonical.replacen( + "no_resource_owned_borrow_shared_or_aggregate_abi", + "xo_resource_owned_borrow_shared_or_aggregate_abi", + 1, + ), + ]; + for mutation in malformed { + assert_spec_error(&program, mutation.as_bytes(), "SPX-B106", b106_message); + } + + let exact_depth = format!("[[[[[[{canonical}]]]]]]"); + assert_eq!(json_depth(exact_depth.as_bytes()).unwrap(), MAX_JSON_DEPTH); + assert_spec_error(&program, exact_depth.as_bytes(), "SPX-B106", b106_message); + let over_depth = format!("[{exact_depth}]"); + assert_spec_error( + &program, + over_depth.as_bytes(), + "SPX-B109", + "Native Rust Interop max_json_depth exceeds 8", + ); + + let exact_cap = vec![b' '; MAX_SPEC_BYTES]; + assert_spec_error(&program, &exact_cap, "SPX-B106", b106_message); + let over_cap = vec![b' '; MAX_SPEC_BYTES + 1]; + assert_spec_error( + &program, + &over_cap, + "SPX-B109", + "Native Rust Interop max_spec_bytes exceeds 1048576", + ); + + let mut exact_source_program = program.clone(); + exact_source_program.functions[0].name.clear(); + let source_overhead = crate::format::canonical(&exact_source_program).len(); + exact_source_program.functions[0].name = "a".repeat(MAX_SOURCE_BYTES - source_overhead); + let exact_source = crate::format::canonical(&exact_source_program); + assert_eq!(exact_source.len(), MAX_SOURCE_BYTES); + CANONICAL_FORMAT_PASS_COUNT.with(|count| count.set(0)); + let (scratch_error, overflowed, consumed) = crate::bounded_output::with_limit_usage( + canonical_format_scratch_capacity(&exact_source_program) + .unwrap() + .bytes() + - 1, + || canonical_source_bounded(&exact_source_program), + ); + let scratch_error = scratch_error.unwrap_err(); + assert!(!overflowed); + assert_eq!(consumed, 0, "rejected scratch reservation leaked budget"); + assert_eq!(scratch_error.code, "SPX-B109"); + assert_eq!( + scratch_error.message, + "Native Rust Interop max_builder_bytes exceeds 33554432" + ); + CANONICAL_FORMAT_PASS_COUNT.with(|count| assert_eq!(count.get(), 0)); + let exact_peak = canonical_format_scratch_capacity(&exact_source_program) + .unwrap() + .bytes() + .checked_add(MAX_SOURCE_BYTES) + .unwrap(); + CANONICAL_FORMAT_PASS_COUNT.with(|count| count.set(0)); + let (bounded_source, overflowed, consumed) = + crate::bounded_output::with_limit_usage(exact_peak, || { + canonical_source_bounded(&exact_source_program) + }); + let bounded_source = bounded_source.unwrap(); + assert!(!overflowed); + assert_eq!(consumed, MAX_SOURCE_BYTES); + assert_eq!(bounded_source.len(), MAX_SOURCE_BYTES); + assert_eq!(bounded_source.capacity(), MAX_SOURCE_BYTES); + assert_eq!(bounded_source, exact_source); + CANONICAL_FORMAT_PASS_COUNT.with(|count| assert_eq!(count.get(), 2)); + CANONICAL_FORMAT_PASS_COUNT.with(|count| count.set(0)); + let (peak_error, overflowed, consumed) = + crate::bounded_output::with_limit_usage(exact_peak - 1, || { + canonical_source_bounded(&exact_source_program) + }); + let peak_error = peak_error.unwrap_err(); + assert!(!overflowed); + assert_eq!(consumed, 0, "failed materialization leaked scratch budget"); + assert_eq!(peak_error.code, "SPX-B109"); + assert_eq!( + peak_error.message, + "Native Rust Interop max_builder_bytes exceeds 33554432" + ); + CANONICAL_FORMAT_PASS_COUNT.with(|count| assert_eq!(count.get(), 1)); + let mut exact_source_spec = parse_spec(&program, canonical.as_bytes()).unwrap(); + exact_source_spec.source_revision = domain_digest(SOURCE_DOMAIN, exact_source.as_bytes()); + parse_spec( + &exact_source_program, + render_spec(&exact_source_spec).as_bytes(), + ) + .unwrap(); + + let mut over_program = exact_source_program; + over_program.functions[0].name.push('a'); + let over_source = crate::format::canonical(&over_program); + assert_eq!(over_source.len(), MAX_SOURCE_BYTES + 1); + CANONICAL_FORMAT_PASS_COUNT.with(|count| count.set(0)); + let (bounded_source, overflowed, consumed) = crate::bounded_output::with_limit_usage( + canonical_format_scratch_capacity(&over_program) + .unwrap() + .bytes(), + || canonical_source_bounded(&over_program), + ); + let bounded_source = bounded_source.unwrap_err(); + assert!(!overflowed); + assert_eq!(consumed, 0, "over-limit counting pass allocated output"); + assert_eq!(bounded_source.code, "SPX-B109"); + assert_eq!( + bounded_source.message, + "Native Rust Interop max_source_bytes exceeds 16777216" + ); + CANONICAL_FORMAT_PASS_COUNT.with(|count| assert_eq!(count.get(), 1)); + assert_eq!( + crate::format::canonical(&over_program), + over_source, + "bounded formatting mutated the source program" + ); + let mut over_source_spec = exact_source_spec; + over_source_spec.source_revision = domain_digest(SOURCE_DOMAIN, over_source.as_bytes()); + let error = match parse_spec(&over_program, render_spec(&over_source_spec).as_bytes()) { + Ok(_) => panic!("over-limit source was accepted"), + Err(error) => error, + }; + assert_eq!(error.code, "SPX-B109"); + assert_eq!( + error.message, + "Native Rust Interop max_source_bytes exceeds 16777216" + ); + + let spec = parse_spec(&program, canonical.as_bytes()).unwrap(); + for mutation in [ + { + let mut value = spec.clone(); + value.module = "forged.module".to_owned(); + value + }, + { + let mut value = spec.clone(); + value.source_revision = "sha256:forged-source".to_owned(); + value + }, + ] { + assert_spec_error( + &program, + render_spec(&mutation).as_bytes(), + "SPX-B107", + "Native Rust Interop declaration set is unsupported: selected identity missing", + ); + } + let mut wrong_target = spec.clone(); + wrong_target.target.triple = "forged-unknown-target".to_owned(); + assert_spec_error( + &program, + render_spec(&wrong_target).as_bytes(), + "SPX-B107", + "Native Rust Interop declaration set is unsupported: target profile mismatch", + ); + let mut wrong_capability = spec.clone(); + wrong_capability.capabilities = vec!["forged.capability".to_owned()]; + let error = match prepare_native_rust_interop( + &program, + render_spec(&wrong_capability).as_bytes(), + ) { + Ok(_) => panic!("forged capability was accepted"), + Err(error) => error, + }; + assert_eq!(error.len(), 1); + assert_eq!(error[0].code, "SPX-B107"); + assert_eq!( + error[0].message, + "Native Rust Interop declaration set is unsupported: effect or capability mismatch" + ); + + let automatic_source = SOURCE.replacen("@id(\"interop.add\")\n", "", 1); + let automatic_program = crate::parse( + &automatic_source, + Path::new("native-rust-interop-automatic-export.spx"), + ) + .unwrap(); + let automatic_id = automatic_program + .functions + .iter() + .find(|function| function.name == "add") + .unwrap() + .stable_id + .clone(); + let mut automatic_spec = spec; + automatic_spec.source_revision = domain_digest( + SOURCE_DOMAIN, + crate::format::canonical(&automatic_program).as_bytes(), + ); + automatic_spec.exports = vec![automatic_id]; + let error = match prepare_native_rust_interop( + &automatic_program, + render_spec(&automatic_spec).as_bytes(), + ) { + Ok(_) => panic!("automatic export identity was accepted"), + Err(error) => error, + }; + assert_eq!(error.len(), 1); + assert_eq!(error[0].code, "SPX-B107"); + assert_eq!( + error[0].message, + "Native Rust Interop declaration set is unsupported: explicit persistent ID required" + ); + } + + #[test] + fn specification_shape_rejects_flat_container_and_scalar_explosions_before_decode() { + let (program, _) = fixture(); + for element in ["[]", "0", "\"\""] { + let mut hostile = String::with_capacity(MAX_SPEC_BYTES); + hostile.push('['); + let mut first = true; + while hostile + .len() + .checked_add(usize::from(!first)) + .and_then(|length| length.checked_add(element.len())) + .is_some_and(|length| length < MAX_SPEC_BYTES) + { + if !first { + hostile.push(','); + } + hostile.push_str(element); + first = false; + } + while hostile.len() + 1 < MAX_SPEC_BYTES { + hostile.push(' '); + } + hostile.push(']'); + assert_eq!(hostile.len(), MAX_SPEC_BYTES); + let error = match parse_spec(&program, hostile.as_bytes()) { + Ok(_) => panic!("hostile generic JSON shape was accepted"), + Err(error) => error, + }; + assert_eq!(error.code, "SPX-B106"); + assert_eq!( + error.message, + "Native Rust Interop specification is not canonical semaprax.native-rust-interop-spec.v1 JSON" + ); + } + } + + #[test] + fn export_import_and_parameter_count_limits_are_exact() { + let mut source = String::from( + "module interop.limit;\n\n@id(\"host.limit\")\ninterface HostLimit\n permits { }\n{\n", + ); + let parameters = (0..MAX_PARAMETERS) + .map(|index| format!("p{index}: i64")) + .collect::>() + .join(", "); + let arguments = (0..MAX_PARAMETERS) + .map(|index| format!("p{index}")) + .collect::>() + .join(", "); + for index in 0..MAX_IMPORTS { + write!( + source, + " @id(\"host.{index:02}\")\n import rust fn import_{index:02}({parameters}) -> i64\n effects {{ }}\n failure infallible;\n" + ) + .unwrap(); + } + source.push_str("}\n\n"); + for index in 0..MAX_EXPORTS { + write!( + source, + "@id(\"export.{index:02}\")\nfn export_{index:02}({parameters}) -> i64\n{{\n import_{index:02}({arguments})\n}}\n\n" + ) + .unwrap(); + } + source.push_str("@id(\"interop.limit.main\")\nfn main() -> i64\n{\n 0\n}\n"); + let program = crate::parse(&source, Path::new("native-rust-limits.spx")).unwrap(); + let canonical_source = crate::format::canonical(&program); + let spec = Spec { + module: program.module.clone(), + source_revision: domain_digest(SOURCE_DOMAIN, canonical_source.as_bytes()), + target: current_target().unwrap(), + exports: (0..MAX_EXPORTS) + .map(|index| format!("export.{index:02}")) + .collect(), + imports: (0..MAX_IMPORTS) + .map(|index| format!("host.{index:02}")) + .collect(), + capabilities: Vec::new(), + }; + let prepared = + prepare_native_rust_interop(&program, render_spec(&spec).as_bytes()).unwrap(); + assert_eq!(prepared.exports.len(), MAX_EXPORTS); + assert_eq!(prepared.imports.len(), MAX_IMPORTS); + assert_eq!(prepared.closure.len(), MAX_EXPORTS); + assert!(prepared + .exports + .iter() + .all(|export| export.parameters.len() == MAX_PARAMETERS)); + assert!(prepared + .imports + .iter() + .all(|import| import.parameters.len() == MAX_PARAMETERS)); + + let mut over_exports = spec.clone(); + over_exports.exports.push("export.over".to_owned()); + let error = match parse_spec(&program, render_spec(&over_exports).as_bytes()) { + Ok(_) => panic!("over-limit export set was accepted"), + Err(error) => error, + }; + assert_eq!(error.code, "SPX-B109"); + assert_eq!(error.message, "Native Rust Interop max_exports exceeds 32"); + + let mut over_imports = spec; + over_imports.imports.push("host.over".to_owned()); + let error = match parse_spec(&program, render_spec(&over_imports).as_bytes()) { + Ok(_) => panic!("over-limit import set was accepted"), + Err(error) => error, + }; + assert_eq!(error.code, "SPX-B109"); + assert_eq!(error.message, "Native Rust Interop max_imports exceeds 32"); + } + + #[test] + fn closure_effect_and_identifier_limits_are_exact() { + let effects = (0..MAX_EFFECTS) + .map(|index| { + let first = char::from(b'a' + u8::try_from(index / 26).unwrap()); + let second = char::from(b'a' + u8::try_from(index % 26).unwrap()); + format!("effect.e{first}{second}") + }) + .collect::>(); + let effect_list = effects.join(", "); + let source = format!( + "module interop.effects;\n\npermit {{ {effect_list} }}\n\n@id(\"host.effects\")\ninterface HostEffects\n permits {{ {effect_list} }}\n{{\n @id(\"host.effects.call\")\n import rust fn host_call(value: i64) -> i64\n effects {{ {effect_list} }}\n failure infallible;\n}}\n\n@id(\"export.effects\")\nfn export_effects(value: i64) -> i64\n uses {{ {effect_list} }}\n{{\n host_call(value)\n}}\n\n@id(\"interop.effects.main\")\nfn main() -> i64\n{{\n 0\n}}\n" + ); + let program = crate::parse(&source, Path::new("native-rust-effects.spx")).unwrap(); + let canonical_source = crate::format::canonical(&program); + let spec = Spec { + module: program.module.clone(), + source_revision: domain_digest(SOURCE_DOMAIN, canonical_source.as_bytes()), + target: current_target().unwrap(), + exports: vec!["export.effects".to_owned()], + imports: vec!["host.effects.call".to_owned()], + capabilities: effects, + }; + let prepared = + prepare_native_rust_interop(&program, render_spec(&spec).as_bytes()).unwrap(); + assert_eq!(prepared.exports[0].effects.len(), MAX_EFFECTS); + assert_eq!(prepared.imports[0].effects.len(), MAX_EFFECTS); + + let mut over_effects = spec.clone(); + over_effects.capabilities.push("effect.over".to_owned()); + let error = match parse_spec(&program, render_spec(&over_effects).as_bytes()) { + Ok(_) => panic!("over-limit capability set was accepted"), + Err(error) => error, + }; + assert_eq!(error.code, "SPX-B109"); + assert_eq!(error.message, "Native Rust Interop max_effects exceeds 64"); + + for (length, code, message) in [ + ( + MAX_IDENTIFIER_BYTES, + "SPX-B107", + "Native Rust Interop declaration set is unsupported: effect or capability mismatch", + ), + ( + MAX_IDENTIFIER_BYTES + 1, + "SPX-B109", + "Native Rust Interop max_identifier_bytes exceeds 128", + ), + ] { + let mut identifier_spec = spec.clone(); + identifier_spec.capabilities = vec!["a".repeat(length)]; + let error = match prepare_native_rust_interop( + &program, + render_spec(&identifier_spec).as_bytes(), + ) { + Ok(_) => panic!("hostile identifier was accepted"), + Err(error) => error, + }; + assert_eq!(error.len(), 1); + assert_eq!(error[0].code, code); + assert_eq!(error[0].message, message); + } + + fn closure_fixture(count: usize) -> (Program, Spec) { + let mut source = String::from( + "module interop.closure;\n\n@id(\"host.closure\")\ninterface HostClosure\n permits { }\n{\n @id(\"host.closure.leaf\")\n import rust fn host_leaf(value: i64) -> i64\n effects { }\n failure infallible;\n}\n\n", + ); + for index in 0..count { + let body = if index + 1 == count { + "host_leaf(value)".to_owned() + } else { + format!("closure_{:03}(value)", index + 1) + }; + write!( + source, + "@id(\"closure.{index:03}\")\nfn closure_{index:03}(value: i64) -> i64\n{{\n {body}\n}}\n\n" + ) + .unwrap(); + } + source.push_str("@id(\"interop.closure.main\")\nfn main() -> i64\n{\n 0\n}\n"); + let program = crate::parse(&source, Path::new("native-rust-closure.spx")).unwrap(); + let canonical = crate::format::canonical(&program); + let spec = Spec { + module: program.module.clone(), + source_revision: domain_digest(SOURCE_DOMAIN, canonical.as_bytes()), + target: current_target().unwrap(), + exports: vec!["closure.000".to_owned()], + imports: vec!["host.closure.leaf".to_owned()], + capabilities: Vec::new(), + }; + (program, spec) + } + + let (program, spec) = closure_fixture(MAX_CALL_DEPTH); + let canonical_source = crate::format::canonical(&program); + let mut hir_scan_stack = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let closure_phase = + hir_pre_resolve_capacity(&program, canonical_source.len(), &mut hir_scan_stack) + .unwrap() + .phase_peaks()[6]; + let terms = hir_capacity_terms_for_test(&program, canonical_source.len()).unwrap(); + assert_eq!(terms.2, 0, "scalar closure has no retained cleanup payload"); + reset_closure_capacity_high_water(); + let prepared = + prepare_native_rust_interop(&program, render_spec(&spec).as_bytes()).unwrap(); + assert_eq!(prepared.closure.len(), MAX_CALL_DEPTH); + let observed_closure_peak = closure_capacity_high_water(); + assert!(observed_closure_peak <= closure_phase); + assert_eq!(observed_closure_peak, 8_220); + let (program, spec) = closure_fixture(MAX_CALL_DEPTH + 1); + let error = match prepare_native_rust_interop(&program, render_spec(&spec).as_bytes()) { + Ok(_) => panic!("over-limit call depth was accepted"), + Err(error) => error, + }; + assert_eq!(error.len(), 1); + assert_eq!(error[0].code, "SPX-B109"); + assert_eq!( + error[0].message, + "Native Rust Interop max_call_depth exceeds 32" + ); + + let cycle_source = "module interop.closure_cycle; @id(\"cycle.a\") fn a(value: i64) -> i64 { b(value) } @id(\"cycle.b\") fn b(value: i64) -> i64 { a(value) } @id(\"app.main\") fn main() -> i64 { 0 }"; + let cycle_program = + crate::parse(cycle_source, Path::new("native-rust-closure-cycle.spx")).unwrap(); + let cycle_spec = Spec { + module: cycle_program.module.clone(), + source_revision: domain_digest( + SOURCE_DOMAIN, + crate::format::canonical(&cycle_program).as_bytes(), + ), + target: current_target().unwrap(), + exports: vec!["cycle.a".to_owned()], + imports: Vec::new(), + capabilities: Vec::new(), + }; + let error = match prepare_native_rust_interop( + &cycle_program, + render_spec(&cycle_spec).as_bytes(), + ) { + Ok(_) => panic!("cyclic closure was accepted"), + Err(error) => error, + }; + assert_eq!(error.len(), 1); + assert_eq!(error[0].code, "SPX-B107"); + assert_eq!( + error[0].message, + "Native Rust Interop declaration set is unsupported: selected closure is cyclic" + ); + } + + #[test] + fn deeply_forged_hir_fails_iteratively_without_stack_growth() { + let (program, _) = fixture(); + let mut resolved = hir::resolve(&program).unwrap(); + let function_index = resolved + .functions + .iter() + .position(|function| function.id.as_str() == "interop.add") + .unwrap(); + let mut expression = resolved.functions[function_index].body.clone(); + for _ in 0..MAX_SEMANTIC_EXPRESSION_DEPTH { + let id = expression.id.clone(); + let ty = expression.ty.clone(); + let ownership = expression.ownership; + let span = expression.span; + expression = ResolvedExpr { + id, + ty, + ownership, + kind: ResolvedExprKind::Unary { + op: crate::ast::UnaryOp::Neg, + value: Box::new(expression), + }, + span, + }; + } + resolved.functions[function_index].body = expression; + let capacity = MAX_SEMANTIC_EXPRESSION_DEPTH * 4 + 32; + let owner = ResolvedProgramOwner::new(resolved, Vec::with_capacity(capacity), capacity); + let error = validate_native_rust_expression_budget(owner.program()).unwrap_err(); + assert_eq!(error.code, "SPX-B109"); + assert_eq!( + error.message, + "Native Rust Interop max_semantic_expression_depth exceeds 512" + ); + drop(owner); + } + + #[test] + fn semantic_expression_depth_512_is_exact_for_source_and_hir() { + fn wrap_source(mut expression: crate::ast::Expr, count: usize) -> crate::ast::Expr { + for _ in 0..count { + let span = expression.span; + expression = crate::ast::Expr { + kind: crate::ast::ExprKind::Unary { + op: crate::ast::UnaryOp::Neg, + value: Box::new(expression), + }, + span, + }; + } + expression + } + + fn wrap_hir(mut expression: ResolvedExpr, count: usize) -> ResolvedExpr { + for _ in 0..count { + let id = expression.id.clone(); + let ty = expression.ty.clone(); + let ownership = expression.ownership; + let span = expression.span; + expression = ResolvedExpr { + id, + ty, + ownership, + kind: ResolvedExprKind::Unary { + op: crate::ast::UnaryOp::Neg, + value: Box::new(expression), + }, + span, + }; + } + expression + } + + // The fixture body has depth four: block -> addition -> import call -> argument. + const EXACT_WRAPPERS: usize = MAX_SEMANTIC_EXPRESSION_DEPTH - 4; + let (program, _) = fixture(); + let mut exact_source = program.clone(); + let function = exact_source + .functions + .iter_mut() + .find(|function| function.stable_id == "interop.add") + .unwrap(); + function.body = wrap_source(function.body.clone(), EXACT_WRAPPERS); + validate_native_rust_source_expression_budget(&exact_source).unwrap(); + let canonical_exact = canonical_source_bounded(&exact_source).unwrap(); + let mut hir_scan_stack = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let hir_upper = + hir_pre_resolve_capacity(&exact_source, canonical_exact.len(), &mut hir_scan_stack) + .unwrap(); + assert!(hir_upper.complete().unwrap() >= canonical_exact.len()); + let resolved_exact_source = hir::resolve(&exact_source).unwrap(); + validate_native_rust_expression_budget(&resolved_exact_source).unwrap(); + let mut over_source = exact_source; + let function = over_source + .functions + .iter_mut() + .find(|function| function.stable_id == "interop.add") + .unwrap(); + function.body = wrap_source(function.body.clone(), 1); + let error = validate_native_rust_source_expression_budget(&over_source).unwrap_err(); + assert_eq!(error.code, "SPX-B109"); + assert_eq!( + error.message, + "Native Rust Interop max_semantic_expression_depth exceeds 512" + ); + + let mut exact_hir = hir::resolve(&program).unwrap(); + let function = exact_hir + .functions + .iter_mut() + .find(|function| function.id.as_str() == "interop.add") + .unwrap(); + function.body = wrap_hir(function.body.clone(), EXACT_WRAPPERS); + validate_native_rust_expression_budget(&exact_hir).unwrap(); + let function = exact_hir + .functions + .iter_mut() + .find(|function| function.id.as_str() == "interop.add") + .unwrap(); + function.body = wrap_hir(function.body.clone(), 1); + let error = validate_native_rust_expression_budget(&exact_hir).unwrap_err(); + assert_eq!(error.code, "SPX-B109"); + assert_eq!( + error.message, + "Native Rust Interop max_semantic_expression_depth exceeds 512" + ); + } + + #[test] + fn canonical_formatter_census_admits_shallow_wide_types_and_patterns() { + const WIDTH: usize = 128; + #[allow(clippy::format_collect)] + let fields = (0..WIDTH) + .map(|index| format!(" @id(\"wide.record.f{index:03}\")\n f{index:03}: i64,\n")) + .collect::(); + let pattern = (0..WIDTH) + .map(|index| format!("f{index:03}: _")) + .collect::>() + .join(", "); + let source = format!( + "module formatter.wide;\n\n@id(\"wide.record\")\nrecord Wide {{\n{fields}}}\n\n@id(\"wide.read\")\nfn read(value: Wide) -> i64\n{{\n match value {{\n Wide {{ {pattern} }} => 0,\n }}\n}}\n\n@id(\"app.main\")\nfn main() -> i64\n{{\n 0\n}}\n" + ); + let program = crate::parse(&source, Path::new("formatter-shallow-wide.spx")).unwrap(); + let canonical = crate::format::canonical(&program); + let scratch = canonical_format_scratch_capacity(&program).unwrap(); + assert_eq!( + scratch.bytes(), + crate::private_format::private_scratch_capacity(3, 1, 1) + .unwrap() + .bytes(), + "width must not be mistaken for recursive formatter depth" + ); + let exact_peak = scratch.bytes().checked_add(canonical.len()).unwrap(); + let (bounded, overflowed, consumed) = + crate::bounded_output::with_limit_usage(exact_peak, || { + canonical_source_bounded(&program) + }); + let bounded = bounded.unwrap(); + assert!(!overflowed); + assert_eq!(bounded, canonical); + assert_eq!(consumed, canonical.len()); + + let mut deep_program = crate::parse( + "module formatter.deep; @id(\"app.main\") fn main() -> i64 { 0 }", + Path::new("formatter-deep-types.spx"), + ) + .unwrap(); + let mut deep_type = crate::ast::Type::I64; + for index in 0..32 { + deep_type = crate::ast::Type::Named { + name: format!("T{index}"), + arguments: vec![deep_type], + }; + } + let call = |index| crate::ast::Expr { + kind: crate::ast::ExprKind::Call { + name: format!("callee_{index}"), + type_arguments: vec![deep_type.clone()], + args: vec![], + }, + span: crate::ast::Span::default(), + }; + deep_program.functions[0].body = crate::ast::Expr { + kind: crate::ast::ExprKind::Block { + statements: (0..64) + .map(|index| crate::ast::Statement::Let { + name: format!("value_{index}"), + name_span: crate::ast::Span::default(), + value: call(index), + span: crate::ast::Span::default(), + }) + .collect(), + tail: Box::new(call(64)), + }, + span: crate::ast::Span::default(), + }; + let canonical = crate::format::canonical(&deep_program); + let scratch = canonical_format_scratch_capacity(&deep_program).unwrap(); + assert_eq!( + scratch.bytes(), + crate::private_format::private_scratch_capacity(2, 33, 1) + .unwrap() + .bytes(), + "statement and type width must not inflate nesting, but embedded type depth must count" + ); + let exact_peak = scratch.bytes().checked_add(canonical.len()).unwrap(); + CANONICAL_FORMAT_PASS_COUNT.with(|count| count.set(0)); + let (bounded, overflowed, consumed) = + crate::bounded_output::with_limit_usage(exact_peak, || { + canonical_source_bounded(&deep_program) + }); + assert_eq!(bounded.unwrap(), canonical); + assert!(!overflowed); + assert_eq!( + consumed, + canonical.len(), + "private formatting charged legacy temporaries" + ); + CANONICAL_FORMAT_PASS_COUNT.with(|count| assert_eq!(count.get(), 2)); + CANONICAL_FORMAT_PASS_COUNT.with(|count| count.set(0)); + let (error, overflowed, consumed) = + crate::bounded_output::with_limit_usage(exact_peak - 1, || { + canonical_source_bounded(&deep_program) + }); + assert_eq!(error.unwrap_err().code, "SPX-B109"); + assert!(!overflowed); + assert_eq!(consumed, 0); + CANONICAL_FORMAT_PASS_COUNT.with(|count| assert_eq!(count.get(), 1)); + } + + #[test] + fn formatter_frame_capacity_covers_nested_delimiters_and_helper_stacks() { + use crate::ast::{Expr, ExprKind, MatchArm, MatchPattern, RecordMatchFieldPattern}; + + let span = crate::ast::Span::default(); + let mut ty = crate::ast::Type::I64; + for index in 0..31 { + ty = crate::ast::Type::Named { + name: format!("T{index}"), + arguments: vec![ty], + }; + } + let mut scrutinee = Expr { + kind: ExprKind::ConstructRecord { + type_name: "Leaf".into(), + type_span: span, + type_arguments: vec![ty.clone()], + fields: vec![], + }, + span, + }; + for _ in 0..64 { + scrutinee = Expr { + kind: ExprKind::If { + condition: Box::new(scrutinee), + then_branch: Box::new(Expr { + kind: ExprKind::Int(1), + span, + }), + else_branch: Box::new(Expr { + kind: ExprKind::Int(0), + span, + }), + }, + span, + }; + } + let mut nested_pattern = RecordMatchFieldPattern::Binding { + name: "value".into(), + span, + }; + for index in 0..31 { + nested_pattern = RecordMatchFieldPattern::Record { + type_name: format!("P{index}"), + type_span: span, + fields: vec![crate::ast::RecordMatchPatternField { + name: "next".into(), + name_span: span, + pattern: nested_pattern, + span, + }], + span, + }; + } + let mut program = crate::parse( + "module formatter.frames; @id(\"app.main\") fn main() -> i64 { 0 }", + Path::new("formatter-frames.spx"), + ) + .unwrap(); + program.functions[0].body = Expr { + kind: ExprKind::Match { + scrutinee: Box::new(scrutinee), + arms: vec![MatchArm { + pattern: MatchPattern::Record { + type_name: "Root".into(), + type_span: span, + fields: vec![crate::ast::RecordMatchPatternField { + name: "next".into(), + name_span: span, + pattern: nested_pattern, + span, + }], + span, + }, + value: Expr { + kind: ExprKind::Call { + name: "typed".into(), + type_arguments: vec![ty], + args: vec![], + }, + span, + }, + span, + }], + }, + span, + }; + + let capacity = canonical_format_scratch_capacity(&program).unwrap(); + crate::private_format::reset_private_scratch_high_water(); + let mut sink = String::new(); + crate::private_format::write_canonical_with_scratch(&program, &mut sink, capacity); + let water = crate::private_format::private_scratch_high_water(); + let slots = capacity.slots(); + for (index, ((length, allocated), admitted)) in water.into_iter().zip(slots).enumerate() { + assert!(length > 0, "formatter helper {index} was not exercised"); + assert!( + length <= admitted, + "formatter helper {index} exceeded census" + ); + assert_eq!(allocated, admitted, "formatter helper {index} grew its Vec"); + } + assert!( + water[0].0 > 120, + "nested delimiter continuations were not retained" + ); + assert!( + water[1].0 > 60, + "nested contains-record traversal was not retained" + ); + assert!(water[2].0 > 30, "nested type traversal was not retained"); + assert!(water[3].0 > 30, "nested pattern traversal was not retained"); + } + + #[test] + fn declaration_dag_capacity_counts_layered_leaf_and_layout_expansion_once() { + fn layered(resource: bool, levels: usize) -> Program { + let mut source = String::from("module capacity.layers;\n\n"); + if resource { + source.push_str("@id(\"layer.r0\")\nresource R0 {\n @id(\"layer.r0.drop\")\n drop trivial;\n}\n\n"); + } else { + source.push_str("@id(\"layer.r0\")\nrecord R0 {\n @id(\"layer.r0.value\")\n value: i64,\n}\n\n"); + } + for level in 1..=levels { + writeln!( + source, + "@id(\"layer.r{level}\")\nrecord R{level} {{\n @id(\"layer.r{level}.a\")\n a: R{},\n @id(\"layer.r{level}.b\")\n b: R{},\n}}\n", + level - 1, + level - 1 + ) + .unwrap(); + } + source.push_str("@id(\"app.main\")\nfn main() -> i64\n{\n 0\n}\n"); + crate::parse(&source, Path::new("layered-capacity.spx")).unwrap() + } + + let resource = declaration_dag_expansion(&layered(true, 12), 0).unwrap(); + assert_eq!(resource.maximum_resource_leaves, 1 << 12); + assert_eq!(resource.maximum_type_occurrences, (1 << 13) - 1); + assert!(resource.maximum_shape_fields >= (1 << 13) - 2); + assert!(resource.maximum_projection_segments >= 12 * (1 << 12)); + assert!(resource.maximum_shape_identity_bytes > 0); + assert!(resource.maximum_lifecycle_identity_bytes > 0); + assert!(resource.maximum_projection_identity_bytes > 0); + let scalar = declaration_dag_expansion(&layered(false, 12), 0).unwrap(); + assert_eq!(scalar.maximum_resource_leaves, 0); + assert_eq!(scalar.maximum_type_occurrences, 3 * (1 << 12) - 1); + assert!(scalar.maximum_shape_fields >= (1 << 13) - 1); + assert_eq!(scalar.maximum_projection_segments, 0); + + let long = "x".repeat(128); + let long_source = format!( + "module capacity.long; @id(\"life.{long}\") resource Leaf {{ @id(\"drop.{long}\") drop trivial; }} @id(\"outer.{long}\") record Outer {{ @id(\"field.{long}\") leaf: Leaf, }} @id(\"app.main\") fn main() -> i64 {{ 0 }}" + ); + let long_program = crate::parse(&long_source, Path::new("long-capacity.spx")).unwrap(); + let long_expansion = declaration_dag_expansion(&long_program, 0).unwrap(); + assert_eq!(long_expansion.maximum_resource_leaves, 1); + assert_eq!(long_expansion.maximum_shape_fields, 1); + assert_eq!(long_expansion.maximum_projection_segments, 1); + assert!(long_expansion.maximum_shape_identity_bytes >= 3 * 128); + assert!(long_expansion.maximum_lifecycle_identity_bytes >= 128); + assert!(long_expansion.maximum_projection_identity_bytes >= 128); + + let cyclic = crate::parse( + "module capacity.cycle;\n\n@id(\"cycle.a\")\nrecord A {\n @id(\"cycle.a.next\")\n next: A,\n}\n\n@id(\"app.main\")\nfn main() -> i64\n{\n 0\n}\n", + Path::new("cycle-capacity.spx"), + ) + .unwrap(); + let error = declaration_dag_expansion(&cyclic, 0).unwrap_err(); + assert_eq!(error.code, "SPX-B107"); + + let mut shallow = String::from("module capacity.shallow;\n\n"); + for index in 0..514 { + writeln!( + shallow, + "@id(\"shallow.r{index}\")\nrecord R{index} {{\n @id(\"shallow.r{index}.value\")\n value: i64,\n}}\n" + ) + .unwrap(); + } + shallow.push_str("@id(\"app.main\")\nfn main() -> i64\n{\n 0\n}\n"); + let shallow = crate::parse(&shallow, Path::new("shallow-declarations.spx")).unwrap(); + let expansion = declaration_dag_expansion(&shallow, 0).unwrap(); + assert_eq!(expansion.maximum_resource_leaves, 0); + assert_eq!(expansion.maximum_type_occurrences, 2); + + let mut chain = String::from( + "module capacity.chain;\n\n@id(\"chain.r0\")\nrecord R0 {\n @id(\"chain.r0.value\")\n value: i64,\n}\n\n", + ); + for index in 1..514 { + writeln!( + chain, + "@id(\"chain.r{index}\")\nrecord R{index} {{\n @id(\"chain.r{index}.next\")\n next: R{},\n}}\n", + index - 1 + ) + .unwrap(); + } + chain.push_str("@id(\"app.main\")\nfn main() -> i64\n{\n 0\n}\n"); + let chain = crate::parse(&chain, Path::new("long-chain.spx")).unwrap(); + let expansion = declaration_dag_expansion(&chain, 0).unwrap(); + assert_eq!(expansion.maximum_resource_leaves, 0); + assert_eq!(expansion.maximum_type_occurrences, 515); + } + + #[test] + fn typed_cleanup_retained_census_covers_long_ids_and_many_owned_roots() { + let long = "x".repeat(128); + let mut source = String::from("module capacity.cleanup_typed;\n\n"); + writeln!( + source, + "@id(\"resource.{long}\") resource R0 {{ @id(\"lifecycle.{long}\") drop trivial; }}" + ) + .unwrap(); + for index in 1..=64 { + writeln!( + source, + "@id(\"record.{index:03}.{long}\") record R{index} {{ @id(\"field.{index:03}.{long}\") next: R{}, }}", + index - 1 + ) + .unwrap(); + } + let parameters = (0..MAX_PARAMETERS) + .map(|index| format!("p{index}: own R64")) + .collect::>() + .join(", "); + writeln!( + source, + "@id(\"consume.typed\") fn consume({parameters}) -> i64 {{ 0 }}" + ) + .unwrap(); + source.push_str("@id(\"app.main\") fn main() -> i64 { 0 }\n"); + + let program = crate::parse(&source, Path::new("typed-cleanup-capacity.spx")).unwrap(); + let canonical = crate::format::canonical(&program); + let mut scan = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let capacity = hir_pre_resolve_capacity(&program, canonical.len(), &mut scan).unwrap(); + let resolved = hir::resolve(&program).unwrap(); + let actual = resolved + .functions + .iter() + .try_fold(0usize, |bytes, function| { + bytes + .checked_add( + crate::private_capacity_contract::cleanup_inventory_owned_capacity( + &function.cleanup, + )?, + )? + .checked_add( + crate::private_capacity_contract::cleanup_plan_owned_capacity( + &function.cleanup_plan, + )?, + ) + }) + .unwrap(); + assert!(actual > MAX_PARAMETERS * 64 * 128); + assert!( + actual <= capacity.cleanup_retained_upper, + "actual cleanup {actual} exceeds derived {}", + capacity.cleanup_retained_upper + ); + } + + #[test] + fn cleanup_retained_census_admits_depth_by_live_roots_with_long_identities() { + let long = "x".repeat(128); + let mut source = String::from("module capacity.cleanup_depth_live;\n\n"); + writeln!( + source, + "@id(\"resource.{long}\") resource R0 {{ @id(\"lifecycle.{long}\") drop trivial; }}" + ) + .unwrap(); + source.push_str("@id(\"identity\") fn identity(value: own R0) -> R0 { value }\n"); + source.push_str("@id(\"consume\") fn consume(value: own R0) -> i64 { 1 }\n"); + let parameters = (0..MAX_PARAMETERS) + .map(|index| format!("p{index}: own R0")) + .chain(std::iter::once("value: i64".to_owned())) + .collect::>() + .join(", "); + writeln!(source, "@id(\"stress\") fn stress({parameters}) -> i64 {{").unwrap(); + for index in 0..MAX_PARAMETERS { + writeln!(source, "let live{index} = identity(p{index});").unwrap(); + } + source.push_str("let checked = value"); + for _ in 0..510 { + source.push_str(" + 1"); + } + source.push_str(";\nchecked + "); + for index in 0..MAX_PARAMETERS { + if index != 0 { + source.push_str(" + "); + } + write!(source, "consume(live{index})").unwrap(); + } + source.push_str("\n}\n@id(\"app.main\") fn main() -> i64 { 0 }\n"); + + let program = crate::parse(&source, Path::new("cleanup-depth-live.spx")).unwrap(); + let function = program + .functions + .iter() + .find(|function| function.name == "stress") + .unwrap(); + let mut depth_scan = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let depth = scan_ast_capacity( + function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures), + &program, + false, + &mut depth_scan, + ) + .unwrap() + .max_depth; + assert_eq!(depth, MAX_SEMANTIC_EXPRESSION_DEPTH); + let canonical = crate::format::canonical(&program); + let mut scan = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let capacity = hir_pre_resolve_capacity(&program, canonical.len(), &mut scan).unwrap(); + let resolved = hir::resolve(&program).unwrap(); + let actual = resolved + .functions + .iter() + .try_fold(0usize, |bytes, function| { + bytes + .checked_add( + crate::private_capacity_contract::cleanup_inventory_owned_capacity( + &function.cleanup, + )?, + )? + .checked_add( + crate::private_capacity_contract::cleanup_plan_owned_capacity( + &function.cleanup_plan, + )?, + ) + }) + .unwrap(); + assert!( + actual <= capacity.cleanup_authority_upper, + "actual cleanup {actual} exceeds authority {}", + capacity.cleanup_authority_upper + ); + assert!( + capacity.complete().unwrap() <= MAX_BUILDER_BYTES, + "depth×live capacity terms: {:?}; actual cleanup: {actual}", + hir_capacity_terms_for_test(&program, canonical.len()).unwrap() + ); + } + + #[test] + fn cleanup_retained_census_releases_sequential_early_move_epochs() { + fn measure(delayed_moves: bool) -> (HirPreResolveCapacity, usize) { + let long = "x".repeat(128); + let mut source = String::from("module capacity.cleanup_sequential_moves;\n\n"); + writeln!( + source, + "@id(\"resource.{long}\") resource R0 {{ @id(\"lifecycle.{long}\") drop trivial; }}" + ) + .unwrap(); + source.push_str("@id(\"identity\") fn identity(value: own R0) -> R0 { value }\n"); + source.push_str("@id(\"consume\") fn consume(value: own R0) -> i64 { 1 }\n"); + let parameters = (0..MAX_PARAMETERS) + .map(|index| format!("p{index}: own R0")) + .collect::>() + .join(", "); + writeln!(source, "@id(\"stress\") fn stress({parameters}) -> i64 {{").unwrap(); + for index in 0..MAX_PARAMETERS { + writeln!(source, "let epoch{index} = identity(p{index});").unwrap(); + if !delayed_moves { + writeln!(source, "let consumed{index} = consume(epoch{index});").unwrap(); + } + } + if delayed_moves { + for index in 0..MAX_PARAMETERS { + writeln!(source, "let consumed{index} = consume(epoch{index});").unwrap(); + } + } + source.push('0'); + for _ in 0..256 { + source.push_str(" + 1"); + } + source.push_str("\n}\n@id(\"app.main\") fn main() -> i64 { 0 }\n"); + + let program = crate::parse(&source, Path::new("cleanup-sequential-moves.spx")).unwrap(); + let canonical = crate::format::canonical(&program); + let mut scan = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let capacity = hir_pre_resolve_capacity(&program, canonical.len(), &mut scan).unwrap(); + let resolved = hir::resolve(&program).unwrap(); + let actual = resolved + .functions + .iter() + .try_fold(0usize, |bytes, function| { + bytes + .checked_add( + crate::private_capacity_contract::cleanup_inventory_owned_capacity( + &function.cleanup, + )?, + )? + .checked_add( + crate::private_capacity_contract::cleanup_plan_owned_capacity( + &function.cleanup_plan, + )?, + ) + }) + .unwrap(); + assert!(actual <= capacity.cleanup_authority_upper); + (capacity, actual) + } + + let (early, early_actual) = measure(false); + let (delayed, delayed_actual) = measure(true); + assert!(early.cleanup_authority_upper < delayed.cleanup_authority_upper); + assert!(early_actual < delayed_actual); + for (arrangement, capacity) in [("early", early), ("delayed", delayed)] { + assert!( + capacity.complete().unwrap() <= MAX_BUILDER_BYTES, + "sequential {arrangement}-move capacity {} exceeds {MAX_BUILDER_BYTES}", + capacity.complete().unwrap() + ); + } + } + + #[test] + fn cleanup_binding_flow_releases_nested_moves_and_preserves_partial_projection() { + fn measure(source: &str) -> (usize, HirPreResolveCapacity, usize, usize) { + let program = crate::parse(source, Path::new("cleanup-nested-move.spx")).unwrap(); + let function = program + .functions + .iter() + .find(|function| function.name == "stress") + .unwrap(); + let mut traversal = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let events = + cleanup_parameter_finalizer_events(function, "value", &program, &mut traversal) + .unwrap(); + let nodes = scan_ast_capacity( + std::iter::once(&function.body), + &program, + false, + &mut traversal, + ) + .unwrap() + .nodes; + let canonical = crate::format::canonical(&program); + let capacity = + hir_pre_resolve_capacity(&program, canonical.len(), &mut traversal).unwrap(); + let resolved = hir::resolve(&program).unwrap(); + let actual = resolved + .functions + .iter() + .try_fold(0usize, |bytes, function| { + bytes + .checked_add( + crate::private_capacity_contract::cleanup_inventory_owned_capacity( + &function.cleanup, + )?, + )? + .checked_add( + crate::private_capacity_contract::cleanup_plan_owned_capacity( + &function.cleanup_plan, + )?, + ) + }) + .unwrap(); + assert!(actual <= capacity.cleanup_authority_upper); + assert!(capacity.complete().unwrap() <= MAX_BUILDER_BYTES); + (events, capacity, actual, nodes) + } + + let definitions = r#" +@id("flow.r") resource R { @id("flow.r.drop") drop trivial; } +@id("flow.consume") fn consume(value: own R) -> i64 { 1 } +"#; + let cases = [ + ( + "block", + "{ let moved = { consume(value) }; let observed = checked + 1; moved + observed }", + "{ let observed = checked + 1; let moved = { consume(value) }; moved + observed }", + false, + true, + "", + "value: own R, checked: i64", + ), + ( + "if", + "{ let moved = if condition { consume(value) } else { consume(value) }; let observed = checked + 1; moved + observed }", + "{ let observed = checked + 1; let moved = if condition { consume(value) } else { consume(value) }; moved + observed }", + false, + true, + "", + "value: own R, checked: i64, condition: bool", + ), + ( + "match", + "{ let moved = match choice { Choice::A {} => consume(value), Choice::B {} => consume(value), }; let observed = checked + 1; moved + observed }", + "{ let observed = checked + 1; let moved = match choice { Choice::A {} => consume(value), Choice::B {} => consume(value), }; moved + observed }", + false, + true, + "@id(\"flow.choice\") variant Choice { @id(\"flow.choice.a\") A {}, @id(\"flow.choice.b\") B {}, }", + "value: own R, checked: i64, choice: Choice", + ), + ( + "construct", + "{ let moved = consume_box(Box { value: value }); let observed = checked + 1; moved + observed }", + "{ let observed = checked + 1; let moved = consume_box(Box { value: value }); moved + observed }", + false, + false, + "@id(\"flow.box\") record Box { @id(\"flow.box.value\") value: R, } @id(\"flow.consume_box\") fn consume_box(value: own Box) -> i64 { 1 }", + "value: own R, checked: i64", + ), + ( + "update", + "{ let moved = consume_box(value with { item: replacement }); let observed = checked + 1; moved + observed }", + "{ let observed = checked + 1; let moved = consume_box(value with { item: replacement }); moved + observed }", + false, + false, + "@id(\"flow.box\") record Box { @id(\"flow.box.item\") item: R, } @id(\"flow.consume_box\") fn consume_box(value: own Box) -> i64 { 1 }", + "value: own Box, replacement: own R, checked: i64", + ), + ( + "projection", + "{ let moved = consume(value.left); let observed = checked + 1; moved + observed }", + "{ let observed = checked + 1; let moved = consume(value.left); moved + observed }", + true, + false, + "@id(\"flow.pair\") record Pair { @id(\"flow.pair.left\") left: R, @id(\"flow.pair.right\") right: R, }", + "value: own Pair, checked: i64", + ), + ]; + for (shape, early_body, delayed_body, conservative, authority_drop, extra, parameters) in + cases + { + let source = |body: &str| { + format!( + "module capacity.flow_{shape};\n{definitions}\n{extra}\n@id(\"flow.stress\") fn stress({parameters}) -> i64 {body}\n@id(\"app.main\") fn main() -> i64 {{ 0 }}\n" + ) + }; + let (early_events, early, _, early_nodes) = measure(&source(early_body)); + let (delayed_events, delayed, _, delayed_nodes) = measure(&source(delayed_body)); + assert_eq!(early_nodes, delayed_nodes, "{shape}"); + if conservative { + assert_eq!(early_events, delayed_events, "{shape}"); + } else { + assert!(early_events < delayed_events, "{shape}"); + } + if authority_drop { + assert!( + early.cleanup_authority_upper < delayed.cleanup_authority_upper, + "{shape}" + ); + } + } + } + + #[test] + fn cleanup_retained_census_joins_mutually_exclusive_owned_branches() { + let long = "x".repeat(128); + let mut source = format!( + "module capacity.cleanup_branch_live;\n@id(\"resource.{long}\") resource R {{ @id(\"lifecycle.{long}\") drop trivial; }}\n@id(\"identity\") fn identity(value: own R) -> R {{ value }}\n@id(\"consume\") fn consume(value: own R) -> i64 {{ 1 }}\n" + ); + let parameters = (0..MAX_PARAMETERS) + .map(|index| format!("p{index}: own R")) + .chain(["condition: bool".to_owned(), "value: i64".to_owned()]) + .collect::>() + .join(", "); + writeln!( + source, + "@id(\"branch.stress\") fn stress({parameters}) -> i64 {{ if condition {{" + ) + .unwrap(); + for index in 0..4 { + writeln!(source, "let live{index} = identity(p{index});").unwrap(); + } + source.push_str("let checked = value"); + for _ in 0..508 { + source.push_str(" + 1"); + } + source.push_str("; checked"); + for index in 0..4 { + write!(source, " + consume(live{index})").unwrap(); + } + source.push_str(" } else { "); + for index in 4..8 { + writeln!(source, "let live{index} = identity(p{index});").unwrap(); + } + source.push_str("let checked = value"); + for _ in 0..508 { + source.push_str(" + 1"); + } + source.push_str("; checked"); + for index in 4..8 { + write!(source, " + consume(live{index})").unwrap(); + } + source.push_str(" } }\n@id(\"app.main\") fn main() -> i64 { 0 }\n"); + + let program = crate::parse(&source, Path::new("cleanup-branch-live.spx")).unwrap(); + let canonical = crate::format::canonical(&program); + let mut scan = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let stress = program + .functions + .iter() + .find(|function| function.name == "stress") + .unwrap(); + assert_eq!( + scan_ast_capacity(std::iter::once(&stress.body), &program, false, &mut scan) + .unwrap() + .max_depth, + MAX_SEMANTIC_EXPRESSION_DEPTH + ); + let capacity = hir_pre_resolve_capacity(&program, canonical.len(), &mut scan).unwrap(); + assert!( + capacity.complete().unwrap() <= MAX_BUILDER_BYTES, + "branch capacity terms {:?}, plan structural {}, complete {}", + hir_capacity_terms_for_test(&program, canonical.len()).unwrap(), + capacity.cleanup_plan_structural_upper, + capacity.complete().unwrap() + ); + let resolved = hir::resolve(&program).unwrap(); + let actual = resolved + .functions + .iter() + .try_fold(0usize, |bytes, function| { + bytes + .checked_add( + crate::private_capacity_contract::cleanup_inventory_owned_capacity( + &function.cleanup, + )?, + )? + .checked_add( + crate::private_capacity_contract::cleanup_plan_owned_capacity( + &function.cleanup_plan, + )?, + ) + }) + .unwrap(); + assert!( + actual <= capacity.cleanup_authority_upper, + "branch actual cleanup {actual} exceeds authority {} (retained {}, structural {})", + capacity.cleanup_authority_upper, + capacity.cleanup_retained_upper, + capacity.cleanup_authority_upper - capacity.cleanup_retained_upper + ); + } + + #[test] + fn type_facts_hostile_envelopes_are_bound_to_canonical_fixtures() { + fn layered(resource: bool, levels: usize) -> String { + let mut source = String::from("module capacity.typefacts.layers;\n\n"); + if resource { + source.push_str( + "@id(\"layer.r0\")\nresource R0 {\n @id(\"layer.r0.drop\")\n drop trivial;\n}\n\n", + ); + } else { + source.push_str( + "@id(\"layer.r0\")\nrecord R0 {\n @id(\"layer.r0.value\")\n value: i64,\n}\n\n", + ); + } + for level in 1..=levels { + writeln!( + source, + "@id(\"layer.r{level}\")\nrecord R{level} {{\n @id(\"layer.r{level}.a\")\n a: R{},\n @id(\"layer.r{level}.b\")\n b: R{},\n}}\n", + level - 1, + level - 1 + ) + .unwrap(); + } + source.push_str("@id(\"app.main\")\nfn main() -> i64 { 0 }\n"); + source + } + + fn envelope(source: &str, name: &str) -> (String, usize, usize, usize) { + let program = crate::parse(source, Path::new(name)).unwrap(); + let canonical = crate::format::canonical(&program); + let mut stack = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let capacity = hir_pre_resolve_capacity(&program, canonical.len(), &mut stack).unwrap(); + let type_facts_phase = capacity.phase_peaks()[7]; + ( + raw_digest(canonical.as_bytes()), + capacity.retained_upper, + type_facts_phase, + capacity + .retained_upper + .checked_add(type_facts_phase) + .unwrap(), + ) + } + + let scalar = layered(false, 12); + let resource = layered(true, 12); + let mut wide = String::from("module capacity.typefacts.wide;\n\n"); + for index in 0..514 { + writeln!( + wide, + "@id(\"wide.r{index}\")\nrecord R{index} {{\n @id(\"wide.r{index}.value\")\n value: i64,\n}}\n" + ) + .unwrap(); + } + wide.push_str("@id(\"app.main\")\nfn main() -> i64 { 0 }\n"); + let mut chain = String::from( + "module capacity.typefacts.chain;\n\n@id(\"chain.r0\")\nrecord R0 {\n @id(\"chain.r0.value\")\n value: i64,\n}\n\n", + ); + for index in 1..514 { + writeln!( + chain, + "@id(\"chain.r{index}\")\nrecord R{index} {{\n @id(\"chain.r{index}.next\")\n next: R{},\n}}\n", + index - 1 + ) + .unwrap(); + } + chain.push_str("@id(\"app.main\")\nfn main() -> i64 { 0 }\n"); + + assert_eq!( + [ + envelope(&scalar, "typefacts-layered-scalar.spx"), + envelope(&resource, "typefacts-layered-resource.spx"), + envelope(&wide, "typefacts-wide.spx"), + envelope(&chain, "typefacts-chain.spx"), + ], + [ + ( + "sha256:cfa16985be87d169c3fb81d5958126347ec82b4c1afed878e2d98d1fbfe72c80" + .to_owned(), + 220_110_854, + 438_720_350, + 658_831_204, + ), + ( + "sha256:461611e4315e312330af0285273568e5d09cd8e5770a35dcf66a82783aa15ae6" + .to_owned(), + 147_075_460, + 293_107_472, + 440_182_932, + ), + ( + "sha256:dc19474b86def3eaf6e3c60cc2224694e6aa7cf2811cca6115943c11102f95fc" + .to_owned(), + 42_048_403, + 80_965_504, + 123_013_907, + ), + ( + "sha256:d2692d4883957575ee95df8f9ee7057343599e1da945c386cedea714c716f66d" + .to_owned(), + 10_529_688_603, + 21_056_178_704, + 31_585_867_307, + ), + ], + "canonical fixture or independently computed envelope terms drifted" + ); + } + + #[test] + fn cleanup_retained_census_covers_shared_transition_and_staging_families() { + let source = include_str!("../../../tests/fixtures/native_rust_hir_capacity.spx"); + let program = crate::parse(source, Path::new("native-rust-hir-capacity.spx")).unwrap(); + let canonical = crate::format::canonical(&program); + let mut scan = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let capacity = hir_pre_resolve_capacity(&program, canonical.len(), &mut scan).unwrap(); + let resolved = hir::resolve(&program).unwrap(); + let actual = resolved + .functions + .iter() + .chain( + resolved + .function_instances + .iter() + .map(|instance| &instance.function), + ) + .try_fold(0usize, |bytes, function| { + bytes + .checked_add( + crate::private_capacity_contract::cleanup_inventory_owned_capacity( + &function.cleanup, + )?, + )? + .checked_add( + crate::private_capacity_contract::cleanup_plan_owned_capacity( + &function.cleanup_plan, + )?, + ) + }) + .unwrap(); + let actual_exits = resolved + .functions + .iter() + .chain( + resolved + .function_instances + .iter() + .map(|instance| &instance.function), + ) + .try_fold(0usize, |count, function| { + count.checked_add(function.cleanup_plan.exits.len()) + }) + .unwrap(); + assert!(resolved.functions.iter().any(|function| { + function.cleanup_plan.blocks.iter().any(|block| { + block.transitions.iter().any(|transition| { + matches!( + transition, + semaprax::cleanup_plan::CleanupTransition::CallCommit { .. } + ) + }) + }) + })); + assert!(resolved.functions.iter().any(|function| { + function.cleanup_plan.blocks.iter().any(|block| { + block.transitions.iter().any(|transition| { + matches!( + transition, + semaprax::cleanup_plan::CleanupTransition::StageCopyResult { .. } + ) + }) + }) + })); + assert!(resolved.functions.iter().any(|function| { + function.cleanup_plan.edges.iter().any(|edge| { + matches!( + edge.condition, + semaprax::cleanup_plan::EdgeCondition::VariantCase { .. } + ) + }) + })); + assert!(actual_exits <= capacity.cleanup_exit_events_upper); + assert!(actual <= capacity.cleanup_retained_upper); + } + + #[test] + fn cleanup_fieldwise_payload_and_vec_floors_are_covered() { + let source = include_str!("../../../tests/fixtures/native_rust_hir_capacity.spx"); + let program = crate::parse(source, Path::new("native-rust-hir-capacity.spx")).unwrap(); + let canonical = crate::format::canonical(&program); + let mut scan = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let capacity = hir_pre_resolve_capacity(&program, canonical.len(), &mut scan).unwrap(); + let proof = capacity.cleanup_proof; + let resolved = hir::resolve(&program).unwrap(); + let mut observed = ObservedCleanupProof::default(); + for function in resolved.functions.iter().chain( + resolved + .function_instances + .iter() + .map(|instance| &instance.function), + ) { + assert!( + observe_cleanup_function(function, &mut observed).is_some(), + "cleanup proof observer encountered an unadmitted non-exhaustive family" + ); + } + + let stats = proof.stats; + assert!(observed.slot_payload_bytes <= stats.ordinary_slot_payload_bytes); + assert!(observed.call_argument_slot_payload_bytes <= stats.call_argument_owned_bytes); + assert!(observed.shape_identity_bytes <= stats.shape_ids * 2); + assert!(observed.flag_lifecycle_bytes <= stats.lifecycle_ids); + assert!(observed.flag_projection_bytes <= stats.projection_ids); + assert!( + observed.place_storage_bytes + <= stats.ordinary_place_storage_bytes + stats.call_argument_owned_bytes + ); + assert!(observed.place_projection_bytes <= stats.place_projection_ids); + assert!(observed.finalizer_storage_bytes <= stats.ordinary_finalizer_storage_bytes); + assert!(observed.finalizer_projection_bytes <= stats.finalizer_projection_ids); + assert!(observed.finalizer_lifecycle_bytes <= stats.finalizer_lifecycle_ids); + + for (observed, derived, family) in [ + ( + observed.inventory_slot_capacity_entries, + proof.inventory_slot_capacity_entries, + "inventory slots", + ), + ( + observed.inventory_flag_capacity_entries, + proof.inventory_flag_capacity_entries, + "inventory flags", + ), + ( + observed.inventory_entry_capacity_entries, + proof.inventory_entry_capacity_entries, + "inventory entry state", + ), + ( + observed.plan_slot_capacity_entries, + proof.plan_slot_capacity_entries, + "plan slots", + ), + ( + observed.plan_entry_capacity_entries, + proof.plan_entry_capacity_entries, + "plan entry state", + ), + ( + observed.shape_field_capacity_entries, + proof.shape_field_capacity_entries, + "shape fields", + ), + ( + observed.flag_projection_capacity_entries, + proof.flag_projection_capacity_entries, + "flag projections", + ), + ( + observed.place_projection_capacity_entries, + proof.place_projection_capacity_entries, + "plan-place projections", + ), + ( + observed.finalizer_projection_capacity_entries, + proof.finalizer_projection_capacity_entries, + "finalizer projections", + ), + ( + observed.finalizer_capacity_entries, + proof.finalizer_capacity_entries, + "finalizers", + ), + ( + observed.block_capacity_entries, + proof.block_capacity_entries, + "blocks", + ), + ( + observed.edge_capacity_entries, + proof.edge_capacity_entries, + "edges", + ), + ( + observed.region_capacity_entries, + proof.region_capacity_entries, + "regions", + ), + ( + observed.exit_capacity_entries, + proof.exit_capacity_entries, + "exits", + ), + ( + observed.status_capacity_entries, + proof.status_capacity_entries, + "status sources", + ), + ( + observed.transition_capacity_entries, + proof.transition_capacity_entries, + "transitions", + ), + ( + observed.branch_edge_capacity_entries, + proof.branch_edge_capacity_entries, + "branch edge vectors", + ), + ( + observed.region_slot_capacity_entries, + proof.region_slot_capacity_entries, + "region slots", + ), + ( + observed.exit_region_capacity_entries, + proof.exit_region_capacity_entries, + "exit region vectors", + ), + ( + observed.status_case_capacity_entries, + proof.status_case_capacity_entries, + "status case vectors", + ), + ] { + assert!( + observed <= derived, + "observed {family} capacity {observed} exceeds derived {derived}" + ); + } + } + + #[test] + fn cleanup_generic_arity_two_checked_call_includes_exact_instance_identities() { + let long = "x".repeat(128); + let source = format!( + r#" +module capacity.cleanup_generic_checked; +@id("checked.{long}") +fn checked(left: T, right: U, value: i64) -> i64 {{ value + 1 }} +@id("app.main") +fn main() -> i64 {{ + checked(1, true, 1) +}} +"# + ); + let program = crate::parse(&source, Path::new("cleanup-generic-checked.spx")).unwrap(); + let canonical = crate::format::canonical(&program); + let template = program + .functions + .iter() + .find(|function| function.name == "checked") + .unwrap(); + let expected_instance_len = generic_function_instance_identity_upper(&program, template) + .expect("valid concrete arity-two arguments have an identity upper"); + let mut scan = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let capacity = hir_pre_resolve_capacity(&program, canonical.len(), &mut scan).unwrap(); + let resolved = hir::resolve(&program).unwrap(); + let instance = resolved + .function_instances + .iter() + .find(|instance| instance.template.as_str() == template.stable_id) + .expect("checked call materializes its generic instance"); + assert_eq!(instance.type_arguments.len(), 2); + assert_eq!(expected_instance_len, instance.id.as_str().len()); + let checked_expression = &instance + .function + .cleanup_plan + .status_sources + .iter() + .find(|status| { + matches!( + status.producer, + semaprax::cleanup_plan::StatusProducer::CheckedArithmetic { .. } + ) + }) + .expect("generic checked body has an arithmetic status source") + .id + .expression; + let mut checked_clones = 0usize; + let mut checked_clone_bytes = 0usize; + let mut note = |expression: &crate::hir::ExpressionId| { + if expression == checked_expression { + checked_clones += 1; + checked_clone_bytes += expression.as_str().len(); + } + }; + for status in &instance.function.cleanup_plan.status_sources { + note(&status.id.expression); + } + for block in &instance.function.cleanup_plan.blocks { + for transition in &block.transitions { + match transition { + semaprax::cleanup_plan::CleanupTransition::Initialize { at, .. } + | semaprax::cleanup_plan::CleanupTransition::Transfer { at, .. } => note(at), + semaprax::cleanup_plan::CleanupTransition::CallCommit { call, .. } => { + note(call) + } + semaprax::cleanup_plan::CleanupTransition::SelectFailure { source } => { + note(&source.expression) + } + semaprax::cleanup_plan::CleanupTransition::StageCopyResult { source } => { + match source { + semaprax::cleanup_plan::StagedCopyResultSource::Body { + expression, + .. + } => note(expression), + semaprax::cleanup_plan::StagedCopyResultSource::TryResidual { + expression, + operand, + .. + } + | semaprax::cleanup_plan::StagedCopyResultSource::TryOptionNone { + expression, + operand, + .. + } => { + note(expression); + note(operand); + } + } + } + } + } + } + for edge in &instance.function.cleanup_plan.edges { + match &edge.condition { + semaprax::cleanup_plan::EdgeCondition::BooleanResult(expression, _) => { + note(expression) + } + semaprax::cleanup_plan::EdgeCondition::VariantCase { scrutinee, .. } => { + note(scrutinee) + } + semaprax::cleanup_plan::EdgeCondition::StatusZero(source) + | semaprax::cleanup_plan::EdgeCondition::StatusNonzero(source) => { + note(&source.expression) + } + semaprax::cleanup_plan::EdgeCondition::Always => {} + } + } + for exit in &instance.function.cleanup_plan.exits { + match &exit.continuation { + semaprax::cleanup_plan::ExitContinuation::CommitResult { + source: semaprax::cleanup_plan::CleanupResultSource::Scalar { expression }, + } => note(expression), + semaprax::cleanup_plan::ExitContinuation::ReturnFailure { source } => { + note(&source.expression) + } + _ => {} + } + } + // StatusSource, SelectFailure, two status edges, ReturnFailure. + assert_eq!(checked_clones, 5); + assert_eq!( + checked_clone_bytes, + checked_clones * checked_expression.as_str().len() + ); + let actual = resolved + .functions + .iter() + .chain( + resolved + .function_instances + .iter() + .map(|instance| &instance.function), + ) + .try_fold(0usize, |bytes, function| { + bytes + .checked_add( + crate::private_capacity_contract::cleanup_inventory_owned_capacity( + &function.cleanup, + )?, + )? + .checked_add( + crate::private_capacity_contract::cleanup_plan_owned_capacity( + &function.cleanup_plan, + )?, + ) + }) + .unwrap(); + assert!( + actual <= capacity.cleanup_authority_upper, + "generic arity-two cleanup {actual} exceeds authority {}", + capacity.cleanup_authority_upper + ); + assert!(capacity.complete().unwrap() <= MAX_BUILDER_BYTES); + } + + #[test] + fn cleanup_source_exit_events_upper_bounds_lowerer_families() { + let source = r#" +module capacity.cleanup_exit_events; +@id("exit.resource") resource R { @id("exit.resource.drop") drop trivial; } +@id("exit.box") record Box { @id("exit.box.value") value: R, } +@id("exit.choice") variant Choice { + @id("exit.choice.first") First, + @id("exit.choice.second") Second, +} +@id("exit.helper") fn helper(value: i64) -> i64 { value } +@id("exit.call") fn call_case(value: i64) -> i64 { helper(value) } +@id("exit.neg") fn neg_case(value: i64) -> i64 { -value } +@id("exit.add") fn add_case(value: i64) -> i64 { value + 1 } +@id("exit.lazy") fn lazy_case(condition: bool) -> bool { condition && true } +@id("exit.if") fn if_case(condition: bool) -> i64 { if condition { 1 } else { 2 } } +@id("exit.match") fn match_case(value: Choice) -> i64 { + match value { + Choice::First {} => 0, + Choice::Second {} => 1, + } +} +@id("exit.update") fn update_case(base: own Box, replacement: own R) -> Box { + base with { value: replacement } +} +@id("app.main") fn main() -> i64 { 0 } +"#; + let program = crate::parse(source, Path::new("cleanup-exit-events.spx")).unwrap(); + let resolved = hir::resolve(&program).unwrap(); + let expected_tail_events = [ + ("helper", 0usize), + ("call_case", 1), + ("neg_case", 1), + ("add_case", 1), + ("lazy_case", 0), + ("if_case", 0), + ("match_case", 0), + ("update_case", 1), + ("main", 0), + ]; + let mut traversal = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + for (name, expected_tail) in expected_tail_events { + let function = program + .functions + .iter() + .find(|function| function.name == name) + .unwrap(); + let crate::ast::ExprKind::Block { tail, .. } = &function.body.kind else { + panic!("function body must retain its authored block"); + }; + assert_eq!(cleanup_source_exit_events(tail), expected_tail, "{name}"); + let source_events = cleanup_function_exit_events(function, &mut traversal).unwrap(); + let actual_exits = resolved + .functions + .iter() + .find(|candidate| candidate.name == name) + .unwrap() + .cleanup_plan + .exits + .len(); + assert_eq!(source_events, actual_exits, "{name}"); + } + } + + #[test] + fn cleanup_retained_census_covers_update_region_with_live_long_id_roots() { + let long = "x".repeat(128); + let parameters = (0..MAX_PARAMETERS) + .map(|index| format!("live{index}: own R")) + .collect::>() + .join(", "); + let source = format!( + "module capacity.cleanup_update;\n@id(\"resource.{long}\") resource R {{ @id(\"lifecycle.{long}\") drop trivial; }}\n@id(\"box.{long}\") record Box {{ @id(\"box.value.{long}\") value: R, }}\n@id(\"update.stress\") fn stress(base: own Box, replacement: own R, {parameters}) -> Box {{ base with {{ value: replacement }} }}\n@id(\"app.main\") fn main() -> i64 {{ 0 }}\n" + ); + let program = crate::parse(&source, Path::new("cleanup-update-live.spx")).unwrap(); + let canonical = crate::format::canonical(&program); + let mut scan = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let capacity = hir_pre_resolve_capacity(&program, canonical.len(), &mut scan).unwrap(); + assert!(capacity.complete().unwrap() <= MAX_BUILDER_BYTES); + let resolved = hir::resolve(&program).unwrap(); + let stress = resolved + .functions + .iter() + .find(|function| function.name == "stress") + .unwrap(); + assert!( + stress + .cleanup_plan + .exits + .iter() + .map(|exit| exit.finalize_in_order.len()) + .max() + .unwrap_or(0) + >= MAX_PARAMETERS + ); + let actual = resolved + .functions + .iter() + .try_fold(0usize, |bytes, function| { + bytes + .checked_add( + crate::private_capacity_contract::cleanup_inventory_owned_capacity( + &function.cleanup, + )?, + )? + .checked_add( + crate::private_capacity_contract::cleanup_plan_owned_capacity( + &function.cleanup_plan, + )?, + ) + }) + .unwrap(); + let actual_exits = resolved + .functions + .iter() + .try_fold(0usize, |count, function| { + count.checked_add(function.cleanup_plan.exits.len()) + }) + .unwrap(); + assert!(actual_exits <= capacity.cleanup_exit_events_upper); + assert!( + actual <= capacity.cleanup_authority_upper, + "update actual {actual} exceeds authority {} (retained {}, structural {}, call epoch {})", + capacity.cleanup_authority_upper, + capacity.cleanup_retained_upper, + capacity.cleanup_authority_upper - capacity.cleanup_retained_upper, + capacity.cleanup_call_argument_owned_upper + ); + assert_eq!(capacity.cleanup_fallback_roots, 0); + } + + #[test] + fn cleanup_update_staged_base_survives_replacement_failure() { + let long = "x".repeat(128); + let source = format!( + "module capacity.cleanup_update_failure;\n@id(\"resource.{long}\") resource R {{ @id(\"lifecycle.{long}\") drop trivial; }}\n@id(\"box.{long}\") record Box {{ @id(\"box.value.{long}\") value: R, }}\n@id(\"update.failure\") fn stress(base: own Box, replacement: own R, checked: i64) -> Box {{ base with {{ value: {{ let observed = checked + 1; replacement }} }} }}\n@id(\"app.main\") fn main() -> i64 {{ 0 }}\n" + ); + let program = crate::parse(&source, Path::new("cleanup-update-failure.spx")).unwrap(); + let canonical = crate::format::canonical(&program); + let mut scan = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let capacity = hir_pre_resolve_capacity(&program, canonical.len(), &mut scan).unwrap(); + let resolved = hir::resolve(&program).unwrap(); + let stress = resolved + .functions + .iter() + .find(|function| function.name == "stress") + .unwrap(); + assert!(stress.cleanup_plan.exits.iter().any(|exit| { + matches!( + exit.continuation, + semaprax::cleanup_plan::ExitContinuation::ReturnFailure { .. } + ) && exit.finalize_in_order.iter().any(|action| { + matches!( + &action.source.storage, + semaprax::cleanup_plan::StorageId::Temporary(expression) + if expression.as_str().contains(".base") + ) && action.lifecycle_id.as_str() == format!("lifecycle.{long}") + && action + .source + .projections + .iter() + .any(|projection| projection.as_str() == format!("box.value.{long}")) + }) + })); + let actual = resolved + .functions + .iter() + .try_fold(0usize, |bytes, function| { + bytes + .checked_add( + crate::private_capacity_contract::cleanup_inventory_owned_capacity( + &function.cleanup, + )?, + )? + .checked_add( + crate::private_capacity_contract::cleanup_plan_owned_capacity( + &function.cleanup_plan, + )?, + ) + }) + .unwrap(); + assert!( + actual <= capacity.cleanup_authority_upper, + "update staged-base cleanup {actual} exceeds authority {}", + capacity.cleanup_authority_upper + ); + assert!(capacity.complete().unwrap() <= MAX_BUILDER_BYTES); + } + + #[test] + fn cleanup_parent_local_update_prefix_survives_later_replacement_failure() { + let long = "x".repeat(128); + let left_field_id = format!("pair.left.{long}"); + let lifecycle_id = format!("resource.drop.{long}"); + let source = format!( + "module capacity.cleanup_update_prefix;\n@id(\"resource.{long}\") resource R {{ @id(\"{lifecycle_id}\") drop trivial; }}\n@id(\"pair.{long}\") record Pair {{ @id(\"{left_field_id}\") left: R, @id(\"pair.right.{long}\") right: R, }}\n@id(\"update.prefix.stress.{long}\") fn stress(base: own Pair, new_left: own R, new_right: own R, checked: i64) -> Pair {{ base with {{ left: new_left, right: {{ let observed = checked + 1; new_right }}, }} }}\n@id(\"app.main\") fn main() -> i64 {{ 0 }}\n" + ); + let program = crate::parse(&source, Path::new("cleanup-update-prefix.spx")).unwrap(); + let canonical = crate::format::canonical(&program); + let mut scan = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let capacity = hir_pre_resolve_capacity(&program, canonical.len(), &mut scan).unwrap(); + let stats = capacity.cleanup_proof.stats; + assert_eq!(stats.parent_local_update_prefix_fields, 1); + assert_eq!(stats.parent_local_update_prefix_exit_groups, 1); + assert_eq!(stats.parent_local_update_prefix_finalizer_copies, 1); + assert_eq!( + stats.parent_local_update_prefix_finalizer_projection_segments, + 1 + ); + assert_eq!( + stats.parent_local_update_prefix_finalizer_lifecycle_ids, + lifecycle_id.len() + ); + assert_eq!( + stats.parent_local_update_prefix_finalizer_projection_ids, + left_field_id.len() + ); + + let resolved = hir::resolve(&program).unwrap(); + let stress = resolved + .functions + .iter() + .find(|function| function.name == "stress") + .unwrap(); + let is_left = |action: &semaprax::cleanup_plan::FinalizeAction| { + action.lifecycle_id.as_str() == lifecycle_id + && action + .source + .projections + .iter() + .any(|projection| projection.as_str() == left_field_id) + }; + let is_destination = |action: &semaprax::cleanup_plan::FinalizeAction| { + is_left(action) + && matches!( + &action.source.storage, + semaprax::cleanup_plan::StorageId::Temporary(expression) + if !expression.as_str().ends_with(".base") + ) + }; + let is_staged_base = |action: &semaprax::cleanup_plan::FinalizeAction| { + is_left(action) + && matches!( + &action.source.storage, + semaprax::cleanup_plan::StorageId::Temporary(expression) + if expression.as_str().ends_with(".base") + ) + }; + let failure = stress + .cleanup_plan + .exits + .iter() + .find(|exit| { + matches!( + exit.continuation, + semaprax::cleanup_plan::ExitContinuation::ReturnFailure { .. } + ) && exit.finalize_in_order.iter().any(&is_destination) + && exit.finalize_in_order.iter().any(&is_staged_base) + }) + .expect("later replacement failure retains new destination and staged old base"); + let destination_actions = failure + .finalize_in_order + .iter() + .filter(|action| is_destination(action)) + .collect::>(); + assert_eq!(destination_actions.len(), 1); + let observed_named = destination_actions + .iter() + .try_fold(0usize, |bytes, action| { + let storage_bytes = match &action.source.storage { + semaprax::cleanup_plan::StorageId::Temporary(expression) => { + expression.as_str().len() + } + _ => 0, + }; + bytes + .checked_add(std::mem::size_of::())? + .checked_add(storage_bytes)? + .checked_add( + action + .source + .projections + .capacity() + .checked_mul(std::mem::size_of::())?, + )? + .checked_add( + action + .source + .projections + .iter() + .try_fold(0usize, |bytes, projection| { + bytes.checked_add(projection.as_str().len()) + })?, + )? + .checked_add(action.lifecycle_id.as_str().len()) + }) + .unwrap(); + assert!( + observed_named <= capacity.cleanup_parent_local_update_prefix_lifetime_upper, + "update-prefix actual {observed_named} exceeds named authority {}", + capacity.cleanup_parent_local_update_prefix_lifetime_upper + ); + let actual = resolved + .functions + .iter() + .try_fold(0usize, |bytes, function| { + bytes + .checked_add( + crate::private_capacity_contract::cleanup_inventory_owned_capacity( + &function.cleanup, + )?, + )? + .checked_add( + crate::private_capacity_contract::cleanup_plan_owned_capacity( + &function.cleanup_plan, + )?, + ) + }) + .unwrap(); + assert!(actual <= capacity.cleanup_authority_upper); + assert!(capacity.complete().unwrap() <= MAX_BUILDER_BYTES); + } + + #[test] + fn cleanup_parent_local_record_prefix_survives_later_field_failure() { + let long = "x".repeat(128); + let first_field_id = format!("pair.first.{long}"); + let lifecycle_id = format!("resource.drop.{long}"); + let source = format!( + "module capacity.cleanup_record_prefix;\n@id(\"resource.{long}\") resource R {{ @id(\"{lifecycle_id}\") drop trivial; }}\n@id(\"pair.{long}\") record Pair {{ @id(\"{first_field_id}\") first: R, @id(\"pair.second.{long}\") second: R, }}\n@id(\"record.prefix.stress.{long}\") fn stress(first: own R, second: own R, checked: i64) -> Pair {{ Pair {{ first: first, second: {{ let observed = checked + 1; second }}, }} }}\n@id(\"app.main\") fn main() -> i64 {{ 0 }}\n" + ); + let program = crate::parse(&source, Path::new("cleanup-record-prefix.spx")).unwrap(); + let canonical = crate::format::canonical(&program); + let mut scan = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let capacity = hir_pre_resolve_capacity(&program, canonical.len(), &mut scan).unwrap(); + let stats = capacity.cleanup_proof.stats; + assert_eq!(stats.parent_local_partial_fields, 1); + assert_eq!(stats.parent_local_finalizer_copies, 1); + assert_eq!(stats.parent_local_finalizer_projection_segments, 1); + assert_eq!( + stats.parent_local_finalizer_lifecycle_ids, + lifecycle_id.len() + ); + assert_eq!( + stats.parent_local_finalizer_projection_ids, + first_field_id.len() + ); + assert!(capacity.cleanup_parent_local_lifetime_upper > 0); + + let resolved = hir::resolve(&program).unwrap(); + let stress = resolved + .functions + .iter() + .find(|function| function.name == "stress") + .unwrap(); + assert!(stress.cleanup_plan.exits.iter().any(|exit| { + matches!( + exit.continuation, + semaprax::cleanup_plan::ExitContinuation::ReturnFailure { .. } + ) && exit.finalize_in_order.iter().any(|action| { + matches!( + action.source.storage, + semaprax::cleanup_plan::StorageId::Temporary(_) + ) && action.lifecycle_id.as_str() == lifecycle_id + && action + .source + .projections + .iter() + .any(|projection| projection.as_str() == first_field_id) + }) + })); + let actual = resolved + .functions + .iter() + .try_fold(0usize, |bytes, function| { + bytes + .checked_add( + crate::private_capacity_contract::cleanup_inventory_owned_capacity( + &function.cleanup, + )?, + )? + .checked_add( + crate::private_capacity_contract::cleanup_plan_owned_capacity( + &function.cleanup_plan, + )?, + ) + }) + .unwrap(); + assert!(actual <= capacity.cleanup_authority_upper); + assert!(capacity.complete().unwrap() <= MAX_BUILDER_BYTES); + } + + #[test] + fn cleanup_parent_local_projection_residual_survives_failure_and_success() { + let long = "x".repeat(128); + let right_field_id = format!("pair.right.{long}"); + let lifecycle_id = format!("resource.drop.{long}"); + let source = format!( + "module capacity.cleanup_projection_residual;\n@id(\"resource.{long}\") resource R {{ @id(\"{lifecycle_id}\") drop trivial; }}\n@id(\"pair.{long}\") record Pair {{ @id(\"pair.left.{long}\") left: R, @id(\"{right_field_id}\") right: R, }}\n@id(\"projection.residual.stress.{long}\") fn stress(left: own R, right: own R, checked: i64) -> R {{ let selected = Pair {{ left: left, right: right, }}.left; let observed = checked + 1; selected }}\n@id(\"app.main\") fn main() -> i64 {{ 0 }}\n" + ); + let program = crate::parse(&source, Path::new("cleanup-projection-residual.spx")).unwrap(); + let canonical = crate::format::canonical(&program); + let mut scan = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let capacity = hir_pre_resolve_capacity(&program, canonical.len(), &mut scan).unwrap(); + let stats = capacity.cleanup_proof.stats; + assert_eq!(stats.parent_local_projection_epochs, 1); + assert_eq!(stats.parent_local_projection_exit_groups, 2); + assert_eq!(stats.parent_local_projection_finalizer_copies, 2); + assert_eq!( + stats.parent_local_projection_finalizer_projection_segments, + 2 + ); + assert_eq!( + stats.parent_local_projection_finalizer_lifecycle_ids, + lifecycle_id.len() * 2 + ); + assert_eq!( + stats.parent_local_projection_finalizer_projection_ids, + right_field_id.len() * 2 + ); + + let resolved = hir::resolve(&program).unwrap(); + let stress = resolved + .functions + .iter() + .find(|function| function.name == "stress") + .unwrap(); + let is_residual = |action: &semaprax::cleanup_plan::FinalizeAction| { + matches!( + action.source.storage, + semaprax::cleanup_plan::StorageId::Temporary(_) + ) && action.lifecycle_id.as_str() == lifecycle_id + && action + .source + .projections + .iter() + .any(|projection| projection.as_str() == right_field_id) + }; + assert!(stress.cleanup_plan.exits.iter().any(|exit| { + matches!( + exit.continuation, + semaprax::cleanup_plan::ExitContinuation::ReturnFailure { .. } + ) && exit.finalize_in_order.iter().any(&is_residual) + })); + assert!(stress.cleanup_plan.exits.iter().any(|exit| { + matches!( + exit.continuation, + semaprax::cleanup_plan::ExitContinuation::CommitResult { .. } + ) && exit.finalize_in_order.iter().any(&is_residual) + })); + let residual_actions = stress + .cleanup_plan + .exits + .iter() + .flat_map(|exit| &exit.finalize_in_order) + .filter(|action| is_residual(action)) + .collect::>(); + assert_eq!(residual_actions.len(), 2); + let observed_named = residual_actions + .iter() + .try_fold(0usize, |bytes, action| { + let storage_bytes = match &action.source.storage { + semaprax::cleanup_plan::StorageId::Temporary(expression) => { + expression.as_str().len() + } + _ => 0, + }; + bytes + .checked_add(std::mem::size_of::())? + .checked_add(storage_bytes)? + .checked_add( + action + .source + .projections + .capacity() + .checked_mul(std::mem::size_of::())?, + )? + .checked_add( + action + .source + .projections + .iter() + .try_fold(0usize, |bytes, projection| { + bytes.checked_add(projection.as_str().len()) + })?, + )? + .checked_add(action.lifecycle_id.as_str().len()) + }) + .unwrap(); + assert!( + observed_named <= capacity.cleanup_parent_local_projection_lifetime_upper, + "projection-residual actual {observed_named} exceeds named authority {}", + capacity.cleanup_parent_local_projection_lifetime_upper + ); + let actual = resolved + .functions + .iter() + .try_fold(0usize, |bytes, function| { + bytes + .checked_add( + crate::private_capacity_contract::cleanup_inventory_owned_capacity( + &function.cleanup, + )?, + )? + .checked_add( + crate::private_capacity_contract::cleanup_plan_owned_capacity( + &function.cleanup_plan, + )?, + ) + }) + .unwrap(); + assert!(actual <= capacity.cleanup_authority_upper); + assert!(capacity.complete().unwrap() <= MAX_BUILDER_BYTES); + } + + #[test] + fn cleanup_typed_roots_resolve_nested_and_later_arm_bindings() { + let source = r#" +module capacity.cleanup_lexical_types; +@id("lexical.resource") resource R { @id("lexical.resource.drop") drop trivial; } +@id("lexical.choice") variant Choice { + @id("lexical.choice.first") First { @id("lexical.choice.first.value") value: i64, }, + @id("lexical.choice.second") Second { @id("lexical.choice.second.value") value: i64, }, +} +@id("lexical.identity") fn identity(value: own R) -> R { value } +@id("lexical.consume") fn consume(value: own R) -> i64 { 1 } +@id("lexical.stress") fn stress(value: own R, choice: Choice) -> i64 { + let outer = identity(value); + let nested = { + let inner = identity(outer); + consume(inner) + }; + nested + match choice { + Choice::First { value: first } => first, + Choice::Second { value: second } => second, + } +} +@id("app.main") fn main() -> i64 { 0 } +"#; + let program = crate::parse(source, Path::new("cleanup-lexical-types.spx")).unwrap(); + let canonical = crate::format::canonical(&program); + let mut scan = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let capacity = hir_pre_resolve_capacity(&program, canonical.len(), &mut scan).unwrap(); + assert_eq!(capacity.cleanup_fallback_roots, 0); + let resolved = hir::resolve(&program).unwrap(); + let actual = resolved + .functions + .iter() + .try_fold(0usize, |bytes, function| { + bytes + .checked_add( + crate::private_capacity_contract::cleanup_inventory_owned_capacity( + &function.cleanup, + )?, + )? + .checked_add( + crate::private_capacity_contract::cleanup_plan_owned_capacity( + &function.cleanup_plan, + )?, + ) + }) + .unwrap(); + assert!( + actual <= capacity.cleanup_authority_upper, + "lexical actual cleanup {actual} exceeds authority {} (retained {}, structural {})", + capacity.cleanup_authority_upper, + capacity.cleanup_retained_upper, + capacity.cleanup_authority_upper - capacity.cleanup_retained_upper + ); + } + + #[test] + fn cleanup_typed_roots_treat_generic_and_prelude_copy_types_as_no_drop() { + let source = r#" +module capacity.cleanup_copy_types; +@id("copy.resource") resource R { @id("copy.resource.drop") drop trivial; } +@id("copy.generic") fn generic(value: T) -> T { value } +@id("copy.option") fn option(value: i64) -> Option { + Option::Some { value: value } +} +@id("copy.result") fn make_result(value: i64) -> Result { + Result::Ok { value: value } +} +@id("copy.outer") fn outer(value: own R) -> R { { value } } +@id("app.main") fn main() -> i64 { + let first = generic(1); + let second = option(first); + let third = make_result(first); + first +} +"#; + let program = crate::parse(source, Path::new("cleanup-copy-types.spx")).unwrap(); + let canonical = crate::format::canonical(&program); + let mut scan = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let capacity = hir_pre_resolve_capacity(&program, canonical.len(), &mut scan).unwrap(); + assert_eq!(capacity.cleanup_fallback_roots, 0); + hir::resolve(&program).unwrap(); + + // Same-name shadowing is rejected by the language, but the + // pre-resolution census must still resolve the outer initializer and + // remain conservative without falling back to an unrelated resource. + let shadow = crate::parse( + r#" +module capacity.cleanup_shadow; +@id("shadow.resource") resource R { @id("shadow.resource.drop") drop trivial; } +@id("shadow.invalid") fn invalid(value: own R) -> R { + let outer = value; + { let outer = outer; outer } +} +@id("app.main") fn main() -> i64 { 0 } +"#, + Path::new("cleanup-shadow.spx"), + ) + .unwrap(); + let shadow_canonical = crate::format::canonical(&shadow); + let shadow_capacity = + hir_pre_resolve_capacity(&shadow, shadow_canonical.len(), &mut scan).unwrap(); + assert_eq!(shadow_capacity.cleanup_fallback_roots, 0); + let diagnostics = hir::resolve(&shadow).unwrap_err(); + assert!(diagnostics + .iter() + .any(|diagnostic| diagnostic.code == "SPX-T209")); + } + + #[test] + fn cleanup_pattern_binding_lookup_is_iterative_at_exact_depth() { + use crate::ast::{ + Expr, ExprKind, FieldDeclaration, MatchArm, MatchPattern, Param, ParamMode, + RecordMatchFieldPattern, RecordMatchPatternField, Type, TypeDeclaration, + TypeDeclarationKind, + }; + + fn program_with_pattern_depth(depth: usize) -> Program { + let span = crate::ast::Span::default(); + let mut program = crate::parse( + "module cleanup.pattern.depth; @id(\"app.inspect\") fn inspect(scrutinee: R0) -> i64 { 0 } @id(\"app.main\") fn main() -> i64 { 0 }", + Path::new("cleanup-pattern-depth.spx"), + ) + .unwrap(); + let mut pattern = RecordMatchFieldPattern::Binding { + name: "value".into(), + span, + }; + for index in (1..depth).rev() { + pattern = RecordMatchFieldPattern::Record { + type_name: format!("R{index}"), + type_span: span, + fields: vec![RecordMatchPatternField { + name: "next".into(), + name_span: span, + pattern, + span, + }], + span, + }; + } + program.types = (0..depth) + .map(|index| TypeDeclaration { + stable_id: format!("cleanup.pattern.r{index}"), + explicit_id: true, + name: format!("R{index}"), + name_span: span, + type_parameters: Vec::new(), + kind: TypeDeclarationKind::Record { + fields: vec![FieldDeclaration { + stable_id: format!("cleanup.pattern.r{index}.next"), + explicit_id: true, + name: "next".into(), + name_span: span, + ty: if index + 1 == depth { + Type::I64 + } else { + Type::Named { + name: format!("R{}", index + 1), + arguments: Vec::new(), + } + }, + span, + }], + }, + span, + }) + .collect(); + program.functions[0].params = vec![Param { + name: "scrutinee".into(), + mode: ParamMode::Value, + ty: Type::Named { + name: "R0".into(), + arguments: Vec::new(), + }, + span, + }]; + program.functions[0].body = Expr { + kind: ExprKind::Match { + scrutinee: Box::new(Expr { + kind: ExprKind::Var("scrutinee".into()), + span, + }), + arms: vec![MatchArm { + pattern: MatchPattern::Record { + type_name: "R0".into(), + type_span: span, + fields: vec![RecordMatchPatternField { + name: "next".into(), + name_span: span, + pattern, + span, + }], + span, + }, + value: Expr { + kind: ExprKind::Var("value".into()), + span, + }, + span, + }], + }, + span, + }; + program + } + + const CHILD_ENV: &str = "SEMAPRAX_TEST_CLEANUP_PATTERN_DEPTH"; + if let Some(depth) = std::env::var_os(CHILD_ENV) { + let depth = depth.to_string_lossy().parse::().unwrap(); + let program = program_with_pattern_depth(depth); + let canonical = crate::format::canonical(&program); + HIR_RESOLVE_PASS_COUNT.with(|count| count.set(0)); + POST_HIR_FACTS_ENTRY_COUNT.with(|count| count.set(0)); + RESOLVED_DISPOSE_COMPLETIONS.with(|count| count.set(0)); + if depth == MAX_SEMANTIC_EXPRESSION_DEPTH { + let mut stack = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let capacity = hir_pre_resolve_capacity(&program, canonical.len(), &mut stack) + .expect("depth-512 pattern capacity"); + assert_eq!(capacity.cleanup_fallback_roots, 0); + note_hir_resolve_pass(); + let resolved = hir::resolve(&program).unwrap(); + let frames = Vec::with_capacity(capacity.disposal_frames); + assert_eq!(frames.capacity(), capacity.disposal_frames); + drop(ResolvedProgramOwner::new( + resolved, + frames, + capacity.disposal_frames, + )); + assert_eq!(HIR_RESOLVE_PASS_COUNT.with(std::cell::Cell::get), 1); + assert_eq!(RESOLVED_DISPOSE_COMPLETIONS.with(std::cell::Cell::get), 1); + } else { + let mut stack = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let diagnostic = + match hir_pre_resolve_capacity(&program, canonical.len(), &mut stack) { + Err(diagnostic) => diagnostic, + Ok(_) => panic!("depth-513 nested record pattern was admitted"), + }; + assert_eq!(diagnostic.code, "SPX-B109"); + assert_eq!(HIR_RESOLVE_PASS_COUNT.with(std::cell::Cell::get), 0); + assert_eq!(POST_HIR_FACTS_ENTRY_COUNT.with(std::cell::Cell::get), 0); + assert_eq!(RESOLVED_DISPOSE_COMPLETIONS.with(std::cell::Cell::get), 0); + } + std::mem::forget(program); + std::process::exit(0); + } + + for depth in [ + MAX_SEMANTIC_EXPRESSION_DEPTH, + MAX_SEMANTIC_EXPRESSION_DEPTH + 1, + ] { + let output = Command::new(std::env::current_exe().unwrap()) + .arg("implementation::tests::cleanup_pattern_binding_lookup_is_iterative_at_exact_depth") + .arg("--exact") + .arg("--nocapture") + .env(CHILD_ENV, depth.to_string()) + .output() + .unwrap(); + assert!( + output.status.success(), + "pattern depth {depth}: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + } + + #[test] + fn cleanup_call_argument_epoch_covers_later_argument_failure() { + let long = "x".repeat(128); + let source = format!( + "module capacity.cleanup_call_epoch;\n@id(\"resource.{long}\") resource R {{ @id(\"lifecycle.{long}\") drop trivial; }}\n@id(\"identity\") fn identity(value: own R) -> R {{ value }}\n@id(\"consume\") fn consume(value: own R) -> i64 {{ 1 }}\n@id(\"combine\") fn combine(first: own R, second: own R) -> i64 {{ let left = consume(first); let right = consume(second); left + right }}\n@id(\"stress\") fn stress(first: own R, second: own R, checked: i64) -> i64 {{ combine(identity(first), {{ let observed = checked + 1; let staged = identity(second); staged }}) }}\n@id(\"app.main\") fn main() -> i64 {{ 0 }}\n" + ); + let program = crate::parse(&source, Path::new("cleanup-call-epoch.spx")).unwrap(); + let canonical = crate::format::canonical(&program); + let mut scan = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let capacity = hir_pre_resolve_capacity(&program, canonical.len(), &mut scan).unwrap(); + assert_eq!(capacity.cleanup_fallback_roots, 0); + let resolved = hir::resolve(&program).unwrap(); + let stress = resolved + .functions + .iter() + .find(|function| function.name == "stress") + .unwrap(); + assert!(stress.cleanup_plan.exits.iter().any(|exit| { + exit.finalize_in_order.iter().any(|action| { + matches!( + action.source.storage, + semaprax::cleanup_plan::StorageId::CallArgument { .. } + ) + }) + })); + let actual_inventory = resolved + .functions + .iter() + .try_fold(0usize, |bytes, function| { + bytes.checked_add( + crate::private_capacity_contract::cleanup_inventory_owned_capacity( + &function.cleanup, + )?, + ) + }) + .unwrap(); + let actual_plan = resolved + .functions + .iter() + .try_fold(0usize, |bytes, function| { + bytes.checked_add( + crate::private_capacity_contract::cleanup_plan_owned_capacity( + &function.cleanup_plan, + )?, + ) + }) + .unwrap(); + let actual = actual_inventory.checked_add(actual_plan).unwrap(); + assert!( + actual <= capacity.cleanup_authority_upper, + "call-epoch inventory {actual_inventory} + plan {actual_plan} = {actual} exceeds authority {} (retained {}, structural {}, call epoch {})", + capacity.cleanup_authority_upper, + capacity.cleanup_retained_upper, + capacity.cleanup_authority_upper - capacity.cleanup_retained_upper, + capacity.cleanup_call_argument_owned_upper + ); + assert!(capacity.complete().unwrap() <= MAX_BUILDER_BYTES); + } + + #[test] + fn inventory_and_cleanup_hostile_envelopes_bind_the_shared_fixture() { + let source = include_str!("../../../tests/fixtures/native_rust_hir_capacity.spx"); + let program = crate::parse(source, Path::new("native-rust-hir-capacity.spx")).unwrap(); + let canonical = crate::format::canonical(&program); + assert_eq!( + raw_digest(canonical.as_bytes()), + "sha256:2a012464bb1bdb624a79972d558fe837f6d55a9cd9f40d2ead16bfbba615f316" + ); + let mut stack = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let capacity = hir_pre_resolve_capacity(&program, canonical.len(), &mut stack).unwrap(); + let peaks = capacity.phase_peaks(); + assert_eq!( + [ + capacity.retained_upper, + peaks[3], + capacity.retained_upper.checked_add(peaks[3]).unwrap(), + peaks[4], + capacity.retained_upper.checked_add(peaks[4]).unwrap(), + ], + [2_803_431, 38_736, 2_842_167, 299_312, 3_102_743], + "retained/inventory/cleanup envelope terms drifted" + ); + let complete = capacity.complete().unwrap(); + HIR_RESOLVE_PASS_COUNT.with(|count| count.set(0)); + let (result, overflowed, consumed) = + crate::bounded_output::with_limit_usage(complete - 1, || { + let _budget = reserve_temporary_exact(complete)?; + note_hir_resolve_pass(); + Ok::<_, Diagnostic>(()) + }); + assert_eq!(result.unwrap_err().code, "SPX-B109"); + assert!(!overflowed); + assert_eq!(consumed, 0); + HIR_RESOLVE_PASS_COUNT.with(|count| assert_eq!(count.get(), 0)); + } + + #[test] + fn hir_capacity_layout_constants_are_bound_to_root_const_assertions() { + let hir = include_str!("../../../src/hir.rs"); + let verifier = include_str!("../../../src/source_verify.rs"); + let cleanup = include_str!("../../../src/cleanup.rs"); + let lower = include_str!("../../../src/cleanup_plan/build.rs"); + let calls = include_str!("../../../src/call_index.rs"); + for (source, expected) in [ + (hir, "size_of::>() == 552"), + (hir, "size_of::>() == 288"), + (verifier, "size_of::>() == 320"), + (verifier, "size_of::>() == 312"), + (cleanup, "size_of::>() == 40"), + (cleanup, "size_of::>() == 24"), + (lower, "size_of::>() == 344"), + (calls, "size_of::>() == 16"), + ] { + assert!( + source.contains(expected), + "missing root layout pin `{expected}`" + ); + } + } + + #[test] + fn hir_complete_reservation_is_exact_and_one_less_prevents_resolution() { + let (program, _) = fixture(); + let canonical = crate::format::canonical(&program); + let mut stack = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let capacity = hir_pre_resolve_capacity(&program, canonical.len(), &mut stack).unwrap(); + assert_eq!(capacity.retained_upper, 49_075); + assert_eq!(capacity.scratch_upper, 16_170); + assert_eq!( + capacity.phase_peaks(), + [5_028, 15_428, 4_900, 3_488, 5_792, 3_456, 16_170, 1_032] + ); + assert_eq!(capacity.complete().unwrap(), 65_245); + assert_eq!( + capacity.scratch_upper, + capacity.phase_peaks().into_iter().max().unwrap(), + "scratch must equal the largest sequential phase" + ); + let complete = capacity.complete().unwrap(); + HIR_RESOLVE_PASS_COUNT.with(|count| count.set(0)); + HIR_POST_RESOLVE_PHASE_COUNT.with(|counts| counts.set([0; 4])); + HIR_POST_RESOLVE_CAPACITY_HIGH_WATER.with(|water| water.set([0; 3])); + let (result, overflowed, consumed) = + crate::bounded_output::with_limit_usage(complete - 1, || { + let budget = reserve_temporary_exact(complete)?; + note_hir_resolve_pass(); + let _ = hir::resolve(&program).map_err(|_| b107("selected identity missing"))?; + drop(budget); + Ok::<_, Diagnostic>(()) + }); + assert_eq!(result.unwrap_err().code, "SPX-B109"); + assert!(!overflowed); + assert_eq!(consumed, 0); + HIR_RESOLVE_PASS_COUNT.with(|count| assert_eq!(count.get(), 0)); + HIR_POST_RESOLVE_PHASE_COUNT.with(|counts| assert_eq!(counts.get(), [0; 4])); + HIR_POST_RESOLVE_CAPACITY_HIGH_WATER.with(|water| assert_eq!(water.get(), [0; 3])); + + HIR_RESOLVE_PASS_COUNT.with(|count| count.set(0)); + HIR_POST_RESOLVE_PHASE_COUNT.with(|counts| counts.set([0; 4])); + HIR_POST_RESOLVE_CAPACITY_HIGH_WATER.with(|water| water.set([0; 3])); + let (result, overflowed, _) = crate::bounded_output::with_limit_usage(complete, || { + let budget = reserve_temporary_exact(complete)?; + note_hir_resolve_pass(); + let resolved = hir::resolve(&program).map_err(|_| b107("selected identity missing"))?; + reset_closure_capacity_high_water(); + let (closure, _) = selected_closure(&resolved, &["interop.add".to_owned()])?; + validate_native_rust_expression_budget_for_closure(&closure, true)?; + validate_selected_scalar_closure(&closure)?; + validate_native_unit_discard_bindings(&closure)?; + assert!(closure_capacity_high_water() <= capacity.phase_peaks()[6]); + drop(budget); + Ok::<_, Diagnostic>(()) + }); + result.unwrap(); + assert!(!overflowed); + HIR_RESOLVE_PASS_COUNT.with(|count| assert_eq!(count.get(), 1)); + HIR_POST_RESOLVE_PHASE_COUNT.with(|counts| assert_eq!(counts.get(), [1; 4])); + HIR_POST_RESOLVE_CAPACITY_HIGH_WATER.with(|water| { + assert!(water.get().into_iter().all(|bytes| bytes > 0)); + }); + } + + #[test] + fn post_hir_nontransfer_reservation_precedes_all_fact_and_render_work() { + let (program, canonical_spec) = fixture(); + let canonical_source = crate::format::canonical(&program); + let spec = + parse_spec_with_source(&program, canonical_spec.as_bytes(), &canonical_source).unwrap(); + let resolved = hir::resolve(&program).unwrap(); + let (closure, _) = selected_closure(&resolved, &spec.exports).unwrap(); + let capacity = post_hir_facts_capacity( + canonical_source.len(), + canonical_spec.len(), + &resolved, + &closure, + &spec, + ) + .unwrap(); + let complete = capacity.complete().unwrap(); + let transfer = prepared_spec_transfer_capacity(&spec).unwrap(); + let reservation = complete.checked_sub(transfer).unwrap(); + + POST_HIR_FACTS_ENTRY_COUNT.with(|count| count.set(0)); + POST_HIR_FACTS_CAPACITY_HIGH_WATER.with(|water| water.set(0)); + POST_HIR_FACTS_SCRATCH_HIGH_WATER.with(|water| water.set(0)); + let (result, overflowed, consumed) = + crate::bounded_output::with_limit_usage(reservation - 1, || { + let _budget = reserve_temporary_exact(reservation)?; + note_post_hir_facts_entry(); + Ok::<_, Diagnostic>(()) + }); + assert_eq!(result.unwrap_err().code, "SPX-B109"); + assert!(!overflowed); + assert_eq!(consumed, 0); + POST_HIR_FACTS_ENTRY_COUNT.with(|count| assert_eq!(count.get(), 0)); + POST_HIR_FACTS_CAPACITY_HIGH_WATER.with(|water| assert_eq!(water.get(), 0)); + + POST_HIR_FACTS_ENTRY_COUNT.with(|count| count.set(0)); + let (result, overflowed, consumed) = + crate::bounded_output::with_limit_usage(reservation, || { + let budget = reserve_temporary_exact(reservation)?; + note_post_hir_facts_entry(); + drop(budget); + Ok::<_, Diagnostic>(()) + }); + result.unwrap(); + assert!(!overflowed); + assert_eq!(consumed, 0); + POST_HIR_FACTS_ENTRY_COUNT.with(|count| assert_eq!(count.get(), 1)); + } + + #[test] + fn post_hir_spec_transfer_is_single_charged_across_target_triple_lengths() { + fn terms(triple: &str) -> [usize; 5] { + with_test_target( + Target { + triple: triple.to_owned(), + pointer_width: 64, + endian: "little".to_owned(), + panic_strategy: "unwind".to_owned(), + thread_policy: "same_thread".to_owned(), + }, + || { + let (program, canonical_spec) = fixture(); + POST_HIR_AUTHORITY_TRANSFER_TERMS.with(|terms| terms.set([0; 5])); + prepare_native_rust_interop(&program, canonical_spec.as_bytes()).unwrap(); + POST_HIR_AUTHORITY_TRANSFER_TERMS.with(std::cell::Cell::get) + }, + ) + } + + // [complete formula, moved Spec ownership, net facts reservation, + // new persistent facts, total persistent Prepared ownership] + let apple = terms("aarch64-apple-darwin"); + let linux = terms("x86_64-unknown-linux-gnu"); + for observed in [apple, linux] { + assert!(observed.into_iter().all(|value| value > 0)); + assert_eq!(observed[0] - observed[1], observed[2]); + assert_eq!(observed[4] - observed[1], observed[3]); + } + assert_eq!(linux[0] - apple[0], 4); + assert_eq!(linux[1] - apple[1], 4); + assert_eq!(linux[2], apple[2]); + assert_eq!(linux[3], apple[3]); + assert_eq!(linux[4] - apple[4], 4); + } + + #[test] + fn post_hir_spec_transfer_capacity_slack_does_not_consume_scratch_authority() { + let (program, canonical_spec) = fixture(); + let canonical_source = crate::format::canonical(&program); + let mut spec = + parse_spec_with_source(&program, canonical_spec.as_bytes(), &canonical_source).unwrap(); + let resolved = hir::resolve(&program).unwrap(); + let (closure, _) = selected_closure(&resolved, &spec.exports).unwrap(); + + let base = post_hir_facts_capacity( + canonical_source.len(), + canonical_spec.len(), + &resolved, + &closure, + &spec, + ) + .unwrap(); + let base_transfer = prepared_spec_transfer_capacity(&spec).unwrap(); + let digest = spec.source_revision.clone(); + let requested_capacity = digest.len() + 37; + let mut over_capacity_digest = String::with_capacity(requested_capacity); + over_capacity_digest.push_str(&digest); + assert!(over_capacity_digest.capacity() > over_capacity_digest.len()); + spec.source_revision = over_capacity_digest; + + let hostile = post_hir_facts_capacity( + canonical_source.len(), + canonical_spec.len(), + &resolved, + &closure, + &spec, + ) + .unwrap(); + let hostile_transfer = prepared_spec_transfer_capacity(&spec).unwrap(); + let transfer_delta = hostile_transfer.checked_sub(base_transfer).unwrap(); + assert!(transfer_delta > 0); + assert_eq!( + hostile.complete().unwrap() - base.complete().unwrap(), + transfer_delta, + ); + assert_eq!( + hostile.complete().unwrap() - hostile_transfer, + base.complete().unwrap() - base_transfer, + ); + } + + #[test] + fn final_artifact_sinks_reject_one_less_before_output_allocation() { + let (program, canonical_spec) = fixture(); + let canonical_source = crate::format::canonical(&program); + let spec = + parse_spec_with_source(&program, canonical_spec.as_bytes(), &canonical_source).unwrap(); + let prepared = prepare_native_rust_interop(&program, canonical_spec.as_bytes()).unwrap(); + let resolved = hir::resolve(&program).unwrap(); + let (closure, _) = selected_closure(&resolved, &spec.exports).unwrap(); + let status_domains = prepared + .imports + .iter() + .filter_map(|import| import.failure.clone()) + .collect::>() + .into_iter() + .collect::>(); + + EXACT_ARTIFACT_OUTPUT_ALLOCATION_COUNT.with(|count| count.set(0)); + let descriptor = render_descriptor_with_limit( + &spec, + &prepared.hir_digest, + &status_domains, + &prepared.exports, + &prepared.imports, + prepared.descriptor.len() - 1, + ) + .unwrap_err(); + let header = generate_header_with_limit( + &prepared.exports, + &prepared.imports, + prepared.generated_header.len() - 1, + ) + .unwrap_err(); + let generated_c = render_exact_artifact( + "max_generated_c_bytes", + prepared.generated_c.len() - 1, + |sink| generate_c_into(sink, &spec, &closure, &prepared.exports, &prepared.imports), + ) + .unwrap_err(); + let rust_combined = prepared + .generated_rust + .len() + .checked_add(prepared.private_ffi_source.len()) + .unwrap(); + let rust_aggregate_one_less = generate_rust_artifacts_with_limit( + &spec, + &prepared.exports, + &prepared.imports, + rust_combined - 1, + ) + .unwrap_err(); + let rust_first_sink_one_less = generate_rust_artifacts_with_limit( + &spec, + &prepared.exports, + &prepared.imports, + prepared.generated_rust.len() - 1, + ) + .unwrap_err(); + for diagnostic in [ + descriptor, + header, + generated_c, + rust_aggregate_one_less, + rust_first_sink_one_less, + ] { + assert_eq!(diagnostic.code, "SPX-B109"); + } + EXACT_ARTIFACT_OUTPUT_ALLOCATION_COUNT.with(|count| assert_eq!(count.get(), 0)); + + assert_eq!( + render_descriptor_with_limit( + &spec, + &prepared.hir_digest, + &status_domains, + &prepared.exports, + &prepared.imports, + prepared.descriptor.len(), + ) + .unwrap(), + prepared.descriptor + ); + assert_eq!( + generate_header_with_limit( + &prepared.exports, + &prepared.imports, + prepared.generated_header.len(), + ) + .unwrap(), + prepared.generated_header + ); + assert_eq!( + render_exact_artifact( + "max_generated_c_bytes", + prepared.generated_c.len(), + |sink| generate_c_into( + sink, + &spec, + &closure, + &prepared.exports, + &prepared.imports, + ), + ) + .unwrap(), + prepared.generated_c + ); + let exact_rust = generate_rust_artifacts_with_limit( + &spec, + &prepared.exports, + &prepared.imports, + rust_combined, + ) + .unwrap(); + assert_eq!(exact_rust.0, prepared.generated_rust); + assert_eq!(exact_rust.1, prepared.private_ffi_source); + EXACT_ARTIFACT_OUTPUT_ALLOCATION_COUNT.with(|count| assert_eq!(count.get(), 5)); + } + + #[test] + fn post_hir_named_phase_envelopes_cover_representative_and_depth_512_c() { + fn measure(program: &Program, spec: &Spec) -> ([usize; 4], [usize; 3]) { + let canonical_source = crate::format::canonical(program); + let canonical_spec = render_spec(spec); + let resolved = hir::resolve(program).unwrap(); + let (closure, _) = selected_closure(&resolved, &spec.exports).unwrap(); + let capacity = post_hir_facts_capacity( + canonical_source.len(), + canonical_spec.len(), + &resolved, + &closure, + spec, + ) + .unwrap(); + POST_HIR_FACTS_CAPACITY_HIGH_WATER.with(|water| water.set(0)); + POST_HIR_RENDER_CAPACITY_HIGH_WATER.with(|water| water.set(0)); + POST_HIR_REPLAY_CAPACITY_HIGH_WATER.with(|water| water.set(0)); + prepare_native_rust_interop(program, canonical_spec.as_bytes()).unwrap(); + let actual = [ + POST_HIR_FACTS_CAPACITY_HIGH_WATER.with(std::cell::Cell::get), + POST_HIR_RENDER_CAPACITY_HIGH_WATER.with(std::cell::Cell::get), + POST_HIR_REPLAY_CAPACITY_HIGH_WATER.with(std::cell::Cell::get), + ]; + assert!( + actual[0] + <= capacity + .retained_upper + .checked_add(capacity.facts_scratch_upper) + .unwrap() + ); + assert!(actual[1] <= capacity.render_scratch_upper); + assert!(actual[2] <= capacity.replay_scratch_upper); + ( + [ + capacity.retained_upper, + capacity.facts_scratch_upper, + capacity.render_scratch_upper, + capacity.replay_scratch_upper, + ], + actual, + ) + } + + // This historical evidence tuple was authorized for the Apple-arm + // target. Freeze that target explicitly so host triple length cannot + // silently repin a target-specific retained-allocation census. + with_test_target( + Target { + triple: "aarch64-apple-darwin".to_owned(), + pointer_width: 64, + endian: "little".to_owned(), + panic_strategy: "unwind".to_owned(), + thread_policy: "same_thread".to_owned(), + }, + || { + let (program, canonical_spec) = fixture(); + let canonical_source = crate::format::canonical(&program); + let spec = + parse_spec_with_source(&program, canonical_spec.as_bytes(), &canonical_source) + .unwrap(); + let representative = measure(&program, &spec); + + let mut deep = program; + let function = deep + .functions + .iter_mut() + .find(|function| function.stable_id == "interop.add") + .unwrap(); + for _ in 0..MAX_SEMANTIC_EXPRESSION_DEPTH - 4 { + let expression = function.body.clone(); + function.body = crate::ast::Expr { + span: expression.span, + kind: crate::ast::ExprKind::Unary { + op: crate::ast::UnaryOp::Neg, + value: Box::new(expression), + }, + }; + } + validate_native_rust_source_expression_budget(&deep).unwrap(); + let deep_source = crate::format::canonical(&deep); + let deep_spec = Spec { + module: deep.module.clone(), + source_revision: domain_digest(SOURCE_DOMAIN, deep_source.as_bytes()), + target: current_target().unwrap(), + exports: vec!["interop.add".to_owned()], + imports: vec!["host.add".to_owned()], + capabilities: vec!["host.math".to_owned()], + }; + let deep = measure(&deep, &deep_spec); + + assert_eq!( + [representative, deep], + [ + ( + [1_630, 115_266, 8_390_881, 8_390_881], + [116_499, 4_195_020, 4_195_020] + ), + ( + [1_630, 115_266, 8_447_777, 8_447_777], + [116_499, 4_251_916, 4_251_916] + ), + ], + "named phase formula or observed high-water pins drifted" + ); + }, + ); + } + + #[test] + fn post_hir_facts_cross_product_maxima_stay_inside_named_scratch() { + let capabilities = (0..MAX_IMPORTS) + .map(|index| format!("cap.c{index:02}")) + .collect::>(); + let capability_list = capabilities.join(", "); + let parameters = (0..MAX_PARAMETERS) + .map(|index| format!("p{index}: i64")) + .collect::>() + .join(", "); + let arguments = (0..MAX_PARAMETERS) + .map(|index| format!("p{index}")) + .collect::>() + .join(", "); + let mut source = format!( + "module post.cross_product; permit {{ {capability_list} }} @id(\"host.cross\") interface HostCross permits {{ {capability_list} }} {{ " + ); + for index in 0..MAX_IMPORTS { + write!( + source, + "@id(\"import.{index:02}\") import rust fn import_{index:02}({parameters}) -> i64 effects {{ cap.c{index:02} }} failure status \"status.{index:02}\"; " + ) + .unwrap(); + } + source.push_str("} "); + let call_sum = (0..MAX_IMPORTS) + .map(|index| format!("import_{index:02}({arguments})")) + .collect::>() + .join(" + "); + write!( + source, + "@id(\"bridge.all\") fn bridge_all({parameters}) -> i64 uses {{ {capability_list} }} {{ {call_sum} }} " + ) + .unwrap(); + for index in 0..MAX_EXPORTS { + write!( + source, + "@id(\"export.{index:02}\") fn export_{index:02}({parameters}) -> i64 uses {{ {capability_list} }} {{ bridge_all({arguments}) }} " + ) + .unwrap(); + } + source.push_str("@id(\"app.main\") fn main() -> i64 { 0 }"); + let program = crate::parse(&source, Path::new("post-hir-cross-product.spx")).unwrap(); + let canonical_source = crate::format::canonical(&program); + let spec = Spec { + module: program.module.clone(), + source_revision: domain_digest(SOURCE_DOMAIN, canonical_source.as_bytes()), + target: current_target().unwrap(), + exports: (0..MAX_EXPORTS) + .map(|index| format!("export.{index:02}")) + .collect(), + imports: (0..MAX_IMPORTS) + .map(|index| format!("import.{index:02}")) + .collect(), + capabilities, + }; + let canonical_spec = render_spec(&spec); + let resolved = hir::resolve(&program).unwrap(); + let (closure, _) = selected_closure(&resolved, &spec.exports).unwrap(); + let capacity = post_hir_facts_capacity( + canonical_source.len(), + canonical_spec.len(), + &resolved, + &closure, + &spec, + ) + .unwrap(); + let mut hir_scan = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let hir_capacity = + hir_pre_resolve_capacity(&program, canonical_source.len(), &mut hir_scan).unwrap(); + POST_HIR_FACTS_CAPACITY_HIGH_WATER.with(|water| water.set(0)); + POST_HIR_FACTS_SCRATCH_HIGH_WATER.with(|water| water.set(0)); + POST_HIR_RENDER_CAPACITY_HIGH_WATER.with(|water| water.set(0)); + POST_HIR_REPLAY_CAPACITY_HIGH_WATER.with(|water| water.set(0)); + prepare_native_rust_interop(&program, canonical_spec.as_bytes()).unwrap_or_else( + |diagnostics| { + panic!( + "cross-product prepare failed: {diagnostics:?}; source={}, spec={}, hir={}, retained={}, facts={}, render={}, replay={}, complete={}", + canonical_source.len(), + canonical_spec.len(), + hir_capacity.complete().unwrap(), + capacity.retained_upper, + capacity.facts_scratch_upper, + capacity.render_scratch_upper, + capacity.replay_scratch_upper, + capacity.complete().unwrap() + ) + }, + ); + let actual = [ + POST_HIR_FACTS_CAPACITY_HIGH_WATER.with(std::cell::Cell::get), + POST_HIR_RENDER_CAPACITY_HIGH_WATER.with(std::cell::Cell::get), + POST_HIR_REPLAY_CAPACITY_HIGH_WATER.with(std::cell::Cell::get), + ]; + let facts_scratch_actual = POST_HIR_FACTS_SCRATCH_HIGH_WATER.with(std::cell::Cell::get); + assert!( + actual[0] + <= capacity + .retained_upper + .checked_add(capacity.facts_scratch_upper) + .unwrap(), + "facts total-live / retained+scratch: {}/{}, all={actual:?}", + actual[0], + capacity.retained_upper + capacity.facts_scratch_upper + ); + assert!(facts_scratch_actual > 0); + assert!(facts_scratch_actual <= capacity.facts_scratch_upper); + assert!( + actual[1] <= capacity.render_scratch_upper, + "render actual/formula: {}/{}, all={actual:?}", + actual[1], + capacity.render_scratch_upper + ); + assert!( + actual[2] <= capacity.replay_scratch_upper, + "replay actual/formula: {}/{}, all={actual:?}", + actual[2], + capacity.replay_scratch_upper + ); + } + + #[test] + fn post_hir_facts_zero_entry_collections_have_zero_backing_and_stay_bounded() { + let empty_strings = Vec::::new(); + let empty_pairs = Vec::<(String, String)>::new(); + let empty_ordinals = Vec::::new(); + let empty_set = BTreeSet::::new(); + assert_eq!( + checked_owned_string_vec(&empty_strings, empty_strings.capacity()), + Some(0) + ); + assert_eq!(checked_owned_string_pairs(&empty_pairs), Some(0)); + assert_eq!(checked_u16_vec(&empty_ordinals), Some(0)); + assert_eq!(checked_owned_string_set(&empty_set), Some(0)); + + let source = "module post.zero; @id(\"zero.export\") fn export(value: i64) -> i64 { value } @id(\"app.main\") fn main() -> i64 { 0 }"; + let program = crate::parse(source, Path::new("post-hir-zero.spx")).unwrap(); + let canonical_source = crate::format::canonical(&program); + let spec = Spec { + module: program.module.clone(), + source_revision: domain_digest(SOURCE_DOMAIN, canonical_source.as_bytes()), + target: current_target().unwrap(), + exports: vec!["zero.export".to_owned()], + imports: Vec::new(), + capabilities: Vec::new(), + }; + let canonical_spec = render_spec(&spec); + let resolved = hir::resolve(&program).unwrap(); + let (closure, _) = selected_closure(&resolved, &spec.exports).unwrap(); + let capacity = post_hir_facts_capacity( + canonical_source.len(), + canonical_spec.len(), + &resolved, + &closure, + &spec, + ) + .unwrap(); + POST_HIR_FACTS_CAPACITY_HIGH_WATER.with(|water| water.set(0)); + POST_HIR_FACTS_SCRATCH_HIGH_WATER.with(|water| water.set(0)); + prepare_native_rust_interop(&program, canonical_spec.as_bytes()).unwrap(); + let total = POST_HIR_FACTS_CAPACITY_HIGH_WATER.with(std::cell::Cell::get); + let scratch = POST_HIR_FACTS_SCRATCH_HIGH_WATER.with(std::cell::Cell::get); + assert!(total <= capacity.retained_upper + capacity.facts_scratch_upper); + assert!(scratch <= capacity.facts_scratch_upper); + + // Unselected source text does not multiply any post-HIR owned + // collection. A near-limit source with this same one-function closure + // therefore has the same fieldwise facts authority and stays admitted. + let near_max_source = post_hir_facts_capacity( + MAX_SOURCE_BYTES, + canonical_spec.len(), + &resolved, + &closure, + &spec, + ) + .unwrap(); + assert_eq!(near_max_source.retained_upper, capacity.retained_upper); + assert_eq!( + near_max_source.facts_scratch_upper, + capacity.facts_scratch_upper + ); + assert!(near_max_source.complete().unwrap() <= MAX_BUILDER_BYTES); + } + + #[test] + fn post_hir_dense_fan_in_duplicates_and_all_interface_imports_stay_bounded() { + let mut source = String::from( + "module post.fanin; permit { cap.fan } @id(\"host.fan\") interface HostFan permits { cap.fan } { @id(\"import.fan\") import rust fn host_fan() -> i64 effects { cap.fan } failure status \"status.fan\"; } @id(\"host.unused\") interface HostUnused permits { cap.fan } { ", + ); + for index in 0..24 { + write!(source, "@id(\"unused.{index:02}\") import rust fn unused_{index:02}() -> i64 effects {{ cap.fan }} failure status \"status.unused.{index:02}\"; ").unwrap(); + } + source.push_str( + "} @id(\"fanin.leaf\") fn fanin_leaf() -> i64 uses { cap.fan } { host_fan() } ", + ); + for index in 0..16 { + write!(source, "@id(\"fanin.mid.{index:02}\") fn fanin_mid_{index:02}() -> i64 uses {{ cap.fan }} {{ fanin_leaf() + fanin_leaf() + fanin_leaf() }} ").unwrap(); + } + let fan_in = (0..16) + .map(|index| format!("fanin_mid_{index:02}()")) + .collect::>() + .join(" + "); + write!(source, "@id(\"fanin.export\") fn fanin_export() -> i64 uses {{ cap.fan }} {{ {fan_in} }} @id(\"app.main\") fn main() -> i64 {{ 0 }}").unwrap(); + let program = crate::parse(&source, Path::new("post-hir-fanin.spx")).unwrap(); + let canonical_source = crate::format::canonical(&program); + let spec = Spec { + module: program.module.clone(), + source_revision: domain_digest(SOURCE_DOMAIN, canonical_source.as_bytes()), + target: current_target().unwrap(), + exports: vec!["fanin.export".to_owned()], + imports: vec!["import.fan".to_owned()], + capabilities: vec!["cap.fan".to_owned()], + }; + let canonical_spec = render_spec(&spec); + let resolved = hir::resolve(&program).unwrap(); + let (closure, _) = selected_closure(&resolved, &spec.exports).unwrap(); + let capacity = post_hir_facts_capacity( + canonical_source.len(), + canonical_spec.len(), + &resolved, + &closure, + &spec, + ) + .unwrap(); + let census = traversal_call_site_census(&closure).unwrap(); + assert!(census.function_sites > closure.len()); + assert_eq!( + capacity.traversal_pending_capacity, + census.function_sites + 1 + ); + POST_HIR_FACTS_CAPACITY_HIGH_WATER.with(|water| water.set(0)); + POST_HIR_FACTS_SCRATCH_HIGH_WATER.with(|water| water.set(0)); + prepare_native_rust_interop(&program, canonical_spec.as_bytes()).unwrap(); + let total = POST_HIR_FACTS_CAPACITY_HIGH_WATER.with(std::cell::Cell::get); + let scratch = POST_HIR_FACTS_SCRATCH_HIGH_WATER.with(std::cell::Cell::get); + assert!(total <= capacity.retained_upper + capacity.facts_scratch_upper); + assert!(scratch <= capacity.facts_scratch_upper); + } + + #[test] + fn serde_json_lock_and_near_max_escaped_payload_match_parser_contract() { + assert!(include_str!("../Cargo.toml").contains("serde_json = \"=1.0.151\"")); + let serde_package = include_str!("../../../Cargo.lock") + .split("[[package]]") + .find(|package| package.lines().any(|line| line == "name = \"serde_json\"")) + .expect("serde_json package is locked"); + for expected in [ + "version = \"1.0.151\"", + "source = \"registry+https://github.com/rust-lang/crates.io-index\"", + "checksum = \"c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14\"", + ] { + assert!(serde_package.lines().any(|line| line == expected)); + } + let mut encoded = String::with_capacity(MAX_DESCRIPTOR_BYTES); + encoded.push_str("{\"escaped\":\""); + while encoded.len() + "\\u0061\"}".len() <= MAX_DESCRIPTOR_BYTES { + encoded.push_str("\\u0061"); + } + encoded.push_str("\"}"); + assert!(encoded.len() >= MAX_DESCRIPTOR_BYTES - 6); + let value: Value = serde_json::from_str(&encoded).unwrap(); + let string_payload = checked_json_string_payload(&value).unwrap(); + assert!(string_payload <= encoded.len()); + assert!(encoded.len().checked_mul(2).unwrap() <= MAX_DESCRIPTOR_BYTES * 2); + } + + #[test] + fn hir_fingerprint_admits_exact_depth_result_and_option_try_chains() { + let (program, _) = fixture(); + let resolved = hir::resolve(&program).unwrap(); + let seed_id = resolved.functions[0].body.id.clone(); + for option in [false, true] { + let leaf = ResolvedExpr { + id: seed_id.clone(), + ty: ResolvedType::I64, + ownership: OwnershipMode::Value, + span: crate::ast::Span::default(), + kind: ResolvedExprKind::Int(1), + }; + let wrap = |operand: ResolvedExpr, _index: usize| ResolvedExpr { + // Fingerprinting does not validate expression identity uniqueness. Reusing a + // resolver-issued ID keeps this forged, parser-independent depth fixture within + // the public HIR construction surface. + id: seed_id.clone(), + ty: ResolvedType::I64, + ownership: OwnershipMode::Value, + span: crate::ast::Span::default(), + kind: if option { + ResolvedExprKind::TryOption { + operand: Box::new(operand), + option: DeclarationId::new("prelude.option".to_owned()), + some_case: DeclarationId::new("prelude.option.some".to_owned()), + some_field: DeclarationId::new("prelude.option.some.value".to_owned()), + none_case: DeclarationId::new("prelude.option.none".to_owned()), + residual_type: ResolvedType::I64, + } + } else { + ResolvedExprKind::Try { + operand: Box::new(operand), + result: DeclarationId::new("prelude.result".to_owned()), + ok_case: DeclarationId::new("prelude.result.ok".to_owned()), + ok_field: DeclarationId::new("prelude.result.ok.value".to_owned()), + err_case: DeclarationId::new("prelude.result.err".to_owned()), + err_field: DeclarationId::new("prelude.result.err.error".to_owned()), + residual_type: ResolvedType::I64, + } + }, + }; + let mut exact = leaf; + for index in 1..MAX_SEMANTIC_EXPRESSION_DEPTH { + exact = wrap(exact, index); + } + let mut hasher = Sha256::new(); + hash_expr(&mut hasher, &exact, 0).unwrap(); + assert_eq!(format!("sha256:{:x}", hasher.finalize()).len(), 71); + + let over = wrap(exact, MAX_SEMANTIC_EXPRESSION_DEPTH); + let mut hasher = Sha256::new(); + assert_eq!( + hash_expr(&mut hasher, &over, 0).unwrap_err().code, + "SPX-B109" + ); + + // Iteratively dismantle this deliberately forged test tree; the + // production builder receives validated HIR through `resolve`. + let mut current = over; + loop { + current = match current.kind { + ResolvedExprKind::Try { operand, .. } + | ResolvedExprKind::TryOption { operand, .. } => *operand, + _ => break, + }; + } + } + } + + #[test] + fn fingerprint_type_identity_exact_writer_matches_hir_and_named_topology() { + for depth in [0usize, 1, 32, MAX_SEMANTIC_EXPRESSION_DEPTH - 1] { + let mut ty = ResolvedType::TypeParameter { + owner: DeclarationId::new("type.owner".to_owned()), + index: u32::MAX, + }; + for index in 0..depth { + ty = ResolvedType::Nominal { + declaration: DeclarationId::new(format!("type.layer.{index}")), + arguments: vec![ty, ResolvedType::Bool], + }; + } + let expected = ty.identity_key(); + let upper = type_identity_scratch_upper(&ty).unwrap(); + POST_HIR_FACTS_CAPACITY_HIGH_WATER.with(|water| water.set(0)); + let actual = fingerprint_type_identity(&ty, 0, 0).unwrap(); + let observed = POST_HIR_FACTS_CAPACITY_HIGH_WATER.with(std::cell::Cell::get); + assert_eq!(actual, expected); + assert!( + observed <= upper, + "depth {depth} identity scratch actual/formula: {observed}/{upper}" + ); + } + let over_work = ResolvedType::Nominal { + declaration: DeclarationId::new("type.too-wide".to_owned()), + arguments: vec![ResolvedType::Bool; FINGERPRINT_ACTION_SLOTS], + }; + assert_eq!( + type_identity_metrics(&over_work, 1).unwrap_err().code, + "SPX-B109" + ); + } + + #[test] + fn resolved_owner_disposal_is_preallocated_and_depth_bounded() { + let source = include_str!("../../../tests/fixtures/native_rust_hir_capacity.spx"); + let program = crate::parse(source, Path::new("native-rust-hir-capacity.spx")).unwrap(); + let canonical = crate::format::canonical(&program); + let mut scan = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let capacity = hir_pre_resolve_capacity(&program, canonical.len(), &mut scan) + .unwrap() + .disposal_frames; + let resolved = hir::resolve(&program).unwrap(); + assert!( + !resolved.function_instances.is_empty(), + "generic instances are required" + ); + assert!(resolved.interfaces.iter().any(|interface| { + interface + .imports + .iter() + .any(|import| !import.parameters.is_empty()) + })); + assert!(resolved.functions.iter().any(|function| { + function.cleanup.slots.iter().any(|slot| { + matches!( + slot.shape, + semaprax::cleanup::FieldLivenessShape::Leaf { .. } + | semaprax::cleanup::FieldLivenessShape::Record { .. } + ) + }) + })); + let mut staged_sources = [false; 3]; + for transition in resolved.functions.iter().flat_map(|function| { + function + .cleanup_plan + .blocks + .iter() + .flat_map(|block| &block.transitions) + }) { + if let crate::cleanup_plan::CleanupTransition::StageCopyResult { source } = transition { + match source { + crate::cleanup_plan::StagedCopyResultSource::Body { .. } => { + staged_sources[0] = true + } + crate::cleanup_plan::StagedCopyResultSource::TryResidual { .. } => { + staged_sources[1] = true + } + crate::cleanup_plan::StagedCopyResultSource::TryOptionNone { .. } => { + staged_sources[2] = true + } + } + } + } + assert_eq!(staged_sources, [true; 3]); + RESOLVED_DISPOSE_HIGH_WATER.with(|water| water.set(0)); + RESOLVED_DISPOSE_COMPLETIONS.with(|count| count.set(0)); + RESOLVED_DISPOSE_CAPACITIES.with(|capacities| capacities.set([0; 2])); + let frames = Vec::with_capacity(capacity); + assert_eq!(frames.capacity(), capacity); + let owner = ResolvedProgramOwner::new(resolved, frames, capacity); + drop(owner); + assert_eq!(RESOLVED_DISPOSE_COMPLETIONS.with(std::cell::Cell::get), 1); + let high_water = RESOLVED_DISPOSE_HIGH_WATER.with(std::cell::Cell::get); + assert!(high_water > 0); + assert!(high_water <= capacity); + assert_eq!( + RESOLVED_DISPOSE_CAPACITIES.with(std::cell::Cell::get), + [capacity; 2] + ); + assert_eq!(std::mem::size_of::(), 56); + } + + #[test] + fn resolved_owner_disposes_nested_patterns_and_514_level_resource_cleanup() { + let pattern_source = "module disposal.patterns; @id(\"disposal.inner\") record Inner { @id(\"disposal.inner.value\") value: i64, } @id(\"disposal.outer\") record Outer { @id(\"disposal.outer.inner\") inner: Inner, } @id(\"disposal.choice\") variant Choice { @id(\"disposal.choice.value\") Value { @id(\"disposal.choice.value.payload\") payload: i64, }, @id(\"disposal.choice.empty\") Empty, } @id(\"disposal.record.match\") fn record_match(input: Outer) -> i64 { match input { Outer { inner: Inner { value } } => value, } } @id(\"disposal.variant.match\") fn variant_match(input: Choice) -> i64 { match input { Choice::Value { payload } => payload, Choice::Empty {} => 0, } } @id(\"app.main\") fn main() -> i64 { 0 }"; + let pattern_program = + crate::parse(pattern_source, Path::new("disposal-patterns.spx")).unwrap(); + let pattern_canonical = crate::format::canonical(&pattern_program); + let mut scan = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let pattern_capacity = + hir_pre_resolve_capacity(&pattern_program, pattern_canonical.len(), &mut scan) + .unwrap() + .disposal_frames; + let pattern_resolved = hir::resolve(&pattern_program).unwrap(); + assert_resolved_owner_disposes_once_without_growth(pattern_resolved, pattern_capacity); + + let mut chain = String::from( + "module disposal.cleanup_chain; @id(\"cleanup.r0\") resource R0 { @id(\"cleanup.r0.drop\") drop trivial; } ", + ); + for index in 1..514 { + use std::fmt::Write as _; + write!( + chain, + "@id(\"cleanup.r{index}\") record R{index} {{ @id(\"cleanup.r{index}.value\") value: R{}, }} ", + index - 1 + ) + .unwrap(); + } + chain.push_str("@id(\"cleanup.consume\") fn consume(value: own R513) -> i64 { 1 } @id(\"app.main\") fn main() -> i64 { 0 }"); + let chain_program = crate::parse(&chain, Path::new("disposal-cleanup-chain.spx")).unwrap(); + let chain_canonical = crate::format::canonical(&chain_program); + let mut scan = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let chain_capacity = + hir_pre_resolve_capacity(&chain_program, chain_canonical.len(), &mut scan) + .unwrap() + .disposal_frames; + let chain_resolved = hir::resolve(&chain_program).unwrap(); + let consume = chain_resolved + .functions + .iter() + .find(|function| function.id.as_str() == "cleanup.consume") + .unwrap(); + let mut maximum_shape_depth = 0usize; + let mut pending = consume + .cleanup_plan + .slots + .iter() + .map(|slot| (&slot.field_liveness_shape, 1usize)) + .collect::>(); + while let Some((shape, depth)) = pending.pop() { + maximum_shape_depth = maximum_shape_depth.max(depth); + if let semaprax::cleanup::FieldLivenessShape::Record { fields, .. } = shape { + pending.extend(fields.iter().map(|field| (&field.shape, depth + 1))); + } + } + assert_eq!(maximum_shape_depth, 514); + assert_resolved_owner_disposes_once_without_growth(chain_resolved, chain_capacity); + } + + fn assert_resolved_owner_disposes_once_without_growth( + resolved: ResolvedProgram, + capacity: usize, + ) -> usize { + RESOLVED_DISPOSE_HIGH_WATER.with(|water| water.set(0)); + RESOLVED_DISPOSE_COMPLETIONS.with(|count| count.set(0)); + RESOLVED_DISPOSE_CAPACITIES.with(|capacities| capacities.set([0; 2])); + let frames = Vec::with_capacity(capacity); + assert_eq!(frames.capacity(), capacity); + let owner = ResolvedProgramOwner::new(resolved, frames, capacity); + drop(owner); + assert_eq!(RESOLVED_DISPOSE_COMPLETIONS.with(std::cell::Cell::get), 1); + let high_water = RESOLVED_DISPOSE_HIGH_WATER.with(std::cell::Cell::get); + assert!(high_water > 0); + assert!(high_water <= capacity); + assert_eq!( + RESOLVED_DISPOSE_CAPACITIES.with(std::cell::Cell::get), + [capacity; 2] + ); + high_water + } + + #[test] + fn resolved_owner_undersized_workspace_aborts_before_post_drop_marker() { + const CHILD_ENV: &str = "SEMAPRAX_TEST_UNDERSIZED_RESOLVED_DISPOSE"; + const BEFORE_MARKER: &str = "before-drop"; + const FORBIDDEN_MARKER: &str = "after-drop"; + + if let Some(marker_root) = std::env::var_os(CHILD_ENV) { + let marker_root = std::path::PathBuf::from(marker_root); + let source = include_str!("../../../tests/fixtures/native_rust_hir_capacity.spx"); + let program = crate::parse(source, Path::new("native-rust-hir-capacity.spx")) + .expect("child fixture parses"); + let resolved = hir::resolve(&program).expect("child fixture resolves"); + let owner = ResolvedProgramOwner::new(resolved, Vec::with_capacity(1), 1); + std::fs::write(marker_root.join(BEFORE_MARKER), b"entered drop") + .expect("write pre-drop marker"); + drop(owner); + std::fs::write(marker_root.join(FORBIDDEN_MARKER), b"drop returned") + .expect("write forbidden post-drop marker"); + return; + } + + let marker_root = + std::env::temp_dir().join(format!("semaprax-resolved-dispose-{}", std::process::id())); + std::fs::create_dir(&marker_root).expect("create child marker directory"); + let output = Command::new(std::env::current_exe().expect("test executable path")) + .arg("implementation::tests::resolved_owner_undersized_workspace_aborts_before_post_drop_marker") + .arg("--exact") + .arg("--nocapture") + .env(CHILD_ENV, &marker_root) + .output() + .expect("undersized disposal child starts"); + assert!(!output.status.success()); + assert!(marker_root.join(BEFORE_MARKER).is_file()); + assert!(!marker_root.join(FORBIDDEN_MARKER).exists()); + std::fs::remove_file(marker_root.join(BEFORE_MARKER)).expect("remove child marker"); + std::fs::remove_dir(&marker_root).expect("remove child marker directory"); + } + + #[test] + fn resolved_owner_disposes_on_every_late_prepare_failure() { + let (program, spec) = fixture(); + for point in [ + PrepareFailurePoint::Closure, + PrepareFailurePoint::Facts, + PrepareFailurePoint::Render, + PrepareFailurePoint::Replay, + ] { + RESOLVED_DISPOSE_COMPLETIONS.with(|count| count.set(0)); + RESOLVED_DISPOSE_CAPACITIES.with(|capacities| capacities.set([0; 2])); + PREPARE_FAILURE_INJECTION.with(|selected| selected.set(Some(point))); + let result = prepare_native_rust_interop(&program, spec.as_bytes()); + PREPARE_FAILURE_INJECTION.with(|selected| selected.set(None)); + let diagnostic = result.err().expect("injected stage must fail"); + assert_eq!(diagnostic.len(), 1, "{point:?}"); + assert_eq!(diagnostic[0].code, "SPX-B107", "{point:?}"); + assert_eq!( + RESOLVED_DISPOSE_COMPLETIONS.with(std::cell::Cell::get), + 1, + "{point:?}" + ); + let capacities = RESOLVED_DISPOSE_CAPACITIES.with(std::cell::Cell::get); + assert!(capacities[0] > 0, "{point:?}"); + assert_eq!(capacities[0], capacities[1], "{point:?}"); + } + } + + #[test] + fn prebuilt_exact_depth_program_prepares_and_disposes_in_child() { + const CHILD_ENV: &str = "SEMAPRAX_TEST_PREBUILT_DEPTH_DISPOSE"; + const CHILD_SHAPE_ENV: &str = "SEMAPRAX_TEST_PREBUILT_DEPTH_SHAPE"; + const CHILD_DEPTH_ENV: &str = "SEMAPRAX_TEST_PREBUILT_DEPTH_VALUE"; + const CHILD_MARKER_ENV: &str = "SEMAPRAX_TEST_PREBUILT_DEPTH_MARKERS"; + const READY: &str = "ready"; + const DONE: &str = "done"; + const REJECTED: &str = "rejected"; + + if std::env::var_os(CHILD_ENV).is_some() { + let shape_value = std::env::var(CHILD_SHAPE_ENV).expect("child shape"); + let shape = shape_value.as_str(); + let over = std::env::var(CHILD_DEPTH_ENV).as_deref() == Ok("513"); + let marker_root = + std::path::PathBuf::from(std::env::var_os(CHILD_MARKER_ENV).expect("marker root")); + let source = format!( + "module prebuilt.{shape}; @id(\"prebuilt.{shape}.deep\") fn deep(value: bool) -> bool {{ value }} @id(\"app.main\") fn main() -> i64 {{ 0 }}" + ); + let mut program = crate::parse(&source, Path::new("prebuilt-depth.spx")).unwrap(); + let mut serial = 1usize; + loop { + let function = program + .functions + .iter_mut() + .find(|function| function.stable_id.ends_with(".deep")) + .expect("selected function exists"); + let body = std::mem::replace( + &mut function.body, + crate::ast::Expr { + span: crate::ast::Span::default(), + kind: crate::ast::ExprKind::Bool(false), + }, + ); + let span = crate::ast::Span { + start: serial, + end: serial + 1, + line: serial + 1, + column: 1, + }; + serial += 2; + function.body = crate::ast::Expr { + span, + kind: if shape == "if" { + crate::ast::ExprKind::If { + condition: Box::new(crate::ast::Expr { + span: crate::ast::Span { + start: serial, + end: serial + 1, + line: serial + 1, + column: 1, + }, + kind: crate::ast::ExprKind::Bool(true), + }), + then_branch: Box::new(body), + else_branch: Box::new(crate::ast::Expr { + span: crate::ast::Span { + start: serial + 2, + end: serial + 3, + line: serial + 3, + column: 1, + }, + kind: crate::ast::ExprKind::Bool(false), + }), + } + } else { + crate::ast::ExprKind::Binary { + op: crate::ast::BinaryOp::And, + left: Box::new(crate::ast::Expr { + span: crate::ast::Span { + start: serial, + end: serial + 1, + line: serial + 1, + column: 1, + }, + kind: crate::ast::ExprKind::Bool(true), + }), + right: Box::new(body), + } + }, + }; + serial += 4; + let _ = function; + if validate_native_rust_source_expression_budget(&program).is_err() { + if !over { + let function = program + .functions + .iter_mut() + .find(|function| function.stable_id.ends_with(".deep")) + .expect("selected function exists"); + let wrapper = std::mem::replace( + &mut function.body, + crate::ast::Expr { + span, + kind: crate::ast::ExprKind::Bool(false), + }, + ); + function.body = match wrapper.kind { + crate::ast::ExprKind::If { then_branch, .. } => *then_branch, + crate::ast::ExprKind::Binary { right, .. } => *right, + _ => unreachable!(), + }; + } + break; + } + } + let canonical = crate::format::canonical(&program); + let spec = render_spec(&Spec { + module: program.module.clone(), + source_revision: domain_digest(SOURCE_DOMAIN, canonical.as_bytes()), + target: current_target().unwrap(), + exports: vec![format!("prebuilt.{shape}.deep")], + imports: Vec::new(), + capabilities: Vec::new(), + }); + std::fs::write(marker_root.join(READY), b"ready").unwrap(); + RESOLVED_DISPOSE_COMPLETIONS.with(|count| count.set(0)); + HIR_RESOLVE_PASS_COUNT.with(|count| count.set(0)); + POST_HIR_FACTS_ENTRY_COUNT.with(|count| count.set(0)); + let result = prepare_native_rust_interop(&program, spec.as_bytes()); + if over { + let diagnostics = match result { + Err(diagnostics) => diagnostics, + Ok(_) => panic!("depth 513 unexpectedly prepared"), + }; + assert_eq!(diagnostics[0].code, "SPX-B109"); + assert_eq!(RESOLVED_DISPOSE_COMPLETIONS.with(std::cell::Cell::get), 0); + HIR_RESOLVE_PASS_COUNT.with(|count| assert_eq!(count.get(), 0)); + POST_HIR_FACTS_ENTRY_COUNT.with(|count| assert_eq!(count.get(), 0)); + std::fs::write(marker_root.join(REJECTED), b"rejected").unwrap(); + } else { + let prepared = result.unwrap(); + assert_eq!(RESOLVED_DISPOSE_COMPLETIONS.with(std::cell::Cell::get), 1); + drop(prepared); + std::fs::write(marker_root.join(DONE), b"done").unwrap(); + } + std::mem::forget(program); + std::process::exit(0); + } + + let marker_root = std::env::temp_dir().join(format!( + "semaprax-prebuilt-depth-dispose-{}", + std::process::id() + )); + std::fs::create_dir(&marker_root).expect("create hosted marker directory"); + for (shape, depth, marker) in [ + ("if", "512", DONE), + ("if", "513", REJECTED), + ("lazy", "512", DONE), + ("lazy", "513", REJECTED), + ] { + let output = Command::new(std::env::current_exe().unwrap()) + .arg("implementation::tests::prebuilt_exact_depth_program_prepares_and_disposes_in_child") + .arg("--exact") + .arg("--nocapture") + .env(CHILD_ENV, "1") + .env(CHILD_SHAPE_ENV, shape) + .env(CHILD_DEPTH_ENV, depth) + .env(CHILD_MARKER_ENV, &marker_root) + .output() + .unwrap(); + assert!( + output.status.success(), + "{shape}/{depth}: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(marker_root.join(READY).is_file()); + std::fs::remove_file(marker_root.join(READY)).unwrap(); + assert!(marker_root.join(marker).is_file()); + std::fs::remove_file(marker_root.join(marker)).unwrap(); + } + std::fs::remove_dir(&marker_root).expect("remove hosted marker directory"); + } + + #[test] + fn every_expression_shape_resolves_at_exact_depth_512_and_rejects_513() { + fn wrap_source(mut expression: crate::ast::Expr, count: usize) -> crate::ast::Expr { + for _ in 0..count { + let span = expression.span; + expression = crate::ast::Expr { + kind: crate::ast::ExprKind::Unary { + op: crate::ast::UnaryOp::Neg, + value: Box::new(expression), + }, + span, + }; + } + expression + } + + fn replace_payload( + expression: &mut crate::ast::Expr, + replacement: &mut Option, + ) -> bool { + use crate::ast::ExprKind; + + if matches!(&expression.kind, ExprKind::Var(name) if name == "payload") { + *expression = replacement.take().expect("payload replacement is unique"); + return true; + } + match &mut expression.kind { + ExprKind::Call { args, .. } => args + .iter_mut() + .any(|child| replace_payload(child, replacement)), + ExprKind::Unary { value, .. } + | ExprKind::Try { operand: value } + | ExprKind::Project { base: value, .. } => replace_payload(value, replacement), + ExprKind::Binary { left, right, .. } => { + replace_payload(left, replacement) || replace_payload(right, replacement) + } + ExprKind::Block { statements, tail } => { + statements.iter_mut().any(|statement| { + let crate::ast::Statement::Let { value, .. } = statement; + replace_payload(value, replacement) + }) || replace_payload(tail, replacement) + } + ExprKind::If { + condition, + then_branch, + else_branch, + } => { + replace_payload(condition, replacement) + || replace_payload(then_branch, replacement) + || replace_payload(else_branch, replacement) + } + ExprKind::ConstructRecord { fields, .. } + | ExprKind::ConstructVariant { fields, .. } => fields + .iter_mut() + .any(|field| replace_payload(&mut field.value, replacement)), + ExprKind::Match { scrutinee, arms } => { + replace_payload(scrutinee, replacement) + || arms + .iter_mut() + .any(|arm| replace_payload(&mut arm.value, replacement)) + } + ExprKind::UpdateRecord { base, fields } => { + replace_payload(base, replacement) + || fields + .iter_mut() + .any(|field| replace_payload(&mut field.value, replacement)) + } + ExprKind::Int(_) | ExprKind::Bool(_) | ExprKind::Var(_) => false, + } + } + + fn source_depth(program: &Program) -> usize { + let mut maximum = 0; + let mut pending = program + .functions + .iter() + .flat_map(|function| { + function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures) + }) + .map(|expression| (expression, 1_usize)) + .collect::>(); + while let Some((expression, depth)) = pending.pop() { + maximum = maximum.max(depth); + let mut index = 0; + while let Some(child) = ast_child(expression, index) { + pending.push((child, depth + 1)); + index += 1; + } + } + maximum + } + + fn payload_depth(program: &Program) -> usize { + let deep = program + .functions + .iter() + .find(|function| function.stable_id.ends_with(".deep")) + .expect("fixture deep function must exist"); + let mut pending = vec![(&deep.body, 1_usize)]; + while let Some((expression, depth)) = pending.pop() { + if matches!(&expression.kind, crate::ast::ExprKind::Var(name) if name == "payload") + { + return depth; + } + let mut index = 0; + while let Some(child) = ast_child(expression, index) { + pending.push((child, depth + 1)); + index += 1; + } + } + panic!("fixture payload must be present") + } + + fn wrap_hir_body_once(program: &mut ResolvedProgram) { + let function = program + .functions + .iter_mut() + .find(|function| function.id.as_str().ends_with(".deep")) + .expect("fixture deep function must resolve"); + let body = function.body.clone(); + function.body = ResolvedExpr { + id: body.id.clone(), + ty: body.ty.clone(), + ownership: body.ownership, + span: body.span, + kind: ResolvedExprKind::Unary { + op: crate::ast::UnaryOp::Neg, + value: Box::new(body), + }, + }; + } + + let cases = [ + ( + "unary", + "module depth.unary; @id(\"depth.unary.deep\") fn deep(payload: i64) -> i64 { -payload } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "binary", + "module depth.binary; @id(\"depth.binary.deep\") fn deep(payload: i64) -> i64 { payload + 0 } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "binary-right", + "module depth.binary_right; @id(\"depth.binary_right.deep\") fn deep(payload: i64) -> i64 { 0 + payload } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "if", + "module depth.if_shape; @id(\"depth.if.deep\") fn deep(payload: i64) -> i64 { if true { payload } else { 0 } } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "if-condition", + "module depth.if_condition; @id(\"depth.if_condition.deep\") fn deep(payload: i64) -> i64 { if payload > 0 { 1 } else { 0 } } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "if-else", + "module depth.if_else; @id(\"depth.if_else.deep\") fn deep(payload: i64) -> i64 { if true { 0 } else { payload } } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "block", + "module depth.block; @id(\"depth.block.deep\") fn deep(payload: i64) -> i64 { let before = 0; payload } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "block-let-rhs", + "module depth.block_let; @id(\"depth.block_let.deep\") fn deep(payload: i64) -> i64 { let value = payload; value } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "call", + "module depth.call; @id(\"depth.call.id\") fn id(value: i64) -> i64 { value } @id(\"depth.call.deep\") fn deep(payload: i64) -> i64 { id(payload) } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "call-first", + "module depth.call_first; @id(\"depth.call_first.sum\") fn sum(a: i64, b: i64, c: i64) -> i64 { a + b + c } @id(\"depth.call_first.deep\") fn deep(payload: i64) -> i64 { sum(payload, 0, 0) } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "call-middle", + "module depth.call_middle; @id(\"depth.call_middle.sum\") fn sum(a: i64, b: i64, c: i64) -> i64 { a + b + c } @id(\"depth.call_middle.deep\") fn deep(payload: i64) -> i64 { sum(0, payload, 0) } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "call-last", + "module depth.call_last; @id(\"depth.call_last.sum\") fn sum(a: i64, b: i64, c: i64) -> i64 { a + b + c } @id(\"depth.call_last.deep\") fn deep(payload: i64) -> i64 { sum(0, 0, payload) } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "native-call", + "module depth.native_call; permit { host.math } @id(\"host.math\") interface HostMath permits { host.math } { @id(\"host.add\") import rust fn host_add(left: i64, right: i64) -> i64 effects { host.math } failure status \"host.math.v1\"; } @id(\"depth.native_call.deep\") fn deep(payload: i64) -> i64 uses { host.math } { host_add(0, payload) } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "try", + "module depth.try_shape; @id(\"depth.try.ok\") fn ok(value: i64) -> Result { Result::Ok { value: value } } @id(\"depth.try.deep\") fn deep(payload: i64) -> Result { Result::Ok { value: ok(payload)? } } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "try-option", + "module depth.try_option; @id(\"depth.try_option.some\") fn some(value: i64) -> Option { Option::Some { value: value } } @id(\"depth.try_option.deep\") fn deep(payload: i64) -> Option { Option::Some { value: some(payload)? } } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "record-project", + "module depth.record_project; @id(\"depth.pair\") record Pair { @id(\"depth.pair.x\") x: i64, } @id(\"depth.record_project.deep\") fn deep(payload: i64) -> i64 { Pair { x: payload }.x } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "record-field-first", + "module depth.record_first; @id(\"depth.record_first.triple\") record Triple { @id(\"depth.record_first.triple.a\") a: i64, @id(\"depth.record_first.triple.b\") b: i64, @id(\"depth.record_first.triple.c\") c: i64, } @id(\"depth.record_first.deep\") fn deep(payload: i64) -> Triple { Triple { a: payload, b: 0, c: 0 } } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "record-field-middle", + "module depth.record_middle; @id(\"depth.record_middle.triple\") record Triple { @id(\"depth.record_middle.triple.a\") a: i64, @id(\"depth.record_middle.triple.b\") b: i64, @id(\"depth.record_middle.triple.c\") c: i64, } @id(\"depth.record_middle.deep\") fn deep(payload: i64) -> Triple { Triple { a: 0, b: payload, c: 0 } } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "record-field-last", + "module depth.record_last; @id(\"depth.record_last.triple\") record Triple { @id(\"depth.record_last.triple.a\") a: i64, @id(\"depth.record_last.triple.b\") b: i64, @id(\"depth.record_last.triple.c\") c: i64, } @id(\"depth.record_last.deep\") fn deep(payload: i64) -> Triple { Triple { a: 0, b: 0, c: payload } } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "variant", + "module depth.variant; @id(\"depth.choice\") variant Choice { @id(\"depth.choice.value\") Value { @id(\"depth.choice.value.value\") value: i64, }, } @id(\"depth.variant.deep\") fn deep(payload: i64) -> Choice { Choice::Value { value: payload } } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "match", + "module depth.match_shape; @id(\"depth.match.choice\") variant Choice { @id(\"depth.match.choice.none\") None, @id(\"depth.match.choice.value\") Value { @id(\"depth.match.choice.value.value\") value: i64, }, } @id(\"depth.match.deep\") fn deep(payload: i64) -> i64 { match Choice::Value { value: 0 } { Choice::Value { value } => payload, Choice::None {} => 0, } } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "match-scrutinee", + "module depth.match_scrutinee; @id(\"depth.match_scrutinee.choice\") variant Choice { @id(\"depth.match_scrutinee.choice.none\") None, @id(\"depth.match_scrutinee.choice.value\") Value { @id(\"depth.match_scrutinee.choice.value.value\") value: i64, }, } @id(\"depth.match_scrutinee.deep\") fn deep(payload: i64) -> i64 { match Choice::Value { value: payload } { Choice::Value { value } => value, Choice::None {} => 0, } } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "match-later-arm", + "module depth.match_later; @id(\"depth.match_later.choice\") variant Choice { @id(\"depth.match_later.choice.a\") A, @id(\"depth.match_later.choice.b\") B, @id(\"depth.match_later.choice.c\") C, } @id(\"depth.match_later.deep\") fn deep(choice: Choice, payload: i64) -> i64 { match choice { Choice::A {} => 0, Choice::B {} => payload, Choice::C {} => 0, } } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "match-nested-record-pattern", + "module depth.match_nested; @id(\"depth.match_nested.inner\") record Inner { @id(\"depth.match_nested.inner.value\") value: i64, } @id(\"depth.match_nested.outer\") record Outer { @id(\"depth.match_nested.outer.inner\") inner: Inner, @id(\"depth.match_nested.outer.other\") other: i64, } @id(\"depth.match_nested.deep\") fn deep(input: Outer, payload: i64) -> i64 { match input { Outer { inner: Inner { value }, other: _ } => payload, } } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "update", + "module depth.update; @id(\"depth.update.pair\") record Pair { @id(\"depth.update.pair.x\") x: i64, } @id(\"depth.update.deep\") fn deep(payload: i64) -> i64 { let pair = Pair { x: 0 }; (pair with { x: payload }).x } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ( + "update-base", + "module depth.update_base; @id(\"depth.update_base.pair\") record Pair { @id(\"depth.update_base.pair.x\") x: i64, } @id(\"depth.update_base.deep\") fn deep(payload: i64) -> i64 { (Pair { x: payload } with { x: 0 }).x } @id(\"app.main\") fn main() -> i64 { 0 }", + ), + ]; + + for (shape, source) in cases { + let mut exact = crate::parse(source, Path::new("all-shape-depth.spx")).unwrap(); + let initial_depth = source_depth(&exact); + assert!(initial_depth < MAX_SEMANTIC_EXPRESSION_DEPTH, "{shape}"); + let payload_depth = payload_depth(&exact); + let replacement = wrap_source( + crate::ast::Expr { + kind: crate::ast::ExprKind::Var("payload".to_owned()), + span: crate::ast::Span::default(), + }, + MAX_SEMANTIC_EXPRESSION_DEPTH - payload_depth, + ); + let function = exact + .functions + .iter_mut() + .find(|function| function.stable_id.ends_with(".deep")) + .expect("fixture deep function must exist"); + assert!(replace_payload(&mut function.body, &mut Some(replacement))); + assert_eq!( + source_depth(&exact), + MAX_SEMANTIC_EXPRESSION_DEPTH, + "{shape}" + ); + validate_native_rust_source_expression_budget(&exact).unwrap(); + let canonical = crate::format::canonical(&exact); + let mut scan = [None; MAX_SEMANTIC_EXPRESSION_DEPTH + 1]; + let disposal_capacity = hir_pre_resolve_capacity(&exact, canonical.len(), &mut scan) + .unwrap() + .disposal_frames; + let resolved = hir::resolve(&exact) + .unwrap_or_else(|diagnostics| panic!("{shape} failed resolution: {diagnostics:?}")); + validate_native_rust_expression_budget(&resolved).unwrap(); + assert_resolved_owner_disposes_once_without_growth(resolved, disposal_capacity); + + let mut resolved = hir::resolve(&exact) + .unwrap_or_else(|diagnostics| panic!("{shape} failed resolution: {diagnostics:?}")); + + let mut over_source = exact; + let function = over_source + .functions + .iter_mut() + .find(|function| function.stable_id.ends_with(".deep")) + .unwrap(); + function.body = wrap_source(function.body.clone(), 1); + let error = validate_native_rust_source_expression_budget(&over_source).unwrap_err(); + assert_eq!(error.code, "SPX-B109", "{shape}"); + assert_eq!( + error.message, "Native Rust Interop max_semantic_expression_depth exceeds 512", + "{shape}" + ); + + wrap_hir_body_once(&mut resolved); + let error = validate_native_rust_expression_budget(&resolved).unwrap_err(); + assert_eq!(error.code, "SPX-B109", "{shape}"); + assert_eq!( + error.message, "Native Rust Interop max_semantic_expression_depth exceeds 512", + "{shape}" + ); + } + } + + #[test] + fn private_b_builds_exact_static_inventory_without_clobber() { + let (program, spec) = fixture(); + let prepared = prepare_native_rust_interop(&program, spec.as_bytes()).unwrap(); + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-interop-test-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + std::fs::write( + root.join("semaprax_native_rust_interop.h"), + &prepared.generated_header, + ) + .unwrap(); + std::fs::write(root.join("module.c"), &prepared.generated_c).unwrap(); + let clang = configured_tool("CLANG").unwrap(); + let probe_object = if cfg!(windows) { + "probe.obj" + } else { + "probe.o" + }; + let mut probe = Command::new(&clang.path); + probe.env_clear().current_dir(&root).args([ + "-std=c11", + "-target", + &prepared.target.triple, + "-Wall", + "-Wextra", + "-Werror", + "-O2", + "-c", + "module.c", + "-o", + probe_object, + ]); + bind_test_tool_environment(&mut probe); + let probe = probe.output().unwrap(); + assert!( + probe.status.success(), + "{}", + String::from_utf8_lossy(&probe.stderr) + ); + let output = root.join("bundle"); + let facts = build_native_rust_interop_bundle(&program, spec.as_bytes(), &output).unwrap(); + assert_eq!(facts.output_directory, output); + assert!(facts.object_path.is_file()); + assert!(facts.descriptor_path.is_file()); + assert!(facts.manifest_path.is_file()); + assert!(facts.manifest_digest.starts_with("sha256:")); + let manifest = std::fs::read_to_string(&facts.manifest_path).unwrap(); + assert!(manifest.ends_with('\n')); + assert_eq!( + domain_digest(BUNDLE_DIGEST_DOMAIN, manifest.as_bytes()), + facts.manifest_digest + ); + let value: Value = serde_json::from_str(&manifest).unwrap(); + let row = value.as_object().unwrap(); + assert_eq!(row.len(), 6); + assert_eq!( + row.get("schema").and_then(Value::as_str), + Some(BUNDLE_SCHEMA) + ); + let descriptor = row.get("descriptor").and_then(Value::as_object).unwrap(); + assert_eq!(descriptor.len(), 3); + assert_eq!( + descriptor.get("schema").and_then(Value::as_str), + Some(DESCRIPTOR_SCHEMA) + ); + assert_eq!( + descriptor.get("digest").and_then(Value::as_str), + Some(prepared.descriptor_digest.as_str()) + ); + assert_eq!( + descriptor.get("bytes").and_then(Value::as_u64), + u64::try_from(prepared.descriptor.len()).ok() + ); + let files = row.get("files").and_then(Value::as_array).unwrap(); + let paths = files + .iter() + .map(|file| file.get("path").and_then(Value::as_str).unwrap()) + .collect::>(); + assert_eq!( + paths, + [ + "descriptor.json", + "module.c", + if cfg!(windows) { + "module.obj" + } else { + "module.o" + }, + "semaprax_native_rust_interop.h", + "semaprax_native_rust_interop.rs", + "semaprax_native_rust_interop_ffi.rs", + ] + ); + for file in files { + let file = file.as_object().unwrap(); + assert_eq!(file.len(), 3); + let path = file.get("path").and_then(Value::as_str).unwrap(); + let bytes = std::fs::read(output.join(path)).unwrap(); + let digest = raw_digest(&bytes); + assert_eq!( + file.get("bytes").and_then(Value::as_u64), + u64::try_from(bytes.len()).ok() + ); + assert_eq!( + file.get("sha256").and_then(Value::as_str), + Some(digest.as_str()) + ); + } + let toolchain = row.get("toolchain").and_then(Value::as_object).unwrap(); + assert_eq!( + toolchain.get("target").and_then(Value::as_str), + Some(prepared.target.triple.as_str()) + ); + assert_eq!( + row.get("nonclaims") + .and_then(Value::as_array) + .unwrap() + .iter() + .map(|item| item.as_str().unwrap()) + .collect::>(), + NONCLAIMS + ); + let retry = match build_native_rust_interop_bundle(&program, spec.as_bytes(), &output) { + Ok(_) => panic!("existing output was overwritten"), + Err(error) => error, + }; + assert_eq!(retry[0].code, "SPX-I232"); + + let foreign_file = root.join("foreign-file"); + std::fs::write(&foreign_file, b"foreign-file-sentinel").unwrap(); + let error = match build_native_rust_interop_bundle(&program, spec.as_bytes(), &foreign_file) + { + Ok(_) => panic!("foreign file was overwritten"), + Err(error) => error, + }; + assert_eq!(error.len(), 1); + assert_eq!(error[0].code, "SPX-I232"); + assert_eq!( + std::fs::read(&foreign_file).unwrap(), + b"foreign-file-sentinel" + ); + + let foreign_directory = root.join("foreign-directory"); + std::fs::create_dir(&foreign_directory).unwrap(); + let sentinel = foreign_directory.join("sentinel"); + std::fs::write(&sentinel, b"foreign-directory-sentinel").unwrap(); + let error = + match build_native_rust_interop_bundle(&program, spec.as_bytes(), &foreign_directory) { + Ok(_) => panic!("foreign directory was overwritten"), + Err(error) => error, + }; + assert_eq!(error.len(), 1); + assert_eq!(error[0].code, "SPX-I232"); + assert_eq!( + std::fs::read(&sentinel).unwrap(), + b"foreign-directory-sentinel" + ); + + #[cfg(unix)] + { + let foreign_target = root.join("foreign-symlink-target"); + std::fs::write(&foreign_target, b"foreign-symlink-sentinel").unwrap(); + let foreign_link = root.join("foreign-symlink"); + std::os::unix::fs::symlink(&foreign_target, &foreign_link).unwrap(); + let error = + match build_native_rust_interop_bundle(&program, spec.as_bytes(), &foreign_link) { + Ok(_) => panic!("foreign symlink was followed"), + Err(error) => error, + }; + assert_eq!(error.len(), 1); + assert_eq!(error[0].code, "SPX-I232"); + assert_eq!( + std::fs::read(&foreign_target).unwrap(), + b"foreign-symlink-sentinel" + ); + assert!(std::fs::symlink_metadata(&foreign_link) + .unwrap() + .file_type() + .is_symlink()); + } + std::fs::remove_dir_all(&root).unwrap(); + } + + #[cfg(unix)] + #[test] + fn held_file_matching_rejects_symlink_identity_and_permission_drift() { + use std::os::unix::fs::PermissionsExt as _; + + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-held-file-test-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + let target = root.join("target"); + let link = root.join("link"); + std::fs::write(&target, b"authenticated-bytes").unwrap(); + std::os::unix::fs::symlink(&target, &link).unwrap(); + let error = match_regular_file(&link, b"authenticated-bytes").unwrap_err(); + assert_eq!(error.code, "SPX-I232"); + assert_eq!( + error.message, + "Native Rust Interop output publication failed" + ); + assert_eq!(std::fs::read(&target).unwrap(), b"authenticated-bytes"); + + let permissions = std::fs::metadata(&target).unwrap().permissions(); + let mut denied = permissions.clone(); + denied.set_mode(0o0); + std::fs::set_permissions(&target, denied).unwrap(); + let result = match_regular_file(&target, b"authenticated-bytes"); + std::fs::set_permissions(&target, permissions).unwrap(); + let error = result.unwrap_err(); + assert_eq!(error.code, "SPX-I232"); + assert_eq!( + error.message, + "Native Rust Interop output publication failed" + ); + std::fs::remove_dir_all(&root).unwrap(); + } + + #[cfg(unix)] + #[test] + fn held_stage_rejects_same_path_directory_and_reparse_substitution() { + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-held-stage-test-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + + let parent = hold_stage(root.clone()).unwrap(); + let slot = StageSlot::new(&root, "sha256:held-stage", "identity").unwrap(); + let inventory = platform::prepare_discard_inventory([]).unwrap(); + let stage = create_stage(&parent, slot, &inventory).unwrap(); + stage.recheck().unwrap(); + let displaced = root.join("displaced-stage"); + std::fs::rename(&stage.path, &displaced).unwrap(); + std::fs::create_dir(&stage.path).unwrap(); + let error = stage.recheck().unwrap_err(); + assert_eq!(error.code, "SPX-I232"); + assert_eq!( + error.message, + "Native Rust Interop output publication failed" + ); + assert!(displaced.is_dir()); + + std::fs::remove_dir(&stage.path).unwrap(); + std::os::unix::fs::symlink(&displaced, &stage.path).unwrap(); + let error = stage.recheck().unwrap_err(); + assert_eq!(error.code, "SPX-I232"); + assert_eq!( + error.message, + "Native Rust Interop output publication failed" + ); + assert!(std::fs::symlink_metadata(&stage.path) + .unwrap() + .file_type() + .is_symlink()); + + std::fs::remove_file(&stage.path).unwrap(); + std::fs::remove_dir_all(&displaced).unwrap(); + std::fs::remove_dir_all(&root).unwrap(); + } + + #[test] + fn linked_bridge_round_trips_rust_to_semaprax_to_rust_and_closes_failures() { + let (program, spec) = fixture(); + let prepared = prepare_native_rust_interop(&program, spec.as_bytes()).unwrap(); + let parsed_spec = parse_spec(&program, spec.as_bytes()).unwrap(); + let export = &prepared.exports[0]; + let import = &prepared.imports[0]; + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-roundtrip-test-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + let output = root.join("bundle"); + build_native_rust_interop_bundle(&program, spec.as_bytes(), &output).unwrap(); + + let harness = format!( + r#"#[path="semaprax_native_rust_interop.rs"]mod semaprax_native_rust_interop; +use core::num::NonZeroU32; +use semaprax_native_rust_interop::*; +struct Host{{mode:u8,panicked:bool}} +impl NativeRustImports for Host{{ +fn {import_method}(&mut self,arg_0:i64,_arg_1:i64)->NativeRustImportResult{{ +match self.mode{{ +0=>NativeRustImportResult::Success(arg_0), +1=>NativeRustImportResult::Status{{code:NonZeroU32::new(7).unwrap(),class:NativeRustStatusClass::Import,retryable:true}}, +2=>NativeRustImportResult::HostFailure, +6=>if self.panicked{{NativeRustImportResult::Success(arg_0)}}else{{self.panicked=true;panic!("panic once")}}, +4=>NativeRustImportResult::Status{{code:NonZeroU32::new(7).unwrap(),class:NativeRustStatusClass::Semantic,retryable:false}}, +_=>panic!("private sentinel must not cross the FFI boundary") +}} +}} +}} +fn bridge(mode:u8)->NativeRustBridge{{ +let caps=NativeRustCapabilities::new(&["host.math"]).unwrap_or_else(|_|std::process::exit(10)); +NativeRustBridge::new(Host{{mode,panicked:false}},caps) +}} +fn main(){{ +std::panic::set_hook(Box::new(|_|{{}})); +if NativeRustCapabilities::new(&["wrong.capability"]).is_ok(){{std::process::exit(11)}} +let mut success=bridge(0); +match success.{export_method}(20,22){{Ok(42)=>{{}},_=>std::process::exit(12)}} +let mut semantic=bridge(0); +match semantic.{export_method}(i64::MAX,1){{ +Err(NativeRustCallError::Semantic{{domain_id:"semaprax.native-rust-semantics.v1",code,class:NativeRustStatusClass::Semantic,retryable:false}}) if code.get()==2=>{{}}, +_=>std::process::exit(19) +}} +let mut status=bridge(1); +match status.{export_method}(1,2){{ +Err(NativeRustCallError::Semantic{{domain_id:"host.math.v1",code,class:NativeRustStatusClass::Import,retryable:true}}) if code.get()==7=>{{}}, +_=>std::process::exit(13) +}} +let mut failed=bridge(2); +match failed.{export_method}(1,2){{Err(NativeRustCallError::HostFailed)=>{{}},_=>std::process::exit(14)}} +let mut panicked=bridge(3); +match panicked.{export_method}(1,2){{Err(NativeRustCallError::HostPanicked)=>{{}},_=>std::process::exit(15)}} +let mut panic_once=bridge(6);match panic_once.{export_method}(1,2){{Err(NativeRustCallError::HostPanicked)=>{{}},_=>std::process::exit(23)}}match panic_once.{export_method}(1,2){{Ok(3)=>{{}},_=>std::process::exit(24)}} +let mut wrong_class=bridge(4); +match wrong_class.{export_method}(1,2){{Err(NativeRustCallError::AdapterRejected)=>{{}},_=>std::process::exit(18)}} +let mut bounded=bridge(0); +for _ in 0..2048{{if !matches!(bounded.{export_method}(1,2),Ok(3)){{std::process::exit(16)}}}} +match bounded.{export_method}(1,2){{Err(NativeRustCallError::AdapterRejected)=>{{}},_=>std::process::exit(17)}} +}} +"#, + import_method = import.rust_method, + export_method = export.rust_method, + ); + let active = prepared + .generated_rust + .find("||core::mem::replace(&mut self.active,true){return Err(NativeRustCallError::AdapterRejected)}") + .unwrap(); + let effect = prepared.generated_rust[active..] + .find("super::ffi::") + .unwrap(); + assert!(effect > 0, "reentry must reject before allocating an FFI result slot or performing an import effect"); + assert!(prepared + .generated_rust + .contains("impl Drop for ActiveGuard<'_>{fn drop(&mut self){*self.active=false;}}")); + let harness_path = output.join("roundtrip.rs"); + std::fs::write(&harness_path, harness).unwrap(); + let executable = if cfg!(windows) { + "roundtrip.exe" + } else { + "roundtrip" + }; + let object = if cfg!(windows) { + "module.obj" + } else { + "module.o" + }; + let rustc = configured_tool("RUSTC").unwrap(); + let clang = configured_tool("CLANG").unwrap(); + let sanitizers = sanitizer_mode().unwrap(); + let o0_object = if cfg!(windows) { + "module_hostile_O0.obj" + } else { + "module_hostile_O0.o" + }; + let mut o0_compile = Command::new(&clang.path); + o0_compile.env_clear().current_dir(&output).args([ + "-std=c11", + "-target", + &prepared.target.triple, + "-Wall", + "-Wextra", + "-Werror", + "-O0", + "-c", + "module.c", + "-o", + o0_object, + ]); + bind_test_tool_environment(&mut o0_compile); + if sanitizers { + o0_compile.args(REQUIRED_NATIVE_RUST_SANITIZER_FLAGS); + } + assert!(o0_compile.status().unwrap().success()); + for (linked_object, linked_executable) in + [(o0_object, "roundtrip_O0"), (object, executable)] + { + let mut roundtrip_compile = Command::new(&rustc.path); + roundtrip_compile.env_clear().current_dir(&output).args([ + "--edition=2021", + "-C", + "panic=unwind", + "-C", + &format!("linker={}", clang.path.display()), + "-C", + &format!("link-arg={linked_object}"), + "roundtrip.rs", + "-o", + linked_executable, + ]); + bind_test_tool_environment(&mut roundtrip_compile); + if sanitizers { + roundtrip_compile.args([ + "-C", + "link-arg=-fsanitize=address,undefined", + "-C", + "link-arg=-fno-sanitize-recover=all", + ]); + } + assert!(roundtrip_compile.status().unwrap().success()); + let mut roundtrip_run = Command::new(output.join(linked_executable)); + roundtrip_run.env_clear().current_dir(&output); + if sanitizers { + roundtrip_run + .env( + "ASAN_OPTIONS", + "detect_leaks=0:halt_on_error=1:abort_on_error=1", + ) + .env("UBSAN_OPTIONS", "halt_on_error=1:print_stacktrace=1"); + } + assert!(roundtrip_run.status().unwrap().success()); + } + + let capability_hex = capability_digest(&parsed_spec.capabilities) + .strip_prefix("sha256:") + .unwrap() + .to_owned(); + let capability_bytes = (0..64) + .step_by(2) + .map(|index| format!("0x{}", &capability_hex[index..index + 2])) + .collect::>() + .join(","); + let abi_harness = format!( + r#"#![allow(unsafe_code)] +use core::ffi::c_void; +type Callback=unsafe extern "C" fn(*mut c_void,i64,i64,*mut i64)->u64; +#[repr(C)]struct Imports{{abi_version:u32,size:u32,callback:Option}} +#[repr(C)]struct Context{{abi_version:u32,size:u32,userdata:*mut c_void,imports:*const Imports,capabilities_digest:[u8;32],call_depth:u32,reserved:u32}} +unsafe extern "C"{{fn {export_symbol}(ctx:*const Context,arg_0:i64,arg_1:i64,result_out:*mut i64)->u64;}} +unsafe extern "C" fn callback(userdata:*mut c_void,left:i64,_right:i64,out:*mut i64)->u64{{ +let injected=if userdata.is_null(){{0}}else{{unsafe{{*(userdata.cast::())}}}}; +if injected!=0{{return injected}}unsafe{{*out=left}};0}} +fn adapter(code:u64)->u64{{(65535u64<<48)|(4u64<<32)|code}} +fn status(domain:u64,class:u64,retry:u64,code:u64)->u64{{(domain<<48)|(retry<<40)|(class<<32)|code}} +macro_rules! rejected{{($context:expr,$wire:expr)=>{{let mut poisoned=0x5a5a_6b6b_7c7c_8d8di64;assert_eq!({export_symbol}($context,1,2,&mut poisoned),$wire);assert_eq!(poisoned,0x5a5a_6b6b_7c7c_8d8di64);}}}} +fn main(){{unsafe{{ +let imports=Imports{{abi_version:1,size:core::mem::size_of::() as u32,callback:Some(callback)}}; +let mut context=Context{{abi_version:1,size:core::mem::size_of::() as u32,userdata:core::ptr::null_mut(),imports:&imports,capabilities_digest:[{capability_bytes}],call_depth:0,reserved:0}}; +let mut out=0i64; +assert_eq!({export_symbol}(&context,20,22,&mut out),0);assert_eq!(out,42); +rejected!(core::ptr::null(),adapter(1)); +context.abi_version=2;rejected!(&context,adapter(1));context.abi_version=1; +context.size=0;rejected!(&context,adapter(1));context.size=core::mem::size_of::() as u32; +context.reserved=1;rejected!(&context,adapter(1));context.reserved=0; +context.imports=core::ptr::null();rejected!(&context,adapter(2));context.imports=&imports; +let bad_imports=Imports{{abi_version:2,size:core::mem::size_of::() as u32,callback:Some(callback)}};context.imports=&bad_imports;rejected!(&context,adapter(2));context.imports=&imports; +let missing_callback=Imports{{abi_version:1,size:core::mem::size_of::() as u32,callback:None}};context.imports=&missing_callback;rejected!(&context,adapter(2));context.imports=&imports; +context.capabilities_digest[0]^=1;rejected!(&context,adapter(3));context.capabilities_digest[0]^=1; +context.call_depth=31;assert_eq!({export_symbol}(&context,1,2,&mut out),0);assert_eq!(out,3); +context.call_depth=32;rejected!(&context,adapter(7));context.call_depth=0; +let mut injected=status(65534,4,0,1);context.userdata=(&mut injected as *mut u64).cast();rejected!(&context,injected); +injected=status(65534,4,0,2);rejected!(&context,injected); +injected=status(65535,4,0,3);rejected!(&context,injected); +for forged in [status(65534,4,0,0),status(65534,4,0,3),status(65534,3,0,1),status(65534,4,1,1),status(65535,4,0,0),status(65535,4,0,9),status(65535,3,0,3),status(65535,4,1,3),status(65535,4,0,3)|(1u64<<41),status(0,4,0,1)]{{injected=forged;context.userdata=core::hint::black_box((&mut injected as *mut u64).cast());rejected!(&context,adapter(8));}} +context.userdata=core::ptr::null_mut(); +assert_eq!({export_symbol}(&context,1,2,core::ptr::null_mut()),adapter(5)); +let mut result_bytes=[0x5au8;16];let before_result_bytes=result_bytes;let misaligned=result_bytes.as_mut_ptr().add(1).cast::();assert_eq!({export_symbol}(&context,1,2,misaligned),adapter(5));assert_eq!(result_bytes,before_result_bytes); +let mut context_bytes=[0u8;128];let misaligned_context=context_bytes.as_mut_ptr().add(1).cast::();rejected!(misaligned_context,adapter(1)); +}}}} +"#, + export_symbol = export.c_symbol, + capability_bytes = capability_bytes, + ); + std::fs::write(output.join("abi_hostile.rs"), abi_harness).unwrap(); + let abi_executable = if cfg!(windows) { + "abi_hostile.exe" + } else { + "abi_hostile" + }; + for (linked_object, linked_executable) in + [(o0_object, "abi_hostile_O0"), (object, abi_executable)] + { + let mut abi_compile = Command::new(&rustc.path); + abi_compile.env_clear().current_dir(&output).args([ + "--edition=2021", + "-C", + "panic=abort", + "-C", + &format!("linker={}", clang.path.display()), + "-C", + &format!("link-arg={linked_object}"), + "abi_hostile.rs", + "-o", + linked_executable, + ]); + bind_test_tool_environment(&mut abi_compile); + if sanitizers { + abi_compile.args([ + "-C", + "link-arg=-fsanitize=address,undefined", + "-C", + "link-arg=-fno-sanitize-recover=all", + ]); + } + assert!(abi_compile.status().unwrap().success()); + let mut abi_run = Command::new(output.join(linked_executable)); + abi_run.env_clear().current_dir(&output); + if sanitizers { + abi_run + .env( + "ASAN_OPTIONS", + "detect_leaks=0:halt_on_error=1:abort_on_error=1", + ) + .env("UBSAN_OPTIONS", "halt_on_error=1:print_stacktrace=1"); + } + assert!(abi_run.status().unwrap().success()); + } + + let cross_thread = format!( + r#"#[path="semaprax_native_rust_interop.rs"]mod semaprax_native_rust_interop; +use semaprax_native_rust_interop::*; +struct Host; +impl NativeRustImports for Host{{fn {import_method}(&mut self,left:i64,_right:i64)->NativeRustImportResult{{NativeRustImportResult::Success(left)}}}} +fn main(){{let caps=NativeRustCapabilities::new(&["host.math"]).unwrap_or_else(|_|std::process::exit(1));let mut bridge=NativeRustBridge::new(Host,caps);std::thread::spawn(move||{{let _=bridge.{export_method}(1,2);}}).join().unwrap();}} +"#, + import_method = import.rust_method, + export_method = export.rust_method, + ); + std::fs::write(output.join("cross_thread.rs"), cross_thread).unwrap(); + let cross_thread_executable = if cfg!(windows) { + "cross_thread.exe" + } else { + "cross_thread" + }; + let compile = Command::new(&rustc.path) + .env_clear() + .current_dir(&output) + .args([ + "--edition=2021", + "-C", + "panic=unwind", + "-C", + &format!("linker={}", clang.path.display()), + "-C", + &format!("link-arg={object}"), + "cross_thread.rs", + "-o", + cross_thread_executable, + ]) + .output() + .unwrap(); + assert!(!compile.status.success()); + assert!(!output.join(cross_thread_executable).exists()); + assert!(String::from_utf8_lossy(&compile.stderr) + .contains("cannot be sent between threads safely")); + + let nested_borrow = format!( + r#"#[path="semaprax_native_rust_interop.rs"]mod semaprax_native_rust_interop; +use semaprax_native_rust_interop::*; +struct Host; +impl NativeRustImports for Host{{fn {import_method}(&mut self,left:i64,_right:i64)->NativeRustImportResult{{NativeRustImportResult::Success(left)}}}} +fn nested(bridge:&mut NativeRustBridge){{let borrow=&mut *bridge;let first=bridge.{export_method}(1,2);let second=borrow.{export_method}(1,2);let _=(first,second);}} +fn main(){{}} +"#, + import_method = import.rust_method, + export_method = export.rust_method, + ); + std::fs::write(output.join("nested_borrow.rs"), nested_borrow).unwrap(); + let nested = Command::new(&rustc.path) + .env_clear() + .current_dir(&output) + .args([ + "--edition=2021", + "--crate-type", + "lib", + "nested_borrow.rs", + "-o", + if cfg!(windows) { + "nested_borrow.rlib" + } else { + "libnested_borrow.rlib" + }, + ]) + .output() + .unwrap(); + assert!(!nested.status.success()); + let nested_stderr = String::from_utf8_lossy(&nested.stderr); + assert!( + nested_stderr.contains("cannot borrow `*bridge` as mutable more than once at a time"), + "{nested_stderr}" + ); + + let ffi_sibling = String::from( + r#"#[path="semaprax_native_rust_interop.rs"]mod semaprax_native_rust_interop; +mod sibling{pub fn forge(){let _=super::semaprax_native_rust_interop::ffi::capabilities_digest();}} +fn main(){sibling::forge();} +"#, + ); + std::fs::write(output.join("ffi_sibling.rs"), ffi_sibling).unwrap(); + let ffi_executable = if cfg!(windows) { + "ffi_sibling.exe" + } else { + "ffi_sibling" + }; + let compile = Command::new(&rustc.path) + .env_clear() + .current_dir(&output) + .args([ + "--edition=2021", + "-C", + "panic=unwind", + "-C", + &format!("linker={}", clang.path.display()), + "-C", + &format!("link-arg={object}"), + "ffi_sibling.rs", + "-o", + ffi_executable, + ]) + .output() + .unwrap(); + assert!(!compile.status.success()); + assert!(!output.join(ffi_executable).exists()); + let stderr = String::from_utf8_lossy(&compile.stderr); + assert!(stderr.contains("module `ffi` is private"), "{stderr}"); + std::fs::remove_dir_all(&root).unwrap(); + } + + #[test] + fn bool_and_infallible_import_abi_is_exact_at_o0_and_o2() { + const BOOL_SOURCE: &str = r#"module interop.bool_fixture; + +@id("host.bool") +interface HostBool + permits { } +{ + @id("host.bool.invert") + import rust fn invert(value: bool) -> bool + effects { } + failure infallible; +} + +@id("interop.bool") +fn call_invert(value: bool) -> bool +{ + invert(value) +} + +@id("interop.bool.main") +fn main() -> i64 +{ + 0 +} +"#; + let program = crate::parse(BOOL_SOURCE, Path::new("native-rust-bool.spx")).unwrap(); + let canonical = crate::format::canonical(&program); + let spec = Spec { + module: program.module.clone(), + source_revision: domain_digest(SOURCE_DOMAIN, canonical.as_bytes()), + target: current_target().unwrap(), + exports: vec!["interop.bool".to_owned()], + imports: vec!["host.bool.invert".to_owned()], + capabilities: Vec::new(), + }; + let spec = render_spec(&spec); + let prepared = prepare_native_rust_interop(&program, spec.as_bytes()).unwrap(); + let parsed_spec = parse_spec(&program, spec.as_bytes()).unwrap(); + let export = &prepared.exports[0]; + let import = &prepared.imports[0]; + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-bool-test-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir(&root).unwrap(); + let output = root.join("bundle"); + build_native_rust_interop_bundle(&program, spec.as_bytes(), &output).unwrap(); + let rustc = configured_tool("RUSTC").unwrap(); + let clang = configured_tool("CLANG").unwrap(); + let sanitizers = sanitizer_mode().unwrap(); + let object = if cfg!(windows) { + "module.obj" + } else { + "module.o" + }; + let o0_object = if cfg!(windows) { + "module_bool_O0.obj" + } else { + "module_bool_O0.o" + }; + let mut o0_compile = Command::new(&clang.path); + o0_compile.env_clear().current_dir(&output).args([ + "-std=c11", + "-target", + &prepared.target.triple, + "-Wall", + "-Wextra", + "-Werror", + "-O0", + "-c", + "module.c", + "-o", + o0_object, + ]); + bind_test_tool_environment(&mut o0_compile); + if sanitizers { + o0_compile.args(REQUIRED_NATIVE_RUST_SANITIZER_FLAGS); + } + assert!(o0_compile.status().unwrap().success()); + + let safe_harness = format!( + r#"#[path="semaprax_native_rust_interop.rs"]mod semaprax_native_rust_interop; +use core::num::NonZeroU32; +use semaprax_native_rust_interop::*; +struct Host{{mode:u8}} +impl NativeRustImports for Host{{fn {import_method}(&mut self,value:bool)->NativeRustImportResult{{match self.mode{{0=>NativeRustImportResult::Success(!value),_=>NativeRustImportResult::Status{{code:NonZeroU32::new(9).unwrap(),class:NativeRustStatusClass::Import,retryable:false}}}}}}}} +fn bridge(mode:u8)->NativeRustBridge{{NativeRustBridge::new(Host{{mode}},NativeRustCapabilities::new(&[]).unwrap_or_else(|_|std::process::exit(10)))}} +fn main(){{let code=NonZeroU32::new(1).unwrap();let _=NativeRustImportResult::::HostFailure;let probe=NativeRustCallError::Semantic{{domain_id:"semaprax.native-rust-semantics.v1",code,class:NativeRustStatusClass::Semantic,retryable:false}};if let NativeRustCallError::Semantic{{domain_id,code,class,retryable}}=probe{{let _=(domain_id,code,class,retryable);}}let mut success=bridge(0);if !matches!(success.{export_method}(false),Ok(true))||!matches!(success.{export_method}(true),Ok(false)){{std::process::exit(11)}}let mut rejected=bridge(1);if !matches!(rejected.{export_method}(false),Err(NativeRustCallError::AdapterRejected)){{std::process::exit(12)}}}} +"#, + import_method = import.rust_method, + export_method = export.rust_method, + ); + std::fs::write(output.join("bool_safe.rs"), safe_harness).unwrap(); + let capability_hex = capability_digest(&parsed_spec.capabilities) + .strip_prefix("sha256:") + .unwrap() + .to_owned(); + let capability_bytes = (0..64) + .step_by(2) + .map(|index| format!("0x{}", &capability_hex[index..index + 2])) + .collect::>() + .join(","); + let raw_harness = format!( + r#"#![allow(unsafe_code)] +use core::ffi::c_void; +type Callback=unsafe extern "C" fn(*mut c_void,u8,*mut u8)->u64; +#[repr(C)]struct Imports{{abi_version:u32,size:u32,callback:Option}} +#[repr(C)]struct Context{{abi_version:u32,size:u32,userdata:*mut c_void,imports:*const Imports,capabilities_digest:[u8;32],call_depth:u32,reserved:u32}} +unsafe extern "C"{{fn {export_symbol}(ctx:*const Context,arg_0:u8,result_out:*mut u8)->u64;}} +fn adapter(code:u64)->u64{{(65535u64<<48)|(4u64<<32)|code}} +unsafe extern "C" fn callback(userdata:*mut c_void,value:u8,out:*mut u8)->u64{{let mode=unsafe{{*(userdata.cast::())}};match mode{{0=>{{unsafe{{*out=u8::from(value==0)}};0}},1=>{{unsafe{{*out=2}};0}},_=>adapter(3)}}}} +fn main(){{unsafe{{let imports=Imports{{abi_version:1,size:core::mem::size_of::() as u32,callback:Some(callback)}};let mut mode=0u8;let context=Context{{abi_version:1,size:core::mem::size_of::() as u32,userdata:(&mut mode as *mut u8).cast(),imports:&imports,capabilities_digest:[{capability_bytes}],call_depth:0,reserved:0}};let mut out=0u8;assert_eq!({export_symbol}(&context,0,&mut out),0);assert_eq!(out,1);assert_eq!({export_symbol}(&context,1,&mut out),0);assert_eq!(out,0);let mut poison=0x5au8;assert_eq!({export_symbol}(&context,2,&mut poison),adapter(4));assert_eq!(poison,0x5a);mode=1;core::hint::black_box(&mode);assert_eq!({export_symbol}(&context,0,&mut poison),adapter(4));assert_eq!(poison,0x5a);mode=2;core::hint::black_box(&mode);assert_eq!({export_symbol}(&context,0,&mut poison),adapter(3));assert_eq!(poison,0x5a);}}}} +"#, + export_symbol = export.c_symbol, + capability_bytes = capability_bytes, + ); + std::fs::write(output.join("bool_raw.rs"), raw_harness).unwrap(); + for (linked_object, suffix) in [(o0_object, "O0"), (object, "O2")] { + for source in ["bool_safe.rs", "bool_raw.rs"] { + let executable = format!( + "{}_{}{}", + source.trim_end_matches(".rs"), + suffix, + if cfg!(windows) { ".exe" } else { "" } + ); + let mut compile = Command::new(&rustc.path); + compile.env_clear().current_dir(&output).args([ + "--edition=2021", + "-Dwarnings", + "-C", + "panic=unwind", + "-C", + &format!("linker={}", clang.path.display()), + "-C", + &format!("link-arg={linked_object}"), + source, + "-o", + &executable, + ]); + bind_test_tool_environment(&mut compile); + if sanitizers { + compile.args([ + "-C", + "link-arg=-fsanitize=address,undefined", + "-C", + "link-arg=-fno-sanitize-recover=all", + ]); + } + assert!(compile.status().unwrap().success()); + let mut run = Command::new(output.join(&executable)); + run.env_clear().current_dir(&output); + if sanitizers { + run.env( + "ASAN_OPTIONS", + "detect_leaks=0:halt_on_error=1:abort_on_error=1", + ) + .env("UBSAN_OPTIONS", "halt_on_error=1:print_stacktrace=1"); + } + assert!(run.status().unwrap().success()); + } + } + std::fs::remove_dir_all(&root).unwrap(); + } +} diff --git a/crates/semaprax-native-rust-interop-builder/src/lib.rs b/crates/semaprax-native-rust-interop-builder/src/lib.rs new file mode 100644 index 0000000..fba94af --- /dev/null +++ b/crates/semaprax-native-rust-interop-builder/src/lib.rs @@ -0,0 +1,194 @@ +//! Unpublished Native Rust Interoperability v1 A+B implementation. + +#![forbid(unsafe_code)] +#![allow(clippy::result_large_err)] +#![allow( + clippy::write_with_newline, + reason = "generated source writers bind their terminal newline in the frozen literal" +)] + +#[cfg(test)] +pub(crate) use semaprax::format; +#[cfg(test)] +pub(crate) use semaprax::parse; +pub(crate) use semaprax::{ast, cleanup, cleanup_plan, diagnostic, hir}; +#[path = "../../../src/private_capacity_contract.rs"] +pub(crate) mod private_capacity_contract; +#[allow(dead_code, clippy::all)] +#[path = "../../../src/format.rs"] +pub(crate) mod private_format; +use semaprax_native_rust_interop_platform as platform; +use std::path::Path; + +pub(crate) mod workspace { + use super::*; + + pub(crate) struct AuthenticatedDirectory(platform::HeldDirectory); + + pub(crate) enum CreatedDirectoryAuthenticationError { + Disagreement(platform::HeldDirectory), + } + + impl AuthenticatedDirectory { + pub(crate) fn recheck(&self) -> Result<(), platform::Error> { + platform::recheck_directory(&self.0) + } + + pub(crate) fn same_directory_path(&self, path: &Path) -> bool { + platform::same_directory_path(&self.0, path).unwrap_or(false) + } + + pub(crate) fn held(&self) -> &platform::HeldDirectory { + &self.0 + } + } + + pub(crate) fn authenticate_directory_held( + path: &Path, + ) -> Result { + platform::hold_directory(path).map(AuthenticatedDirectory) + } + + pub(crate) fn authenticate_created_directory( + path: &Path, + held: platform::HeldDirectory, + ) -> Result { + match platform::same_directory_path(&held, path) { + Ok(true) => Ok(AuthenticatedDirectory(held)), + Ok(false) | Err(_) => Err(CreatedDirectoryAuthenticationError::Disagreement(held)), + } + } +} + +pub(crate) mod bounded_output { + use std::cell::{Cell, RefCell}; + use std::fmt; + use std::rc::Rc; + + struct Budget { + initial: usize, + remaining: Cell, + overflowed: Cell, + } + + thread_local! { + static ACTIVE: RefCell>> = const { RefCell::new(None) }; + } + + pub(crate) fn with_limit(limit: usize, operation: impl FnOnce() -> T) -> (T, bool) { + let (value, overflowed, _) = with_limit_usage(limit, operation); + (value, overflowed) + } + + pub(crate) fn with_limit_usage( + limit: usize, + operation: impl FnOnce() -> T, + ) -> (T, bool, usize) { + struct Restore { + previous: Option>, + current: Rc, + } + impl Drop for Restore { + fn drop(&mut self) { + let consumed = self + .current + .initial + .saturating_sub(self.current.remaining.get()); + let previous = self.previous.take(); + ACTIVE.with(|active| active.replace(previous.clone())); + if let Some(parent) = previous { + let remaining = parent.remaining.get(); + if consumed > remaining { + parent.overflowed.set(true); + } else { + parent.remaining.set(remaining - consumed); + } + } + } + } + let parent = ACTIVE.with(|active| active.borrow().clone()); + let effective_limit = parent + .as_ref() + .map_or(limit, |budget| limit.min(budget.remaining.get())); + let budget = Rc::new(Budget { + initial: effective_limit, + remaining: Cell::new(effective_limit), + overflowed: Cell::new(false), + }); + let previous = ACTIVE.with(|active| active.replace(Some(Rc::clone(&budget)))); + let restore = Restore { + previous, + current: Rc::clone(&budget), + }; + let value = operation(); + let overflowed = budget.overflowed.get(); + let consumed = effective_limit.saturating_sub(budget.remaining.get()); + drop(restore); + (value, overflowed, consumed) + } + + pub(crate) fn reserve_active(length: usize) -> bool { + ACTIVE.with(|active| { + let active = active.borrow(); + let Some(budget) = active.as_ref() else { + return true; + }; + let remaining = budget.remaining.get(); + if length > remaining { + budget.overflowed.set(true); + return false; + } + budget.remaining.set(remaining - length); + true + }) + } + + pub(crate) fn remaining_active() -> Option { + ACTIVE.with(|active| { + active + .borrow() + .as_ref() + .map(|budget| budget.remaining.get()) + }) + } + + pub(crate) fn release_active(length: usize) { + ACTIVE.with(|active| { + if let Some(budget) = active.borrow().as_ref() { + budget + .remaining + .set(budget.remaining.get().saturating_add(length)); + } + }); + } + + #[allow(dead_code)] + pub(crate) struct CappedString(String); + + #[allow(dead_code)] + impl CappedString { + pub(crate) fn new() -> Self { + Self(String::new()) + } + + pub(crate) fn into_string(self) -> String { + self.0 + } + } + + #[allow(dead_code)] + impl fmt::Write for CappedString { + fn write_str(&mut self, value: &str) -> fmt::Result { + if reserve_active(value.len()) { + self.0.push_str(value); + } + Ok(()) + } + } +} + +#[allow( + dead_code, + reason = "private A+B has no externally callable surface before evidence-gated public phase C" +)] +mod implementation; diff --git a/crates/semaprax-native-rust-interop-builder/tests/opacity.rs b/crates/semaprax-native-rust-interop-builder/tests/opacity.rs new file mode 100644 index 0000000..accf031 --- /dev/null +++ b/crates/semaprax-native-rust-interop-builder/tests/opacity.rs @@ -0,0 +1,135 @@ +use std::fs::{self, File}; +use std::io::Read as _; +use std::ops::Deref; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use semaprax_native_rust_interop_platform::{ + hold_directory, recheck_directory, same_directory_path, HeldDirectory, +}; + +struct OwnedRoot { + path: PathBuf, + authority: Option, +} + +impl Deref for OwnedRoot { + type Target = Path; + + fn deref(&self) -> &Self::Target { + &self.path + } +} + +impl Drop for OwnedRoot { + fn drop(&mut self) { + let Some(authority) = self.authority.take() else { + return; + }; + let Ok(metadata) = fs::symlink_metadata(&self.path) else { + return; + }; + let identity_matches = recheck_directory(&authority).is_ok() + && same_directory_path(&authority, &self.path) == Ok(true); + drop(authority); + if metadata.is_dir() && !metadata.file_type().is_symlink() && identity_matches { + fs::remove_dir_all(&self.path).unwrap(); + } + } +} + +fn owned_root() -> OwnedRoot { + let parent = fs::canonicalize(std::env::temp_dir()).unwrap(); + for _ in 0..32 { + let mut random = [0_u8; 16]; + File::open(if cfg!(windows) { "NUL" } else { "/dev/urandom" }) + .and_then(|mut file| file.read_exact(&mut random)) + .unwrap_or_else(|_| { + random[..8].copy_from_slice( + &std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + .to_le_bytes()[..8], + ); + }); + use std::fmt::Write as _; + let nonce = random.iter().fold(String::new(), |mut nonce, byte| { + write!(nonce, "{byte:02x}").expect("write to string"); + nonce + }); + let path = parent.join(format!( + "semaprax-native-rust-interop-opacity-{}-{nonce}", + std::process::id() + )); + match fs::create_dir(&path) { + Ok(()) => { + let authority = hold_directory(&path).unwrap(); + return OwnedRoot { + path, + authority: Some(authority), + }; + } + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {} + Err(error) => panic!("create owned opacity root: {error}"), + } + } + panic!("could not create owned opacity root") +} + +#[test] +fn external_consumer_cannot_reach_private_preparation_build_or_facts() { + let root = owned_root(); + fs::create_dir(root.join("src")).unwrap(); + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + fs::write( + root.join("Cargo.toml"), + format!( + "[package]\nname='native-rust-interop-opacity-probe'\nversion='0.0.0'\nedition='2021'\n[dependencies]\nsemaprax-native-rust-interop={{path={manifest_dir:?}}}\n" + ), + ) + .unwrap(); + fs::write( + root.join("src/main.rs"), + r#"use semaprax_native_rust_interop::{build,prepare,Bundle,Prepared}; +fn main(){ + let _ = core::mem::size_of::(); + let _ = core::mem::size_of::(); + let _ = prepare; + let _ = build; +} +"#, + ) + .unwrap(); + let output = Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into())) + .env("CARGO_NET_OFFLINE", "true") + .current_dir(&root.path) + .args(["check", "--offline", "--quiet"]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + for name in ["build", "prepare", "Bundle", "Prepared"] { + assert!( + stderr.contains(name), + "compiler did not reject `{name}`: {stderr}" + ); + } + assert!(!root + .join("target/debug/native-rust-interop-opacity-probe") + .exists()); + + fs::write( + root.join("src/main.rs"), + "fn main(){let _=semaprax_native_rust_interop::implementation::prepare_native_rust_interop;}\n", + ) + .unwrap(); + let output = Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into())) + .env("CARGO_NET_OFFLINE", "true") + .current_dir(&root.path) + .args(["check", "--offline", "--quiet"]) + .output() + .unwrap(); + assert!(!output.status.success()); + assert!(String::from_utf8_lossy(&output.stderr).contains("implementation")); +} diff --git a/crates/semaprax-native-rust-interop-platform-sys/Cargo.toml b/crates/semaprax-native-rust-interop-platform-sys/Cargo.toml new file mode 100644 index 0000000..74e0a41 --- /dev/null +++ b/crates/semaprax-native-rust-interop-platform-sys/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "semaprax-native-rust-interop-platform-sys" +version = "0.1.0" +edition = "2021" +rust-version = "1.85" +publish = false +description = "Audited OS quarantine for private SEMAPRAX Native Rust Interop builds" +license = "Apache-2.0" + +[target.'cfg(unix)'.dependencies] +libc = "=0.2.189" +sha2 = "=0.10.9" + +[target.'cfg(windows)'.dependencies] +sha2 = "=0.10.9" +windows-sys = { version = "=0.61.2", features = [ + "Wdk_Foundation", + "Wdk_Storage_FileSystem", + "Win32_Foundation", + "Win32_Storage_FileSystem", + "Win32_System_IO", + "Win32_System_JobObjects", + "Win32_System_Pipes", + "Win32_System_Threading", + "Win32_Security", +] } + +[lints.rust] +unsafe_op_in_unsafe_fn = "deny" diff --git a/crates/semaprax-native-rust-interop-platform-sys/src/lib.rs b/crates/semaprax-native-rust-interop-platform-sys/src/lib.rs new file mode 100644 index 0000000..7e160c2 --- /dev/null +++ b/crates/semaprax-native-rust-interop-platform-sys/src/lib.rs @@ -0,0 +1,8442 @@ +//! Audited operating-system quarantine for Native Rust Interop bundle builds. +//! +//! This crate is unpublished. Its public surface exists only so the sibling +//! safe facade can own opaque held objects without exposing handles upstream. + +#![deny(unsafe_op_in_unsafe_fn)] + +#[cfg(unix)] +use std::ffi::CString; +use std::ffi::OsStr; +use std::fs::File; +use std::io::Write as _; +use std::path::Path; + +#[cfg(test)] +static TEST_PREPARED_FILE_SYSCALL_ENTRIES: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); + +fn enter_prepared_file_syscalls(resolved: Result<&T, Error>) -> Result<&T, Error> { + let resolved = resolved?; + #[cfg(test)] + TEST_PREPARED_FILE_SYSCALL_ENTRIES.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + Ok(resolved) +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Error { + Invalid, + Exists, + Changed, + Unsupported, + Spawn, + Exit, + OutputLimit, +} + +#[cfg(test)] +#[allow(clippy::enum_variant_names)] +#[repr(u8)] +#[derive(Clone, Copy)] +enum TestSettlementFailure { + #[cfg(unix)] + UnixWait, + #[cfg(unix)] + UnixGroup, + #[cfg(unix)] + UnixSettleClose, + #[cfg(unix)] + UnixSuccessReadClose, + #[cfg(unix)] + UnixParentWriteClose, + #[cfg(unix)] + UnixParentNullClose, + #[cfg(unix)] + UnixPipeReadFcntl, + #[cfg(unix)] + UnixPipeWriteFcntl, + #[cfg(unix)] + UnixDrainFcntl, + #[cfg(unix)] + UnixPoll, + #[cfg(unix)] + UnixRead, + #[cfg(unix)] + UnixReadConversion, + #[cfg(unix)] + UnixWaitpid, + #[cfg(unix)] + UnixDeadline, + #[cfg(target_os = "macos")] + DarwinActionsDestroy, + #[cfg(target_os = "macos")] + DarwinAttributesDestroy, + #[cfg(target_os = "macos")] + DarwinAttest, + #[cfg(target_os = "macos")] + DarwinSigcont, + #[cfg(target_os = "windows")] + WindowsImage, + #[cfg(target_os = "windows")] + WindowsAssign, + #[cfg(target_os = "windows")] + WindowsResume, + #[cfg(target_os = "windows")] + WindowsPeek, + #[cfg(target_os = "windows")] + WindowsRead, + #[cfg(target_os = "windows")] + WindowsUnassigned, + #[cfg(target_os = "windows")] + WindowsTerminateProcess, + #[cfg(target_os = "windows")] + WindowsWaitUnassigned, + #[cfg(target_os = "windows")] + WindowsJob, + #[cfg(target_os = "windows")] + WindowsTerminateJob, + #[cfg(target_os = "windows")] + WindowsQueryJob, +} + +#[cfg(test)] +static TEST_SETTLEMENT_FAILURES: std::sync::atomic::AtomicU64 = + std::sync::atomic::AtomicU64::new(0); + +#[cfg(test)] +#[allow(dead_code)] +fn set_test_settlement_failures(points: &[TestSettlementFailure]) { + let mut mask = 0_u64; + for point in points { + mask |= 1_u64 << (*point as u8); + } + TEST_SETTLEMENT_FAILURES.store(mask, std::sync::atomic::Ordering::SeqCst); +} + +#[cfg(test)] +fn test_settlement_failure(point: TestSettlementFailure) -> bool { + TEST_SETTLEMENT_FAILURES.load(std::sync::atomic::Ordering::SeqCst) & (1_u64 << (point as u8)) + != 0 +} + +macro_rules! injected_settlement_failure { + ($point:ident) => {{ + #[cfg(test)] + { + test_settlement_failure(TestSettlementFailure::$point) + } + #[cfg(not(test))] + { + false + } + }}; +} + +#[cfg(all(test, unix))] +fn trace_error(context: &str, error: Error) -> Error { + eprintln!("platform {context}: {error:?}"); + error +} + +#[cfg(all(not(test), unix))] +fn trace_error(_: &str, error: Error) -> Error { + error +} + +#[cfg(unix)] +mod platform { + use super::*; + use sha2::{Digest as _, Sha256}; + use std::os::fd::{AsRawFd as _, FromRawFd as _, RawFd}; + use std::os::unix::ffi::OsStrExt as _; + use std::os::unix::fs::{FileExt as _, MetadataExt as _}; + + struct CheckedFd(Option); + + impl CheckedFd { + fn new(fd: RawFd) -> Self { + Self(Some(fd)) + } + + fn raw(&self) -> RawFd { + self.0.expect("checked descriptor remains owned") + } + + fn close(mut self) -> Result<(), Error> { + let descriptor = self.0.take().expect("checked descriptor remains owned"); + if unsafe { libc::close(descriptor) } == 0 { + Ok(()) + } else { + Err(Error::Spawn) + } + } + + fn close_injected(self, point: TestClosePoint) -> Result<(), Error> { + let result = self.close(); + if point.injected() { + Err(Error::Spawn) + } else { + result + } + } + } + + impl Drop for CheckedFd { + fn drop(&mut self) { + if let Some(descriptor) = self.0.take() { + if unsafe { libc::close(descriptor) } != 0 { + std::process::abort(); + } + } + } + } + + #[derive(Clone, Copy)] + enum TestClosePoint { + Settle, + SuccessRead, + ParentWrite, + ParentNull, + } + + impl TestClosePoint { + fn injected(self) -> bool { + match self { + Self::Settle => injected_settlement_failure!(UnixSettleClose), + Self::SuccessRead => injected_settlement_failure!(UnixSuccessReadClose), + Self::ParentWrite => injected_settlement_failure!(UnixParentWriteClose), + Self::ParentNull => injected_settlement_failure!(UnixParentNullClose), + } + } + } + + pub struct Directory { + file: File, + dev: u64, + ino: u64, + mode: u32, + #[cfg(target_os = "macos")] + generation: u32, + } + + pub struct RegularFile { + file: File, + dev: u64, + ino: u64, + mode: u32, + len: u64, + digest: [u8; 32], + #[cfg(target_os = "macos")] + generation: u32, + } + + pub struct Executable { + file: RegularFile, + slice_offset: u64, + slice_size: u64, + } + + pub struct RustcDiscovery(Executable); + + pub struct DirectRustc { + executable: Executable, + sysroot: Directory, + } + + pub struct PreparedRelativeName(CString); + + pub struct PreparedRelativeNameArena { + bytes: Vec, + maximum: usize, + } + + pub struct PreparedVersionInvocation { + argument: CString, + output: Vec, + } + + pub struct PreparedSysrootInvocation(PreparedVersionInvocation); + pub struct PreparedRustcVersionInvocation(PreparedVersionInvocation); + + pub struct PreparedProcessArena { + remaining: usize, + } + + pub struct PreparedProcessArenaPlan { + uses: usize, + } + + impl Drop for PreparedProcessArena { + fn drop(&mut self) {} + } + + pub struct PreparedToolResolver { + candidate: Vec, + canonical: Vec, + display: String, + fallback: CString, + maximum: usize, + } + + struct PreparedCommand { + arguments: Vec, + output: Vec, + } + + pub struct PreparedCCompileInvocation(PreparedCommand); + pub struct PreparedRustCompileInvocation { + command: PreparedCommand, + output_name: PreparedRelativeName, + } + pub struct PreparedLinkInvocation { + command: PreparedCommand, + output_name: PreparedRelativeName, + } + pub struct PreparedRunInvocation(PreparedCommand); + + #[cfg(target_os = "linux")] + const LINUX_LINKER_ARGUMENT: &str = "-fuse-ld=/usr/bin/ld"; + + #[cfg(target_os = "linux")] + const LINUX_RUST_STATICLIB_NATIVE_LIBS: [&str; 7] = [ + "-lgcc_s", + "-lutil", + "-lrt", + "-lpthread", + "-lm", + "-ldl", + "-lc", + ]; + + fn prepare_command(values: &[&str], output_capacity: usize) -> Result { + let mut arguments = Vec::with_capacity(values.len()); + if arguments.capacity() != values.len() { + return Err(Error::OutputLimit); + } + for value in values { + arguments.push(argument(value)?); + } + let output = Vec::with_capacity(output_capacity); + if output.capacity() != output_capacity { + return Err(Error::OutputLimit); + } + Ok(PreparedCommand { arguments, output }) + } + + fn prepared_command_owned_capacity(command: &PreparedCommand) -> usize { + command + .arguments + .capacity() + .saturating_mul(std::mem::size_of::()) + .saturating_add( + command + .arguments + .iter() + .map(|value| value.as_bytes_with_nul().len()) + .sum::(), + ) + .saturating_add(command.output.capacity()) + } + + pub fn prepare_tool_resolver( + fallback: &str, + maximum: usize, + ) -> Result { + if maximum == 0 + || maximum > 32_768 + || fallback.is_empty() + || fallback.as_bytes().contains(&b'/') + { + return Err(Error::Invalid); + } + let candidate = Vec::with_capacity(maximum); + let canonical = Vec::with_capacity(maximum); + let display = String::with_capacity(maximum); + if candidate.capacity() != maximum + || canonical.capacity() != maximum + || display.capacity() != maximum + { + return Err(Error::OutputLimit); + } + Ok(PreparedToolResolver { + candidate, + canonical, + display, + fallback: CString::new(fallback).map_err(|_| Error::Invalid)?, + maximum, + }) + } + + pub fn prepared_tool_resolver_owned_capacity(prepared: &PreparedToolResolver) -> usize { + prepared + .candidate + .capacity() + .saturating_add(prepared.canonical.capacity()) + .saturating_add(prepared.display.capacity()) + .saturating_add(prepared.fallback.as_bytes_with_nul().len()) + } + + pub fn prepare_version_invocation( + argument: &str, + maximum: usize, + ) -> Result { + if maximum > 65_536 { + return Err(Error::OutputLimit); + } + let argument = CString::new(argument).map_err(|_| Error::Invalid)?; + let output = Vec::with_capacity(maximum); + if output.capacity() != maximum { + return Err(Error::OutputLimit); + } + Ok(PreparedVersionInvocation { argument, output }) + } + + pub fn prepare_sysroot_invocation(maximum: usize) -> Result { + prepare_version_invocation("--print=sysroot", maximum).map(PreparedSysrootInvocation) + } + + pub fn prepare_rustc_version_invocation( + maximum: usize, + ) -> Result { + prepare_version_invocation("-vV", maximum).map(PreparedRustcVersionInvocation) + } + + pub fn prepared_sysroot_owned_capacity(prepared: &PreparedSysrootInvocation) -> usize { + prepared_version_owned_capacity(&prepared.0) + } + + pub fn prepared_rustc_version_owned_capacity( + prepared: &PreparedRustcVersionInvocation, + ) -> usize { + prepared_version_owned_capacity(&prepared.0) + } + + pub fn prepared_version_owned_capacity(prepared: &PreparedVersionInvocation) -> usize { + prepared + .argument + .as_bytes_with_nul() + .len() + .saturating_add(prepared.output.capacity()) + } + + pub fn prepare_process_arena_plan(uses: usize) -> Result { + if uses == 0 || uses > 32 { + return Err(Error::Invalid); + } + Ok(PreparedProcessArenaPlan { uses }) + } + + pub fn prepare_process_arena_plan_with_environment( + uses: usize, + include: Option<&OsStr>, + libraries: Option<&OsStr>, + ) -> Result { + if include.is_some() || libraries.is_some() { + return Err(Error::Invalid); + } + prepare_process_arena_plan(uses) + } + + pub fn prepared_process_arena_plan_capacity(_: &PreparedProcessArenaPlan) -> usize { + 0 + } + + pub fn materialize_process_arena( + plan: PreparedProcessArenaPlan, + ) -> Result { + Ok(PreparedProcessArena { + remaining: plan.uses, + }) + } + + pub fn materialize_process_arena_with_environment( + plan: PreparedProcessArenaPlan, + include: Option<&OsStr>, + libraries: Option<&OsStr>, + ) -> Result { + if include.is_some() || libraries.is_some() { + return Err(Error::Invalid); + } + materialize_process_arena(plan) + } + + pub fn prepare_process_arena(uses: usize) -> Result { + materialize_process_arena(prepare_process_arena_plan(uses)?) + } + + pub fn prepared_process_arena_owned_capacity(_: &PreparedProcessArena) -> usize { + 0 + } + + pub fn prepared_process_arena_remaining(prepared: &PreparedProcessArena) -> usize { + prepared.remaining + } + + pub(super) fn consume_process_arena(prepared: &mut PreparedProcessArena) -> Result<(), Error> { + prepared.remaining = prepared + .remaining + .checked_sub(1) + .ok_or(Error::OutputLimit)?; + Ok(()) + } + + pub struct PreparedDiscardNames { + names: [Option; N], + } + + pub struct PreparedLinkOrCopy { + destination_index: usize, + destination: PreparedRelativeName, + #[cfg(debug_assertions)] + fail_before_authentication: bool, + } + + #[derive(Clone, Copy, Eq, PartialEq)] + struct PreparedDirectoryIdentity { + dev: u64, + ino: u64, + mode: u32, + #[cfg(target_os = "macos")] + generation: u32, + } + + pub struct PreparedInventoryExact { + names: [Option; N], + bindings: [(usize, usize); N], + storage: Box<[u64]>, + directory_identity: Option, + remaining: u8, + #[cfg(test)] + scan_entries: usize, + #[cfg(test)] + fail_initial_seek: bool, + #[cfg(test)] + fail_reset_seek: bool, + #[cfg(test)] + fail_rebound_authentication: bool, + #[cfg(test)] + fail_rebound_close: bool, + } + + pub struct PreparedPublishDirectory { + destination: CString, + exact_capacity: usize, + remaining: u8, + #[cfg(debug_assertions)] + fail_before_open: bool, + #[cfg(debug_assertions)] + fail_information: bool, + #[cfg(debug_assertions)] + fail_close: bool, + #[cfg(debug_assertions)] + fail_rename: bool, + } + + #[cfg(target_os = "linux")] + const INVENTORY_EXACT_ARENA_WORDS: usize = 8192; + #[cfg(target_os = "macos")] + const INVENTORY_EXACT_ARENA_WORDS: usize = 131_072; + + fn prepared_name_bindings( + names: &PreparedDiscardNames, + ) -> Result<[(usize, usize); N], Error> { + let mut bindings = [(0, 0); N]; + for (index, binding) in bindings.iter_mut().enumerate() { + let name = prepared_discard_name(names, index)?; + *binding = (name.0.as_ptr() as usize, name.0.as_bytes_with_nul().len()); + } + Ok(bindings) + } + + pub fn inventory_exact_required_capacity( + names: &PreparedDiscardNames, + ) -> Result { + let mut total = 0usize; + for index in 0..N { + total = total + .checked_add( + prepared_discard_name(names, index)? + .0 + .as_bytes_with_nul() + .len(), + ) + .ok_or(Error::OutputLimit)?; + } + total + .checked_add( + INVENTORY_EXACT_ARENA_WORDS + .checked_mul(std::mem::size_of::()) + .ok_or(Error::OutputLimit)?, + ) + .ok_or(Error::OutputLimit) + } + + pub fn prepare_inventory_exact( + names: &PreparedDiscardNames, + ) -> Result, Error> { + let bindings = prepared_name_bindings(names)?; + let mut copied = [const { None }; N]; + for (index, slot) in copied.iter_mut().enumerate() { + let source = prepared_discard_name(names, index)?; + let exact = source.0.as_bytes_with_nul().len(); + let mut bytes = Vec::with_capacity(exact); + bytes.extend_from_slice(source.0.as_bytes_with_nul()); + if bytes.capacity() != exact { + return Err(Error::OutputLimit); + } + *slot = Some( + CString::from_vec_with_nul(bytes) + .map(PreparedRelativeName) + .map_err(|_| Error::Invalid)?, + ); + } + Ok(PreparedInventoryExact { + names: copied, + bindings, + storage: vec![0_u64; INVENTORY_EXACT_ARENA_WORDS].into_boxed_slice(), + directory_identity: None, + remaining: 2, + #[cfg(test)] + scan_entries: 0, + #[cfg(test)] + fail_initial_seek: false, + #[cfg(test)] + fail_reset_seek: false, + #[cfg(test)] + fail_rebound_authentication: false, + #[cfg(test)] + fail_rebound_close: false, + }) + } + + pub fn prepared_inventory_exact_owned_capacity( + prepared: &PreparedInventoryExact, + ) -> usize { + prepared + .names + .iter() + .filter_map(Option::as_ref) + .map(|name| name.0.as_bytes_with_nul().len()) + .sum::() + .saturating_add( + prepared + .storage + .len() + .saturating_mul(std::mem::size_of::()), + ) + } + + pub fn prepared_inventory_exact_remaining( + prepared: &PreparedInventoryExact, + ) -> u8 { + prepared.remaining + } + + pub fn publish_directory_required_capacity(name: &OsStr) -> Result { + validated_c_name_bytes(name)? + .len() + .checked_add(1) + .ok_or(Error::OutputLimit) + } + + pub fn prepare_publish_directory(name: &OsStr) -> Result { + let bytes = validated_c_name_bytes(name)?; + let exact_capacity = bytes.len().checked_add(1).ok_or(Error::OutputLimit)?; + let mut copied = Vec::with_capacity(exact_capacity); + copied.extend_from_slice(bytes); + copied.push(0); + if copied.capacity() != exact_capacity { + return Err(Error::OutputLimit); + } + let destination = CString::from_vec_with_nul(copied).map_err(|_| Error::Invalid)?; + Ok(PreparedPublishDirectory { + destination, + exact_capacity, + remaining: 1, + #[cfg(debug_assertions)] + fail_before_open: false, + #[cfg(debug_assertions)] + fail_information: false, + #[cfg(debug_assertions)] + fail_close: false, + #[cfg(debug_assertions)] + fail_rename: false, + }) + } + + pub fn prepared_publish_directory_owned_capacity(prepared: &PreparedPublishDirectory) -> usize { + prepared.destination.as_bytes_with_nul().len() + } + + pub fn prepared_publish_directory_remaining(prepared: &PreparedPublishDirectory) -> u8 { + prepared.remaining + } + + #[cfg(debug_assertions)] + pub fn inject_publish_directory_failure( + prepared: &mut PreparedPublishDirectory, + point: u8, + ) -> Result<(), Error> { + match point { + 1 => prepared.fail_before_open = true, + 2 => prepared.fail_information = true, + 3 => prepared.fail_close = true, + 4 => prepared.fail_rename = true, + _ => return Err(Error::Invalid), + } + Ok(()) + } + + #[cfg(test)] + pub(crate) fn test_inventory_exact_failures( + prepared: &mut PreparedInventoryExact, + initial_seek: bool, + reset_seek: bool, + rebound_authentication: bool, + rebound_close: bool, + ) { + prepared.fail_initial_seek = initial_seek; + prepared.fail_reset_seek = reset_seek; + prepared.fail_rebound_authentication = rebound_authentication; + prepared.fail_rebound_close = rebound_close; + } + + #[cfg(test)] + pub(crate) fn test_inventory_exact_scan_entries( + prepared: &PreparedInventoryExact, + ) -> usize { + prepared.scan_entries + } + + pub fn prepare_link_or_copy( + names: &PreparedDiscardNames, + destination_index: usize, + ) -> Result { + let destination = prepared_discard_name(names, destination_index)?; + let exact = destination.0.as_bytes_with_nul().len(); + let mut bytes = Vec::with_capacity(exact); + bytes.extend_from_slice(destination.0.as_bytes_with_nul()); + if bytes.capacity() != exact { + return Err(Error::OutputLimit); + } + let destination = CString::from_vec_with_nul(bytes) + .map(PreparedRelativeName) + .map_err(|_| Error::Invalid)?; + Ok(PreparedLinkOrCopy { + destination_index, + destination, + #[cfg(debug_assertions)] + fail_before_authentication: false, + }) + } + + pub fn link_or_copy_required_capacity( + names: &PreparedDiscardNames, + destination_index: usize, + ) -> Result { + Ok(prepared_discard_name(names, destination_index)? + .0 + .as_bytes_with_nul() + .len()) + } + + pub fn prepared_link_or_copy_owned_capacity(prepared: &PreparedLinkOrCopy) -> usize { + prepared.destination.0.as_bytes_with_nul().len() + } + + #[cfg(debug_assertions)] + pub fn inject_link_or_copy_failure_before_authentication(prepared: &mut PreparedLinkOrCopy) { + prepared.fail_before_authentication = true; + } + + pub fn prepare_relative_name(name: &OsStr) -> Result { + let bytes = name.as_bytes(); + if bytes.is_empty() + || bytes == b"." + || bytes == b".." + || bytes.contains(&b'/') + || bytes.contains(&0) + { + return Err(Error::Invalid); + } + let exact = bytes.len().checked_add(1).ok_or(Error::OutputLimit)?; + let mut owned = Vec::with_capacity(exact); + owned.extend_from_slice(bytes); + owned.push(0); + if owned.capacity() != exact { + return Err(Error::OutputLimit); + } + CString::from_vec_with_nul(owned) + .map(PreparedRelativeName) + .map_err(|_| Error::Invalid) + } + + pub fn prepare_relative_name_arena(maximum: usize) -> Result { + let capacity = maximum.checked_add(1).ok_or(Error::OutputLimit)?; + let bytes = Vec::with_capacity(capacity); + if bytes.capacity() != capacity { + return Err(Error::OutputLimit); + } + Ok(PreparedRelativeNameArena { bytes, maximum }) + } + + pub fn set_relative_name_arena( + arena: &mut PreparedRelativeNameArena, + name: &OsStr, + ) -> Result<(), Error> { + let bytes = name.as_bytes(); + if bytes.is_empty() + || bytes.len() > arena.maximum + || bytes == b"." + || bytes == b".." + || bytes.contains(&b'/') + || bytes.contains(&0) + { + return Err(Error::Invalid); + } + let capacity = arena.maximum.checked_add(1).ok_or(Error::OutputLimit)?; + arena.bytes.clear(); + arena.bytes.extend_from_slice(bytes); + arena.bytes.push(0); + if arena.bytes.capacity() != capacity { + return Err(Error::OutputLimit); + } + Ok(()) + } + + pub fn relative_name_arena_capacity(arena: &PreparedRelativeNameArena) -> usize { + arena.bytes.capacity() + } + + fn relative_name_arena_cstr( + arena: &PreparedRelativeNameArena, + ) -> Result<&std::ffi::CStr, Error> { + std::ffi::CStr::from_bytes_with_nul(&arena.bytes).map_err(|_| Error::Invalid) + } + + pub fn prepare_discard_names( + names: [&OsStr; N], + ) -> Result, Error> { + let names = names.map(|name| prepare_relative_name(name).ok()); + if names.iter().any(Option::is_none) { + return Err(Error::Invalid); + } + for left in 0..N { + for right in 0..left { + if names[left].as_ref().expect("validated").0.as_bytes() + == names[right].as_ref().expect("validated").0.as_bytes() + { + return Err(Error::Invalid); + } + } + } + Ok(PreparedDiscardNames { names }) + } + + pub fn prepared_discard_names_owned_capacity( + prepared: &PreparedDiscardNames, + ) -> usize { + prepared + .names + .iter() + .filter_map(Option::as_ref) + .map(|name| name.0.as_bytes_with_nul().len()) + .sum() + } + + fn prepared_discard_name( + prepared: &PreparedDiscardNames, + index: usize, + ) -> Result<&PreparedRelativeName, Error> { + prepared + .names + .get(index) + .and_then(Option::as_ref) + .ok_or(Error::Invalid) + } + + #[cfg(target_os = "macos")] + fn metadata_generation(metadata: &std::fs::Metadata) -> u32 { + use std::os::macos::fs::MetadataExt as _; + metadata.st_gen() + } + + fn digest_file(file: &File, length: u64) -> Result<[u8; 32], Error> { + let mut hasher = Sha256::new(); + let mut offset = 0_u64; + let mut buffer = [0_u8; 8192]; + while offset < length { + let remaining = usize::try_from((length - offset).min(buffer.len() as u64)) + .map_err(|_| Error::OutputLimit)?; + let count = file + .read_at(&mut buffer[..remaining], offset) + .map_err(|_| Error::Changed)?; + if count == 0 { + return Err(Error::Changed); + } + hasher.update(&buffer[..count]); + offset = offset + .checked_add(u64::try_from(count).map_err(|_| Error::OutputLimit)?) + .ok_or(Error::OutputLimit)?; + } + Ok(hasher.finalize().into()) + } + + fn digest_bytes(bytes: &[u8]) -> [u8; 32] { + Sha256::digest(bytes).into() + } + + #[cfg(target_os = "macos")] + fn executable_slice(file: &RegularFile) -> Result<(u64, u64), Error> { + let mut prefix = [0_u8; 32]; + if file + .file + .read_at(&mut prefix, 0) + .map_err(|_| Error::Changed)? + != prefix.len() + { + return Err(Error::Invalid); + } + let current_cpu = if cfg!(target_arch = "aarch64") { + 0x0100_000c_u32 + } else if cfg!(target_arch = "x86_64") { + 0x0100_0007_u32 + } else { + return Err(Error::Unsupported); + }; + let little = u32::from_le_bytes(prefix[0..4].try_into().map_err(|_| Error::Invalid)?); + if little == 0xfeed_facf { + let cpu = u32::from_le_bytes(prefix[4..8].try_into().map_err(|_| Error::Invalid)?); + let subtype = u32::from_le_bytes(prefix[8..12].try_into().map_err(|_| Error::Invalid)?) + & 0x00ff_ffff; + let compatible_subtype = if cfg!(target_arch = "aarch64") { + matches!(subtype, 0 | 2) + } else { + subtype == 3 + }; + let filetype = + u32::from_le_bytes(prefix[12..16].try_into().map_err(|_| Error::Invalid)?); + if cpu != current_cpu || !compatible_subtype || filetype != 2 { + return Err(Error::Invalid); + } + return Ok((0, file.len)); + } + let magic = u32::from_be_bytes(prefix[0..4].try_into().map_err(|_| Error::Invalid)?); + let entry_size = match magic { + 0xcafe_babe => 20_usize, + 0xcafe_babf => 32_usize, + _ => return Err(Error::Invalid), + }; + let count = usize::try_from(u32::from_be_bytes( + prefix[4..8].try_into().map_err(|_| Error::Invalid)?, + )) + .map_err(|_| Error::Invalid)?; + if count == 0 || count > 64 { + return Err(Error::Invalid); + } + let table_size = count.checked_mul(entry_size).ok_or(Error::Invalid)?; + let table_end = 8_usize.checked_add(table_size).ok_or(Error::Invalid)?; + if u64::try_from(table_end).map_err(|_| Error::Invalid)? > file.len { + return Err(Error::Invalid); + } + let mut table = [0_u8; 64 * 32]; + if file + .file + .read_at(&mut table[..table_size], 8) + .map_err(|_| Error::Changed)? + != table_size + { + return Err(Error::Changed); + } + let mut rows = [(0_u32, 0_u32, 0_u64, 0_u64); 64]; + let mut row_count = 0usize; + for index in 0..count { + let start = index.checked_mul(entry_size).ok_or(Error::Invalid)?; + let row = table.get(start..start + entry_size).ok_or(Error::Invalid)?; + let cpu = u32::from_be_bytes(row[0..4].try_into().map_err(|_| Error::Invalid)?); + let subtype = u32::from_be_bytes(row[4..8].try_into().map_err(|_| Error::Invalid)?); + let (offset, size, alignment, reserved) = if entry_size == 20 { + ( + u64::from(u32::from_be_bytes( + row[8..12].try_into().map_err(|_| Error::Invalid)?, + )), + u64::from(u32::from_be_bytes( + row[12..16].try_into().map_err(|_| Error::Invalid)?, + )), + u32::from_be_bytes(row[16..20].try_into().map_err(|_| Error::Invalid)?), + 0, + ) + } else { + ( + u64::from_be_bytes(row[8..16].try_into().map_err(|_| Error::Invalid)?), + u64::from_be_bytes(row[16..24].try_into().map_err(|_| Error::Invalid)?), + u32::from_be_bytes(row[24..28].try_into().map_err(|_| Error::Invalid)?), + u32::from_be_bytes(row[28..32].try_into().map_err(|_| Error::Invalid)?), + ) + }; + let end = offset.checked_add(size).ok_or(Error::Invalid)?; + if size < 32 + || end > file.len + || offset < u64::try_from(table_end).map_err(|_| Error::Invalid)? + || alignment > 63 + || offset % (1_u64 << alignment) != 0 + || reserved != 0 + || rows[..row_count] + .iter() + .any(|(_, _, prior_offset, prior_end)| { + offset < *prior_end && *prior_offset < end + }) + || rows[..row_count] + .iter() + .any(|(prior_cpu, prior_subtype, _, _)| { + *prior_cpu == cpu && *prior_subtype == subtype + }) + { + return Err(Error::Invalid); + } + rows[row_count] = (cpu, subtype, offset, end); + row_count += 1; + } + let mut selected = None; + for (cpu, subtype, offset, end) in &rows[..row_count] { + let masked_subtype = *subtype & 0x00ff_ffff; + let matches_current = *cpu == current_cpu + && if cfg!(target_arch = "aarch64") { + matches!(masked_subtype, 0 | 2) + } else { + masked_subtype == 3 + }; + if matches_current + && selected + .replace((masked_subtype, *offset, *end - *offset)) + .is_some() + { + return Err(Error::Invalid); + } + } + let Some((selected_subtype, offset, size)) = selected else { + return Err(Error::Invalid); + }; + let mut header = [0_u8; 16]; + if file + .file + .read_at(&mut header, offset) + .map_err(|_| Error::Changed)? + != header.len() + { + return Err(Error::Changed); + } + if u32::from_le_bytes(header[0..4].try_into().map_err(|_| Error::Invalid)?) != 0xfeed_facf + || u32::from_le_bytes(header[4..8].try_into().map_err(|_| Error::Invalid)?) + != current_cpu + || u32::from_le_bytes(header[12..16].try_into().map_err(|_| Error::Invalid)?) != 2 + || (u32::from_le_bytes(header[8..12].try_into().map_err(|_| Error::Invalid)?) + & 0x00ff_ffff) + != selected_subtype + { + return Err(Error::Invalid); + } + Ok((offset, size)) + } + + #[cfg(target_os = "linux")] + fn executable_slice(file: &RegularFile) -> Result<(u64, u64), Error> { + let mut header = [0_u8; 64]; + if file + .file + .read_at(&mut header, 0) + .map_err(|_| Error::Changed)? + != header.len() + || &header[..4] != b"\x7fELF" + || header[4] != 2 + || header[5] != 1 + || header[6] != 1 + || !matches!(u16::from_le_bytes([header[16], header[17]]), 2 | 3) + || u32::from_le_bytes(header[20..24].try_into().map_err(|_| Error::Invalid)?) != 1 + || u16::from_le_bytes([header[52], header[53]]) != 64 + { + return Err(Error::Invalid); + } + let machine = u16::from_le_bytes([header[18], header[19]]); + if (cfg!(target_arch = "x86_64") && machine != 62) + || (cfg!(target_arch = "aarch64") && machine != 183) + { + return Err(Error::Invalid); + } + let program_offset = + u64::from_le_bytes(header[32..40].try_into().map_err(|_| Error::Invalid)?); + let entry_size = usize::from(u16::from_le_bytes([header[54], header[55]])); + let entry_count = usize::from(u16::from_le_bytes([header[56], header[57]])); + if entry_size != 56 || entry_count == 0 || entry_count > 4096 { + return Err(Error::Invalid); + } + let table_size = entry_size.checked_mul(entry_count).ok_or(Error::Invalid)?; + let table_end = program_offset + .checked_add(u64::try_from(table_size).map_err(|_| Error::Invalid)?) + .ok_or(Error::Invalid)?; + if table_end > file.len { + return Err(Error::Invalid); + } + let mut table = [0_u8; 56 * 4096]; + if file + .file + .read_at(&mut table[..table_size], program_offset) + .map_err(|_| Error::Changed)? + != table_size + { + return Err(Error::Changed); + } + let entry = u64::from_le_bytes(header[24..32].try_into().map_err(|_| Error::Invalid)?); + let mut executable_load = false; + for row in table[..table_size].chunks_exact(entry_size) { + let kind = u32::from_le_bytes(row[0..4].try_into().map_err(|_| Error::Invalid)?); + let flags = u32::from_le_bytes(row[4..8].try_into().map_err(|_| Error::Invalid)?); + let offset = u64::from_le_bytes(row[8..16].try_into().map_err(|_| Error::Invalid)?); + let virtual_address = + u64::from_le_bytes(row[16..24].try_into().map_err(|_| Error::Invalid)?); + let file_size = u64::from_le_bytes(row[32..40].try_into().map_err(|_| Error::Invalid)?); + let memory_size = + u64::from_le_bytes(row[40..48].try_into().map_err(|_| Error::Invalid)?); + let alignment = u64::from_le_bytes(row[48..56].try_into().map_err(|_| Error::Invalid)?); + if offset + .checked_add(file_size) + .is_none_or(|end| end > file.len) + || virtual_address.checked_add(memory_size).is_none() + || file_size > memory_size + || (alignment > 1 + && (!alignment.is_power_of_two() + || offset % alignment != virtual_address % alignment)) + { + return Err(Error::Invalid); + } + if kind == 1 + && flags & 1 != 0 + && entry >= virtual_address + && entry + < virtual_address + .checked_add(memory_size) + .ok_or(Error::Invalid)? + { + executable_load = true; + } + } + if !executable_load { + return Err(Error::Invalid); + } + Ok((0, file.len)) + } + + fn validated_c_name_bytes(name: &OsStr) -> Result<&[u8], Error> { + let bytes = name.as_bytes(); + if bytes.is_empty() + || bytes == b"." + || bytes == b".." + || bytes.contains(&b'/') + || bytes.contains(&0) + { + return Err(Error::Invalid); + } + Ok(bytes) + } + + fn c_name(name: &OsStr) -> Result { + CString::new(validated_c_name_bytes(name)?).map_err(|_| Error::Invalid) + } + + fn wait_child(pid: libc::pid_t, kill_first: bool) -> Result { + if kill_first { + let _ = unsafe { libc::kill(-pid, libc::SIGKILL) }; + let _ = unsafe { libc::kill(pid, libc::SIGKILL) }; + } + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30); + loop { + let mut status = 0; + let waited = unsafe { libc::waitpid(pid, &mut status, libc::WNOHANG) }; + if waited == pid { + return Ok(status); + } + if waited == 0 { + if std::time::Instant::now() >= deadline { + let _ = unsafe { libc::kill(pid, libc::SIGKILL) }; + return Err(Error::Spawn); + } + std::thread::sleep(std::time::Duration::from_millis(1)); + continue; + } + match std::io::Error::last_os_error().raw_os_error() { + Some(libc::EINTR) => continue, + // ECHILD is accepted only after the kernel proves that this exact + // pid is no longer a waitable child. No retry or stronger signal + // authority is used. + Some(libc::ECHILD) => return Err(Error::Spawn), + _ => return Err(Error::Spawn), + } + } + } + + fn quiesce_group(pid: libc::pid_t) -> Result<(), Error> { + let _ = unsafe { libc::kill(-pid, libc::SIGKILL) }; + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30); + loop { + let result = unsafe { libc::kill(-pid, 0) }; + if result != 0 { + match std::io::Error::last_os_error().raw_os_error() { + Some(libc::ESRCH) => return Ok(()), + Some(libc::EINTR) => continue, + _ => return Err(Error::Spawn), + } + } + if std::time::Instant::now() >= deadline { + return Err(Error::Spawn); + } + std::thread::sleep(std::time::Duration::from_millis(1)); + } + } + + fn settle_failed_group( + pid: libc::pid_t, + pipe: CheckedFd, + leader_reaped: bool, + ) -> Result<(), Error> { + let close_failed = pipe.close_injected(TestClosePoint::Settle).is_err(); + let mut leader = if leader_reaped { + let _ = unsafe { libc::kill(-pid, libc::SIGKILL) }; + Ok(()) + } else { + wait_child(pid, true).map(|_| ()) + }; + let mut group = quiesce_group(pid); + if injected_settlement_failure!(UnixWait) { + leader = Err(Error::Spawn); + } + if injected_settlement_failure!(UnixGroup) { + group = Err(Error::Spawn); + } + if close_failed || leader.is_err() || group.is_err() { + Err(Error::Spawn) + } else { + Ok(()) + } + } + + fn must_settle_failed_group(pid: libc::pid_t, pipe: CheckedFd, leader_reaped: bool) { + if settle_failed_group(pid, pipe, leader_reaped).is_err() { + std::process::abort(); + } + } + + fn drain_and_wait( + pid: libc::pid_t, + pipe: CheckedFd, + stdout_limit: usize, + mut output: Vec, + ) -> Result<(Vec, libc::c_int), Error> { + if injected_settlement_failure!(UnixDrainFcntl) + || unsafe { libc::fcntl(pipe.raw(), libc::F_SETFL, libc::O_NONBLOCK) } != 0 + { + must_settle_failed_group(pid, pipe, false); + return Err(Error::Spawn); + } + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30); + let fixed_output = output.capacity() != 0 || stdout_limit == 0; + if (fixed_output && output.capacity() != stdout_limit) || !output.is_empty() { + must_settle_failed_group(pid, pipe, false); + return Err(Error::OutputLimit); + } + let mut status = None; + let mut eof = false; + loop { + let mut poll_fd = libc::pollfd { + fd: pipe.raw(), + events: libc::POLLIN | libc::POLLHUP | libc::POLLERR, + revents: 0, + }; + let polled = unsafe { libc::poll(&mut poll_fd, 1, 25) }; + if injected_settlement_failure!(UnixPoll) + || (polled < 0 + && std::io::Error::last_os_error().raw_os_error() != Some(libc::EINTR)) + { + must_settle_failed_group(pid, pipe, status.is_some()); + return Err(Error::Spawn); + } + if polled > 0 { + loop { + let mut buffer = [0_u8; 8192]; + if injected_settlement_failure!(UnixRead) { + must_settle_failed_group(pid, pipe, status.is_some()); + return Err(Error::Spawn); + } + let read = + unsafe { libc::read(pipe.raw(), buffer.as_mut_ptr().cast(), buffer.len()) }; + match read.cmp(&0) { + std::cmp::Ordering::Greater => { + if injected_settlement_failure!(UnixReadConversion) { + must_settle_failed_group(pid, pipe, status.is_some()); + return Err(Error::OutputLimit); + } + let count = match usize::try_from(read) { + Ok(count) => count, + Err(_) => { + must_settle_failed_group(pid, pipe, status.is_some()); + return Err(Error::OutputLimit); + } + }; + if count > stdout_limit.saturating_sub(output.len()) { + must_settle_failed_group(pid, pipe, status.is_some()); + return Err(Error::OutputLimit); + } + output.extend_from_slice(&buffer[..count]); + if fixed_output && output.capacity() != stdout_limit { + must_settle_failed_group(pid, pipe, status.is_some()); + return Err(Error::OutputLimit); + } + } + std::cmp::Ordering::Equal => { + eof = true; + break; + } + std::cmp::Ordering::Less => { + match std::io::Error::last_os_error().raw_os_error() { + Some(libc::EAGAIN) => break, + Some(libc::EINTR) => continue, + _ => { + must_settle_failed_group(pid, pipe, status.is_some()); + return Err(Error::Spawn); + } + } + } + } + } + } + if status.is_none() { + let mut child_status = 0; + if injected_settlement_failure!(UnixWaitpid) { + must_settle_failed_group(pid, pipe, false); + return Err(Error::Spawn); + } + let waited = unsafe { libc::waitpid(pid, &mut child_status, libc::WNOHANG) }; + match waited { + waited if waited == pid => { + status = Some(child_status); + if !eof { + // A descendant retaining the private pipe is not part of + // the admitted tool result. Close the whole private group. + let _ = unsafe { libc::kill(-pid, libc::SIGKILL) }; + } + } + 0 => {} + -1 if std::io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) => {} + _ => { + must_settle_failed_group(pid, pipe, status.is_some()); + return Err(Error::Spawn); + } + } + } + if eof { + if let Some(status) = status { + // Quiesce every descendant in the private group even if it + // closed stdout before the leader exited. + let close_failed = pipe.close_injected(TestClosePoint::SuccessRead).is_err(); + let group = quiesce_group(pid); + if close_failed || group.is_err() { + std::process::abort(); + } + return Ok((output, status)); + } + } + if injected_settlement_failure!(UnixDeadline) || std::time::Instant::now() >= deadline { + must_settle_failed_group(pid, pipe, status.is_some()); + return Err(Error::Spawn); + } + } + } + + fn identity(metadata: &std::fs::Metadata) -> (u64, u64) { + (metadata.dev(), metadata.ino()) + } + + fn open_directory_at(parent: RawFd, name: &std::ffi::CStr) -> Result { + let fd = unsafe { + libc::openat( + parent, + name.as_ptr(), + libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC, + ) + }; + if fd < 0 { + return Err(Error::Changed); + } + let file = unsafe { File::from_raw_fd(fd) }; + let metadata = file.metadata().map_err(|_| Error::Changed)?; + let (dev, ino) = identity(&metadata); + Ok(Directory { + file, + dev, + ino, + mode: metadata.mode(), + #[cfg(target_os = "macos")] + generation: metadata_generation(&metadata), + }) + } + + pub fn hold_directory(path: &Path) -> Result { + use std::path::Component; + if !path.is_absolute() { + return Err(Error::Invalid); + } + let c_path = CString::new("/").expect("literal"); + let fd = unsafe { + libc::open( + c_path.as_ptr(), + libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC, + ) + }; + if fd < 0 { + return Err(Error::Changed); + } + let file = unsafe { File::from_raw_fd(fd) }; + let metadata = file.metadata().map_err(|_| Error::Changed)?; + let (dev, ino) = identity(&metadata); + let mut current = Directory { + file, + dev, + ino, + mode: metadata.mode(), + #[cfg(target_os = "macos")] + generation: metadata_generation(&metadata), + }; + for component in path.components() { + match component { + Component::RootDir => {} + Component::Normal(name) => { + let name = c_name(name)?; + current = open_directory_at(current.file.as_raw_fd(), &name)?; + } + _ => return Err(Error::Invalid), + } + } + Ok(current) + } + + pub fn recheck_directory(directory: &Directory) -> Result<(), Error> { + let metadata = directory.file.metadata().map_err(|_| Error::Changed)?; + if identity(&metadata) != (directory.dev, directory.ino) + || metadata.mode() != directory.mode + || !metadata.is_dir() + { + return Err(Error::Changed); + } + #[cfg(target_os = "macos")] + if metadata_generation(&metadata) != directory.generation { + return Err(Error::Changed); + } + Ok(()) + } + + pub fn same_directory_path(directory: &Directory, path: &Path) -> Result { + recheck_directory(directory)?; + let rebound = hold_directory(path)?; + Ok((rebound.dev, rebound.ino, rebound.mode) + == (directory.dev, directory.ino, directory.mode)) + } + + pub fn create_directory_new( + parent: &Directory, + name: &OsStr, + mode: u32, + ) -> Result { + recheck_directory(parent)?; + let name = c_name(name)?; + let mode = libc::mode_t::try_from(mode).map_err(|_| Error::Invalid)?; + let result = unsafe { libc::mkdirat(parent.file.as_raw_fd(), name.as_ptr(), mode) }; + if result != 0 { + return Err(match std::io::Error::last_os_error().raw_os_error() { + Some(libc::EEXIST) => Error::Exists, + _ => Error::Changed, + }); + } + open_directory_at(parent.file.as_raw_fd(), &name) + } + + pub fn create_directory_new_prepared( + parent: &Directory, + name: &PreparedRelativeNameArena, + mode: u32, + ) -> Result { + recheck_directory(parent)?; + let name = relative_name_arena_cstr(name)?; + let mode = libc::mode_t::try_from(mode).map_err(|_| Error::Invalid)?; + let result = unsafe { libc::mkdirat(parent.file.as_raw_fd(), name.as_ptr(), mode) }; + if result != 0 { + return Err(match std::io::Error::last_os_error().raw_os_error() { + Some(libc::EEXIST) => Error::Exists, + _ => Error::Changed, + }); + } + open_directory_at(parent.file.as_raw_fd(), name) + } + + pub fn write_file_new( + directory: &Directory, + name: &OsStr, + bytes: &[u8], + mode: u32, + ) -> Result { + recheck_directory(directory)?; + let name = c_name(name)?; + let fd = unsafe { + libc::openat( + directory.file.as_raw_fd(), + name.as_ptr(), + libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_NOFOLLOW | libc::O_CLOEXEC, + mode, + ) + }; + if fd < 0 { + return Err(Error::Exists); + } + let mut file = unsafe { File::from_raw_fd(fd) }; + file.write_all(bytes).map_err(|_| Error::Changed)?; + file.sync_data().map_err(|_| Error::Changed)?; + drop(file); + hold_regular_file( + directory, + OsStr::new(name.to_str().map_err(|_| Error::Invalid)?), + ) + } + + pub fn write_file_new_prepared( + directory: &Directory, + names: &PreparedDiscardNames, + index: usize, + bytes: &[u8], + mode: u32, + ) -> Result { + let name = enter_prepared_file_syscalls(prepared_discard_name(names, index))?; + recheck_directory(directory)?; + let fd = unsafe { + libc::openat( + directory.file.as_raw_fd(), + name.0.as_ptr(), + libc::O_RDWR | libc::O_CREAT | libc::O_EXCL | libc::O_NOFOLLOW | libc::O_CLOEXEC, + mode, + ) + }; + if fd < 0 { + return Err(Error::Exists); + } + let mut file = unsafe { File::from_raw_fd(fd) }; + file.write_all(bytes).map_err(|_| Error::Changed)?; + file.sync_data().map_err(|_| Error::Changed)?; + authenticate_regular_file(file) + } + + pub fn hold_regular_file(directory: &Directory, name: &OsStr) -> Result { + recheck_directory(directory)?; + let name = prepare_relative_name(name)?; + hold_regular_file_name_prepared(directory, &name) + } + + fn authenticate_regular_file(file: File) -> Result { + let metadata = file.metadata().map_err(|_| Error::Changed)?; + if !metadata.is_file() { + return Err(Error::Changed); + } + let (dev, ino) = identity(&metadata); + let digest = digest_file(&file, metadata.len())?; + Ok(RegularFile { + file, + dev, + ino, + mode: metadata.mode(), + len: metadata.len(), + digest, + #[cfg(target_os = "macos")] + generation: metadata_generation(&metadata), + }) + } + + fn hold_regular_file_name_prepared( + directory: &Directory, + name: &PreparedRelativeName, + ) -> Result { + let fd = unsafe { + libc::openat( + directory.file.as_raw_fd(), + name.0.as_ptr(), + libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC, + ) + }; + if fd < 0 { + return Err(Error::Changed); + } + authenticate_regular_file(unsafe { File::from_raw_fd(fd) }) + } + + fn hold_regular_file_cstr( + directory: &Directory, + name: &std::ffi::CStr, + ) -> Result { + let fd = unsafe { + libc::openat( + directory.file.as_raw_fd(), + name.as_ptr(), + libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC, + ) + }; + if fd < 0 { + return Err(Error::Changed); + } + authenticate_regular_file(unsafe { File::from_raw_fd(fd) }) + } + + fn hold_executable_cstr( + directory: &Directory, + name: &std::ffi::CStr, + ) -> Result { + let file = hold_regular_file_cstr(directory, name)?; + if file.mode & 0o111 == 0 { + return Err(Error::Invalid); + } + let (slice_offset, slice_size) = executable_slice(&file)?; + Ok(Executable { + file, + slice_offset, + slice_size, + }) + } + + pub fn hold_regular_file_prepared( + directory: &Directory, + names: &PreparedDiscardNames, + index: usize, + tracked: &RegularFile, + ) -> Result { + let name = enter_prepared_file_syscalls(prepared_discard_name(names, index))?; + recheck_directory(directory)?; + let rebound = hold_regular_file_name_prepared(directory, name)?; + if rebound.dev != tracked.dev + || rebound.ino != tracked.ino + || rebound.mode != tracked.mode + || rebound.len != tracked.len + || rebound.digest != tracked.digest + || cfg!(target_os = "macos") && { + #[cfg(target_os = "macos")] + { + rebound.generation != tracked.generation + } + #[cfg(not(target_os = "macos"))] + { + false + } + } + { + return Err(Error::Changed); + } + Ok(rebound) + } + + pub fn hold_external_executable(path: &Path) -> Result { + let parent = path.parent().ok_or(Error::Invalid)?; + let name = path.file_name().ok_or(Error::Invalid)?; + let directory = hold_directory(parent)?; + hold_executable(&directory, name) + } + + fn set_tool_candidate( + prepared: &mut PreparedToolResolver, + directory: Option<&[u8]>, + configured: Option<&[u8]>, + ) -> Result<(), Error> { + prepared.candidate.clear(); + if let Some(configured) = configured { + if configured.is_empty() || configured.contains(&0) { + return Err(Error::Invalid); + } + if configured.len().saturating_add(1) > prepared.maximum { + return Err(Error::OutputLimit); + } + prepared.candidate.extend_from_slice(configured); + } else { + let directory = directory.ok_or(Error::Invalid)?; + let directory = if directory.is_empty() { + b"." + } else { + directory + }; + let fallback = prepared.fallback.as_bytes(); + let separator = usize::from(!directory.ends_with(b"/")); + if directory + .len() + .checked_add(separator) + .and_then(|length| length.checked_add(fallback.len())) + .and_then(|length| length.checked_add(1)) + .is_none_or(|length| length > prepared.maximum) + { + return Err(Error::OutputLimit); + } + prepared.candidate.extend_from_slice(directory); + if separator != 0 { + prepared.candidate.push(b'/'); + } + prepared.candidate.extend_from_slice(fallback); + } + prepared.candidate.push(0); + if prepared.candidate.capacity() != prepared.maximum { + return Err(Error::OutputLimit); + } + Ok(()) + } + + #[cfg(target_os = "linux")] + fn canonical_tool_path(file: &File, output: &mut Vec, maximum: usize) -> Result<(), Error> { + let mut link = [0_u8; 64]; + let prefix = b"/proc/self/fd/"; + link[..prefix.len()].copy_from_slice(prefix); + let mut digits = [0_u8; 20]; + let mut value = u64::try_from(file.as_raw_fd()).map_err(|_| Error::Changed)?; + let mut count = 0usize; + loop { + digits[count] = b'0' + u8::try_from(value % 10).map_err(|_| Error::Changed)?; + count += 1; + value /= 10; + if value == 0 { + break; + } + } + for index in 0..count { + link[prefix.len() + index] = digits[count - index - 1]; + } + link[prefix.len() + count] = 0; + output.clear(); + output.resize(maximum, 0); + let length = unsafe { + libc::readlink( + link.as_ptr().cast(), + output.as_mut_ptr().cast(), + output.len(), + ) + }; + if length <= 0 { + return Err(Error::Changed); + } + let length = usize::try_from(length).map_err(|_| Error::Changed)?; + if length >= maximum { + return Err(Error::OutputLimit); + } + output.truncate(length); + Ok(()) + } + + #[cfg(target_os = "macos")] + fn canonical_tool_path(file: &File, output: &mut Vec, maximum: usize) -> Result<(), Error> { + output.clear(); + output.resize(maximum, 0); + if unsafe { libc::fcntl(file.as_raw_fd(), libc::F_GETPATH, output.as_mut_ptr()) } != 0 { + return Err(Error::Changed); + } + let length = output + .iter() + .position(|byte| *byte == 0) + .ok_or(Error::OutputLimit)?; + output.truncate(length); + Ok(()) + } + + fn hold_tool_candidate( + prepared: &mut PreparedToolResolver, + ) -> Result, Error> { + let candidate = prepared.candidate.as_ptr().cast(); + let fd = unsafe { libc::open(candidate, libc::O_RDONLY | libc::O_CLOEXEC) }; + if fd < 0 { + return Ok(None); + } + let file = unsafe { File::from_raw_fd(fd) }; + let metadata = file.metadata().map_err(|_| Error::Changed)?; + if !metadata.is_file() { + return Ok(None); + } + if metadata.mode() & 0o111 == 0 { + return Err(Error::Invalid); + } + canonical_tool_path(&file, &mut prepared.canonical, prepared.maximum)?; + let canonical = std::str::from_utf8(&prepared.canonical).map_err(|_| Error::Invalid)?; + prepared.display.clear(); + prepared.display.push_str(canonical); + if prepared.display.capacity() != prepared.maximum { + return Err(Error::OutputLimit); + } + let (dev, ino) = identity(&metadata); + let digest = digest_file(&file, metadata.len())?; + let regular = RegularFile { + file, + dev, + ino, + mode: metadata.mode(), + len: metadata.len(), + digest, + #[cfg(target_os = "macos")] + generation: metadata_generation(&metadata), + }; + let (slice_offset, slice_size) = executable_slice(®ular)?; + Ok(Some(Executable { + file: regular, + slice_offset, + slice_size, + })) + } + + pub fn resolve_and_hold_tool_prepared( + mut prepared: PreparedToolResolver, + configured: Option<&OsStr>, + paths: Option<&OsStr>, + ) -> Result<(Executable, String), Error> { + if let Some(configured) = configured { + set_tool_candidate(&mut prepared, None, Some(configured.as_bytes()))?; + let executable = hold_tool_candidate(&mut prepared)?.ok_or(Error::Changed)?; + return Ok((executable, prepared.display)); + } + let paths = paths.ok_or(Error::Invalid)?.as_bytes(); + for directory in paths.split(|byte| *byte == b':') { + set_tool_candidate(&mut prepared, Some(directory), None)?; + if let Some(executable) = hold_tool_candidate(&mut prepared)? { + return Ok((executable, prepared.display)); + } + } + Err(Error::Changed) + } + + pub fn hold_rustc_discovery_prepared( + prepared: PreparedToolResolver, + configured: &OsStr, + ) -> Result { + if !Path::new(configured).is_absolute() { + return Err(Error::Invalid); + } + let (executable, _) = resolve_and_hold_tool_prepared(prepared, Some(configured), None)?; + Ok(RustcDiscovery(executable)) + } + + pub fn rustc_discovery_output_prepared( + discovery: &RustcDiscovery, + cwd: &Directory, + prepared: PreparedSysrootInvocation, + process_arena: &mut PreparedProcessArena, + ) -> Result, Error> { + version_prepared(&discovery.0, cwd, prepared.0, process_arena) + } + + pub(super) fn one_sysroot_line(output: &[u8]) -> Result<&[u8], Error> { + let line = output.strip_suffix(b"\n").ok_or(Error::Invalid)?; + let line = line.strip_suffix(b"\r").unwrap_or(line); + if line.is_empty() + || line.contains(&0) + || line.contains(&b'\n') + || line.contains(&b'\r') + || std::str::from_utf8(line).is_err() + { + return Err(Error::Invalid); + } + Ok(line) + } + + fn held_sysroot_from_output( + prepared: &mut PreparedToolResolver, + output: &[u8], + ) -> Result { + let line = one_sysroot_line(output)?; + if line + .len() + .checked_add(1) + .is_none_or(|length| length > prepared.maximum) + { + return Err(Error::OutputLimit); + } + prepared.candidate.clear(); + prepared.candidate.extend_from_slice(line); + prepared.candidate.push(0); + if prepared.candidate.capacity() != prepared.maximum { + return Err(Error::OutputLimit); + } + if prepared.candidate.first() != Some(&b'/') { + return Err(Error::Invalid); + } + let root_fd = unsafe { + libc::open( + c"/".as_ptr(), + libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC, + ) + }; + if root_fd < 0 { + return Err(Error::Changed); + } + let root_file = unsafe { File::from_raw_fd(root_fd) }; + let root_metadata = root_file.metadata().map_err(|_| Error::Changed)?; + let (dev, ino) = identity(&root_metadata); + let mut current = Directory { + file: root_file, + dev, + ino, + mode: root_metadata.mode(), + #[cfg(target_os = "macos")] + generation: metadata_generation(&root_metadata), + }; + let mut start = 1usize; + let end_of_line = prepared.candidate.len() - 1; + while start < end_of_line { + let end = prepared.candidate[start..end_of_line] + .iter() + .position(|byte| *byte == b'/') + .map_or(end_of_line, |offset| start + offset); + if end == start + || prepared.candidate[start..end] == *b"." + || prepared.candidate[start..end] == *b".." + { + return Err(Error::Invalid); + } + let saved = prepared.candidate[end]; + prepared.candidate[end] = 0; + let component = std::ffi::CStr::from_bytes_with_nul(&prepared.candidate[start..=end]) + .map_err(|_| Error::Invalid)?; + current = open_directory_at(current.file.as_raw_fd(), component)?; + prepared.candidate[end] = saved; + start = end.saturating_add(1); + } + Ok(current) + } + + pub fn hold_direct_rustc_prepared( + mut prepared: PreparedToolResolver, + output: &[u8], + ) -> Result { + let sysroot = held_sysroot_from_output(&mut prepared, output)?; + let bin = open_directory_at(sysroot.file.as_raw_fd(), c"bin")?; + let executable = hold_executable_cstr(&bin, c"rustc")?; + Ok(DirectRustc { + executable, + sysroot, + }) + } + + pub fn direct_rustc_output_prepared( + direct: &DirectRustc, + cwd: &Directory, + prepared: PreparedSysrootInvocation, + process_arena: &mut PreparedProcessArena, + ) -> Result, Error> { + version_prepared(&direct.executable, cwd, prepared.0, process_arena) + } + + pub fn direct_rustc_version_prepared( + direct: &DirectRustc, + cwd: &Directory, + prepared: PreparedRustcVersionInvocation, + process_arena: &mut PreparedProcessArena, + ) -> Result, Error> { + recheck_directory(&direct.sysroot)?; + version_prepared(&direct.executable, cwd, prepared.0, process_arena) + } + + pub fn direct_rustc_reproduces_sysroot( + direct: &DirectRustc, + mut prepared: PreparedToolResolver, + output: &[u8], + ) -> Result<(), Error> { + let rebound = held_sysroot_from_output(&mut prepared, output)?; + if rebound.dev != direct.sysroot.dev + || rebound.ino != direct.sysroot.ino + || rebound.mode != direct.sysroot.mode + || cfg!(target_os = "macos") && { + #[cfg(target_os = "macos")] + { + rebound.generation != direct.sysroot.generation + } + #[cfg(not(target_os = "macos"))] + { + false + } + } + { + return Err(Error::Changed); + } + recheck_executable(&direct.executable)?; + recheck_directory(&direct.sysroot) + } + + pub fn hold_executable(directory: &Directory, name: &OsStr) -> Result { + let file = hold_regular_file(directory, name)?; + if file.mode & 0o111 == 0 { + return Err(Error::Invalid); + } + let (slice_offset, slice_size) = executable_slice(&file)?; + Ok(Executable { + file, + slice_offset, + slice_size, + }) + } + + pub fn executable_regular_file(executable: &Executable) -> Result { + recheck_executable(executable)?; + Ok(RegularFile { + file: executable + .file + .file + .try_clone() + .map_err(|_| Error::Changed)?, + dev: executable.file.dev, + ino: executable.file.ino, + mode: executable.file.mode, + len: executable.file.len, + digest: executable.file.digest, + #[cfg(target_os = "macos")] + generation: executable.file.generation, + }) + } + + fn recheck_executable(executable: &Executable) -> Result<(), Error> { + recheck_regular(&executable.file)?; + if executable_slice(&executable.file)? != (executable.slice_offset, executable.slice_size) { + return Err(Error::Changed); + } + Ok(()) + } + + pub fn recheck_regular(file: &RegularFile) -> Result<(), Error> { + let metadata = file.file.metadata().map_err(|_| Error::Changed)?; + if identity(&metadata) != (file.dev, file.ino) + || metadata.mode() != file.mode + || metadata.len() != file.len + || !metadata.is_file() + || digest_file(&file.file, file.len)? != file.digest + { + return Err(Error::Changed); + } + #[cfg(target_os = "macos")] + if metadata_generation(&metadata) != file.generation { + return Err(Error::Changed); + } + Ok(()) + } + + pub fn read_exact(file: &RegularFile, maximum: usize) -> Result, Error> { + recheck_regular(file)?; + let length = usize::try_from(file.len).map_err(|_| Error::OutputLimit)?; + if length > maximum { + return Err(Error::OutputLimit); + } + let mut bytes = vec![0; length]; + let mut offset = 0; + while offset < length { + let count = file + .file + .read_at(&mut bytes[offset..], offset as u64) + .map_err(|_| Error::Changed)?; + if count == 0 { + return Err(Error::Changed); + } + offset += count; + } + recheck_regular(file)?; + Ok(bytes) + } + + pub fn compare_exact( + file: &RegularFile, + expected: &[u8], + scratch: &mut [u8; 8192], + ) -> Result { + recheck_regular(file)?; + if usize::try_from(file.len).map_err(|_| Error::OutputLimit)? != expected.len() { + return Ok(false); + } + let mut offset = 0usize; + while offset < expected.len() { + let chunk = (expected.len() - offset).min(scratch.len()); + let count = file + .file + .read_at( + &mut scratch[..chunk], + u64::try_from(offset).map_err(|_| Error::OutputLimit)?, + ) + .map_err(|_| Error::Changed)?; + if count == 0 || scratch[..count] != expected[offset..offset + count] { + return Ok(false); + } + offset = offset.checked_add(count).ok_or(Error::OutputLimit)?; + } + recheck_regular(file)?; + Ok(true) + } + + pub fn link_or_copy_new_prepared( + prepared: PreparedLinkOrCopy, + source: &RegularFile, + directory: &Directory, + names: &PreparedDiscardNames, + destination_index: usize, + source_bytes: &[u8], + ) -> Result { + if prepared.destination_index != destination_index { + return Err(Error::Invalid); + } + let expected = prepared_discard_name(names, destination_index)?; + if expected.0.as_bytes() != prepared.destination.0.as_bytes() { + return Err(Error::Invalid); + } + let name = &prepared.destination; + if usize::try_from(source.len).map_err(|_| Error::OutputLimit)? != source_bytes.len() + || digest_bytes(source_bytes) != source.digest + { + return Err(Error::Changed); + } + recheck_regular(source)?; + recheck_directory(directory)?; + #[cfg(debug_assertions)] + let fail_before_authentication = prepared.fail_before_authentication; + #[cfg(not(debug_assertions))] + let fail_before_authentication = false; + #[cfg(target_os = "macos")] + { + copy_regular_file_new_prepared( + source, + directory, + name, + source_bytes, + fail_before_authentication, + ) + } + #[cfg(target_os = "linux")] + { + let result = unsafe { + libc::linkat( + source.file.as_raw_fd(), + c"".as_ptr(), + directory.file.as_raw_fd(), + name.0.as_ptr(), + libc::AT_EMPTY_PATH, + ) + }; + if result == 0 { + #[cfg(debug_assertions)] + if fail_before_authentication { + return Err(Error::Changed); + } + let destination = hold_regular_file_name_prepared(directory, name)?; + if destination.dev != source.dev + || destination.ino != source.ino + || destination.mode != source.mode + || destination.len != source.len + || destination.digest != source.digest + { + return Err(Error::Changed); + } + return Ok(destination); + } + let errno = std::io::Error::last_os_error() + .raw_os_error() + .ok_or(Error::Changed)?; + if errno == libc::EEXIST { + return Err(Error::Exists); + } + if ![ + libc::EPERM, + libc::EACCES, + libc::EOPNOTSUPP, + libc::ENOSYS, + libc::EINVAL, + libc::ENOENT, + ] + .contains(&errno) + { + return Err(Error::Changed); + } + copy_regular_file_new_prepared( + source, + directory, + name, + source_bytes, + fail_before_authentication, + ) + } + } + + fn copy_regular_file_new_prepared( + source: &RegularFile, + directory: &Directory, + name: &PreparedRelativeName, + source_bytes: &[u8], + fail_before_authentication: bool, + ) -> Result { + let fd = unsafe { + libc::openat( + directory.file.as_raw_fd(), + name.0.as_ptr(), + libc::O_RDWR | libc::O_CREAT | libc::O_EXCL | libc::O_NOFOLLOW | libc::O_CLOEXEC, + source.mode & 0o777, + ) + }; + if fd < 0 { + return Err( + if std::io::Error::last_os_error().raw_os_error() == Some(libc::EEXIST) { + Error::Exists + } else { + Error::Changed + }, + ); + } + let mut file = unsafe { File::from_raw_fd(fd) }; + file.write_all(source_bytes).map_err(|_| Error::Changed)?; + file.sync_data().map_err(|_| Error::Changed)?; + #[cfg(not(debug_assertions))] + let _ = fail_before_authentication; + #[cfg(debug_assertions)] + if fail_before_authentication { + return Err(Error::Changed); + } + let destination = authenticate_regular_file(file)?; + if destination.len != source.len + || destination.digest != source.digest + || destination.mode & 0o777 != source.mode & 0o777 + { + return Err(Error::Changed); + } + Ok(destination) + } + + fn admit_inventory_entry( + prepared: &PreparedInventoryExact, + files: &[Option<&RegularFile>; N], + seen: &mut [bool; N], + count: &mut usize, + actual: &[u8], + inode: u64, + ) -> Result<(), Error> { + if actual == b"." || actual == b".." { + return Ok(()); + } + let Some(index) = prepared + .names + .iter() + .position(|expected| expected.as_ref().expect("prepared name").0.as_bytes() == actual) + else { + return Err(Error::Changed); + }; + if seen[index] || inode != files[index].expect("attached").ino { + return Err(Error::Changed); + } + seen[index] = true; + *count = count.checked_add(1).ok_or(Error::OutputLimit)?; + if *count > N { + return Err(Error::Changed); + } + Ok(()) + } + + fn prepared_directory_identity(directory: &Directory) -> PreparedDirectoryIdentity { + PreparedDirectoryIdentity { + dev: directory.dev, + ino: directory.ino, + mode: directory.mode, + #[cfg(target_os = "macos")] + generation: directory.generation, + } + } + + struct ObservedRegularIdentity { + dev: u64, + ino: u64, + mode: u32, + len: u64, + digest: [u8; 32], + #[cfg(target_os = "macos")] + generation: u32, + } + + fn same_regular_identity(left: &ObservedRegularIdentity, right: &RegularFile) -> bool { + let same = left.dev == right.dev + && left.ino == right.ino + && left.mode == right.mode + && left.len == right.len + && left.digest == right.digest; + #[cfg(target_os = "macos")] + { + same && left.generation == right.generation + } + #[cfg(not(target_os = "macos"))] + { + same + } + } + + fn must_close_inventory_descriptor(descriptor: RawFd, inject_failure: bool) { + let failed = unsafe { libc::close(descriptor) } != 0; + if failed || inject_failure { + std::process::abort(); + } + } + + fn observe_inventory_rebound( + directory: &Directory, + name: &PreparedRelativeName, + fail_authentication: bool, + fail_close: bool, + ) -> Result { + let descriptor = unsafe { + libc::openat( + directory.file.as_raw_fd(), + name.0.as_ptr(), + libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC, + ) + }; + if descriptor < 0 { + return Err(Error::Changed); + } + let file = std::mem::ManuallyDrop::new(unsafe { File::from_raw_fd(descriptor) }); + let observed = (|| { + if fail_authentication { + return Err(Error::Changed); + } + let metadata = file.metadata().map_err(|_| Error::Changed)?; + if !metadata.is_file() { + return Err(Error::Changed); + } + let (dev, ino) = identity(&metadata); + Ok(ObservedRegularIdentity { + dev, + ino, + mode: metadata.mode(), + len: metadata.len(), + digest: digest_file(&file, metadata.len())?, + #[cfg(target_os = "macos")] + generation: metadata_generation(&metadata), + }) + })(); + must_close_inventory_descriptor(descriptor, fail_close); + observed + } + + #[cfg(target_os = "linux")] + fn parse_linux_inventory_records( + bytes: &[u8], + mut admit: impl FnMut(&[u8], u64) -> Result<(), Error>, + ) -> Result<(), Error> { + let mut offset = 0usize; + while offset < bytes.len() { + let header_end = offset.checked_add(19).ok_or(Error::Changed)?; + if header_end > bytes.len() { + return Err(Error::Changed); + } + let inode = + unsafe { std::ptr::read_unaligned(bytes.as_ptr().add(offset).cast::()) }; + let record = usize::from(unsafe { + std::ptr::read_unaligned(bytes.as_ptr().add(offset + 16).cast::()) + }); + let next = offset.checked_add(record).ok_or(Error::Changed)?; + if record < 20 + || record % std::mem::align_of::() != 0 + || next <= offset + || next > bytes.len() + { + return Err(Error::Changed); + } + let name = &bytes[header_end..next]; + let nul = name + .iter() + .position(|byte| *byte == 0) + .ok_or(Error::Changed)?; + if nul == 0 || inode == 0 { + return Err(Error::Changed); + } + admit(&name[..nul], inode)?; + offset = next; + } + if offset != bytes.len() { + return Err(Error::Changed); + } + Ok(()) + } + + #[cfg(all(test, target_os = "linux"))] + pub(crate) fn test_parse_inventory_records( + bytes: &[u8], + expected: &[(&[u8], u64)], + ) -> Result<(), Error> { + if expected.len() > 16 { + return Err(Error::Invalid); + } + let mut seen = [false; 16]; + parse_linux_inventory_records(bytes, |name, inode| { + let Some(index) = expected.iter().position(|(expected_name, expected_inode)| { + *expected_name == name && *expected_inode == inode + }) else { + return Err(Error::Changed); + }; + if seen[index] { + return Err(Error::Changed); + } + seen[index] = true; + Ok(()) + })?; + if seen[..expected.len()].iter().any(|seen| !seen) { + return Err(Error::Changed); + } + Ok(()) + } + + #[cfg(target_os = "linux")] + fn scan_prepared_directory( + prepared: &mut PreparedInventoryExact, + directory: &Directory, + files: &[Option<&RegularFile>; N], + ) -> Result<(), Error> { + #[cfg(test)] + { + prepared.scan_entries = prepared.scan_entries.saturating_add(1); + } + let mut seen = [false; N]; + let mut count = 0usize; + let mut raw_records = 0usize; + let mut queries = 0usize; + let mut saw_dot = false; + let mut saw_dot_dot = false; + let maximum_records = N.checked_add(2).ok_or(Error::OutputLimit)?; + let maximum_queries = N.checked_add(3).ok_or(Error::OutputLimit)?; + let capacity = prepared + .storage + .len() + .checked_mul(std::mem::size_of::()) + .ok_or(Error::OutputLimit)?; + let bytes_limit = libc::c_uint::try_from(capacity).map_err(|_| Error::OutputLimit)?; + loop { + queries = queries.checked_add(1).ok_or(Error::OutputLimit)?; + if queries > maximum_queries { + return Err(Error::Changed); + } + prepared.storage.fill(u64::MAX); + let read = unsafe { + libc::syscall( + libc::SYS_getdents64, + directory.file.as_raw_fd(), + prepared.storage.as_mut_ptr().cast::(), + bytes_limit, + ) + }; + if read < 0 { + return Err(Error::Changed); + } + let used = usize::try_from(libc::c_uint::try_from(read).map_err(|_| Error::Changed)?) + .map_err(|_| Error::Changed)?; + if used == 0 { + break; + } + if used > capacity { + return Err(Error::Changed); + } + let bytes = + unsafe { std::slice::from_raw_parts(prepared.storage.as_ptr().cast::(), used) }; + parse_linux_inventory_records(bytes, |name, inode| { + raw_records = raw_records.checked_add(1).ok_or(Error::OutputLimit)?; + if raw_records > maximum_records { + return Err(Error::Changed); + } + if name == b"." { + if saw_dot { + return Err(Error::Changed); + } + saw_dot = true; + } else if name == b".." { + if saw_dot_dot { + return Err(Error::Changed); + } + saw_dot_dot = true; + } + admit_inventory_entry(prepared, files, &mut seen, &mut count, name, inode) + })?; + } + if count != N || seen.iter().any(|seen| !seen) { + return Err(Error::Changed); + } + Ok(()) + } + + #[cfg(target_os = "macos")] + fn parse_darwin_inventory_records( + bytes: &[u8], + mut admit: impl FnMut(&[u8], u64) -> Result<(), Error>, + ) -> Result<(), Error> { + let header = std::mem::offset_of!(libc::dirent, d_name); + let mut offset = 0usize; + while offset < bytes.len() { + let header_end = offset.checked_add(header).ok_or(Error::Changed)?; + if header_end > bytes.len() { + return Err(Error::Changed); + } + let entry = unsafe { bytes.as_ptr().add(offset).cast::() }; + let inode = unsafe { std::ptr::addr_of!((*entry).d_ino).read_unaligned() }; + let record = + usize::from(unsafe { std::ptr::addr_of!((*entry).d_reclen).read_unaligned() }); + let name_length = + usize::from(unsafe { std::ptr::addr_of!((*entry).d_namlen).read_unaligned() }); + let name_end = header_end.checked_add(name_length).ok_or(Error::Changed)?; + let next = offset.checked_add(record).ok_or(Error::Changed)?; + if record < header + 1 + || record % 4 != 0 + || name_length > 1023 + || name_end >= next + || next <= offset + || next > bytes.len() + { + return Err(Error::Changed); + } + let name = &bytes[header_end..name_end]; + if bytes[name_end] != 0 || name.contains(&0) { + return Err(Error::Changed); + } + if inode == 0 { + offset = next; + continue; + } + if name.is_empty() { + return Err(Error::Changed); + } + admit(name, inode)?; + offset = next; + } + if offset != bytes.len() { + return Err(Error::Changed); + } + Ok(()) + } + + #[cfg(all(test, target_os = "macos"))] + pub(crate) fn test_parse_inventory_records( + bytes: &[u8], + expected: &[(&[u8], u64)], + ) -> Result<(), Error> { + if expected.len() > 16 { + return Err(Error::Invalid); + } + let mut seen = [false; 16]; + parse_darwin_inventory_records(bytes, |name, inode| { + let Some(index) = expected.iter().position(|(expected_name, expected_inode)| { + *expected_name == name && *expected_inode == inode + }) else { + return Err(Error::Changed); + }; + if seen[index] { + return Err(Error::Changed); + } + seen[index] = true; + Ok(()) + })?; + if seen[..expected.len()].iter().any(|seen| !seen) { + return Err(Error::Changed); + } + Ok(()) + } + + #[cfg(target_os = "macos")] + fn scan_prepared_directory( + prepared: &mut PreparedInventoryExact, + directory: &Directory, + files: &[Option<&RegularFile>; N], + ) -> Result<(), Error> { + #[cfg(test)] + { + prepared.scan_entries = prepared.scan_entries.saturating_add(1); + } + const SYS_GETDIRENTRIES64: libc::c_int = 344; + const _: () = assert!(std::mem::size_of::() == 8); + const _: () = assert!(std::mem::offset_of!(libc::dirent, d_ino) == 0); + const _: () = assert!(std::mem::offset_of!(libc::dirent, d_reclen) == 16); + const _: () = assert!(std::mem::offset_of!(libc::dirent, d_namlen) == 18); + const _: () = assert!(std::mem::offset_of!(libc::dirent, d_name) == 21); + let mut seen = [false; N]; + let mut count = 0usize; + let mut raw_records = 0usize; + let mut queries = 0usize; + let mut saw_dot = false; + let mut saw_dot_dot = false; + let maximum_records = N.checked_add(2).ok_or(Error::OutputLimit)?; + let maximum_queries = N.checked_add(3).ok_or(Error::OutputLimit)?; + let capacity = prepared + .storage + .len() + .checked_mul(std::mem::size_of::()) + .ok_or(Error::OutputLimit)?; + let bytes_limit: libc::size_t = capacity; + let mut base: libc::off_t = 0; + loop { + queries = queries.checked_add(1).ok_or(Error::OutputLimit)?; + if queries > maximum_queries { + return Err(Error::Changed); + } + prepared.storage.fill(u64::MAX); + let read = unsafe { + libc::syscall( + SYS_GETDIRENTRIES64, + directory.file.as_raw_fd(), + prepared.storage.as_mut_ptr().cast::(), + bytes_limit, + &mut base, + ) + }; + if read < 0 { + return Err(Error::Changed); + } + let used = usize::try_from(read).map_err(|_| Error::Changed)?; + if used == 0 { + break; + } + if used > capacity { + return Err(Error::Changed); + } + let bytes = + unsafe { std::slice::from_raw_parts(prepared.storage.as_ptr().cast::(), used) }; + parse_darwin_inventory_records(bytes, |name, inode| { + raw_records = raw_records.checked_add(1).ok_or(Error::OutputLimit)?; + if raw_records > maximum_records { + return Err(Error::Changed); + } + if name == b"." { + if saw_dot { + return Err(Error::Changed); + } + saw_dot = true; + } else if name == b".." { + if saw_dot_dot { + return Err(Error::Changed); + } + saw_dot_dot = true; + } + admit_inventory_entry(prepared, files, &mut seen, &mut count, name, inode) + })?; + } + if count != N || seen.iter().any(|seen| !seen) { + return Err(Error::Changed); + } + Ok(()) + } + + pub fn inventory_exact_prepared( + prepared: &mut PreparedInventoryExact, + directory: &Directory, + names: &PreparedDiscardNames, + files: [Option<&RegularFile>; N], + ) -> Result<(), Error> { + let current_bindings = prepared_name_bindings(names)?; + if prepared.remaining == 0 + || prepared.bindings != current_bindings + || files.iter().any(Option::is_none) + { + return Err(Error::Invalid); + } + let directory_identity = prepared_directory_identity(directory); + match prepared.directory_identity { + Some(first) if first != directory_identity => return Err(Error::Changed), + None => prepared.directory_identity = Some(directory_identity), + Some(_) => {} + } + prepared.remaining -= 1; + recheck_directory(directory)?; + for file in files.iter().flatten() { + recheck_regular(file)?; + } + #[cfg(test)] + let fail_initial_seek = prepared.fail_initial_seek; + #[cfg(not(test))] + let fail_initial_seek = false; + if fail_initial_seek + || unsafe { libc::lseek(directory.file.as_raw_fd(), 0, libc::SEEK_SET) } < 0 + { + return Err(Error::Changed); + } + let scan = scan_prepared_directory(prepared, directory, &files); + #[cfg(test)] + let fail_reset_seek = prepared.fail_reset_seek; + #[cfg(not(test))] + let fail_reset_seek = false; + let reset = if fail_reset_seek { + -1 + } else { + unsafe { libc::lseek(directory.file.as_raw_fd(), 0, libc::SEEK_SET) } + }; + if scan.is_err() || reset < 0 { + return Err(Error::Changed); + } + for (index, tracked) in files.iter().enumerate() { + #[cfg(test)] + let (fail_authentication, fail_close) = ( + prepared.fail_rebound_authentication, + prepared.fail_rebound_close, + ); + #[cfg(not(test))] + let (fail_authentication, fail_close) = (false, false); + let rebound = observe_inventory_rebound( + directory, + prepared.names[index].as_ref().expect("prepared name"), + fail_authentication, + fail_close, + )?; + if !same_regular_identity(&rebound, tracked.expect("attached")) { + return Err(Error::Changed); + } + } + recheck_directory(directory)?; + for file in files.iter().flatten() { + recheck_regular(file)?; + } + Ok(()) + } + + fn observe_publish_rebound( + parent: &Directory, + name: &std::ffi::CStr, + fail_information: bool, + fail_close: bool, + ) -> Result { + let descriptor = unsafe { + libc::openat( + parent.file.as_raw_fd(), + name.as_ptr(), + libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC, + ) + }; + if descriptor < 0 { + return Err(Error::Changed); + } + let file = std::mem::ManuallyDrop::new(unsafe { File::from_raw_fd(descriptor) }); + let observed = (|| { + if fail_information { + return Err(Error::Changed); + } + let metadata = file.metadata().map_err(|_| Error::Changed)?; + if !metadata.is_dir() { + return Err(Error::Changed); + } + let (dev, ino) = identity(&metadata); + Ok(PreparedDirectoryIdentity { + dev, + ino, + mode: metadata.mode(), + #[cfg(target_os = "macos")] + generation: metadata_generation(&metadata), + }) + })(); + let close_failed = unsafe { libc::close(descriptor) } != 0; + if close_failed || fail_close { + std::process::abort(); + } + observed + } + + pub fn publish_directory_new_prepared( + prepared: &mut PreparedPublishDirectory, + parent: &Directory, + stage: &Directory, + stage_name: &PreparedRelativeNameArena, + output_name: &OsStr, + ) -> Result<(), Error> { + let stage_name = relative_name_arena_cstr(stage_name)?; + let output_bytes = validated_c_name_bytes(output_name)?; + if prepared.remaining != 1 + || prepared.exact_capacity != prepared.destination.as_bytes_with_nul().len() + || prepared.destination.as_bytes() != output_bytes + { + return Err(Error::Invalid); + } + prepared.remaining = 0; + recheck_directory(parent)?; + recheck_directory(stage)?; + #[cfg(debug_assertions)] + if prepared.fail_before_open { + return Err(Error::Changed); + } + #[cfg(debug_assertions)] + let (fail_information, fail_close) = (prepared.fail_information, prepared.fail_close); + #[cfg(not(debug_assertions))] + let (fail_information, fail_close) = (false, false); + if observe_publish_rebound(parent, stage_name, fail_information, fail_close)? + != prepared_directory_identity(stage) + { + return Err(Error::Changed); + } + #[cfg(debug_assertions)] + if prepared.fail_rename { + return Err(Error::Changed); + } + #[cfg(target_os = "linux")] + let result = unsafe { + libc::syscall( + libc::SYS_renameat2, + parent.file.as_raw_fd(), + stage_name.as_ptr(), + parent.file.as_raw_fd(), + prepared.destination.as_ptr(), + 1_u32, + ) + } as i32; + #[cfg(target_os = "macos")] + let result = unsafe { + unsafe extern "C" { + fn renameatx_np( + fromfd: libc::c_int, + from: *const libc::c_char, + tofd: libc::c_int, + to: *const libc::c_char, + flags: libc::c_uint, + ) -> libc::c_int; + } + renameatx_np( + parent.file.as_raw_fd(), + stage_name.as_ptr(), + parent.file.as_raw_fd(), + prepared.destination.as_ptr(), + 0x0000_0004, + ) + }; + #[cfg(not(any(target_os = "linux", target_os = "macos")))] + let result = -1; + if result != 0 { + return Err(Error::Exists); + } + Ok(()) + } + + pub fn discard_owned_stage_prepared( + parent: &Directory, + stage: &Directory, + stage_name: &PreparedRelativeNameArena, + names: &PreparedDiscardNames, + files: &[Option<&RegularFile>; N], + #[cfg(debug_assertions)] failure_after_delete: Option, + ) -> Result<(), Error> { + recheck_directory(parent)?; + recheck_directory(stage)?; + let stage_name = relative_name_arena_cstr(stage_name)?; + let rebound = open_directory_at(parent.file.as_raw_fd(), stage_name)?; + if (rebound.dev, rebound.ino, rebound.mode) != (stage.dev, stage.ino, stage.mode) { + return Err(Error::Changed); + } + let attached = files.iter().take_while(|file| file.is_some()).count(); + if files[attached..].iter().any(Option::is_some) { + return Err(Error::Invalid); + } + + let duplicate = stage.file.try_clone().map_err(|_| Error::Changed)?; + let fd = unsafe { libc::dup(duplicate.as_raw_fd()) }; + if fd < 0 { + return Err(Error::Changed); + } + let stream = unsafe { libc::fdopendir(fd) }; + if stream.is_null() { + unsafe { libc::close(fd) }; + return Err(Error::Changed); + } + let scan = (|| { + unsafe { libc::rewinddir(stream) }; + let mut seen = [false; N]; + let mut count = 0usize; + loop { + #[cfg(target_os = "linux")] + unsafe { + *libc::__errno_location() = 0; + } + #[cfg(target_os = "macos")] + unsafe { + *libc::__error() = 0; + } + let entry = unsafe { libc::readdir(stream) }; + if entry.is_null() { + #[cfg(target_os = "linux")] + let errno = unsafe { *libc::__errno_location() }; + #[cfg(target_os = "macos")] + let errno = unsafe { *libc::__error() }; + if errno != 0 { + return Err(Error::Changed); + } + break; + } + let actual = + unsafe { std::ffi::CStr::from_ptr((*entry).d_name.as_ptr()) }.to_bytes(); + if actual == b"." || actual == b".." { + continue; + } + let Some(index) = names.names[..attached].iter().position(|expected| { + expected.as_ref().expect("validated").0.as_bytes() == actual + }) else { + return Err(Error::Changed); + }; + if seen[index] { + return Err(Error::Changed); + } + seen[index] = true; + count = count.checked_add(1).ok_or(Error::OutputLimit)?; + } + if count != attached || seen[..attached].iter().any(|seen| !seen) { + return Err(Error::Changed); + } + Ok(()) + })(); + unsafe { libc::closedir(stream) }; + scan?; + + for (file, name) in files[..attached].iter().zip(&names.names[..attached]) { + let file = file.expect("attached prefix"); + recheck_regular(file)?; + let name = name.as_ref().expect("validated"); + let rebound = hold_regular_file_name_prepared(stage, name)?; + if ( + rebound.dev, + rebound.ino, + rebound.mode, + rebound.len, + rebound.digest, + ) != (file.dev, file.ino, file.mode, file.len, file.digest) + { + return Err(Error::Changed); + } + } + for (deleted, name) in names.names[..attached].iter().enumerate() { + #[cfg(not(debug_assertions))] + let _ = deleted; + #[cfg(debug_assertions)] + if failure_after_delete == Some(deleted) { + return Err(Error::Changed); + } + let name = name.as_ref().expect("validated"); + if unsafe { libc::unlinkat(stage.file.as_raw_fd(), name.0.as_ptr(), 0) } != 0 { + return Err(Error::Changed); + } + } + #[cfg(debug_assertions)] + if failure_after_delete == Some(attached) { + return Err(Error::Changed); + } + if unsafe { + libc::unlinkat( + parent.file.as_raw_fd(), + stage_name.as_ptr(), + libc::AT_REMOVEDIR, + ) + } != 0 + { + return Err(Error::Changed); + } + Ok(()) + } + + #[cfg(target_os = "linux")] + fn run_argv( + executable: &Executable, + cwd: &Directory, + arguments: &[CString], + stdout_limit: usize, + output: Vec, + process_arena: &mut PreparedProcessArena, + ) -> Result, Error> { + if arguments.len() > 32 || output.capacity() != stdout_limit || !output.is_empty() { + return Err(Error::Invalid); + } + consume_process_arena(process_arena)?; + recheck_executable(executable)?; + recheck_directory(cwd)?; + let mut pipe = [0; 2]; + if unsafe { libc::pipe(pipe.as_mut_ptr()) } != 0 { + return Err(Error::Spawn); + } + let read_pipe = CheckedFd::new(pipe[0]); + let write_pipe = CheckedFd::new(pipe[1]); + if injected_settlement_failure!(UnixPipeReadFcntl) { + return Err(Error::Spawn); + } + if unsafe { libc::fcntl(read_pipe.raw(), libc::F_SETFD, libc::FD_CLOEXEC) } != 0 { + return Err(Error::Spawn); + } + if injected_settlement_failure!(UnixPipeWriteFcntl) { + return Err(Error::Spawn); + } + if unsafe { libc::fcntl(write_pipe.raw(), libc::F_SETFD, libc::FD_CLOEXEC) } != 0 { + return Err(Error::Spawn); + } + let dev_null = c"/dev/null"; + let null_fd = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDWR | libc::O_CLOEXEC) }; + if null_fd < 0 { + return Err(Error::Spawn); + } + let null_fd = CheckedFd::new(null_fd); + let mut argv = [std::ptr::null::(); 34]; + for (index, argument) in arguments.iter().enumerate() { + argv[index + 1] = argument.as_ptr(); + } + let env = [std::ptr::null::()]; + const EXECUTABLE_FD: libc::c_int = 1020; + const EXECUTABLE_FD_PATH: &std::ffi::CStr = c"/proc/self/fd/1020"; + let mut argv0 = [0_u8; 32_770]; + let pid = unsafe { libc::fork() }; + if pid < 0 { + return Err(Error::Spawn); + } + if pid == 0 { + unsafe { + if libc::close(read_pipe.raw()) != 0 { + libc::_exit(126); + } + if libc::setpgid(0, 0) != 0 { + libc::_exit(126); + } + let executable_fd = libc::fcntl( + executable.file.file.as_raw_fd(), + libc::F_DUPFD, + EXECUTABLE_FD, + ); + if executable_fd != EXECUTABLE_FD { + libc::_exit(126); + } + if libc::fcntl(executable_fd, libc::F_SETFD, libc::FD_CLOEXEC) != 0 { + libc::_exit(126); + } + if libc::fchdir(cwd.file.as_raw_fd()) != 0 + || libc::dup2(null_fd.raw(), libc::STDIN_FILENO) < 0 + || libc::dup2(write_pipe.raw(), libc::STDOUT_FILENO) < 0 + || libc::dup2(null_fd.raw(), libc::STDERR_FILENO) < 0 + { + libc::_exit(126); + } + if libc::close(write_pipe.raw()) != 0 || libc::close(null_fd.raw()) != 0 { + libc::_exit(126); + } + if libc::syscall(libc::SYS_close_range, 3_u32, 1019_u32, 0_u32) != 0 + || libc::syscall(libc::SYS_close_range, 1021_u32, u32::MAX, 0_u32) != 0 + { + libc::_exit(126); + } + let argv0_length = libc::readlink( + EXECUTABLE_FD_PATH.as_ptr(), + argv0.as_mut_ptr().cast(), + argv0.len() - 1, + ); + if argv0_length <= 0 { + libc::_exit(126); + } + let argv0_length = argv0_length as usize; + if argv0_length >= argv0.len() - 1 { + libc::_exit(126); + } + argv0[argv0_length] = 0; + argv[0] = argv0.as_ptr().cast(); + unsafe extern "C" { + fn fexecve( + fd: libc::c_int, + argv: *const *const libc::c_char, + envp: *const *const libc::c_char, + ) -> libc::c_int; + } + fexecve(executable_fd, argv.as_ptr(), env.as_ptr()); + libc::_exit(127); + } + } + let _ = unsafe { libc::setpgid(pid, pid) }; + let write_close = write_pipe.close_injected(TestClosePoint::ParentWrite); + let null_close = null_fd.close_injected(TestClosePoint::ParentNull); + if write_close.is_err() || null_close.is_err() { + must_settle_failed_group(pid, read_pipe, false); + std::process::abort(); + } + let (output, status) = drain_and_wait(pid, read_pipe, stdout_limit, output)?; + if !libc::WIFEXITED(status) || libc::WEXITSTATUS(status) != 0 { + #[cfg(test)] + eprintln!("linux platform child status={status} args={arguments:?}"); + return Err(Error::Exit); + } + recheck_executable(executable)?; + recheck_directory(cwd)?; + Ok(output) + } + + #[cfg(target_os = "macos")] + fn run_argv( + executable: &Executable, + cwd: &Directory, + arguments: &[CString], + stdout_limit: usize, + output: Vec, + process_arena: &mut PreparedProcessArena, + ) -> Result, Error> { + if arguments.len() > 32 || output.capacity() != stdout_limit || !output.is_empty() { + return Err(Error::Invalid); + } + consume_process_arena(process_arena)?; + #[repr(C)] + struct RegionInfo { + protection: u32, + max_protection: u32, + inheritance: u32, + flags: u32, + offset: u64, + behavior: u32, + user_wired: u32, + tag: u32, + resident: u32, + shared_private: u32, + swapped: u32, + dirtied: u32, + refs: u32, + shadow: u32, + share_mode: u32, + private_resident: u32, + shared_resident: u32, + object: u32, + depth: u32, + address: u64, + size: u64, + } + #[repr(C)] + #[derive(Clone, Copy)] + struct VnodeStat { + dev: u32, + mode: u16, + nlink: u16, + ino: u64, + uid: u32, + gid: u32, + atime: i64, + atime_ns: i64, + mtime: i64, + mtime_ns: i64, + ctime: i64, + ctime_ns: i64, + birth: i64, + birth_ns: i64, + size: i64, + blocks: i64, + block_size: i32, + flags: u32, + generation: u32, + rdev: u32, + spare: [i64; 2], + } + #[repr(C)] + #[derive(Clone, Copy)] + struct VnodeInfo { + stat: VnodeStat, + kind: i32, + pad: i32, + fsid: [i32; 2], + } + #[repr(C)] + #[derive(Clone, Copy)] + struct VnodePath { + info: VnodeInfo, + path: [libc::c_char; 1024], + } + #[repr(C)] + struct RegionPath { + region: RegionInfo, + vnode: VnodePath, + } + #[repr(C)] + struct VnodePaths { + cwd: VnodePath, + root: VnodePath, + } + unsafe extern "C" { + fn posix_spawn_file_actions_addfchdir_np( + actions: *mut libc::posix_spawn_file_actions_t, + fd: libc::c_int, + ) -> libc::c_int; + } + #[link(name = "proc")] + unsafe extern "C" { + fn proc_pidinfo( + pid: libc::c_int, + flavor: libc::c_int, + arg: u64, + buffer: *mut libc::c_void, + size: libc::c_int, + ) -> libc::c_int; + } + recheck_executable(executable)?; + recheck_directory(cwd)?; + let mut path = [0_u8; 1024]; + if unsafe { libc::fcntl(executable.file.file.as_raw_fd(), 50, path.as_mut_ptr()) } != 0 { + return Err(Error::Changed); + } + let executable_path = + unsafe { std::ffi::CStr::from_ptr(path.as_ptr().cast::()) }; + let mut pipe = [0; 2]; + if unsafe { libc::pipe(pipe.as_mut_ptr()) } != 0 { + return Err(Error::Spawn); + } + let read_pipe = CheckedFd::new(pipe[0]); + let write_pipe = CheckedFd::new(pipe[1]); + if injected_settlement_failure!(UnixPipeReadFcntl) { + return Err(Error::Spawn); + } + if unsafe { libc::fcntl(read_pipe.raw(), libc::F_SETFD, libc::FD_CLOEXEC) } != 0 { + return Err(Error::Spawn); + } + if injected_settlement_failure!(UnixPipeWriteFcntl) { + return Err(Error::Spawn); + } + if unsafe { libc::fcntl(write_pipe.raw(), libc::F_SETFD, libc::FD_CLOEXEC) } != 0 { + return Err(Error::Spawn); + } + let null_path = c"/dev/null"; + let null_fd = unsafe { libc::open(null_path.as_ptr(), libc::O_RDWR | libc::O_CLOEXEC) }; + if null_fd < 0 { + return Err(Error::Spawn); + } + let null_fd = CheckedFd::new(null_fd); + let mut actions = std::ptr::null_mut(); + let mut attributes = std::ptr::null_mut(); + let mut pid = 0; + let mut argv = [std::ptr::null_mut::(); 34]; + argv[0] = c"semaprax-native-rust-interop-tool".as_ptr().cast_mut(); + for (index, argument) in arguments.iter().enumerate() { + argv[index + 1] = argument.as_ptr().cast_mut(); + } + let env = [std::ptr::null_mut::()]; + let flags = libc::c_short::try_from( + libc::POSIX_SPAWN_START_SUSPENDED + | libc::POSIX_SPAWN_CLOEXEC_DEFAULT + | libc::POSIX_SPAWN_SETPGROUP, + ) + .map_err(|_| Error::Unsupported)?; + let spawn = unsafe { + let init = libc::posix_spawn_file_actions_init(&mut actions); + let attr_init = libc::posix_spawnattr_init(&mut attributes); + let actions_initialized = init == 0; + let attributes_initialized = attr_init == 0; + let configured = actions_initialized + && attributes_initialized + && libc::posix_spawnattr_setflags(&mut attributes, flags) == 0 + && libc::posix_spawnattr_setpgroup(&mut attributes, 0) == 0 + && posix_spawn_file_actions_addfchdir_np(&mut actions, cwd.file.as_raw_fd()) == 0 + && libc::posix_spawn_file_actions_adddup2(&mut actions, null_fd.raw(), 0) == 0 + && libc::posix_spawn_file_actions_adddup2(&mut actions, write_pipe.raw(), 1) == 0 + && libc::posix_spawn_file_actions_adddup2(&mut actions, null_fd.raw(), 2) == 0; + let result = if configured { + libc::posix_spawn( + &mut pid, + executable_path.as_ptr(), + &actions, + &attributes, + argv.as_ptr(), + env.as_ptr(), + ) + } else { + libc::EINVAL + }; + let actions_destroyed = !actions_initialized + || (libc::posix_spawn_file_actions_destroy(&mut actions) == 0 + && !injected_settlement_failure!(DarwinActionsDestroy)); + let attributes_destroyed = !attributes_initialized + || (libc::posix_spawnattr_destroy(&mut attributes) == 0 + && !injected_settlement_failure!(DarwinAttributesDestroy)); + (result, actions_destroyed && attributes_destroyed) + }; + let write_close = write_pipe.close_injected(TestClosePoint::ParentWrite); + let null_close = null_fd.close_injected(TestClosePoint::ParentNull); + if !spawn.1 || write_close.is_err() || null_close.is_err() { + if spawn.0 == 0 { + must_settle_failed_group(pid, read_pipe, false); + } else if read_pipe.close().is_err() { + std::process::abort(); + } + std::process::abort(); + } + if spawn.0 != 0 { + return Err(Error::Spawn); + } + let attest = (|| { + if injected_settlement_failure!(DarwinAttest) { + return Err(Error::Changed); + } + let mut cwd_info = std::mem::MaybeUninit::::zeroed(); + let cwd_size = libc::c_int::try_from(std::mem::size_of::()) + .map_err(|_| Error::Changed)?; + let cwd_returned = + unsafe { proc_pidinfo(pid, 9, 0, cwd_info.as_mut_ptr().cast(), cwd_size) }; + if cwd_returned != cwd_size { + return Err(Error::Changed); + } + let cwd_info = unsafe { cwd_info.assume_init() }; + if u64::from(cwd_info.cwd.info.stat.dev) != cwd.dev + || cwd_info.cwd.info.stat.ino != cwd.ino + || u32::from(cwd_info.cwd.info.stat.mode) != cwd.mode + || cwd_info.cwd.info.stat.generation != cwd.generation + || cwd_info.cwd.info.kind != 2 + { + return Err(Error::Changed); + } + let mut address = 0_u64; + let mut matching = 0_u32; + let mut enumerated = 0_u32; + let mut terminal = false; + let mut previous_end = None; + for _ in 0..4096 { + if previous_end.is_some_and(|end| end != address) { + return Err(Error::Changed); + } + let mut info = std::mem::MaybeUninit::::zeroed(); + let size = libc::c_int::try_from(std::mem::size_of::()) + .map_err(|_| Error::Changed)?; + unsafe { + *libc::__error() = 0; + } + let returned = + unsafe { proc_pidinfo(pid, 8, address, info.as_mut_ptr().cast(), size) }; + let query_errno = unsafe { *libc::__error() }; + if returned == 0 && query_errno == 0 { + terminal = enumerated != 0; + break; + } + if returned == 0 && query_errno == libc::EINVAL { + terminal = enumerated != 0 && matching == 1; + break; + } + if returned != size || query_errno != 0 { + return Err(Error::Changed); + } + let info = unsafe { info.assume_init() }; + if info.region.size == 0 || info.region.address < address { + return Err(Error::Changed); + } + enumerated = enumerated.checked_add(1).ok_or(Error::Changed)?; + if u64::from(info.vnode.info.stat.dev) == executable.file.dev + && info.vnode.info.stat.ino == executable.file.ino + && info.vnode.info.stat.generation == executable.file.generation + && info.vnode.info.stat.size >= 0 + && u64::try_from(info.vnode.info.stat.size).map_err(|_| Error::Changed)? + == executable.file.len + && u32::from(info.vnode.info.stat.mode) == executable.file.mode + && info.vnode.info.kind == 1 + && info.region.protection & libc::VM_PROT_EXECUTE as u32 != 0 + && info.region.offset == executable.slice_offset + { + matching = matching.checked_add(1).ok_or(Error::Changed)?; + } + address = info + .region + .address + .checked_add(info.region.size) + .ok_or(Error::Changed)?; + previous_end = Some(address); + if address == 0 { + return Err(Error::Changed); + } + } + if !terminal || matching != 1 { + return Err(Error::Changed); + } + recheck_executable(executable)?; + recheck_directory(cwd)?; + Ok(()) + })(); + let resumed = attest.is_ok() + && !injected_settlement_failure!(DarwinSigcont) + && unsafe { libc::kill(pid, libc::SIGCONT) } == 0; + if attest.is_err() || !resumed { + let selected = match attest { + Ok(()) => Error::Spawn, + Err(error) => error, + }; + must_settle_failed_group(pid, read_pipe, false); + return Err(selected); + } + let (output, status) = drain_and_wait(pid, read_pipe, stdout_limit, output)?; + #[cfg(test)] + if !libc::WIFEXITED(status) || libc::WEXITSTATUS(status) != 0 { + eprintln!("platform child status={status} args={arguments:?}"); + } + if !libc::WIFEXITED(status) || libc::WEXITSTATUS(status) != 0 { + #[cfg(test)] + eprintln!("darwin platform child status={status} args={arguments:?}"); + return Err(Error::Exit); + } + recheck_executable(executable)?; + recheck_directory(cwd)?; + Ok(output) + } + + fn argument(value: &str) -> Result { + CString::new(value).map_err(|_| Error::Invalid) + } + + pub fn rustc_version( + executable: &Executable, + cwd: &Directory, + maximum: usize, + ) -> Result, Error> { + let prepared = prepare_version_invocation("-vV", maximum.min(65_536))?; + let mut process_arena = prepare_process_arena(1)?; + version_prepared(executable, cwd, prepared, &mut process_arena) + } + + pub fn clang_version( + executable: &Executable, + cwd: &Directory, + maximum: usize, + ) -> Result, Error> { + let prepared = prepare_version_invocation("--version", maximum.min(65_536))?; + let mut process_arena = prepare_process_arena(1)?; + version_prepared(executable, cwd, prepared, &mut process_arena) + } + + pub fn version_prepared( + executable: &Executable, + cwd: &Directory, + prepared: PreparedVersionInvocation, + process_arena: &mut PreparedProcessArena, + ) -> Result, Error> { + let maximum = prepared.output.capacity(); + run_argv( + executable, + cwd, + &[prepared.argument], + maximum, + prepared.output, + process_arena, + ) + } + + pub fn prepare_c_compile_invocation( + target: &str, + input: &OsStr, + optimization: u8, + sanitizers: bool, + maximum: usize, + ) -> Result { + let _ = c_name(input)?; + if target.is_empty() + || !target + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')) + || !matches!(optimization, 0 | 2) + || (sanitizers && !cfg!(target_os = "linux")) + { + return Err(Error::Invalid); + } + let input = input.to_str().ok_or(Error::Invalid)?; + let mut values = [""; 16]; + let mut count = 0usize; + for value in ["-std=c11", "-target", target, "-Wall", "-Wextra", "-Werror"] { + values[count] = value; + count += 1; + } + if sanitizers { + for value in ["-fsanitize=address,undefined", "-fno-sanitize-recover=all"] { + values[count] = value; + count += 1; + } + } + for value in [ + if optimization == 0 { "-O0" } else { "-O2" }, + "-c", + input, + "-o", + "-", + ] { + values[count] = value; + count += 1; + } + #[cfg(target_os = "macos")] + for value in [ + "-isysroot", + "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk", + ] { + values[count] = value; + count += 1; + } + Ok(PreparedCCompileInvocation(prepare_command( + &values[..count], + maximum.min(33_554_432), + )?)) + } + + pub fn prepared_c_compile_owned_capacity(prepared: &PreparedCCompileInvocation) -> usize { + prepared_command_owned_capacity(&prepared.0) + } + + pub fn compile_c_prepared( + executable: &Executable, + cwd: &Directory, + prepared: PreparedCCompileInvocation, + process_arena: &mut PreparedProcessArena, + ) -> Result, Error> { + let maximum = prepared.0.output.capacity(); + run_argv( + executable, + cwd, + &prepared.0.arguments, + maximum, + prepared.0.output, + process_arena, + ) + } + + pub fn prepare_rust_compile_invocation( + target: &str, + source: &OsStr, + output: &OsStr, + ) -> Result { + let _ = c_name(source)?; + let output_name = prepare_relative_name(output)?; + if target.is_empty() + || !target + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')) + { + return Err(Error::Invalid); + } + let command = prepare_command( + &[ + "--edition=2021", + "-Dwarnings", + "--crate-type", + "staticlib", + "-C", + "panic=unwind", + "--target", + target, + source.to_str().ok_or(Error::Invalid)?, + "-o", + output.to_str().ok_or(Error::Invalid)?, + ], + 0, + )?; + Ok(PreparedRustCompileInvocation { + command, + output_name, + }) + } + + pub fn prepared_rust_compile_owned_capacity(prepared: &PreparedRustCompileInvocation) -> usize { + prepared_command_owned_capacity(&prepared.command) + .saturating_add(prepared.output_name.0.as_bytes_with_nul().len()) + } + + fn compile_rust_prepared_inner( + rustc: &Executable, + cwd: &Directory, + prepared: PreparedRustCompileInvocation, + process_arena: &mut PreparedProcessArena, + ) -> Result { + if hold_regular_file_name_prepared(cwd, &prepared.output_name).is_ok() { + return Err(Error::Exists); + } + if !run_argv( + rustc, + cwd, + &prepared.command.arguments, + 0, + prepared.command.output, + process_arena, + ) + .map_err(|error| trace_error("rustc", error))? + .is_empty() + { + return Err(Error::OutputLimit); + } + hold_regular_file_name_prepared(cwd, &prepared.output_name) + } + + pub fn compile_direct_rustc_prepared( + rustc: &DirectRustc, + cwd: &Directory, + prepared: PreparedRustCompileInvocation, + process_arena: &mut PreparedProcessArena, + ) -> Result { + recheck_directory(&rustc.sysroot)?; + compile_rust_prepared_inner(&rustc.executable, cwd, prepared, process_arena) + } + + #[allow(clippy::too_many_arguments)] + pub fn prepare_link_invocation( + target: &str, + harness: &OsStr, + c_object: &OsStr, + rust_archive: &OsStr, + output: &OsStr, + sanitizers: bool, + ) -> Result { + for name in [harness, c_object, rust_archive, output] { + let _ = c_name(name)?; + } + if target.is_empty() + || !target + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')) + || (sanitizers && !cfg!(target_os = "linux")) + { + return Err(Error::Invalid); + } + let mut values = [""; 20]; + let mut count = 0usize; + for value in ["-target", target] { + values[count] = value; + count += 1; + } + if sanitizers { + for value in ["-fsanitize=address,undefined", "-fno-sanitize-recover=all"] { + values[count] = value; + count += 1; + } + } + #[cfg(target_os = "linux")] + { + values[count] = LINUX_LINKER_ARGUMENT; + count += 1; + } + #[cfg(target_os = "macos")] + { + values[count] = "-Wl,-no_warn_duplicate_libraries"; + count += 1; + } + for value in [ + harness.to_str().ok_or(Error::Invalid)?, + c_object.to_str().ok_or(Error::Invalid)?, + rust_archive.to_str().ok_or(Error::Invalid)?, + "-o", + output.to_str().ok_or(Error::Invalid)?, + ] { + values[count] = value; + count += 1; + } + #[cfg(target_os = "linux")] + for value in LINUX_RUST_STATICLIB_NATIVE_LIBS { + values[count] = value; + count += 1; + } + #[cfg(target_os = "macos")] + for value in [ + "-isysroot", + "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk", + ] { + values[count] = value; + count += 1; + } + Ok(PreparedLinkInvocation { + command: prepare_command(&values[..count], 0)?, + output_name: prepare_relative_name(output)?, + }) + } + + pub fn prepared_link_owned_capacity(prepared: &PreparedLinkInvocation) -> usize { + prepared_command_owned_capacity(&prepared.command) + .saturating_add(prepared.output_name.0.as_bytes_with_nul().len()) + } + + pub fn link_prepared( + clang: &Executable, + cwd: &Directory, + prepared: PreparedLinkInvocation, + process_arena: &mut PreparedProcessArena, + ) -> Result { + if hold_regular_file_name_prepared(cwd, &prepared.output_name).is_ok() { + return Err(Error::Exists); + } + if !run_argv( + clang, + cwd, + &prepared.command.arguments, + 0, + prepared.command.output, + process_arena, + ) + .map_err(|error| trace_error("clang-link", error))? + .is_empty() + { + return Err(Error::OutputLimit); + } + let file = hold_regular_file_name_prepared(cwd, &prepared.output_name)?; + if file.mode & 0o111 == 0 { + return Err(Error::Invalid); + } + let (slice_offset, slice_size) = executable_slice(&file)?; + Ok(Executable { + file, + slice_offset, + slice_size, + }) + } + + pub fn prepare_run_invocation() -> Result { + Ok(PreparedRunInvocation(prepare_command(&[], 0)?)) + } + + pub fn prepared_run_owned_capacity(prepared: &PreparedRunInvocation) -> usize { + prepared_command_owned_capacity(&prepared.0) + } + + pub fn run_prepared( + executable: &Executable, + cwd: &Directory, + prepared: PreparedRunInvocation, + process_arena: &mut PreparedProcessArena, + ) -> Result<(), Error> { + if run_argv( + executable, + cwd, + &prepared.0.arguments, + 0, + prepared.0.output, + process_arena, + )? + .is_empty() + { + Ok(()) + } else { + Err(Error::OutputLimit) + } + } + + pub fn compile_c_to_stdout( + executable: &Executable, + cwd: &Directory, + target: &str, + input: &OsStr, + optimization: u8, + sanitizers: bool, + maximum: usize, + ) -> Result, Error> { + let _ = c_name(input)?; + if target.is_empty() + || !target + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')) + || !matches!(optimization, 0 | 2) + { + return Err(Error::Invalid); + } + let input = input.to_str().ok_or(Error::Invalid)?; + let mut arguments = vec![ + argument("-std=c11")?, + argument("-target")?, + argument(target)?, + argument("-Wall")?, + argument("-Wextra")?, + argument("-Werror")?, + argument(if optimization == 0 { "-O0" } else { "-O2" })?, + argument("-c")?, + argument(input)?, + argument("-o")?, + argument("-")?, + ]; + if sanitizers { + if !cfg!(target_os = "linux") { + return Err(Error::Unsupported); + } + arguments.insert(6, argument("-fsanitize=address,undefined")?); + arguments.insert(7, argument("-fno-sanitize-recover=all")?); + } + #[cfg(target_os = "macos")] + arguments.extend([ + argument("-isysroot")?, + argument("/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk")?, + ]); + let mut process_arena = prepare_process_arena(1)?; + run_argv( + executable, + cwd, + &arguments, + maximum.min(33_554_432), + Vec::new(), + &mut process_arena, + ) + } + + pub fn execute_harness(executable: &Executable, cwd: &Directory) -> Result<(), Error> { + let mut process_arena = prepare_process_arena(1)?; + if run_argv(executable, cwd, &[], 0, Vec::new(), &mut process_arena)?.is_empty() { + Ok(()) + } else { + Err(Error::OutputLimit) + } + } + + #[allow(clippy::too_many_arguments)] + pub fn link_harness( + clang: &Executable, + cwd: &Directory, + target: &str, + harness: &OsStr, + c_object: &OsStr, + rust_archive: &OsStr, + output: &OsStr, + sanitizers: bool, + ) -> Result { + for name in [harness, c_object, rust_archive, output] { + let _ = c_name(name)?; + } + if target.is_empty() + || !target + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')) + || hold_regular_file(cwd, output).is_ok() + { + return Err(Error::Invalid); + } + if sanitizers && !cfg!(target_os = "linux") { + return Err(Error::Unsupported); + } + let mut arguments = vec![ + argument("-target")?, + argument(target)?, + argument(harness.to_str().ok_or(Error::Invalid)?)?, + argument(c_object.to_str().ok_or(Error::Invalid)?)?, + argument(rust_archive.to_str().ok_or(Error::Invalid)?)?, + argument("-o")?, + argument(output.to_str().ok_or(Error::Invalid)?)?, + ]; + #[cfg(target_os = "linux")] + arguments.insert(2, argument(LINUX_LINKER_ARGUMENT)?); + #[cfg(target_os = "linux")] + arguments.extend( + LINUX_RUST_STATICLIB_NATIVE_LIBS + .into_iter() + .map(argument) + .collect::, _>>()?, + ); + #[cfg(target_os = "macos")] + arguments.insert(2, argument("-Wl,-no_warn_duplicate_libraries")?); + if sanitizers { + arguments.insert(2, argument("-fsanitize=address,undefined")?); + arguments.insert(3, argument("-fno-sanitize-recover=all")?); + } + #[cfg(target_os = "macos")] + arguments.extend([ + argument("-isysroot")?, + argument("/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk")?, + ]); + let mut process_arena = prepare_process_arena(1)?; + if !run_argv(clang, cwd, &arguments, 0, Vec::new(), &mut process_arena) + .map_err(|error| trace_error("clang-link", error))? + .is_empty() + { + return Err(Error::OutputLimit); + } + hold_executable(cwd, output) + } +} + +#[cfg(windows)] +mod platform { + use super::*; + use sha2::{Digest as _, Sha256}; + use std::io::{Read as _, Seek as _, SeekFrom}; + use std::os::windows::ffi::OsStrExt as _; + use std::os::windows::fs::FileExt as _; + use std::os::windows::io::{AsRawHandle as _, FromRawHandle as _, IntoRawHandle as _}; + use windows_sys::Wdk::Foundation::OBJECT_ATTRIBUTES; + use windows_sys::Wdk::Storage::FileSystem::{ + FileLinkInformationEx, NtCreateFile, NtSetInformationFile, FILE_CREATE, + FILE_DIRECTORY_FILE, FILE_NON_DIRECTORY_FILE, FILE_OPEN, FILE_OPEN_REPARSE_POINT, + FILE_SYNCHRONOUS_IO_NONALERT, + }; + use windows_sys::Win32::Foundation::{ + CloseHandle, GetLastError, SetHandleInformation, ERROR_BROKEN_PIPE, + ERROR_INSUFFICIENT_BUFFER, ERROR_NO_MORE_FILES, ERROR_PIPE_NOT_CONNECTED, HANDLE, + HANDLE_FLAG_INHERIT, INVALID_HANDLE_VALUE, STATUS_OBJECT_NAME_COLLISION, UNICODE_STRING, + WAIT_FAILED, WAIT_OBJECT_0, WAIT_TIMEOUT, + }; + use windows_sys::Win32::Security::SECURITY_ATTRIBUTES; + use windows_sys::Win32::Storage::FileSystem::{ + CreateFileW, FileDispositionInfoEx, FileIdBothDirectoryInfo, + FileIdBothDirectoryRestartInfo, FileIdExtdDirectoryInfo, FileIdExtdDirectoryRestartInfo, + FileIdInfo, FileRenameInfoEx, GetFileInformationByHandle, GetFileInformationByHandleEx, + GetFinalPathNameByHandleW, ReadFile, SetFileInformationByHandle, + BY_HANDLE_FILE_INFORMATION, DELETE, FILE_ADD_FILE, FILE_ADD_SUBDIRECTORY, + FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_NORMAL, FILE_ATTRIBUTE_REPARSE_POINT, + FILE_DELETE_CHILD, FILE_DISPOSITION_FLAG_DELETE, FILE_DISPOSITION_FLAG_POSIX_SEMANTICS, + FILE_DISPOSITION_INFO_EX, FILE_EXECUTE, FILE_FLAG_BACKUP_SEMANTICS, + FILE_FLAG_OPEN_REPARSE_POINT, FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_ID_BOTH_DIR_INFO, + FILE_ID_EXTD_DIR_INFO, FILE_ID_INFO, FILE_LIST_DIRECTORY, FILE_READ_ATTRIBUTES, + FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_TRAVERSE, FILE_WRITE_ATTRIBUTES, + OPEN_EXISTING, SYNCHRONIZE, + }; + use windows_sys::Win32::System::JobObjects::{ + AssignProcessToJobObject, CreateJobObjectW, JobObjectBasicAccountingInformation, + JobObjectExtendedLimitInformation, QueryInformationJobObject, SetInformationJobObject, + TerminateJobObject, JOBOBJECT_BASIC_ACCOUNTING_INFORMATION, + JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, + }; + use windows_sys::Win32::System::Pipes::{CreatePipe, PeekNamedPipe}; + use windows_sys::Win32::System::Threading::{ + CreateProcessW, DeleteProcThreadAttributeList, InitializeProcThreadAttributeList, + QueryFullProcessImageNameW, ResumeThread, TerminateProcess, UpdateProcThreadAttribute, + WaitForSingleObject, CREATE_SUSPENDED, CREATE_UNICODE_ENVIRONMENT, + EXTENDED_STARTUPINFO_PRESENT, PROCESS_INFORMATION, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + STARTF_USESTDHANDLES, STARTUPINFOEXW, + }; + use windows_sys::Win32::System::IO::IO_STATUS_BLOCK; + + const NORMAL_FILE_FLAGS: u32 = FILE_FLAG_OPEN_REPARSE_POINT; + const DIRECTORY_FLAGS: u32 = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT; + const DIRECTORY_READ_ACCESS: u32 = + FILE_LIST_DIRECTORY | FILE_TRAVERSE | FILE_READ_ATTRIBUTES | SYNCHRONIZE; + const DIRECTORY_OWNED_ACCESS: u32 = DIRECTORY_READ_ACCESS + | FILE_ADD_FILE + | FILE_ADD_SUBDIRECTORY + | FILE_DELETE_CHILD + | FILE_WRITE_ATTRIBUTES + | DELETE; + const REGULAR_READ_ACCESS: u32 = FILE_GENERIC_READ | FILE_EXECUTE | SYNCHRONIZE; + const REGULAR_OWNED_ACCESS: u32 = FILE_GENERIC_READ | FILE_GENERIC_WRITE | DELETE | SYNCHRONIZE; + const HELD_SHARE: u32 = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; + const OBJ_CASE_INSENSITIVE: u32 = 0x40; + + #[derive(Clone, Copy, Eq, PartialEq)] + struct Identity { + volume: u64, + file_id: [u8; 16], + attributes: u32, + length: u64, + } + + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct DirectoryIdentity { + volume: u64, + file_id: [u8; 16], + stable_attributes: u32, + } + + pub struct Directory { + file: File, + identity: DirectoryIdentity, + } + + pub struct RegularFile { + file: File, + identity: Identity, + digest: [u8; 32], + } + + pub struct Executable { + file: RegularFile, + } + + pub struct RustcDiscovery(Executable); + + pub struct DirectRustc { + executable: Executable, + sysroot: Directory, + } + + pub struct PreparedRelativeName(Vec); + + pub struct PreparedRelativeNameArena { + units: Vec, + maximum: usize, + } + + pub struct PreparedVersionInvocation { + command_line: Vec, + output: Vec, + } + + pub struct PreparedSysrootInvocation(PreparedVersionInvocation); + pub struct PreparedRustcVersionInvocation(PreparedVersionInvocation); + + const PROCESS_PATH_UNITS: usize = 32_769; + const MAX_PROCESS_ENVIRONMENT_UNITS: usize = 32_768; + const MAX_PROCESS_ATTRIBUTE_BYTES: usize = 1_048_576; + + pub struct PreparedProcessArena { + application: Vec, + cwd: Vec, + environment: Vec, + attributes: Vec, + attribute_bytes: usize, + remaining: usize, + } + + pub struct PreparedProcessArenaPlan { + uses: usize, + attribute_bytes: usize, + attribute_words: usize, + environment_units: usize, + owned_capacity: usize, + } + + pub struct PreparedToolResolver { + candidate: Vec, + canonical: Vec, + display: String, + fallback: Vec, + maximum: usize, + } + + struct PreparedCommand { + arguments: Vec, + command_line: Vec, + output: Vec, + } + + pub struct PreparedCCompileInvocation(PreparedCommand); + pub struct PreparedRustCompileInvocation { + command: PreparedCommand, + output_name: PreparedRelativeName, + } + pub struct PreparedLinkInvocation { + command: PreparedCommand, + output_name: PreparedRelativeName, + } + pub struct PreparedRunInvocation(PreparedCommand); + + fn prepare_command(values: &[&str], output_capacity: usize) -> Result { + let mut arguments = Vec::with_capacity(values.len()); + if arguments.capacity() != values.len() { + return Err(Error::OutputLimit); + } + for value in values { + arguments.push((*value).to_owned()); + } + let command_line = windows_command_line(&arguments)?; + let output = Vec::with_capacity(output_capacity); + if output.capacity() != output_capacity { + return Err(Error::OutputLimit); + } + Ok(PreparedCommand { + arguments, + command_line, + output, + }) + } + + fn prepared_command_owned_capacity(command: &PreparedCommand) -> usize { + command + .arguments + .capacity() + .saturating_mul(std::mem::size_of::()) + .saturating_add( + command + .arguments + .iter() + .map(String::capacity) + .sum::(), + ) + .saturating_add( + command + .command_line + .capacity() + .saturating_mul(std::mem::size_of::()), + ) + .saturating_add(command.output.capacity()) + } + + pub fn prepare_tool_resolver( + fallback: &str, + maximum: usize, + ) -> Result { + if maximum == 0 + || maximum > 32_768 + || fallback.is_empty() + || fallback.contains(['/', '\\', '\0']) + { + return Err(Error::Invalid); + } + let candidate = Vec::with_capacity(maximum); + let canonical = Vec::with_capacity(maximum); + let display_capacity = maximum.checked_mul(3).ok_or(Error::OutputLimit)?; + let display = String::with_capacity(display_capacity); + let fallback = fallback.encode_utf16().collect::>(); + if candidate.capacity() != maximum + || canonical.capacity() != maximum + || display.capacity() != display_capacity + { + return Err(Error::OutputLimit); + } + Ok(PreparedToolResolver { + candidate, + canonical, + display, + fallback, + maximum, + }) + } + + pub fn prepared_tool_resolver_owned_capacity(prepared: &PreparedToolResolver) -> usize { + prepared + .candidate + .capacity() + .saturating_add(prepared.canonical.capacity()) + .saturating_add(prepared.fallback.capacity()) + .saturating_mul(std::mem::size_of::()) + .saturating_add(prepared.display.capacity()) + } + + pub fn prepare_version_invocation( + argument: &str, + maximum: usize, + ) -> Result { + if maximum > 65_536 { + return Err(Error::OutputLimit); + } + let command_line = windows_command_line(&[argument.to_owned()])?; + let output = Vec::with_capacity(maximum); + if output.capacity() != maximum { + return Err(Error::OutputLimit); + } + Ok(PreparedVersionInvocation { + command_line, + output, + }) + } + + pub fn prepared_version_owned_capacity(prepared: &PreparedVersionInvocation) -> usize { + prepared + .command_line + .capacity() + .saturating_mul(std::mem::size_of::()) + .saturating_add(prepared.output.capacity()) + } + + pub fn prepare_sysroot_invocation(maximum: usize) -> Result { + prepare_version_invocation("--print=sysroot", maximum).map(PreparedSysrootInvocation) + } + + pub fn prepare_rustc_version_invocation( + maximum: usize, + ) -> Result { + prepare_version_invocation("-vV", maximum).map(PreparedRustcVersionInvocation) + } + + pub fn prepared_sysroot_owned_capacity(prepared: &PreparedSysrootInvocation) -> usize { + prepared_version_owned_capacity(&prepared.0) + } + + pub fn prepared_rustc_version_owned_capacity( + prepared: &PreparedRustcVersionInvocation, + ) -> usize { + prepared_version_owned_capacity(&prepared.0) + } + + pub(super) fn process_arena_plan( + uses: usize, + attribute_bytes: usize, + environment_units: usize, + ) -> Result { + if uses == 0 || uses > 32 { + return Err(Error::Invalid); + } + if attribute_bytes == 0 { + return Err(Error::Unsupported); + } + if attribute_bytes > MAX_PROCESS_ATTRIBUTE_BYTES { + return Err(Error::OutputLimit); + } + if !(2..=MAX_PROCESS_ENVIRONMENT_UNITS).contains(&environment_units) { + return Err(Error::OutputLimit); + } + let attribute_words = attribute_bytes + .checked_add(std::mem::size_of::() - 1) + .and_then(|bytes| bytes.checked_div(std::mem::size_of::())) + .ok_or(Error::OutputLimit)?; + let path_capacity = PROCESS_PATH_UNITS + .checked_mul(std::mem::size_of::()) + .and_then(|bytes| bytes.checked_mul(2)) + .ok_or(Error::OutputLimit)?; + let owned_capacity = attribute_words + .checked_mul(std::mem::size_of::()) + .and_then(|bytes| path_capacity.checked_add(bytes)) + .and_then(|bytes| { + environment_units + .checked_mul(std::mem::size_of::()) + .and_then(|environment| bytes.checked_add(environment)) + }) + .ok_or(Error::OutputLimit)?; + Ok(PreparedProcessArenaPlan { + uses, + attribute_bytes, + attribute_words, + environment_units, + owned_capacity, + }) + } + + fn process_environment_units( + include: Option<&OsStr>, + libraries: Option<&OsStr>, + ) -> Result { + match (include, libraries) { + (None, None) => Ok(2), + (Some(include), Some(libraries)) => { + let include_units = include.encode_wide().try_fold(0usize, |count, unit| { + if unit == 0 { + Err(Error::Invalid) + } else { + count.checked_add(1).ok_or(Error::OutputLimit) + } + })?; + let library_units = libraries.encode_wide().try_fold(0usize, |count, unit| { + if unit == 0 { + Err(Error::Invalid) + } else { + count.checked_add(1).ok_or(Error::OutputLimit) + } + })?; + 8usize + .checked_add(include_units) + .and_then(|units| units.checked_add(1)) + .and_then(|units| units.checked_add(4)) + .and_then(|units| units.checked_add(library_units)) + .and_then(|units| units.checked_add(2)) + .filter(|units| *units <= MAX_PROCESS_ENVIRONMENT_UNITS) + .ok_or(Error::OutputLimit) + } + _ => Err(Error::Invalid), + } + } + + pub fn prepare_process_arena_plan(uses: usize) -> Result { + if uses == 0 || uses > 32 { + return Err(Error::Invalid); + } + let mut attribute_bytes = 0_usize; + let initialized = unsafe { + InitializeProcThreadAttributeList(std::ptr::null_mut(), 1, 0, &mut attribute_bytes) + }; + if initialized != 0 || unsafe { GetLastError() } != ERROR_INSUFFICIENT_BUFFER { + return Err(Error::Unsupported); + } + process_arena_plan(uses, attribute_bytes, 2) + } + + pub fn prepare_process_arena_plan_with_environment( + uses: usize, + include: Option<&OsStr>, + libraries: Option<&OsStr>, + ) -> Result { + let environment_units = process_environment_units(include, libraries)?; + let mut attribute_bytes = 0_usize; + let initialized = unsafe { + InitializeProcThreadAttributeList(std::ptr::null_mut(), 1, 0, &mut attribute_bytes) + }; + if initialized != 0 || unsafe { GetLastError() } != ERROR_INSUFFICIENT_BUFFER { + return Err(Error::Unsupported); + } + process_arena_plan(uses, attribute_bytes, environment_units) + } + + pub fn prepared_process_arena_plan_capacity(plan: &PreparedProcessArenaPlan) -> usize { + plan.owned_capacity + } + + pub fn materialize_process_arena( + plan: PreparedProcessArenaPlan, + ) -> Result { + materialize_process_arena_with_environment(plan, None, None) + } + + pub fn materialize_process_arena_with_environment( + plan: PreparedProcessArenaPlan, + include: Option<&OsStr>, + libraries: Option<&OsStr>, + ) -> Result { + if process_environment_units(include, libraries)? != plan.environment_units { + return Err(Error::Invalid); + } + let application = Vec::with_capacity(PROCESS_PATH_UNITS); + let cwd = Vec::with_capacity(PROCESS_PATH_UNITS); + let mut environment = Vec::with_capacity(plan.environment_units); + match (include, libraries) { + (None, None) => environment.extend([0, 0]), + (Some(include), Some(libraries)) => { + environment.extend("INCLUDE=".encode_utf16()); + environment.extend(include.encode_wide()); + environment.push(0); + environment.extend("LIB=".encode_utf16()); + environment.extend(libraries.encode_wide()); + environment.extend([0, 0]); + } + _ => return Err(Error::Invalid), + } + let attributes = Vec::with_capacity(plan.attribute_words); + if application.capacity() != PROCESS_PATH_UNITS + || cwd.capacity() != PROCESS_PATH_UNITS + || environment.capacity() != plan.environment_units + || environment.len() != plan.environment_units + || attributes.capacity() != plan.attribute_words + { + return Err(Error::OutputLimit); + } + Ok(PreparedProcessArena { + application, + cwd, + environment, + attributes, + attribute_bytes: plan.attribute_bytes, + remaining: plan.uses, + }) + } + + pub fn prepare_process_arena(uses: usize) -> Result { + materialize_process_arena(prepare_process_arena_plan(uses)?) + } + + pub fn prepared_process_arena_owned_capacity(prepared: &PreparedProcessArena) -> usize { + prepared + .application + .capacity() + .saturating_mul(std::mem::size_of::()) + .saturating_add( + prepared + .environment + .capacity() + .saturating_mul(std::mem::size_of::()), + ) + .saturating_add( + prepared + .cwd + .capacity() + .saturating_mul(std::mem::size_of::()), + ) + .saturating_add( + prepared + .attributes + .capacity() + .saturating_mul(std::mem::size_of::()), + ) + } + + pub fn prepared_process_arena_remaining(prepared: &PreparedProcessArena) -> usize { + prepared.remaining + } + + pub(super) fn consume_process_arena(prepared: &mut PreparedProcessArena) -> Result<(), Error> { + let attribute_words = prepared + .attribute_bytes + .checked_add(std::mem::size_of::() - 1) + .and_then(|bytes| bytes.checked_div(std::mem::size_of::())) + .ok_or(Error::OutputLimit)?; + if prepared.application.capacity() != PROCESS_PATH_UNITS + || prepared.cwd.capacity() != PROCESS_PATH_UNITS + || !(2..=MAX_PROCESS_ENVIRONMENT_UNITS).contains(&prepared.environment.capacity()) + || prepared.environment.len() != prepared.environment.capacity() + || !prepared.environment.ends_with(&[0, 0]) + || prepared.attributes.capacity() != attribute_words + { + return Err(Error::OutputLimit); + } + prepared.remaining = prepared + .remaining + .checked_sub(1) + .ok_or(Error::OutputLimit)?; + prepared.application.clear(); + prepared.cwd.clear(); + prepared.attributes.clear(); + Ok(()) + } + + pub struct PreparedDiscardNames { + names: [Option; N], + } + + pub struct PreparedLinkOrCopy { + destination_index: usize, + storage: Box<[usize]>, + total: usize, + #[cfg(debug_assertions)] + fail_before_authentication: bool, + } + + const INVENTORY_EXACT_ARENA_WORDS: usize = 8192; + + pub struct PreparedInventoryExact { + names: [Option; N], + bindings: [(usize, usize); N], + storage: Box<[u64]>, + directory_identity: Option, + remaining: u8, + } + + pub struct PreparedPublishDirectory { + storage: Box<[usize]>, + total: usize, + name_units: usize, + exact_capacity: usize, + remaining: u8, + #[cfg(debug_assertions)] + fail_before_open: bool, + #[cfg(debug_assertions)] + fail_information: bool, + #[cfg(debug_assertions)] + fail_close: bool, + #[cfg(debug_assertions)] + fail_rename: bool, + } + + fn prepared_name_bindings( + names: &PreparedDiscardNames, + ) -> Result<[(usize, usize); N], Error> { + let mut bindings = [(0, 0); N]; + for (index, binding) in bindings.iter_mut().enumerate() { + let name = prepared_discard_name(names, index)?; + *binding = (name.0.as_ptr() as usize, name.0.len()); + } + Ok(bindings) + } + + pub fn inventory_exact_required_capacity( + names: &PreparedDiscardNames, + ) -> Result { + let names = (0..N).try_fold(0usize, |total, index| { + total + .checked_add( + prepared_discard_name(names, index)? + .0 + .len() + .checked_mul(std::mem::size_of::()) + .ok_or(Error::OutputLimit)?, + ) + .ok_or(Error::OutputLimit) + })?; + names + .checked_add( + INVENTORY_EXACT_ARENA_WORDS + .checked_mul(std::mem::size_of::()) + .ok_or(Error::OutputLimit)?, + ) + .ok_or(Error::OutputLimit) + } + + pub fn prepare_inventory_exact( + names: &PreparedDiscardNames, + ) -> Result, Error> { + let bindings = prepared_name_bindings(names)?; + let mut copied = [const { None }; N]; + for (index, slot) in copied.iter_mut().enumerate() { + let source = prepared_discard_name(names, index)?; + let mut units = Vec::with_capacity(source.0.len()); + units.extend_from_slice(&source.0); + if units.capacity() != source.0.len() { + return Err(Error::OutputLimit); + } + *slot = Some(PreparedRelativeName(units)); + } + let storage = vec![0_u64; INVENTORY_EXACT_ARENA_WORDS].into_boxed_slice(); + Ok(PreparedInventoryExact { + names: copied, + bindings, + storage, + directory_identity: None, + remaining: 2, + }) + } + + pub fn prepared_inventory_exact_owned_capacity( + prepared: &PreparedInventoryExact, + ) -> usize { + prepared + .names + .iter() + .filter_map(Option::as_ref) + .map(|name| name.0.capacity().saturating_mul(std::mem::size_of::())) + .sum::() + .saturating_add( + prepared + .storage + .len() + .saturating_mul(std::mem::size_of::()), + ) + } + + pub fn prepared_inventory_exact_remaining( + prepared: &PreparedInventoryExact, + ) -> u8 { + prepared.remaining + } + + fn publish_information_layout(name_units: usize) -> Result<(usize, usize), Error> { + let name_bytes = name_units.checked_mul(2).ok_or(Error::OutputLimit)?; + let fixed = std::mem::offset_of!(NamedInformation, file_name); + let total = fixed + .checked_add(name_bytes) + .ok_or(Error::OutputLimit)? + .max(std::mem::size_of::()); + let words = total + .checked_add(std::mem::size_of::() - 1) + .ok_or(Error::OutputLimit)? + / std::mem::size_of::(); + Ok((total, words)) + } + + pub fn publish_directory_required_capacity(name: &OsStr) -> Result { + let text = prepared_normal_name(name)?; + let (_, words) = publish_information_layout(text.encode_utf16().count())?; + words + .checked_mul(std::mem::size_of::()) + .ok_or(Error::OutputLimit) + } + + pub fn prepare_publish_directory(name: &OsStr) -> Result { + let text = prepared_normal_name(name)?; + let name_units = text.encode_utf16().count(); + let (total, words) = publish_information_layout(name_units)?; + let exact_capacity = words + .checked_mul(std::mem::size_of::()) + .ok_or(Error::OutputLimit)?; + let mut storage = vec![0_usize; words].into_boxed_slice(); + let information = storage.as_mut_ptr().cast::(); + unsafe { + (*information).flags = 0; + (*information).root_directory = std::ptr::null_mut(); + (*information).file_name_length = + u32::try_from(name_units.checked_mul(2).ok_or(Error::OutputLimit)?) + .map_err(|_| Error::OutputLimit)?; + let destination = std::ptr::addr_of_mut!((*information).file_name).cast::(); + for (index, unit) in text.encode_utf16().enumerate() { + destination.add(index).write(unit); + } + } + Ok(PreparedPublishDirectory { + storage, + total, + name_units, + exact_capacity, + remaining: 1, + #[cfg(debug_assertions)] + fail_before_open: false, + #[cfg(debug_assertions)] + fail_information: false, + #[cfg(debug_assertions)] + fail_close: false, + #[cfg(debug_assertions)] + fail_rename: false, + }) + } + + pub fn prepared_publish_directory_owned_capacity(prepared: &PreparedPublishDirectory) -> usize { + prepared + .storage + .len() + .saturating_mul(std::mem::size_of::()) + } + + pub fn prepared_publish_directory_remaining(prepared: &PreparedPublishDirectory) -> u8 { + prepared.remaining + } + + #[cfg(debug_assertions)] + pub fn inject_publish_directory_failure( + prepared: &mut PreparedPublishDirectory, + point: u8, + ) -> Result<(), Error> { + match point { + 1 => prepared.fail_before_open = true, + 2 => prepared.fail_information = true, + 3 => prepared.fail_close = true, + 4 => prepared.fail_rename = true, + _ => return Err(Error::Invalid), + } + Ok(()) + } + + pub fn prepare_link_or_copy( + names: &PreparedDiscardNames, + destination_index: usize, + ) -> Result { + let name = prepared_discard_name(names, destination_index)?; + let (total, words) = link_information_layout(name)?; + let name_bytes = name.0.len().checked_mul(2).ok_or(Error::OutputLimit)?; + let mut storage = vec![0_usize; words].into_boxed_slice(); + let information = storage.as_mut_ptr().cast::(); + unsafe { + (*information).flags = 0; + (*information).root_directory = std::ptr::null_mut(); + (*information).file_name_length = + u32::try_from(name_bytes).map_err(|_| Error::OutputLimit)?; + std::ptr::copy_nonoverlapping( + name.0.as_ptr(), + std::ptr::addr_of_mut!((*information).file_name).cast::(), + name.0.len(), + ); + } + Ok(PreparedLinkOrCopy { + destination_index, + storage, + total, + #[cfg(debug_assertions)] + fail_before_authentication: false, + }) + } + + fn link_information_layout(name: &PreparedRelativeName) -> Result<(usize, usize), Error> { + let name_bytes = name.0.len().checked_mul(2).ok_or(Error::OutputLimit)?; + let fixed = std::mem::offset_of!(NamedInformation, file_name); + let total = fixed + .checked_add(name_bytes) + .ok_or(Error::OutputLimit)? + .max(std::mem::size_of::()); + let words = total + .checked_add(std::mem::size_of::() - 1) + .ok_or(Error::OutputLimit)? + / std::mem::size_of::(); + Ok((total, words)) + } + + pub fn link_or_copy_required_capacity( + names: &PreparedDiscardNames, + destination_index: usize, + ) -> Result { + let name = prepared_discard_name(names, destination_index)?; + let (_, words) = link_information_layout(name)?; + words + .checked_mul(std::mem::size_of::()) + .ok_or(Error::OutputLimit) + } + + pub fn prepared_link_or_copy_owned_capacity(prepared: &PreparedLinkOrCopy) -> usize { + prepared + .storage + .len() + .saturating_mul(std::mem::size_of::()) + } + + #[cfg(debug_assertions)] + pub fn inject_link_or_copy_failure_before_authentication(prepared: &mut PreparedLinkOrCopy) { + prepared.fail_before_authentication = true; + } + + fn ascii_fold(value: u16) -> u16 { + if value >= u16::from(b'a') && value <= u16::from(b'z') { + value - u16::from(b'a' - b'A') + } else { + value + } + } + + fn prepared_names_equal(left: &PreparedRelativeName, right: &PreparedRelativeName) -> bool { + left.0.len() == right.0.len() + && left + .0 + .iter() + .zip(&right.0) + .all(|(left, right)| ascii_fold(*left) == ascii_fold(*right)) + } + + fn prepared_discard_name( + prepared: &PreparedDiscardNames, + index: usize, + ) -> Result<&PreparedRelativeName, Error> { + prepared + .names + .get(index) + .and_then(Option::as_ref) + .ok_or(Error::Invalid) + } + + fn prepared_matches_slice(expected: &PreparedRelativeName, actual: &[u16]) -> bool { + expected.0.len() == actual.len() + && expected + .0 + .iter() + .zip(actual) + .all(|(expected, actual)| ascii_fold(*expected) == ascii_fold(*actual)) + } + + fn prepared_normal_name(name: &OsStr) -> Result<&str, Error> { + let text = name.to_str().ok_or(Error::Invalid)?; + if text.is_empty() + || !text.is_ascii() + || matches!(text, "." | "..") + || text.contains(['/', '\\', '\0']) + || text.ends_with([' ', '.']) + || text.contains(':') + { + return Err(Error::Invalid); + } + let stem = text.split('.').next().ok_or(Error::Invalid)?; + if ["CON", "PRN", "AUX", "NUL", "CLOCK$"] + .iter() + .any(|reserved| stem.eq_ignore_ascii_case(reserved)) + || (stem.len() == 4 + && (stem[..3].eq_ignore_ascii_case("COM") || stem[..3].eq_ignore_ascii_case("LPT")) + && matches!(stem.as_bytes()[3], b'1'..=b'9')) + { + return Err(Error::Invalid); + } + Ok(text) + } + + pub fn prepare_relative_name(name: &OsStr) -> Result { + let text = prepared_normal_name(name)?; + let exact = text.encode_utf16().count(); + let mut encoded = Vec::with_capacity(exact); + encoded.extend(text.encode_utf16()); + if encoded.len() != exact || encoded.capacity() != exact { + return Err(Error::OutputLimit); + } + Ok(PreparedRelativeName(encoded)) + } + + pub fn prepare_relative_name_arena(maximum: usize) -> Result { + let units = Vec::with_capacity(maximum); + if units.capacity() != maximum { + return Err(Error::OutputLimit); + } + Ok(PreparedRelativeNameArena { units, maximum }) + } + + pub fn set_relative_name_arena( + arena: &mut PreparedRelativeNameArena, + name: &OsStr, + ) -> Result<(), Error> { + let text = prepared_normal_name(name)?; + if text.encode_utf16().count() > arena.maximum { + return Err(Error::OutputLimit); + } + arena.units.clear(); + arena.units.extend(text.encode_utf16()); + if arena.units.capacity() != arena.maximum { + return Err(Error::OutputLimit); + } + Ok(()) + } + + pub fn relative_name_arena_capacity(arena: &PreparedRelativeNameArena) -> usize { + arena.units.capacity() + } + + pub fn prepare_discard_names( + names: [&OsStr; N], + ) -> Result, Error> { + let names = names.map(|name| prepare_relative_name(name).ok()); + if names.iter().any(Option::is_none) { + return Err(Error::Invalid); + } + for left in 0..N { + for right in 0..left { + if prepared_names_equal( + names[left].as_ref().expect("validated"), + names[right].as_ref().expect("validated"), + ) { + return Err(Error::Invalid); + } + } + } + Ok(PreparedDiscardNames { names }) + } + + pub fn prepared_discard_names_owned_capacity( + prepared: &PreparedDiscardNames, + ) -> usize { + prepared + .names + .iter() + .filter_map(Option::as_ref) + .map(|name| name.0.capacity().saturating_mul(std::mem::size_of::())) + .sum() + } + + fn normal_name(name: &OsStr) -> Result<(), Error> { + let text = name.to_str().ok_or(Error::Invalid)?; + if text.is_empty() + || !text.is_ascii() + || matches!(text, "." | "..") + || text.contains(['/', '\\', '\0']) + || text.ends_with([' ', '.']) + || text.contains(':') + { + return Err(Error::Invalid); + } + let stem = text + .split('.') + .next() + .ok_or(Error::Invalid)? + .to_ascii_uppercase(); + if matches!(stem.as_str(), "CON" | "PRN" | "AUX" | "NUL" | "CLOCK$") + || stem.strip_prefix("COM").is_some_and(|suffix| { + matches!(suffix, "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9") + }) + || stem.strip_prefix("LPT").is_some_and(|suffix| { + matches!(suffix, "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9") + }) + { + return Err(Error::Invalid); + } + Ok(()) + } + + fn open_directory(path: &Path) -> Result { + open_absolute(path, DIRECTORY_READ_ACCESS, DIRECTORY_FLAGS) + } + + fn open_absolute(path: &Path, access: u32, flags: u32) -> Result { + let path = wide_null(path.as_os_str())?; + let handle = unsafe { + CreateFileW( + path.as_ptr(), + access, + HELD_SHARE, + std::ptr::null(), + OPEN_EXISTING, + flags, + std::ptr::null_mut(), + ) + }; + if handle == INVALID_HANDLE_VALUE { + return Err(Error::Changed); + } + Ok(unsafe { File::from_raw_handle(handle.cast()) }) + } + + fn relative_file( + parent: &File, + name: &OsStr, + access: u32, + disposition: u32, + create_options: u32, + ) -> Result { + let name = prepare_relative_name(name)?; + relative_file_prepared(parent, &name, access, disposition, create_options) + } + + fn relative_file_prepared( + parent: &File, + name: &PreparedRelativeName, + access: u32, + disposition: u32, + create_options: u32, + ) -> Result { + relative_file_units(parent, &name.0, access, disposition, create_options) + } + + fn relative_file_arena( + parent: &File, + name: &PreparedRelativeNameArena, + access: u32, + disposition: u32, + create_options: u32, + ) -> Result { + relative_file_units(parent, &name.units, access, disposition, create_options) + } + + fn relative_file_units( + parent: &File, + name: &[u16], + access: u32, + disposition: u32, + create_options: u32, + ) -> Result { + let byte_length = name.len().checked_mul(2).ok_or(Error::Invalid)?; + let length = u16::try_from(byte_length).map_err(|_| Error::Invalid)?; + let unicode = UNICODE_STRING { + Length: length, + MaximumLength: length, + Buffer: name.as_ptr().cast_mut(), + }; + let attributes = OBJECT_ATTRIBUTES { + Length: u32::try_from(std::mem::size_of::()) + .map_err(|_| Error::Changed)?, + RootDirectory: parent.as_raw_handle().cast(), + ObjectName: &unicode, + Attributes: OBJ_CASE_INSENSITIVE, + SecurityDescriptor: std::ptr::null(), + SecurityQualityOfService: std::ptr::null(), + }; + let mut io = IO_STATUS_BLOCK::default(); + let mut handle = std::ptr::null_mut(); + let status = unsafe { + NtCreateFile( + &mut handle, + access, + &attributes, + &mut io, + std::ptr::null(), + FILE_ATTRIBUTE_NORMAL, + HELD_SHARE, + disposition, + create_options | FILE_OPEN_REPARSE_POINT | FILE_SYNCHRONOUS_IO_NONALERT, + std::ptr::null(), + 0, + ) + }; + if status < 0 { + return Err(if status == STATUS_OBJECT_NAME_COLLISION { + Error::Exists + } else { + Error::Changed + }); + } + if handle.is_null() || handle == INVALID_HANDLE_VALUE { + return Err(Error::Changed); + } + Ok(unsafe { File::from_raw_handle(handle.cast()) }) + } + + fn open_relative_regular_read(parent: &Directory, name: &OsStr) -> Result { + relative_file( + &parent.file, + name, + REGULAR_READ_ACCESS, + FILE_OPEN, + FILE_NON_DIRECTORY_FILE, + ) + } + + fn information(file: &File) -> Result { + let mut information = BY_HANDLE_FILE_INFORMATION::default(); + if unsafe { + GetFileInformationByHandle( + file.as_raw_handle().cast::(), + &mut information, + ) + } == 0 + { + return Err(Error::Changed); + } + let mut file_id = FILE_ID_INFO::default(); + if unsafe { + GetFileInformationByHandleEx( + file.as_raw_handle().cast::(), + FileIdInfo, + (&mut file_id as *mut FILE_ID_INFO).cast(), + u32::try_from(std::mem::size_of::()).map_err(|_| Error::Changed)?, + ) + } == 0 + { + return Err(Error::Changed); + } + Ok(Identity { + volume: file_id.VolumeSerialNumber, + file_id: file_id.FileId.Identifier, + attributes: information.dwFileAttributes, + length: (u64::from(information.nFileSizeHigh) << 32) + | u64::from(information.nFileSizeLow), + }) + } + + fn stable_directory_identity(identity: Identity) -> Result { + let stable_attributes = + identity.attributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT); + if stable_attributes != FILE_ATTRIBUTE_DIRECTORY { + return Err(Error::Changed); + } + Ok(DirectoryIdentity { + volume: identity.volume, + file_id: identity.file_id, + stable_attributes, + }) + } + + fn directory_information(file: &File) -> Result { + let identity = stable_directory_identity(information(file)?)?; + if !file.metadata().map_err(|_| Error::Changed)?.is_dir() { + return Err(Error::Changed); + } + Ok(identity) + } + + fn digest(file: &File, length: u64) -> Result<[u8; 32], Error> { + let mut file = file.try_clone().map_err(|_| Error::Changed)?; + file.seek(SeekFrom::Start(0)).map_err(|_| Error::Changed)?; + let mut hasher = Sha256::new(); + let mut remaining = length; + let mut buffer = [0_u8; 8192]; + while remaining != 0 { + let maximum = usize::try_from(remaining.min(buffer.len() as u64)) + .map_err(|_| Error::OutputLimit)?; + let count = file + .read(&mut buffer[..maximum]) + .map_err(|_| Error::Changed)?; + if count == 0 { + return Err(Error::Changed); + } + hasher.update(&buffer[..count]); + remaining -= u64::try_from(count).map_err(|_| Error::OutputLimit)?; + } + Ok(hasher.finalize().into()) + } + + fn digest_bytes(bytes: &[u8]) -> [u8; 32] { + Sha256::digest(bytes).into() + } + + fn final_path_prepared(file: &File, output: &mut Vec) -> Result<(), Error> { + let maximum = output.capacity(); + if maximum != PROCESS_PATH_UNITS { + return Err(Error::OutputLimit); + } + output.clear(); + output.resize(maximum, 0); + let written = unsafe { + GetFinalPathNameByHandleW( + file.as_raw_handle().cast(), + output.as_mut_ptr(), + u32::try_from(maximum).map_err(|_| Error::OutputLimit)?, + 0, + ) + }; + let written = usize::try_from(written).map_err(|_| Error::Changed)?; + if written == 0 || written >= maximum { + return Err(Error::OutputLimit); + } + output.truncate(written); + let prefix = [ + u16::from(b'\\'), + u16::from(b'\\'), + u16::from(b'?'), + u16::from(b'\\'), + ]; + if output.starts_with(&prefix) { + output.copy_within(prefix.len().., 0); + output.truncate(written - prefix.len()); + } + output.push(0); + if output.capacity() != maximum { + return Err(Error::OutputLimit); + } + Ok(()) + } + + pub fn hold_directory(path: &Path) -> Result { + if !path.is_absolute() { + return Err(Error::Invalid); + } + let canonical = path.canonicalize().map_err(|_| Error::Changed)?; + if canonical != path { + return Err(Error::Changed); + } + let file = open_directory(path)?; + let identity = directory_information(&file)?; + Ok(Directory { file, identity }) + } + + pub fn recheck_directory(directory: &Directory) -> Result<(), Error> { + if directory_information(&directory.file)? != directory.identity { + return Err(Error::Changed); + } + Ok(()) + } + + pub fn same_directory_path(directory: &Directory, path: &Path) -> Result { + if !path.is_absolute() { + return Err(Error::Invalid); + } + let rebound = open_directory(path)?; + Ok(directory_information(&rebound)? == directory.identity) + } + + pub fn create_directory_new( + parent: &Directory, + name: &OsStr, + _mode: u32, + ) -> Result { + recheck_directory(parent)?; + normal_name(name)?; + let file = relative_file( + &parent.file, + name, + DIRECTORY_OWNED_ACCESS, + FILE_CREATE, + FILE_DIRECTORY_FILE, + )?; + let identity = directory_information(&file)?; + Ok(Directory { file, identity }) + } + + pub fn create_directory_new_prepared( + parent: &Directory, + name: &PreparedRelativeNameArena, + _mode: u32, + ) -> Result { + recheck_directory(parent)?; + let file = relative_file_arena( + &parent.file, + name, + DIRECTORY_OWNED_ACCESS, + FILE_CREATE, + FILE_DIRECTORY_FILE, + )?; + let identity = directory_information(&file)?; + Ok(Directory { file, identity }) + } + + pub fn write_file_new( + directory: &Directory, + name: &OsStr, + bytes: &[u8], + _mode: u32, + ) -> Result { + recheck_directory(directory)?; + normal_name(name)?; + let mut file = relative_file( + &directory.file, + name, + REGULAR_OWNED_ACCESS, + FILE_CREATE, + FILE_NON_DIRECTORY_FILE, + )?; + file.write_all(bytes).map_err(|_| Error::Changed)?; + file.sync_all().map_err(|_| Error::Changed)?; + let identity = information(&file)?; + if identity.attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 + || !file.metadata().map_err(|_| Error::Changed)?.is_file() + { + return Err(Error::Changed); + } + let digest = digest(&file, identity.length)?; + Ok(RegularFile { + file, + identity, + digest, + }) + } + + pub fn write_file_new_prepared( + directory: &Directory, + names: &PreparedDiscardNames, + index: usize, + bytes: &[u8], + _mode: u32, + ) -> Result { + let name = enter_prepared_file_syscalls(prepared_discard_name(names, index))?; + recheck_directory(directory)?; + let mut file = relative_file_prepared( + &directory.file, + name, + REGULAR_OWNED_ACCESS, + FILE_CREATE, + FILE_NON_DIRECTORY_FILE, + )?; + file.write_all(bytes).map_err(|_| Error::Changed)?; + file.sync_all().map_err(|_| Error::Changed)?; + authenticate_regular_file(file) + } + + pub fn hold_regular_file(directory: &Directory, name: &OsStr) -> Result { + recheck_directory(directory)?; + let name = prepare_relative_name(name)?; + hold_regular_file_name_prepared(directory, &name) + } + + fn authenticate_regular_file(file: File) -> Result { + let identity = information(&file)?; + if identity.attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 + || !file.metadata().map_err(|_| Error::Changed)?.is_file() + { + return Err(Error::Changed); + } + let digest = digest(&file, identity.length)?; + Ok(RegularFile { + file, + identity, + digest, + }) + } + + fn hold_regular_file_name_prepared( + directory: &Directory, + name: &PreparedRelativeName, + ) -> Result { + let file = relative_file_prepared( + &directory.file, + name, + REGULAR_OWNED_ACCESS, + FILE_OPEN, + FILE_NON_DIRECTORY_FILE, + )?; + authenticate_regular_file(file) + } + + pub fn hold_regular_file_prepared( + directory: &Directory, + names: &PreparedDiscardNames, + index: usize, + tracked: &RegularFile, + ) -> Result { + let name = enter_prepared_file_syscalls(prepared_discard_name(names, index))?; + recheck_directory(directory)?; + let rebound = hold_regular_file_name_prepared(directory, name)?; + if rebound.identity != tracked.identity || rebound.digest != tracked.digest { + return Err(Error::Changed); + } + Ok(rebound) + } + + pub fn recheck_regular(file: &RegularFile) -> Result<(), Error> { + recheck_held_regular(file) + } + + fn recheck_held_regular(file: &RegularFile) -> Result<(), Error> { + let held = information(&file.file)?; + if held != file.identity || digest(&file.file, file.identity.length)? != file.digest { + return Err(Error::Changed); + } + Ok(()) + } + + pub fn hold_external_executable(path: &Path) -> Result { + let parent_path = path + .parent() + .ok_or(Error::Invalid)? + .canonicalize() + .map_err(|_| Error::Changed)?; + let directory = hold_directory(&parent_path)?; + let name = path.file_name().ok_or(Error::Invalid)?; + normal_name(name)?; + let file = open_relative_regular_read(&directory, name)?; + let identity = information(&file)?; + if identity.attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 + || !file.metadata().map_err(|_| Error::Changed)?.is_file() + { + return Err(Error::Changed); + } + let digest = digest(&file, identity.length)?; + let regular = RegularFile { + file, + identity, + digest, + }; + let mut prefix = [0_u8; 2]; + let mut duplicate = regular.file.try_clone().map_err(|_| Error::Changed)?; + duplicate + .seek(SeekFrom::Start(0)) + .map_err(|_| Error::Changed)?; + duplicate + .read_exact(&mut prefix) + .map_err(|_| Error::Invalid)?; + if prefix != *b"MZ" { + return Err(Error::Invalid); + } + Ok(Executable { file: regular }) + } + + fn append_tool_fallback(prepared: &mut PreparedToolResolver) -> Result<(), Error> { + if prepared.candidate.first() == Some(&u16::from(b'"')) + && prepared.candidate.last() == Some(&u16::from(b'"')) + && prepared.candidate.len() >= 2 + { + prepared.candidate.remove(0); + prepared.candidate.pop(); + } + if prepared.candidate.is_empty() { + prepared.candidate.push(u16::from(b'.')); + } + if !prepared.candidate.ends_with(&[u16::from(b'/')]) + && !prepared.candidate.ends_with(&[u16::from(b'\\')]) + { + prepared.candidate.push(u16::from(b'\\')); + } + if prepared + .candidate + .len() + .checked_add(prepared.fallback.len()) + .and_then(|length| length.checked_add(1)) + .is_none_or(|length| length > prepared.maximum) + { + return Err(Error::OutputLimit); + } + prepared.candidate.extend_from_slice(&prepared.fallback); + Ok(()) + } + + fn hold_tool_candidate( + prepared: &mut PreparedToolResolver, + ) -> Result, Error> { + if prepared.candidate.len().saturating_add(1) > prepared.maximum { + return Err(Error::OutputLimit); + } + prepared.candidate.push(0); + let handle = unsafe { + CreateFileW( + prepared.candidate.as_ptr(), + REGULAR_READ_ACCESS, + HELD_SHARE, + std::ptr::null(), + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + std::ptr::null_mut(), + ) + }; + prepared.candidate.pop(); + if handle == INVALID_HANDLE_VALUE { + return Ok(None); + } + let file = unsafe { File::from_raw_handle(handle.cast()) }; + let identity = information(&file)?; + if !file.metadata().map_err(|_| Error::Changed)?.is_file() { + return Ok(None); + } + prepared.canonical.clear(); + prepared.canonical.resize(prepared.maximum, 0); + let written = unsafe { + GetFinalPathNameByHandleW( + file.as_raw_handle().cast(), + prepared.canonical.as_mut_ptr(), + u32::try_from(prepared.canonical.len()).map_err(|_| Error::OutputLimit)?, + 0, + ) + }; + let written = usize::try_from(written).map_err(|_| Error::Changed)?; + if written == 0 || written >= prepared.maximum { + return Err(Error::OutputLimit); + } + prepared.canonical.truncate(written); + let prefix = [ + u16::from(b'\\'), + u16::from(b'\\'), + u16::from(b'?'), + u16::from(b'\\'), + ]; + if prepared.canonical.starts_with(&prefix) { + prepared.canonical.copy_within(prefix.len().., 0); + prepared.canonical.truncate(written - prefix.len()); + } + prepared.display.clear(); + for character in char::decode_utf16(prepared.canonical.iter().copied()) { + prepared + .display + .push(character.map_err(|_| Error::Invalid)?); + } + if prepared.display.capacity() != prepared.maximum.saturating_mul(3) { + return Err(Error::OutputLimit); + } + let digest = digest(&file, identity.length)?; + let regular = RegularFile { + file, + identity, + digest, + }; + let mut prefix = [0_u8; 2]; + let mut duplicate = regular.file.try_clone().map_err(|_| Error::Changed)?; + duplicate + .seek(SeekFrom::Start(0)) + .map_err(|_| Error::Changed)?; + duplicate + .read_exact(&mut prefix) + .map_err(|_| Error::Invalid)?; + if prefix != *b"MZ" { + return Err(Error::Invalid); + } + Ok(Some(Executable { file: regular })) + } + + pub fn resolve_and_hold_tool_prepared( + mut prepared: PreparedToolResolver, + configured: Option<&OsStr>, + paths: Option<&OsStr>, + ) -> Result<(Executable, String), Error> { + if let Some(configured) = configured { + prepared.candidate.clear(); + for unit in configured.encode_wide() { + if prepared.candidate.len().saturating_add(1) >= prepared.maximum { + return Err(Error::OutputLimit); + } + prepared.candidate.push(unit); + } + if prepared.candidate.is_empty() { + return Err(Error::Invalid); + } + let executable = hold_tool_candidate(&mut prepared)?.ok_or(Error::Changed)?; + return Ok((executable, prepared.display)); + } + let paths = paths.ok_or(Error::Invalid)?; + prepared.candidate.clear(); + for unit in paths.encode_wide().chain(std::iter::once(u16::from(b';'))) { + if unit == u16::from(b';') { + append_tool_fallback(&mut prepared)?; + if let Some(executable) = hold_tool_candidate(&mut prepared)? { + return Ok((executable, prepared.display)); + } + prepared.candidate.clear(); + } else { + if prepared.candidate.len().saturating_add(2) > prepared.maximum { + return Err(Error::OutputLimit); + } + prepared.candidate.push(unit); + } + } + Err(Error::Changed) + } + + fn windows_sysroot_line_actual(output: &[u8]) -> Result<&str, Error> { + let line = output.strip_suffix(b"\n").ok_or(Error::Invalid)?; + let line = line.strip_suffix(b"\r").unwrap_or(line); + if line.is_empty() || line.contains(&0) || line.contains(&b'\n') || line.contains(&b'\r') { + return Err(Error::Invalid); + } + std::str::from_utf8(line).map_err(|_| Error::Invalid) + } + + fn windows_sysroot_directory_actual( + prepared: &mut PreparedToolResolver, + output: &[u8], + ) -> Result { + let line = windows_sysroot_line_actual(output)?; + prepared.candidate.clear(); + for unit in line.encode_utf16() { + if prepared.candidate.len().saturating_add(1) >= prepared.maximum { + return Err(Error::OutputLimit); + } + prepared.candidate.push(unit); + } + let drive = prepared.candidate.first().copied().ok_or(Error::Invalid)?; + let drive_is_ascii_alphabetic = + u8::try_from(drive).is_ok_and(|unit| unit.is_ascii_alphabetic()); + if prepared.candidate.capacity() != prepared.maximum + || prepared.candidate.len() < 4 + || !drive_is_ascii_alphabetic + || prepared.candidate[1] != u16::from(b':') + || !matches!(prepared.candidate[2], 47 | 92) + || matches!(prepared.candidate.last(), Some(47 | 92)) + { + return Err(Error::Invalid); + } + let root = [prepared.candidate[0], u16::from(b':'), u16::from(b'\\'), 0]; + let handle = unsafe { + CreateFileW( + root.as_ptr(), + DIRECTORY_READ_ACCESS, + HELD_SHARE, + std::ptr::null(), + OPEN_EXISTING, + DIRECTORY_FLAGS, + std::ptr::null_mut(), + ) + }; + if handle == INVALID_HANDLE_VALUE { + return Err(Error::Changed); + } + let file = unsafe { File::from_raw_handle(handle.cast()) }; + let identity = directory_information(&file)?; + let mut current = Directory { file, identity }; + let mut start = 3usize; + while start < prepared.candidate.len() { + let end = prepared.candidate[start..] + .iter() + .position(|unit| matches!(*unit, 47 | 92)) + .map_or(prepared.candidate.len(), |offset| start + offset); + let component = &prepared.candidate[start..end]; + if component.is_empty() + || component == [u16::from(b'.')] + || component == [u16::from(b'.'), u16::from(b'.')] + { + return Err(Error::Invalid); + } + let file = relative_file_units( + ¤t.file, + component, + DIRECTORY_READ_ACCESS, + FILE_OPEN, + FILE_DIRECTORY_FILE, + )?; + let identity = directory_information(&file)?; + current = Directory { file, identity }; + start = end.saturating_add(1); + } + Ok(current) + } + + pub fn hold_rustc_discovery_prepared( + prepared: PreparedToolResolver, + configured: &OsStr, + ) -> Result { + if !Path::new(configured).is_absolute() { + return Err(Error::Invalid); + } + let (executable, _) = resolve_and_hold_tool_prepared(prepared, Some(configured), None)?; + Ok(RustcDiscovery(executable)) + } + + pub fn rustc_discovery_output_prepared( + discovery: &RustcDiscovery, + cwd: &Directory, + prepared: PreparedSysrootInvocation, + process_arena: &mut PreparedProcessArena, + ) -> Result, Error> { + version_prepared(&discovery.0, cwd, prepared.0, process_arena) + } + + pub fn hold_direct_rustc_prepared( + mut prepared: PreparedToolResolver, + output: &[u8], + ) -> Result { + let sysroot = windows_sysroot_directory_actual(&mut prepared, output)?; + let bin = relative_file_units( + &sysroot.file, + &[98, 105, 110], + DIRECTORY_READ_ACCESS, + FILE_OPEN, + FILE_DIRECTORY_FILE, + )?; + let bin_identity = information(&bin)?; + if bin_identity.attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 + || !bin.metadata().map_err(|_| Error::Changed)?.is_dir() + { + return Err(Error::Changed); + } + let file = relative_file_units( + &bin, + &[114, 117, 115, 116, 99, 46, 101, 120, 101], + REGULAR_READ_ACCESS, + FILE_OPEN, + FILE_NON_DIRECTORY_FILE, + )?; + let identity = information(&file)?; + if identity.attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 + || !file.metadata().map_err(|_| Error::Changed)?.is_file() + { + return Err(Error::Changed); + } + let digest = digest(&file, identity.length)?; + let regular = RegularFile { + file, + identity, + digest, + }; + let mut prefix = [0_u8; 2]; + let mut duplicate = regular.file.try_clone().map_err(|_| Error::Changed)?; + duplicate + .seek(SeekFrom::Start(0)) + .map_err(|_| Error::Changed)?; + duplicate + .read_exact(&mut prefix) + .map_err(|_| Error::Invalid)?; + if prefix != *b"MZ" { + return Err(Error::Invalid); + } + Ok(DirectRustc { + executable: Executable { file: regular }, + sysroot, + }) + } + + pub fn direct_rustc_output_prepared( + direct: &DirectRustc, + cwd: &Directory, + prepared: PreparedSysrootInvocation, + process_arena: &mut PreparedProcessArena, + ) -> Result, Error> { + version_prepared(&direct.executable, cwd, prepared.0, process_arena) + } + + pub fn direct_rustc_version_prepared( + direct: &DirectRustc, + cwd: &Directory, + prepared: PreparedRustcVersionInvocation, + process_arena: &mut PreparedProcessArena, + ) -> Result, Error> { + recheck_directory(&direct.sysroot)?; + version_prepared(&direct.executable, cwd, prepared.0, process_arena) + } + + pub fn direct_rustc_reproduces_sysroot( + direct: &DirectRustc, + mut prepared: PreparedToolResolver, + output: &[u8], + ) -> Result<(), Error> { + let rebound = windows_sysroot_directory_actual(&mut prepared, output)?; + if rebound.identity != direct.sysroot.identity { + return Err(Error::Changed); + } + recheck_held_regular(&direct.executable.file)?; + recheck_directory(&direct.sysroot) + } + + pub fn hold_executable(directory: &Directory, name: &OsStr) -> Result { + let file = hold_regular_file(directory, name)?; + let mut prefix = [0_u8; 2]; + let mut duplicate = file.file.try_clone().map_err(|_| Error::Changed)?; + duplicate + .seek(SeekFrom::Start(0)) + .map_err(|_| Error::Changed)?; + duplicate + .read_exact(&mut prefix) + .map_err(|_| Error::Invalid)?; + if prefix != *b"MZ" { + return Err(Error::Invalid); + } + Ok(Executable { file }) + } + + pub fn executable_regular_file(executable: &Executable) -> Result { + recheck_held_regular(&executable.file)?; + Ok(RegularFile { + file: executable + .file + .file + .try_clone() + .map_err(|_| Error::Changed)?, + identity: executable.file.identity, + digest: executable.file.digest, + }) + } + + pub fn read_exact(file: &RegularFile, maximum: usize) -> Result, Error> { + recheck_regular(file)?; + let length = usize::try_from(file.identity.length).map_err(|_| Error::OutputLimit)?; + if length > maximum { + return Err(Error::OutputLimit); + } + let mut bytes = vec![0_u8; length]; + let mut duplicate = file.file.try_clone().map_err(|_| Error::Changed)?; + duplicate + .seek(SeekFrom::Start(0)) + .map_err(|_| Error::Changed)?; + duplicate + .read_exact(&mut bytes) + .map_err(|_| Error::Changed)?; + recheck_regular(file)?; + Ok(bytes) + } + + pub fn compare_exact( + file: &RegularFile, + expected: &[u8], + scratch: &mut [u8; 8192], + ) -> Result { + recheck_regular(file)?; + if usize::try_from(file.identity.length).map_err(|_| Error::OutputLimit)? != expected.len() + { + return Ok(false); + } + let mut offset = 0usize; + while offset < expected.len() { + let chunk = (expected.len() - offset).min(scratch.len()); + let count = file + .file + .seek_read( + &mut scratch[..chunk], + u64::try_from(offset).map_err(|_| Error::OutputLimit)?, + ) + .map_err(|_| Error::Changed)?; + if count == 0 || scratch[..count] != expected[offset..offset + count] { + return Ok(false); + } + offset = offset.checked_add(count).ok_or(Error::OutputLimit)?; + } + recheck_regular(file)?; + Ok(true) + } + + pub fn link_or_copy_new_prepared( + mut prepared: PreparedLinkOrCopy, + source: &RegularFile, + directory: &Directory, + names: &PreparedDiscardNames, + destination_index: usize, + source_bytes: &[u8], + ) -> Result { + if prepared.destination_index != destination_index { + return Err(Error::Invalid); + } + let name = prepared_discard_name(names, destination_index)?; + if usize::try_from(source.identity.length).map_err(|_| Error::OutputLimit)? + != source_bytes.len() + || digest_bytes(source_bytes) != source.digest + { + return Err(Error::Changed); + } + recheck_held_regular(source)?; + recheck_directory(directory)?; + let information = prepared.storage.as_mut_ptr().cast::(); + unsafe { + (*information).root_directory = directory.file.as_raw_handle().cast(); + } + let mut io = IO_STATUS_BLOCK::default(); + let status = unsafe { + NtSetInformationFile( + source.file.as_raw_handle().cast(), + &mut io, + prepared.storage.as_mut_ptr().cast(), + u32::try_from(prepared.total).map_err(|_| Error::Invalid)?, + FileLinkInformationEx, + ) + }; + if status < 0 { + return Err(if status == STATUS_OBJECT_NAME_COLLISION { + Error::Exists + } else { + Error::Changed + }); + } + #[cfg(debug_assertions)] + if prepared.fail_before_authentication { + return Err(Error::Changed); + } + let destination = hold_regular_file_name_prepared(directory, name)?; + if destination.identity != source.identity || destination.digest != source.digest { + return Err(Error::Changed); + } + Ok(destination) + } + + pub fn inventory_exact_prepared( + prepared: &mut PreparedInventoryExact, + directory: &Directory, + names: &PreparedDiscardNames, + files: [Option<&RegularFile>; N], + ) -> Result<(), Error> { + if prepared.remaining == 0 + || prepared.bindings != prepared_name_bindings(names)? + || files.iter().any(Option::is_none) + { + return Err(Error::Invalid); + } + match prepared.directory_identity { + Some(first) if first != directory.identity => return Err(Error::Changed), + None => prepared.directory_identity = Some(directory.identity), + Some(_) => {} + } + prepared.remaining -= 1; + recheck_directory(directory)?; + for file in files.iter().flatten() { + recheck_held_regular(file)?; + } + let mut seen = [false; N]; + let mut count = 0usize; + let mut raw_records = 0usize; + let mut queries = 0usize; + let mut saw_dot = false; + let mut saw_dot_dot = false; + let maximum_records = N.checked_add(2).ok_or(Error::OutputLimit)?; + let maximum_queries = N.checked_add(3).ok_or(Error::OutputLimit)?; + let mut restart = true; + loop { + queries = queries.checked_add(1).ok_or(Error::OutputLimit)?; + if queries > maximum_queries { + return Err(Error::Changed); + } + prepared.storage.fill(u64::MAX); + let class = if restart { + FileIdExtdDirectoryRestartInfo + } else { + FileIdExtdDirectoryInfo + }; + restart = false; + let ok = unsafe { + GetFileInformationByHandleEx( + directory.file.as_raw_handle().cast(), + class, + prepared.storage.as_mut_ptr().cast(), + u32::try_from(prepared.storage.len() * std::mem::size_of::()) + .map_err(|_| Error::Changed)?, + ) + }; + if ok == 0 { + if unsafe { GetLastError() } == ERROR_NO_MORE_FILES { + break; + } + return Err(Error::Changed); + } + let byte_length = prepared.storage.len() * std::mem::size_of::(); + let mut offset = 0_usize; + loop { + let record_header_end = offset + .checked_add(std::mem::size_of::()) + .ok_or(Error::Changed)?; + let header_end = offset + .checked_add(std::mem::offset_of!(FILE_ID_EXTD_DIR_INFO, FileName)) + .ok_or(Error::Changed)?; + if record_header_end > byte_length || header_end > byte_length { + return Err(Error::Changed); + } + let entry = unsafe { + &*prepared + .storage + .as_ptr() + .cast::() + .add(offset) + .cast::() + }; + let name_bytes = + usize::try_from(entry.FileNameLength).map_err(|_| Error::Changed)?; + if name_bytes % 2 != 0 { + return Err(Error::Changed); + } + let name_end = header_end.checked_add(name_bytes).ok_or(Error::Changed)?; + if name_end > byte_length { + return Err(Error::Changed); + } + let name = unsafe { + std::slice::from_raw_parts( + prepared + .storage + .as_ptr() + .cast::() + .add(header_end) + .cast::(), + name_bytes / 2, + ) + }; + let dot = name == [u16::from(b'.')]; + let dot_dot = name == [u16::from(b'.'), u16::from(b'.')]; + raw_records = raw_records.checked_add(1).ok_or(Error::OutputLimit)?; + if raw_records > maximum_records { + return Err(Error::Changed); + } + if dot { + if saw_dot { + return Err(Error::Changed); + } + saw_dot = true; + } else if dot_dot { + if saw_dot_dot { + return Err(Error::Changed); + } + saw_dot_dot = true; + } + if !dot && !dot_dot { + let Some(index) = prepared.names.iter().position(|expected| { + prepared_matches_slice(expected.as_ref().expect("prepared name"), name) + }) else { + return Err(Error::Changed); + }; + let tracked = files[index].expect("attached"); + if seen[index] || entry.FileId.Identifier != tracked.identity.file_id { + return Err(Error::Changed); + } + seen[index] = true; + count = count.checked_add(1).ok_or(Error::OutputLimit)?; + if count > N { + return Err(Error::Changed); + } + } + if entry.NextEntryOffset == 0 { + break; + } + let next = usize::try_from(entry.NextEntryOffset).map_err(|_| Error::Changed)?; + let minimum = name_end.checked_sub(offset).ok_or(Error::Changed)?; + let next_end = offset.checked_add(next).ok_or(Error::Changed)?; + if next < minimum + || next % std::mem::align_of::() != 0 + || next_end > byte_length + { + return Err(Error::Changed); + } + offset = next_end; + } + } + if count != N || seen.iter().any(|seen| !seen) { + return Err(Error::Changed); + } + recheck_directory(directory)?; + for file in files.iter().flatten() { + recheck_held_regular(file)?; + } + Ok(()) + } + + fn observe_publish_rebound( + parent: &Directory, + stage_name: &PreparedRelativeNameArena, + fail_information: bool, + fail_close: bool, + ) -> Result { + let file = relative_file_arena( + &parent.file, + stage_name, + DIRECTORY_READ_ACCESS, + FILE_OPEN, + FILE_DIRECTORY_FILE, + )?; + let handle = file.into_raw_handle(); + let file = std::mem::ManuallyDrop::new(unsafe { File::from_raw_handle(handle) }); + let observed = if fail_information { + Err(Error::Changed) + } else { + directory_information(&file) + }; + let close_failed = unsafe { CloseHandle(handle.cast()) } == 0; + if close_failed || fail_close { + std::process::abort(); + } + observed + } + + pub fn publish_directory_new_prepared( + prepared: &mut PreparedPublishDirectory, + parent: &Directory, + stage: &Directory, + stage_name: &PreparedRelativeNameArena, + output_name: &OsStr, + ) -> Result<(), Error> { + let output = prepared_normal_name(output_name)?; + let information = prepared.storage.as_mut_ptr().cast::(); + let stored_name = unsafe { + std::slice::from_raw_parts( + std::ptr::addr_of!((*information).file_name).cast::(), + prepared.name_units, + ) + }; + let total = u32::try_from(prepared.total).map_err(|_| Error::Invalid)?; + if prepared.remaining != 1 + || prepared.exact_capacity + != prepared + .storage + .len() + .saturating_mul(std::mem::size_of::()) + || !output.encode_utf16().eq(stored_name.iter().copied()) + || stage_name.units.is_empty() + { + return Err(Error::Invalid); + } + prepared.remaining = 0; + recheck_directory(parent)?; + recheck_directory(stage)?; + #[cfg(debug_assertions)] + if prepared.fail_before_open { + return Err(Error::Changed); + } + #[cfg(debug_assertions)] + let (fail_information, fail_close) = (prepared.fail_information, prepared.fail_close); + #[cfg(not(debug_assertions))] + let (fail_information, fail_close) = (false, false); + if observe_publish_rebound(parent, stage_name, fail_information, fail_close)? + != stage.identity + { + return Err(Error::Changed); + } + #[cfg(debug_assertions)] + if prepared.fail_rename { + return Err(Error::Changed); + } + unsafe { + (*information).root_directory = parent.file.as_raw_handle().cast(); + } + if unsafe { + SetFileInformationByHandle( + stage.file.as_raw_handle().cast(), + FileRenameInfoEx, + information.cast(), + total, + ) + } == 0 + { + return Err(Error::Exists); + } + Ok(()) + } + + pub fn discard_owned_stage_prepared( + parent: &Directory, + stage: &Directory, + stage_name: &PreparedRelativeNameArena, + names: &PreparedDiscardNames, + files: &[Option<&RegularFile>; N], + #[cfg(debug_assertions)] failure_after_delete: Option, + ) -> Result<(), Error> { + recheck_directory(parent)?; + recheck_directory(stage)?; + let rebound = relative_file_arena( + &parent.file, + stage_name, + DIRECTORY_READ_ACCESS, + FILE_OPEN, + FILE_DIRECTORY_FILE, + )?; + if directory_information(&rebound)? != stage.identity { + return Err(Error::Changed); + } + let attached = files.iter().take_while(|file| file.is_some()).count(); + if files[attached..].iter().any(Option::is_some) { + return Err(Error::Invalid); + } + + let mut seen = [false; N]; + let mut storage = [0_u64; 8192]; + let mut restart = true; + loop { + let class = if restart { + FileIdBothDirectoryRestartInfo + } else { + FileIdBothDirectoryInfo + }; + restart = false; + let ok = unsafe { + GetFileInformationByHandleEx( + stage.file.as_raw_handle().cast(), + class, + storage.as_mut_ptr().cast(), + u32::try_from(storage.len() * std::mem::size_of::()) + .map_err(|_| Error::Changed)?, + ) + }; + if ok == 0 { + if unsafe { GetLastError() } == ERROR_NO_MORE_FILES { + break; + } + return Err(Error::Changed); + } + let byte_length = storage.len() * std::mem::size_of::(); + let mut offset = 0_usize; + loop { + let header_end = offset + .checked_add(std::mem::offset_of!(FILE_ID_BOTH_DIR_INFO, FileName)) + .ok_or(Error::Changed)?; + if header_end > byte_length { + return Err(Error::Changed); + } + let entry = unsafe { + &*storage + .as_ptr() + .cast::() + .add(offset) + .cast::() + }; + let name_bytes = + usize::try_from(entry.FileNameLength).map_err(|_| Error::Changed)?; + if name_bytes % 2 != 0 { + return Err(Error::Changed); + } + let name_end = header_end.checked_add(name_bytes).ok_or(Error::Changed)?; + if name_end > byte_length { + return Err(Error::Changed); + } + let actual = unsafe { + std::slice::from_raw_parts( + storage.as_ptr().cast::().add(header_end).cast::(), + name_bytes / 2, + ) + }; + let dot = actual == [u16::from(b'.')]; + let dot_dot = actual == [u16::from(b'.'), u16::from(b'.')]; + if !dot && !dot_dot { + let Some(index) = names.names[..attached].iter().position(|expected| { + prepared_matches_slice(expected.as_ref().expect("validated"), actual) + }) else { + return Err(Error::Changed); + }; + if seen[index] { + return Err(Error::Changed); + } + seen[index] = true; + } + if entry.NextEntryOffset == 0 { + break; + } + let next = usize::try_from(entry.NextEntryOffset).map_err(|_| Error::Changed)?; + if next == 0 || next % std::mem::align_of::() != 0 { + return Err(Error::Changed); + } + offset = offset.checked_add(next).ok_or(Error::Changed)?; + } + } + if seen[..attached].iter().any(|seen| !seen) { + return Err(Error::Changed); + } + for (index, file) in files[..attached].iter().enumerate() { + let file = file.expect("attached prefix"); + recheck_held_regular(file)?; + let name = names.names[index].as_ref().expect("validated"); + if hold_regular_file_name_prepared(stage, name)?.identity != file.identity { + return Err(Error::Changed); + } + } + for (deleted, file) in files[..attached].iter().flatten().enumerate() { + #[cfg(not(debug_assertions))] + let _ = deleted; + #[cfg(debug_assertions)] + if failure_after_delete == Some(deleted) { + return Err(Error::Changed); + } + disposition_delete(&file.file)?; + } + #[cfg(debug_assertions)] + if failure_after_delete == Some(attached) { + return Err(Error::Changed); + } + disposition_delete(&stage.file) + } + + #[repr(C)] + struct NamedInformation { + flags: u32, + root_directory: HANDLE, + file_name_length: u32, + file_name: [u16; 1], + } + + fn disposition_delete(file: &File) -> Result<(), Error> { + let information = FILE_DISPOSITION_INFO_EX { + Flags: FILE_DISPOSITION_FLAG_DELETE | FILE_DISPOSITION_FLAG_POSIX_SEMANTICS, + }; + if unsafe { + SetFileInformationByHandle( + file.as_raw_handle().cast(), + FileDispositionInfoEx, + (&information as *const FILE_DISPOSITION_INFO_EX).cast(), + u32::try_from(std::mem::size_of_val(&information)).map_err(|_| Error::Changed)?, + ) + } == 0 + { + return Err(Error::Changed); + } + Ok(()) + } + + fn run_argv( + executable: &Executable, + cwd: &Directory, + arguments: &[String], + stdout_limit: usize, + prepared_command_line: Option>, + prepared_output: Option>, + process_arena: &mut PreparedProcessArena, + ) -> Result, Error> { + if arguments.len() > 32 + || prepared_command_line.as_ref().is_none_or(Vec::is_empty) + || prepared_output + .as_ref() + .is_none_or(|output| output.capacity() != stdout_limit || !output.is_empty()) + { + return Err(Error::Invalid); + } + consume_process_arena(process_arena)?; + struct CheckedHandle(Option); + impl CheckedHandle { + fn new(handle: HANDLE) -> Self { + Self(Some(handle)) + } + + fn raw(&self) -> HANDLE { + self.0.expect("checked handle remains owned") + } + + fn close(mut self) -> Result<(), Error> { + let handle = self.0.take().expect("checked handle remains owned"); + if unsafe { CloseHandle(handle) } == 0 { + Err(Error::Spawn) + } else { + Ok(()) + } + } + } + impl Drop for CheckedHandle { + fn drop(&mut self) { + if let Some(handle) = self.0.take() { + if unsafe { CloseHandle(handle) } == 0 { + std::process::abort(); + } + } + } + } + fn must_close(handles: [CheckedHandle; 4]) { + let mut failed = false; + for handle in handles { + failed |= handle.close().is_err(); + } + if failed { + std::process::abort(); + } + } + recheck_held_regular(&executable.file)?; + recheck_directory(cwd)?; + final_path_prepared(&executable.file.file, &mut process_arena.application)?; + final_path_prepared(&cwd.file, &mut process_arena.cwd)?; + let mut command_line = prepared_command_line.ok_or(Error::Invalid)?; + + let security = SECURITY_ATTRIBUTES { + nLength: u32::try_from(std::mem::size_of::()) + .map_err(|_| Error::Spawn)?, + lpSecurityDescriptor: std::ptr::null_mut(), + bInheritHandle: 1, + }; + let mut read_pipe = std::ptr::null_mut(); + let mut write_pipe = std::ptr::null_mut(); + if unsafe { CreatePipe(&mut read_pipe, &mut write_pipe, &security, 0) } == 0 { + return Err(Error::Spawn); + } + let read_pipe = CheckedHandle::new(read_pipe); + let write_pipe = CheckedHandle::new(write_pipe); + if unsafe { SetHandleInformation(read_pipe.raw(), HANDLE_FLAG_INHERIT, 0) } == 0 { + return Err(Error::Spawn); + } + let null_name = [u16::from(b'N'), u16::from(b'U'), u16::from(b'L'), 0]; + let null_handle = unsafe { + CreateFileW( + null_name.as_ptr(), + FILE_GENERIC_READ | FILE_GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + std::ptr::null(), + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + std::ptr::null_mut(), + ) + }; + if null_handle == INVALID_HANDLE_VALUE { + return Err(Error::Spawn); + } + let null_handle = CheckedHandle::new(null_handle); + if unsafe { + SetHandleInformation(null_handle.raw(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) + } == 0 + { + return Err(Error::Spawn); + } + + let inherited = [null_handle.raw(), write_pipe.raw()]; + let mut attribute_bytes = process_arena.attribute_bytes; + let attribute_words = attribute_bytes + .checked_add(std::mem::size_of::() - 1) + .and_then(|bytes| bytes.checked_div(std::mem::size_of::())) + .ok_or(Error::OutputLimit)?; + process_arena.attributes.resize(attribute_words, 0); + if attribute_bytes + > process_arena + .attributes + .len() + .saturating_mul(std::mem::size_of::()) + || process_arena.attributes.capacity() != attribute_words + { + return Err(Error::OutputLimit); + } + let attribute_list = process_arena.attributes.as_mut_ptr().cast(); + if unsafe { InitializeProcThreadAttributeList(attribute_list, 1, 0, &mut attribute_bytes) } + == 0 + || attribute_bytes != process_arena.attribute_bytes + { + return Err(Error::Spawn); + } + struct AttributeList(windows_sys::Win32::System::Threading::LPPROC_THREAD_ATTRIBUTE_LIST); + impl Drop for AttributeList { + fn drop(&mut self) { + unsafe { DeleteProcThreadAttributeList(self.0) }; + } + } + let attribute_list = AttributeList(attribute_list); + if unsafe { + UpdateProcThreadAttribute( + attribute_list.0, + 0, + PROC_THREAD_ATTRIBUTE_HANDLE_LIST as usize, + inherited.as_ptr().cast(), + std::mem::size_of_val(&inherited), + std::ptr::null_mut(), + std::ptr::null(), + ) + } == 0 + { + return Err(Error::Spawn); + } + + let job = unsafe { CreateJobObjectW(std::ptr::null(), std::ptr::null()) }; + if job.is_null() { + return Err(Error::Spawn); + } + let job = CheckedHandle::new(job); + let mut job_limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default(); + job_limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + if unsafe { + SetInformationJobObject( + job.raw(), + JobObjectExtendedLimitInformation, + (&job_limits as *const JOBOBJECT_EXTENDED_LIMIT_INFORMATION).cast(), + u32::try_from(std::mem::size_of_val(&job_limits)).map_err(|_| Error::Spawn)?, + ) + } == 0 + { + return Err(Error::Spawn); + } + + let mut startup = STARTUPINFOEXW::default(); + startup.StartupInfo.cb = + u32::try_from(std::mem::size_of::()).map_err(|_| Error::Spawn)?; + startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES; + startup.StartupInfo.hStdInput = null_handle.raw(); + startup.StartupInfo.hStdOutput = write_pipe.raw(); + startup.StartupInfo.hStdError = null_handle.raw(); + startup.lpAttributeList = attribute_list.0; + let mut process = PROCESS_INFORMATION::default(); + let created = unsafe { + CreateProcessW( + process_arena.application.as_ptr(), + command_line.as_mut_ptr(), + std::ptr::null(), + std::ptr::null(), + 1, + CREATE_SUSPENDED | EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, + process_arena.environment.as_ptr().cast(), + process_arena.cwd.as_ptr(), + &startup.StartupInfo, + &mut process, + ) + }; + drop(attribute_list); + if created == 0 { + if write_pipe.close().is_err() { + std::process::abort(); + } + return Err(Error::Spawn); + } + let process_handle = CheckedHandle::new(process.hProcess); + let thread_handle = CheckedHandle::new(process.hThread); + if write_pipe.close().is_err() { + if terminate_unassigned(process_handle.raw()).is_err() { + std::process::abort(); + } + let mut failed = false; + failed |= thread_handle.close().is_err(); + failed |= read_pipe.close().is_err(); + failed |= null_handle.close().is_err(); + failed |= process_handle.close().is_err(); + failed |= job.close().is_err(); + let _ = failed; + std::process::abort(); + } + + fn must_terminate_unassigned(process: HANDLE) { + if terminate_unassigned(process).is_err() { + std::process::abort(); + } + } + + fn must_settle_job(job: HANDLE, process: HANDLE, terminate: bool) { + if settle_job(job, process, terminate).is_err() { + std::process::abort(); + } + } + + let image_matches = (|| { + process_arena.application.clear(); + process_arena.application.resize(PROCESS_PATH_UNITS, 0); + let mut image_len = + u32::try_from(process_arena.application.len()).map_err(|_| Error::Spawn)?; + if unsafe { + QueryFullProcessImageNameW( + process_handle.raw(), + 0, + process_arena.application.as_mut_ptr(), + &mut image_len, + ) + } == 0 + { + return Err(Error::Changed); + } + let image_len = usize::try_from(image_len).map_err(|_| Error::Spawn)?; + if image_len == 0 || image_len.saturating_add(1) > PROCESS_PATH_UNITS { + return Err(Error::OutputLimit); + } + process_arena.application.truncate(image_len); + process_arena.application.push(0); + let file_handle = unsafe { + CreateFileW( + process_arena.application.as_ptr(), + REGULAR_READ_ACCESS, + HELD_SHARE, + std::ptr::null(), + OPEN_EXISTING, + NORMAL_FILE_FLAGS, + std::ptr::null_mut(), + ) + }; + if file_handle == INVALID_HANDLE_VALUE { + return Err(Error::Changed); + } + let file = unsafe { File::from_raw_handle(file_handle.cast()) }; + let identity = information(&file)?; + let bytes = digest(&file, identity.length)?; + recheck_held_regular(&executable.file)?; + recheck_directory(cwd)?; + Ok(!injected_settlement_failure!(WindowsImage) + && identity == executable.file.identity + && bytes == executable.file.digest) + })(); + if image_matches != Ok(true) { + must_terminate_unassigned(process_handle.raw()); + return Err(Error::Changed); + } + if injected_settlement_failure!(WindowsAssign) + || unsafe { AssignProcessToJobObject(job.raw(), process_handle.raw()) } == 0 + { + must_terminate_unassigned(process_handle.raw()); + return Err(Error::Changed); + } + if injected_settlement_failure!(WindowsResume) + || unsafe { ResumeThread(thread_handle.raw()) } == u32::MAX + { + must_settle_job(job.raw(), process_handle.raw(), true); + return Err(Error::Spawn); + } + if thread_handle.close().is_err() { + must_settle_job(job.raw(), process_handle.raw(), true); + std::process::abort(); + } + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30); + let output_is_prepared = prepared_output.is_some(); + let mut output = prepared_output.unwrap_or_default(); + if output_is_prepared && (output.capacity() != stdout_limit || !output.is_empty()) { + must_settle_job(job.raw(), process_handle.raw(), true); + return Err(Error::OutputLimit); + } + let mut selected_error = None; + loop { + let mut available = 0_u32; + if injected_settlement_failure!(WindowsPeek) { + selected_error = Some(Error::Spawn); + break; + } + if unsafe { + PeekNamedPipe( + read_pipe.raw(), + std::ptr::null_mut(), + 0, + std::ptr::null_mut(), + &mut available, + std::ptr::null_mut(), + ) + } == 0 + { + let error = unsafe { GetLastError() }; + if !matches!(error, ERROR_BROKEN_PIPE | ERROR_PIPE_NOT_CONNECTED) { + selected_error = Some(Error::Spawn); + break; + } + available = 0; + } + while available != 0 { + let count = usize::try_from(available).unwrap_or(usize::MAX).min(8192); + if count > stdout_limit.saturating_sub(output.len()) { + selected_error = Some(Error::OutputLimit); + break; + } + let mut buffer = [0_u8; 8192]; + let mut read = 0_u32; + if injected_settlement_failure!(WindowsRead) + || unsafe { + ReadFile( + read_pipe.raw(), + buffer.as_mut_ptr().cast(), + u32::try_from(count).map_err(|_| Error::Spawn)?, + &mut read, + std::ptr::null_mut(), + ) + } == 0 + { + selected_error = Some(Error::Spawn); + break; + } + let read = usize::try_from(read).map_err(|_| Error::Spawn)?; + if read == 0 { + break; + } + output.extend_from_slice(&buffer[..read]); + if output_is_prepared && output.capacity() != stdout_limit { + selected_error = Some(Error::OutputLimit); + break; + } + available = available.saturating_sub(u32::try_from(read).unwrap_or(u32::MAX)); + } + if selected_error.is_some() { + break; + } + match unsafe { WaitForSingleObject(process_handle.raw(), 0) } { + WAIT_OBJECT_0 => break, + WAIT_TIMEOUT => {} + WAIT_FAILED => { + selected_error = Some(Error::Spawn); + break; + } + _ => { + selected_error = Some(Error::Spawn); + break; + } + } + if std::time::Instant::now() >= deadline { + selected_error = Some(Error::Spawn); + break; + } + std::thread::sleep(std::time::Duration::from_millis(1)); + } + if selected_error.is_none() { + let mut exit_code = u32::MAX; + if unsafe { + windows_sys::Win32::System::Threading::GetExitCodeProcess( + process_handle.raw(), + &mut exit_code, + ) + } == 0 + { + selected_error = Some(Error::Spawn); + } else if exit_code != 0 { + selected_error = Some(Error::Exit); + } + } + must_settle_job(job.raw(), process_handle.raw(), true); + if let Some(error) = selected_error { + must_close([read_pipe, null_handle, process_handle, job]); + return Err(error); + } + let result = (|| { + loop { + let mut available = 0_u32; + if unsafe { + PeekNamedPipe( + read_pipe.raw(), + std::ptr::null_mut(), + 0, + std::ptr::null_mut(), + &mut available, + std::ptr::null_mut(), + ) + } == 0 + { + let error = unsafe { GetLastError() }; + if matches!(error, ERROR_BROKEN_PIPE | ERROR_PIPE_NOT_CONNECTED) { + break; + } + return Err(Error::Spawn); + } + if available == 0 { + break; + } + let count = usize::try_from(available).unwrap_or(usize::MAX).min(8192); + if count > stdout_limit.saturating_sub(output.len()) { + return Err(Error::OutputLimit); + } + let mut buffer = [0_u8; 8192]; + let mut read = 0_u32; + if unsafe { + ReadFile( + read_pipe.raw(), + buffer.as_mut_ptr().cast(), + u32::try_from(count).map_err(|_| Error::Spawn)?, + &mut read, + std::ptr::null_mut(), + ) + } == 0 + { + return Err(Error::Spawn); + } + let read = usize::try_from(read).map_err(|_| Error::Spawn)?; + if read == 0 { + break; + } + output.extend_from_slice(&buffer[..read]); + } + recheck_regular(&executable.file)?; + recheck_directory(cwd)?; + Ok(output) + })(); + must_close([read_pipe, null_handle, process_handle, job]); + result + } + + fn terminate_unassigned(process: HANDLE) -> Result<(), Error> { + let terminate_failed = unsafe { TerminateProcess(process, 126) } == 0; + let wait = unsafe { WaitForSingleObject(process, 30_000) }; + if terminate_failed + || wait != WAIT_OBJECT_0 + || injected_settlement_failure!(WindowsUnassigned) + || injected_settlement_failure!(WindowsTerminateProcess) + || injected_settlement_failure!(WindowsWaitUnassigned) + { + return Err(Error::Spawn); + } + Ok(()) + } + + fn settle_job(job: HANDLE, process: HANDLE, terminate: bool) -> Result<(), Error> { + let terminate_failed = terminate + && (unsafe { TerminateJobObject(job, 126) } == 0 + || injected_settlement_failure!(WindowsJob) + || injected_settlement_failure!(WindowsTerminateJob)); + let leader_wait = unsafe { WaitForSingleObject(process, 30_000) }; + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30); + loop { + let mut accounting = JOBOBJECT_BASIC_ACCOUNTING_INFORMATION::default(); + if injected_settlement_failure!(WindowsQueryJob) + || unsafe { + QueryInformationJobObject( + job, + JobObjectBasicAccountingInformation, + (&mut accounting as *mut JOBOBJECT_BASIC_ACCOUNTING_INFORMATION).cast(), + u32::try_from(std::mem::size_of_val(&accounting)) + .map_err(|_| Error::Spawn)?, + std::ptr::null_mut(), + ) + } == 0 + { + return Err(Error::Spawn); + } + if accounting.ActiveProcesses == 0 { + return if terminate_failed || leader_wait != WAIT_OBJECT_0 { + Err(Error::Spawn) + } else { + Ok(()) + }; + } + if std::time::Instant::now() >= deadline { + return Err(Error::Spawn); + } + std::thread::sleep(std::time::Duration::from_millis(1)); + } + } + + fn wide_null(value: &OsStr) -> Result, Error> { + let mut wide = value.encode_wide().collect::>(); + if wide.is_empty() || wide.contains(&0) { + return Err(Error::Invalid); + } + wide.push(0); + Ok(wide) + } + + fn windows_command_line(arguments: &[String]) -> Result, Error> { + let mut line = String::from("semaprax-native-rust-interop-tool"); + for argument in arguments { + if argument.contains(['\0', '\r', '\n']) { + return Err(Error::Invalid); + } + line.push(' '); + line.push('"'); + let mut slashes = 0_usize; + for character in argument.chars() { + if character == '\\' { + slashes += 1; + } else { + if character == '"' { + line.extend(std::iter::repeat_n('\\', slashes * 2 + 1)); + } else { + line.extend(std::iter::repeat_n('\\', slashes)); + } + slashes = 0; + line.push(character); + } + } + line.extend(std::iter::repeat_n('\\', slashes * 2)); + line.push('"'); + } + wide_null(OsStr::new(&line)) + } + + pub fn rustc_version( + executable: &Executable, + cwd: &Directory, + maximum: usize, + ) -> Result, Error> { + let prepared = prepare_version_invocation("-vV", maximum.min(65_536))?; + let mut process_arena = prepare_process_arena(1)?; + version_prepared(executable, cwd, prepared, &mut process_arena) + } + pub fn clang_version( + executable: &Executable, + cwd: &Directory, + maximum: usize, + ) -> Result, Error> { + let prepared = prepare_version_invocation("--version", maximum.min(65_536))?; + let mut process_arena = prepare_process_arena(1)?; + version_prepared(executable, cwd, prepared, &mut process_arena) + } + + pub fn version_prepared( + executable: &Executable, + cwd: &Directory, + prepared: PreparedVersionInvocation, + process_arena: &mut PreparedProcessArena, + ) -> Result, Error> { + let maximum = prepared.output.capacity(); + run_argv( + executable, + cwd, + &[], + maximum, + Some(prepared.command_line), + Some(prepared.output), + process_arena, + ) + } + + pub fn prepare_c_compile_invocation( + target: &str, + input: &OsStr, + optimization: u8, + sanitizers: bool, + maximum: usize, + ) -> Result { + normal_name(input)?; + if sanitizers + || !matches!(optimization, 0 | 2) + || target.is_empty() + || !target + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')) + { + return Err(Error::Invalid); + } + let input = input.to_str().ok_or(Error::Invalid)?; + Ok(PreparedCCompileInvocation(prepare_command( + &[ + "-std=c11", + "-target", + target, + "-Wall", + "-Wextra", + "-Werror", + if optimization == 0 { "-O0" } else { "-O2" }, + "-c", + input, + "-o", + "-", + ], + maximum.min(33_554_432), + )?)) + } + + pub fn prepared_c_compile_owned_capacity(prepared: &PreparedCCompileInvocation) -> usize { + prepared_command_owned_capacity(&prepared.0) + } + + pub fn compile_c_prepared( + executable: &Executable, + cwd: &Directory, + prepared: PreparedCCompileInvocation, + process_arena: &mut PreparedProcessArena, + ) -> Result, Error> { + let maximum = prepared.0.output.capacity(); + run_argv( + executable, + cwd, + &prepared.0.arguments, + maximum, + Some(prepared.0.command_line), + Some(prepared.0.output), + process_arena, + ) + } + + pub fn prepare_rust_compile_invocation( + target: &str, + source: &OsStr, + output: &OsStr, + ) -> Result { + normal_name(source)?; + normal_name(output)?; + if target.is_empty() + || !target + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')) + { + return Err(Error::Invalid); + } + Ok(PreparedRustCompileInvocation { + command: prepare_command( + &[ + "--edition=2021", + "-Dwarnings", + "--crate-type", + "staticlib", + "-C", + "panic=unwind", + "--target", + target, + source.to_str().ok_or(Error::Invalid)?, + "-o", + output.to_str().ok_or(Error::Invalid)?, + ], + 0, + )?, + output_name: prepare_relative_name(output)?, + }) + } + + pub fn prepared_rust_compile_owned_capacity(prepared: &PreparedRustCompileInvocation) -> usize { + prepared_command_owned_capacity(&prepared.command).saturating_add( + prepared + .output_name + .0 + .capacity() + .saturating_mul(std::mem::size_of::()), + ) + } + + fn compile_rust_prepared_inner( + rustc: &Executable, + cwd: &Directory, + prepared: PreparedRustCompileInvocation, + process_arena: &mut PreparedProcessArena, + ) -> Result { + if hold_regular_file_name_prepared(cwd, &prepared.output_name).is_ok() { + return Err(Error::Exists); + } + if !run_argv( + rustc, + cwd, + &prepared.command.arguments, + 0, + Some(prepared.command.command_line), + Some(prepared.command.output), + process_arena, + )? + .is_empty() + { + return Err(Error::OutputLimit); + } + hold_regular_file_name_prepared(cwd, &prepared.output_name) + } + + pub fn compile_direct_rustc_prepared( + rustc: &DirectRustc, + cwd: &Directory, + prepared: PreparedRustCompileInvocation, + process_arena: &mut PreparedProcessArena, + ) -> Result { + recheck_directory(&rustc.sysroot)?; + compile_rust_prepared_inner(&rustc.executable, cwd, prepared, process_arena) + } + + #[allow(clippy::too_many_arguments)] + pub fn prepare_link_invocation( + target: &str, + harness: &OsStr, + c_object: &OsStr, + rust_archive: &OsStr, + output: &OsStr, + sanitizers: bool, + ) -> Result { + for name in [harness, c_object, rust_archive, output] { + normal_name(name)?; + } + if sanitizers + || target.is_empty() + || !target + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')) + { + return Err(Error::Invalid); + } + Ok(PreparedLinkInvocation { + command: prepare_command( + &[ + "-target", + target, + harness.to_str().ok_or(Error::Invalid)?, + c_object.to_str().ok_or(Error::Invalid)?, + rust_archive.to_str().ok_or(Error::Invalid)?, + "-o", + output.to_str().ok_or(Error::Invalid)?, + ], + 0, + )?, + output_name: prepare_relative_name(output)?, + }) + } + + pub fn prepared_link_owned_capacity(prepared: &PreparedLinkInvocation) -> usize { + prepared_command_owned_capacity(&prepared.command).saturating_add( + prepared + .output_name + .0 + .capacity() + .saturating_mul(std::mem::size_of::()), + ) + } + + pub fn link_prepared( + clang: &Executable, + cwd: &Directory, + prepared: PreparedLinkInvocation, + process_arena: &mut PreparedProcessArena, + ) -> Result { + if hold_regular_file_name_prepared(cwd, &prepared.output_name).is_ok() { + return Err(Error::Exists); + } + if !run_argv( + clang, + cwd, + &prepared.command.arguments, + 0, + Some(prepared.command.command_line), + Some(prepared.command.output), + process_arena, + )? + .is_empty() + { + return Err(Error::OutputLimit); + } + let file = hold_regular_file_name_prepared(cwd, &prepared.output_name)?; + let mut prefix = [0_u8; 2]; + let mut duplicate = file.file.try_clone().map_err(|_| Error::Changed)?; + duplicate + .seek(SeekFrom::Start(0)) + .map_err(|_| Error::Changed)?; + duplicate + .read_exact(&mut prefix) + .map_err(|_| Error::Invalid)?; + if prefix != *b"MZ" { + return Err(Error::Invalid); + } + Ok(Executable { file }) + } + + pub fn prepare_run_invocation() -> Result { + Ok(PreparedRunInvocation(prepare_command(&[], 0)?)) + } + + pub fn prepared_run_owned_capacity(prepared: &PreparedRunInvocation) -> usize { + prepared_command_owned_capacity(&prepared.0) + } + + pub fn run_prepared( + executable: &Executable, + cwd: &Directory, + prepared: PreparedRunInvocation, + process_arena: &mut PreparedProcessArena, + ) -> Result<(), Error> { + if run_argv( + executable, + cwd, + &prepared.0.arguments, + 0, + Some(prepared.0.command_line), + Some(prepared.0.output), + process_arena, + )? + .is_empty() + { + Ok(()) + } else { + Err(Error::OutputLimit) + } + } + + pub fn compile_c_to_stdout( + executable: &Executable, + cwd: &Directory, + target: &str, + input: &OsStr, + optimization: u8, + sanitizers: bool, + maximum: usize, + ) -> Result, Error> { + normal_name(input)?; + if sanitizers || !matches!(optimization, 0 | 2) { + return Err(Error::Invalid); + } + let arguments = vec![ + "-std=c11".to_owned(), + "-target".to_owned(), + target.to_owned(), + "-Wall".to_owned(), + "-Wextra".to_owned(), + "-Werror".to_owned(), + if optimization == 0 { + "-O0".to_owned() + } else { + "-O2".to_owned() + }, + "-c".to_owned(), + input.to_string_lossy().into_owned(), + "-o".to_owned(), + "-".to_owned(), + ]; + let command_line = windows_command_line(&arguments)?; + let output = Vec::with_capacity(maximum.min(33_554_432)); + let mut process_arena = prepare_process_arena(1)?; + run_argv( + executable, + cwd, + &arguments, + maximum.min(33_554_432), + Some(command_line), + Some(output), + &mut process_arena, + ) + } + + pub fn execute_harness(executable: &Executable, cwd: &Directory) -> Result<(), Error> { + let command_line = windows_command_line(&[])?; + let mut process_arena = prepare_process_arena(1)?; + if run_argv( + executable, + cwd, + &[], + 0, + Some(command_line), + Some(Vec::new()), + &mut process_arena, + )? + .is_empty() + { + Ok(()) + } else { + Err(Error::OutputLimit) + } + } + + #[cfg(test)] + pub(super) fn execute_harness_with_argument( + executable: &Executable, + cwd: &Directory, + argument: &str, + ) -> Result<(), Error> { + let arguments = [argument.to_owned()]; + let command_line = windows_command_line(&arguments)?; + let mut process_arena = prepare_process_arena(1)?; + if run_argv( + executable, + cwd, + &arguments, + 0, + Some(command_line), + Some(Vec::new()), + &mut process_arena, + )? + .is_empty() + { + Ok(()) + } else { + Err(Error::OutputLimit) + } + } + + #[allow(clippy::too_many_arguments)] + pub fn link_harness( + clang: &Executable, + cwd: &Directory, + target: &str, + harness: &OsStr, + c_object: &OsStr, + rust_archive: &OsStr, + output: &OsStr, + sanitizers: bool, + ) -> Result { + for name in [harness, c_object, rust_archive, output] { + normal_name(name)?; + } + if sanitizers { + return Err(Error::Invalid); + } + let arguments = vec![ + "-target".to_owned(), + target.to_owned(), + harness.to_string_lossy().into_owned(), + c_object.to_string_lossy().into_owned(), + rust_archive.to_string_lossy().into_owned(), + "-o".to_owned(), + output.to_string_lossy().into_owned(), + ]; + let command_line = windows_command_line(&arguments)?; + let mut process_arena = prepare_process_arena(1)?; + if !run_argv( + clang, + cwd, + &arguments, + 0, + Some(command_line), + Some(Vec::new()), + &mut process_arena, + )? + .is_empty() + { + return Err(Error::OutputLimit); + } + hold_executable(cwd, output) + } +} + +pub use platform::*; + +#[cfg(test)] +mod tests { + use super::{enter_prepared_file_syscalls, Error, TEST_PREPARED_FILE_SYSCALL_ENTRIES}; + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] + use super::{set_test_settlement_failures, TestSettlementFailure}; + use std::sync::atomic::Ordering; + + #[cfg(target_os = "linux")] + fn linux_runner_failure_helper( + points: &[TestSettlementFailure], + expected: Option, + sentinel: &str, + ) { + let Some(root) = std::env::var_os("SEMAPRAX_SYS_TEST_HELPER_ROOT") else { + return; + }; + set_test_settlement_failures(points); + let root = std::path::PathBuf::from(root); + let directory = super::platform::hold_directory(&root).unwrap(); + let executable = + super::platform::hold_executable(&directory, std::ffi::OsStr::new("noisy")).unwrap(); + let result = super::platform::execute_harness(&executable, &directory); + if let Some(expected) = expected { + assert_eq!(result, Err(expected)); + } + std::fs::write(root.join(sentinel), b"returned").unwrap(); + } + + #[cfg(target_os = "linux")] + macro_rules! linux_runner_helper { + ($name:ident, [$($point:ident),+], $expected:expr, $sentinel:literal) => { + #[test] + fn $name() { + linux_runner_failure_helper( + &[$(TestSettlementFailure::$point),+], + $expected, + $sentinel, + ); + } + }; + } + + #[cfg(target_os = "linux")] + linux_runner_helper!( + helper_linux_pipe_read_fcntl, + [UnixPipeReadFcntl], + Some(Error::Spawn), + "settled" + ); + #[cfg(target_os = "linux")] + linux_runner_helper!( + helper_linux_pipe_write_fcntl, + [UnixPipeWriteFcntl], + Some(Error::Spawn), + "settled" + ); + #[cfg(target_os = "linux")] + linux_runner_helper!( + helper_linux_drain_fcntl, + [UnixDrainFcntl], + Some(Error::Spawn), + "settled" + ); + #[cfg(target_os = "linux")] + linux_runner_helper!(helper_linux_poll, [UnixPoll], Some(Error::Spawn), "settled"); + #[cfg(target_os = "linux")] + linux_runner_helper!(helper_linux_read, [UnixRead], Some(Error::Spawn), "settled"); + #[cfg(target_os = "linux")] + linux_runner_helper!( + helper_linux_read_conversion, + [UnixReadConversion], + Some(Error::OutputLimit), + "settled" + ); + #[cfg(target_os = "linux")] + linux_runner_helper!( + helper_linux_waitpid, + [UnixWaitpid], + Some(Error::Spawn), + "settled" + ); + #[cfg(target_os = "linux")] + linux_runner_helper!( + helper_linux_deadline, + [UnixDeadline], + Some(Error::Spawn), + "settled" + ); + #[cfg(target_os = "linux")] + linux_runner_helper!( + helper_linux_parent_write_close, + [UnixParentWriteClose], + None, + "post-fail-stop" + ); + #[cfg(target_os = "linux")] + linux_runner_helper!( + helper_linux_parent_null_close, + [UnixParentNullClose], + None, + "post-fail-stop" + ); + #[cfg(target_os = "linux")] + linux_runner_helper!( + helper_linux_settle_close, + [UnixDrainFcntl, UnixSettleClose], + None, + "post-fail-stop" + ); + #[cfg(target_os = "linux")] + linux_runner_helper!( + helper_linux_success_read_close, + [UnixSuccessReadClose], + None, + "post-fail-stop" + ); + #[cfg(target_os = "linux")] + linux_runner_helper!( + helper_linux_wait_settlement, + [UnixDrainFcntl, UnixWait], + None, + "post-fail-stop" + ); + #[cfg(target_os = "linux")] + linux_runner_helper!( + helper_linux_group_settlement, + [UnixDrainFcntl, UnixGroup], + None, + "post-fail-stop" + ); + + #[cfg(target_os = "linux")] + #[test] + fn linux_runner_boundaries_settle_or_fail_stop_without_later_action() { + use std::os::unix::process::ExitStatusExt as _; + use std::process::Command; + + let parent = std::fs::canonicalize(std::env::temp_dir()).unwrap(); + let root = parent.join(format!( + "semaprax-sys-runner-boundaries-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::create_dir(&root).unwrap(); + let source = root.join("noisy.c"); + std::fs::write( + &source, + "#include \n#include \nint main(void){FILE *f=fopen(\"leader.pid\",\"w\");if(!f)return 2;fprintf(f,\"%ld\",(long)getpid());fclose(f);fputs(\"x\",stdout);fflush(stdout);sleep(1);return 0;}\n", + ) + .unwrap(); + let compiler = std::env::var_os("CC").unwrap_or_else(|| "cc".into()); + let built = Command::new(compiler) + .env_clear() + .env("TMPDIR", &root) + .args(["-std=c11", "-Wall", "-Wextra", "-Werror", "-O2"]) + .arg(&source) + .arg("-o") + .arg(root.join("noisy")) + .output() + .unwrap(); + assert!( + built.status.success(), + "{}", + String::from_utf8_lossy(&built.stderr) + ); + let current = std::env::current_exe().unwrap(); + for helper in [ + "tests::helper_linux_pipe_read_fcntl", + "tests::helper_linux_pipe_write_fcntl", + "tests::helper_linux_drain_fcntl", + "tests::helper_linux_poll", + "tests::helper_linux_read", + "tests::helper_linux_read_conversion", + "tests::helper_linux_waitpid", + "tests::helper_linux_deadline", + ] { + let sentinel = root.join("settled"); + let _ = std::fs::remove_file(&sentinel); + let status = Command::new(¤t) + .env("SEMAPRAX_SYS_TEST_HELPER_ROOT", &root) + .args(["--exact", helper, "--nocapture"]) + .status() + .unwrap(); + assert!(status.success(), "settled boundary failed: {helper}"); + assert!( + sentinel.exists(), + "settled boundary did not return: {helper}" + ); + } + for helper in [ + "tests::helper_linux_parent_write_close", + "tests::helper_linux_parent_null_close", + "tests::helper_linux_settle_close", + "tests::helper_linux_success_read_close", + "tests::helper_linux_wait_settlement", + "tests::helper_linux_group_settlement", + ] { + let sentinel = root.join("post-fail-stop"); + let _ = std::fs::remove_file(&sentinel); + let status = Command::new(¤t) + .env("SEMAPRAX_SYS_TEST_HELPER_ROOT", &root) + .args(["--exact", helper, "--nocapture"]) + .status() + .unwrap(); + assert!(!status.success(), "fail-stop boundary returned: {helper}"); + assert!( + status.signal().is_some(), + "fail-stop did not abort: {helper}" + ); + assert!( + !sentinel.exists(), + "later action ran after fail-stop: {helper}" + ); + } + std::fs::remove_dir_all(&root).unwrap(); + } + + #[cfg(target_os = "macos")] + fn darwin_failure_helper(points: &[TestSettlementFailure]) { + let Some(root) = std::env::var_os("SEMAPRAX_SYS_TEST_HELPER_ROOT") else { + return; + }; + set_test_settlement_failures(points); + let root = std::path::PathBuf::from(root); + let directory = super::platform::hold_directory(&root).unwrap(); + let executable = + super::platform::hold_executable(&directory, std::ffi::OsStr::new("quiet")).unwrap(); + let _ = super::platform::execute_harness(&executable, &directory); + std::fs::write(root.join("post-fail-stop"), b"returned").unwrap(); + } + + #[cfg(target_os = "macos")] + fn darwin_returning_failure_helper(point: TestSettlementFailure, expected: Error) { + let Some(root) = std::env::var_os("SEMAPRAX_SYS_TEST_HELPER_ROOT") else { + return; + }; + set_test_settlement_failures(&[point]); + let root = std::path::PathBuf::from(root); + let directory = super::platform::hold_directory(&root).unwrap(); + let executable = + super::platform::hold_executable(&directory, std::ffi::OsStr::new("quiet")).unwrap(); + assert_eq!( + super::platform::execute_harness(&executable, &directory), + Err(expected) + ); + std::fs::write(root.join("post-return"), b"returned").unwrap(); + } + + #[cfg(target_os = "macos")] + #[test] + fn helper_darwin_actions_destroy() { + darwin_failure_helper(&[TestSettlementFailure::DarwinActionsDestroy]); + } + + #[cfg(target_os = "macos")] + #[test] + fn helper_darwin_attributes_destroy() { + darwin_failure_helper(&[TestSettlementFailure::DarwinAttributesDestroy]); + } + + #[cfg(target_os = "macos")] + #[test] + fn helper_darwin_attest_settlement_fail_stop() { + darwin_failure_helper(&[ + TestSettlementFailure::DarwinAttest, + TestSettlementFailure::UnixWait, + ]); + } + + #[cfg(target_os = "macos")] + #[test] + fn helper_darwin_sigcont_settlement_fail_stop() { + darwin_failure_helper(&[ + TestSettlementFailure::DarwinSigcont, + TestSettlementFailure::UnixGroup, + ]); + } + + #[cfg(target_os = "macos")] + #[test] + fn helper_darwin_attest_returns_changed_after_settlement() { + darwin_returning_failure_helper(TestSettlementFailure::DarwinAttest, Error::Changed); + } + + #[cfg(target_os = "macos")] + #[test] + fn helper_darwin_sigcont_returns_spawn_after_settlement() { + darwin_returning_failure_helper(TestSettlementFailure::DarwinSigcont, Error::Spawn); + } + + #[cfg(target_os = "macos")] + #[test] + fn darwin_spawn_resource_destroy_uncertainty_fail_stops_without_later_action() { + use std::os::unix::process::ExitStatusExt as _; + use std::process::Command; + + let parent = std::fs::canonicalize(std::env::temp_dir()).unwrap(); + let root = parent.join(format!( + "semaprax-sys-darwin-destroy-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::create_dir(&root).unwrap(); + let source = root.join("quiet.c"); + std::fs::write(&source, "int main(void){return 0;}\n").unwrap(); + let compiler = std::env::var_os("CC").unwrap_or_else(|| "cc".into()); + let built = Command::new(compiler) + .env_clear() + .env("TMPDIR", &root) + .args(["-std=c11", "-Wall", "-Wextra", "-Werror", "-O2"]) + .arg(&source) + .arg("-o") + .arg(root.join("quiet")) + .output() + .unwrap(); + assert!( + built.status.success(), + "{}", + String::from_utf8_lossy(&built.stderr) + ); + let current = std::env::current_exe().unwrap(); + for helper in [ + "tests::helper_darwin_attest_returns_changed_after_settlement", + "tests::helper_darwin_sigcont_returns_spawn_after_settlement", + ] { + let sentinel = root.join("post-return"); + let _ = std::fs::remove_file(&sentinel); + let status = Command::new(¤t) + .env("SEMAPRAX_SYS_TEST_HELPER_ROOT", &root) + .args(["--exact", helper, "--nocapture"]) + .status() + .unwrap(); + assert!( + status.success(), + "settled operation did not return: {helper}" + ); + assert!( + sentinel.exists(), + "post-return sentinel missing after settled operation: {helper}" + ); + } + for helper in [ + "tests::helper_darwin_actions_destroy", + "tests::helper_darwin_attributes_destroy", + "tests::helper_darwin_attest_settlement_fail_stop", + "tests::helper_darwin_sigcont_settlement_fail_stop", + ] { + let sentinel = root.join("post-fail-stop"); + let _ = std::fs::remove_file(&sentinel); + let status = Command::new(¤t) + .env("SEMAPRAX_SYS_TEST_HELPER_ROOT", &root) + .args(["--exact", helper, "--nocapture"]) + .status() + .unwrap(); + assert!(!status.success(), "destroy uncertainty returned: {helper}"); + assert!( + status.signal().is_some(), + "destroy uncertainty did not abort: {helper}" + ); + assert!( + !sentinel.exists(), + "later action ran after destroy uncertainty" + ); + } + std::fs::remove_dir_all(&root).unwrap(); + } + + #[cfg(target_os = "windows")] + fn windows_runner_failure_helper( + points: &[TestSettlementFailure], + executable_name: &str, + expected: Option, + bounded_output: bool, + sentinel: &str, + ) { + let Some(root) = std::env::var_os("SEMAPRAX_SYS_TEST_HELPER_ROOT") else { + return; + }; + set_test_settlement_failures(points); + let root = std::path::PathBuf::from(root); + let directory = super::platform::hold_directory(&root).unwrap(); + let executable = + super::platform::hold_executable(&directory, std::ffi::OsStr::new(executable_name)) + .unwrap(); + let result = if bounded_output { + super::platform::clang_version(&executable, &directory, 64).map(|_| ()) + } else { + super::platform::execute_harness(&executable, &directory) + }; + if let Some(expected) = expected { + assert_eq!(result, Err(expected)); + } + std::fs::write(root.join(sentinel), b"returned").unwrap(); + } + + #[cfg(target_os = "windows")] + macro_rules! windows_runner_helper { + ($name:ident, [$($point:ident),+], $exe:literal, $expected:expr, $bounded:expr, $sentinel:literal) => { + #[test] + fn $name() { + windows_runner_failure_helper( + &[$(TestSettlementFailure::$point),+], + $exe, + $expected, + $bounded, + $sentinel, + ); + } + }; + } + + #[cfg(target_os = "windows")] + windows_runner_helper!( + helper_windows_image, + [WindowsImage], + "quiet.exe", + Some(Error::Changed), + false, + "settled" + ); + #[cfg(target_os = "windows")] + windows_runner_helper!( + helper_windows_assign, + [WindowsAssign], + "quiet.exe", + Some(Error::Changed), + false, + "settled" + ); + #[cfg(target_os = "windows")] + windows_runner_helper!( + helper_windows_resume, + [WindowsResume], + "quiet.exe", + Some(Error::Spawn), + false, + "settled" + ); + #[cfg(target_os = "windows")] + windows_runner_helper!( + helper_windows_peek, + [WindowsPeek], + "quiet.exe", + Some(Error::Spawn), + false, + "settled" + ); + #[cfg(target_os = "windows")] + windows_runner_helper!( + helper_windows_read, + [WindowsRead], + "output.exe", + Some(Error::Spawn), + true, + "settled" + ); + #[cfg(target_os = "windows")] + windows_runner_helper!( + helper_windows_unassigned_fail_stop, + [WindowsImage, WindowsTerminateProcess], + "quiet.exe", + None, + false, + "post-fail-stop" + ); + #[cfg(target_os = "windows")] + windows_runner_helper!( + helper_windows_wait_unassigned_fail_stop, + [WindowsImage, WindowsWaitUnassigned], + "quiet.exe", + None, + false, + "post-fail-stop" + ); + #[cfg(target_os = "windows")] + windows_runner_helper!( + helper_windows_terminate_job_fail_stop, + [WindowsPeek, WindowsTerminateJob], + "quiet.exe", + None, + false, + "post-fail-stop" + ); + #[cfg(target_os = "windows")] + windows_runner_helper!( + helper_windows_query_job_fail_stop, + [WindowsPeek, WindowsQueryJob], + "quiet.exe", + None, + false, + "post-fail-stop" + ); + + #[cfg(target_os = "windows")] + #[test] + fn windows_runner_failures_use_only_explicit_test_state() { + use std::process::Command; + + let parent = std::fs::canonicalize(std::env::temp_dir()).unwrap(); + let root = parent.join(format!( + "semaprax-sys-runner-boundaries-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::create_dir(&root).unwrap(); + for (name, source) in [ + ("quiet", "int main(void){return 0;}\n"), + ( + "output", + "#include \n#include \nint main(void){fputs(\"x\",stdout);fflush(stdout);Sleep(30000);return 0;}\n", + ), + ( + "handle_probe", + "#include \n#include \n#include \nint main(int argc,char **argv){if(argc!=2)return 7;char *end=0;uintptr_t handle=(uintptr_t)_strtoui64(argv[1],&end,10);if(!end||*end)return 6;DWORD flags=0;if(getenv(\"PATH\")!=0)return 8;if(GetHandleInformation((HANDLE)handle,&flags))return 9;return 0;}\n", + ), + ] { + let source_path = root.join(format!("{name}.c")); + std::fs::write(&source_path, source).unwrap(); + let compiler = std::env::var_os("CLANG").unwrap_or_else(|| "clang".into()); + let built = Command::new(compiler) + .env("TMP", &root) + .env("TEMP", &root) + .args(["-std=c11", "-Wall", "-Wextra", "-Werror", "-O2"]) + .arg(&source_path) + .arg("-o") + .arg(root.join(format!("{name}.exe"))) + .output() + .unwrap(); + assert!( + built.status.success(), + "{}", + String::from_utf8_lossy(&built.stderr) + ); + } + use std::os::windows::io::AsRawHandle as _; + let inherited = std::fs::File::open("NUL").unwrap(); + let raw = inherited.as_raw_handle(); + assert_ne!( + unsafe { + windows_sys::Win32::Foundation::SetHandleInformation( + raw.cast(), + windows_sys::Win32::Foundation::HANDLE_FLAG_INHERIT, + windows_sys::Win32::Foundation::HANDLE_FLAG_INHERIT, + ) + }, + 0 + ); + let directory = super::platform::hold_directory(&root).unwrap(); + let executable = + super::platform::hold_executable(&directory, std::ffi::OsStr::new("handle_probe.exe")) + .unwrap(); + super::platform::execute_harness_with_argument( + &executable, + &directory, + &(raw as usize).to_string(), + ) + .unwrap(); + drop(executable); + drop(directory); + drop(inherited); + let current = std::env::current_exe().unwrap(); + for helper in [ + "tests::helper_windows_image", + "tests::helper_windows_assign", + "tests::helper_windows_resume", + "tests::helper_windows_peek", + "tests::helper_windows_read", + ] { + let sentinel = root.join("settled"); + let _ = std::fs::remove_file(&sentinel); + let status = Command::new(¤t) + .env("SEMAPRAX_SYS_TEST_HELPER_ROOT", &root) + .args(["--exact", helper, "--nocapture"]) + .status() + .unwrap(); + assert!(status.success(), "settled boundary failed: {helper}"); + assert!( + sentinel.exists(), + "settled boundary did not return: {helper}" + ); + } + for helper in [ + "tests::helper_windows_unassigned_fail_stop", + "tests::helper_windows_wait_unassigned_fail_stop", + "tests::helper_windows_terminate_job_fail_stop", + "tests::helper_windows_query_job_fail_stop", + ] { + let sentinel = root.join("post-fail-stop"); + let _ = std::fs::remove_file(&sentinel); + let status = Command::new(¤t) + .env("SEMAPRAX_SYS_TEST_HELPER_ROOT", &root) + .args(["--exact", helper, "--nocapture"]) + .status() + .unwrap(); + assert!(!status.success(), "fail-stop boundary returned: {helper}"); + assert!( + !sentinel.exists(), + "later action ran after fail-stop: {helper}" + ); + } + std::fs::remove_dir_all(&root).unwrap(); + } + + #[cfg(target_os = "linux")] + fn inventory_record(name: &[u8], inode: u64) -> Vec { + let length = (19 + name.len() + 1 + 7) & !7; + let mut bytes = vec![0_u8; length]; + bytes[..8].copy_from_slice(&inode.to_ne_bytes()); + bytes[16..18].copy_from_slice(&u16::try_from(length).unwrap().to_ne_bytes()); + bytes[18] = 8; + bytes[19..19 + name.len()].copy_from_slice(name); + bytes + } + + #[cfg(unix)] + fn with_inventory_fixture( + root: &std::path::Path, + action: impl FnOnce( + &super::platform::Directory, + &super::platform::PreparedDiscardNames<1>, + &super::platform::RegularFile, + &mut super::platform::PreparedInventoryExact<1>, + ), + ) { + use std::ffi::OsStr; + + let _ = std::fs::remove_dir_all(root); + std::fs::create_dir_all(root).unwrap(); + let directory = super::platform::hold_directory(root).unwrap(); + let names = super::platform::prepare_discard_names([OsStr::new("a")]).unwrap(); + let file = + super::platform::write_file_new_prepared(&directory, &names, 0, b"inventory", 0o600) + .unwrap(); + let mut prepared = super::platform::prepare_inventory_exact(&names).unwrap(); + action(&directory, &names, &file, &mut prepared); + drop((prepared, file, names, directory)); + std::fs::remove_dir_all(root).unwrap(); + } + + #[cfg(target_os = "macos")] + fn inventory_record(name: &[u8], inode: u64) -> Vec { + let length = (21 + name.len() + 1 + 3) & !3; + let mut bytes = vec![0_u8; length]; + bytes[..8].copy_from_slice(&inode.to_ne_bytes()); + bytes[16..18].copy_from_slice(&u16::try_from(length).unwrap().to_ne_bytes()); + bytes[18..20].copy_from_slice(&u16::try_from(name.len()).unwrap().to_ne_bytes()); + bytes[20] = 8; + bytes[21..21 + name.len()].copy_from_slice(name); + bytes + } + + #[test] + fn prepared_file_syscall_gate_resolves_name_before_entry() { + TEST_PREPARED_FILE_SYSCALL_ENTRIES.store(0, Ordering::Relaxed); + assert_eq!( + enter_prepared_file_syscalls::<()>(Err(Error::Invalid)), + Err(Error::Invalid) + ); + assert_eq!( + TEST_PREPARED_FILE_SYSCALL_ENTRIES.load(Ordering::Relaxed), + 0 + ); + + let resolved = (); + assert!(enter_prepared_file_syscalls(Ok(&resolved)).is_ok()); + assert_eq!( + TEST_PREPARED_FILE_SYSCALL_ENTRIES.load(Ordering::Relaxed), + 1 + ); + } + + #[test] + fn production_source_exposes_no_prepared_file_syscall_observer() { + let source = include_str!("lib.rs"); + assert!(!source.contains(concat!("pub fn reset_prepared_file_", "syscall_entries"))); + assert!(!source.contains(concat!("pub fn prepared_file_", "syscall_entries"))); + assert!(!source.contains(concat!("static PREPARED_FILE_", "SYSCALL_ENTRIES"))); + } + + #[cfg(unix)] + #[test] + fn prepared_inventory_record_parser_rejects_malformed_and_stale_bytes() { + use super::platform::test_parse_inventory_records; + + let valid = inventory_record(b"a", 7); + assert_eq!( + test_parse_inventory_records(&valid, &[(b"a".as_slice(), 7)]), + Ok(()) + ); + + let header = if cfg!(target_os = "macos") { 21 } else { 19 }; + assert!(test_parse_inventory_records(&vec![0_u8; header - 1], &[]).is_err()); + for record_length in [0_u16, 8, 21, u16::try_from(valid.len() + 8).unwrap()] { + let mut malformed = valid.clone(); + malformed[16..18].copy_from_slice(&record_length.to_ne_bytes()); + assert!(test_parse_inventory_records(&malformed, &[(b"a".as_slice(), 7)]).is_err()); + } + + let mut missing_nul = valid.clone(); + let terminator = header + 1; + missing_nul[terminator..].fill(0xff); + assert!(test_parse_inventory_records(&missing_nul, &[(b"a".as_slice(), 7)]).is_err()); + + let early_nul = inventory_record(b"a\0late", 7); + #[cfg(target_os = "linux")] + assert_eq!( + test_parse_inventory_records(&early_nul, &[(b"a".as_slice(), 7)]), + Ok(()) + ); + #[cfg(target_os = "macos")] + assert!(test_parse_inventory_records(&early_nul, &[(b"a".as_slice(), 7)]).is_err()); + + let mut nonzero_padding = valid.clone(); + nonzero_padding[terminator + 1..].fill(0xa5); + assert_eq!( + test_parse_inventory_records(&nonzero_padding, &[(b"a".as_slice(), 7)]), + Ok(()) + ); + + let mut poisoned_tail = valid.clone(); + poisoned_tail.extend_from_slice(&[0xff; 3]); + assert!(test_parse_inventory_records(&poisoned_tail, &[(b"a".as_slice(), 7)]).is_err()); + + let mut duplicate = valid.clone(); + duplicate.extend_from_slice(&valid); + assert!(test_parse_inventory_records(&duplicate, &[(b"a".as_slice(), 7)]).is_err()); + assert!(test_parse_inventory_records( + &inventory_record(b"unknown", 7), + &[(b"a".as_slice(), 7)] + ) + .is_err()); + #[cfg(target_os = "linux")] + assert!( + test_parse_inventory_records(&inventory_record(b"a", 0), &[(b"a".as_slice(), 0)]) + .is_err() + ); + + #[cfg(target_os = "macos")] + { + let mut with_tombstone = inventory_record(b"a", 7); + with_tombstone.extend_from_slice(&inventory_record(b"", 0)); + with_tombstone.extend_from_slice(&inventory_record(b"b", 8)); + assert_eq!( + test_parse_inventory_records( + &with_tombstone, + &[(b"a".as_slice(), 7), (b"b".as_slice(), 8)] + ), + Ok(()) + ); + let overlong = inventory_record(&vec![b'a'; 1024], 7); + assert!(test_parse_inventory_records(&overlong, &[]).is_err()); + } + } + + #[cfg(unix)] + #[test] + fn prepared_inventory_seek_reset_and_authentication_failures_are_bounded() { + let base = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!("semaprax-inventory-failure-{}", std::process::id())); + for (suffix, failures, expected_scans) in [ + ("initial", (true, false, false, false), 0), + ("reset", (false, true, false, false), 1), + ("authentication", (false, false, true, false), 1), + ] { + let root = base.join(suffix); + with_inventory_fixture(&root, |directory, names, file, prepared| { + super::platform::test_inventory_exact_failures( + prepared, failures.0, failures.1, failures.2, failures.3, + ); + assert!(super::platform::inventory_exact_prepared( + prepared, + directory, + names, + [Some(file)] + ) + .is_err()); + assert_eq!( + super::platform::test_inventory_exact_scan_entries(prepared), + expected_scans + ); + assert_eq!( + super::platform::prepared_inventory_exact_remaining(prepared), + 1 + ); + }); + } + let _ = std::fs::remove_dir_all(base); + } + + #[cfg(unix)] + #[test] + fn prepared_inventory_rebound_close_failure_child() { + let Ok(root) = std::env::var("SEMAPRAX_INVENTORY_CLOSE_FAILURE_ROOT") else { + return; + }; + let root = std::path::Path::new(&root); + with_inventory_fixture(root, |directory, names, file, prepared| { + super::platform::test_inventory_exact_failures(prepared, false, false, true, true); + let _ = + super::platform::inventory_exact_prepared(prepared, directory, names, [Some(file)]); + std::fs::write(root.join("later-action"), b"must not exist").unwrap(); + }); + } + + #[cfg(unix)] + #[test] + fn prepared_inventory_rebound_close_failure_is_fail_stop() { + let root = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-inventory-close-failure-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + let status = std::process::Command::new(std::env::current_exe().unwrap()) + .arg("--exact") + .arg("tests::prepared_inventory_rebound_close_failure_child") + .arg("--nocapture") + .env("SEMAPRAX_INVENTORY_CLOSE_FAILURE_ROOT", &root) + .status() + .unwrap(); + assert!(!status.success()); + assert!(!root.join("later-action").exists()); + let _ = std::fs::remove_dir_all(root); + } + + #[test] + fn linux_prepared_transfer_has_injection_and_allocation_free_copy_fallback_contract() { + let source = include_str!("lib.rs"); + let link = source + .find("libc::AT_EMPTY_PATH") + .expect("Linux prepared transfer uses the held source descriptor"); + let fallback = source[link..] + .find("fn copy_regular_file_new_prepared") + .map(|offset| link + offset) + .expect("Linux fallback is independently authored"); + let linux = &source[link..fallback]; + let linked = linux.find("if result == 0").expect("link success branch"); + let injected = linux + .find("if fail_before_authentication") + .expect("debug failure precedes reopen authentication"); + let reopened = linux + .find("hold_regular_file_name_prepared") + .expect("prepared destination reopen"); + assert!(linked < injected && injected < reopened); + for errno in ["libc::EPERM", "libc::EACCES", "libc::EOPNOTSUPP"] { + assert!(linux.contains(errno)); + } + assert!(linux.contains("copy_regular_file_new_prepared(")); + + let copy = &source[fallback..]; + for required in [ + "libc::O_EXCL", + "file.write_all(source_bytes)", + "file.sync_data()", + "authenticate_regular_file(file)", + ] { + assert!(copy.contains(required)); + } + assert!(!source.contains(concat!("CAP_DAC_", "READ_SEARCH"))); + } + + #[test] + fn prepared_inventory_exact_source_contract_is_raw_bounded_and_allocation_free() { + let source = include_str!("lib.rs"); + let linux_start = source + .find("#[cfg(target_os = \"linux\")]\n fn parse_linux_inventory_records") + .expect("Linux raw inventory scanner"); + let darwin_start = source[linux_start..] + .find("#[cfg(target_os = \"macos\")]\n fn parse_darwin_inventory_records") + .map(|offset| linux_start + offset) + .expect("Darwin raw inventory scanner"); + let inventory_start = source[darwin_start..] + .find("pub fn inventory_exact_prepared") + .map(|offset| darwin_start + offset) + .expect("Unix prepared inventory entry point"); + let linux = &source[linux_start..darwin_start]; + let darwin = &source[darwin_start..inventory_start]; + for required in [ + "libc::SYS_getdents64", + "let bytes_limit = libc::c_uint::try_from(capacity)", + "prepared.storage.fill(u64::MAX)", + "record < 20", + "record % std::mem::align_of::() != 0", + "next > bytes.len()", + "maximum_records", + "maximum_queries", + ] { + assert!( + linux.contains(required), + "missing Linux contract: {required}" + ); + } + for required in [ + "SYS_GETDIRENTRIES64", + "let bytes_limit: libc::size_t", + "let mut base: libc::off_t", + "prepared.storage.fill(u64::MAX)", + "record % 4 != 0", + "name_length > 1023", + "name_end >= next", + "next > bytes.len()", + "maximum_records", + "maximum_queries", + ] { + assert!( + darwin.contains(required), + "missing Darwin contract: {required}" + ); + } + for forbidden in ["fdopendir", "readdir", "BTreeSet", "to_vec("] { + assert!(!linux.contains(forbidden)); + assert!(!darwin.contains(forbidden)); + } + + let windows_start = source + .match_indices("pub fn inventory_exact_prepared") + .nth(1) + .map(|(offset, _)| offset) + .expect("Windows prepared inventory entry point"); + let windows_end = source[windows_start..] + .find("pub fn publish_directory_new") + .map(|offset| windows_start + offset) + .expect("end of Windows inventory scanner"); + let windows = &source[windows_start..windows_end]; + for required in [ + "FileIdExtdDirectoryRestartInfo", + "FILE_ID_EXTD_DIR_INFO", + "prepared.storage.fill(u64::MAX)", + "entry.FileId.Identifier != tracked.identity.file_id", + "std::mem::size_of::()", + "record_header_end > byte_length", + "next < minimum", + "next_end > byte_length", + "maximum_records", + "maximum_queries", + ] { + assert!( + windows.contains(required), + "missing Windows contract: {required}" + ); + } + let full_header_bound = windows + .find("if record_header_end > byte_length") + .expect("complete Windows record must fit"); + let entry_reference = windows + .find("let entry = unsafe") + .expect("Windows entry reference"); + assert!(full_header_bound < entry_reference); + assert!(!windows.contains("FILE_ID_BOTH_DIR_INFO")); + assert!(!windows.contains("String::from_utf16")); + } + + #[test] + fn prepared_publish_source_contract_has_no_late_name_or_handle_allocation() { + let source = include_str!("lib.rs"); + let unix_start = source + .find("fn observe_publish_rebound") + .expect("Unix prepared publish"); + let unix_end = source[unix_start..] + .find("pub fn discard_owned_stage_prepared") + .map(|offset| unix_start + offset) + .expect("end Unix prepared publish"); + let unix = &source[unix_start..unix_end]; + for required in [ + "prepared.remaining != 1", + "prepared.exact_capacity", + "relative_name_arena_cstr", + "observe_publish_rebound", + "prepared_directory_identity(stage)", + "libc::SYS_renameat2", + "renameatx_np", + ] { + assert!( + unix.contains(required), + "missing Unix publish contract: {required}" + ); + } + for forbidden in ["c_name(", "try_clone", "CString::new", "Vec::"] { + assert!( + !unix.contains(forbidden), + "late Unix publish operation: {forbidden}" + ); + } + + let windows_start = source + .match_indices("fn observe_publish_rebound") + .nth(1) + .map(|(offset, _)| offset) + .expect("Windows prepared publish"); + let windows_end = source[windows_start..] + .find("pub fn discard_owned_stage_prepared") + .map(|offset| windows_start + offset) + .expect("end Windows prepared publish"); + let windows = &source[windows_start..windows_end]; + for required in [ + "prepared.remaining != 1", + "prepared.exact_capacity", + "relative_file_arena", + "observe_publish_rebound", + "SetFileInformationByHandle", + "FileRenameInfoEx", + ] { + assert!( + windows.contains(required), + "missing Windows publish contract: {required}" + ); + } + for forbidden in [ + "prepare_relative_name(", + "named_information(", + "try_clone", + "collect::()) * std::mem::size_of::(); + assert_eq!( + super::platform::prepared_process_arena_plan_capacity(&plan), + 131_080 + aligned + ); + let arena = super::platform::materialize_process_arena(plan).unwrap(); + assert_eq!( + super::platform::prepared_process_arena_owned_capacity(&arena), + 131_080 + aligned + ); + } + assert!(matches!( + super::platform::process_arena_plan(12, 0, 2), + Err(Error::Unsupported) + )); + assert!(matches!( + super::platform::process_arena_plan(12, MAX_ATTRIBUTE_BYTES + 1, 2), + Err(Error::OutputLimit) + )); + + let include = std::ffi::OsStr::new(r"C:\sdk\include;C:\msvc\include"); + let libraries = std::ffi::OsStr::new(r"C:\sdk\lib;C:\msvc\lib"); + let plan = super::platform::prepare_process_arena_plan_with_environment( + 12, + Some(include), + Some(libraries), + ) + .unwrap(); + let required = super::platform::prepared_process_arena_plan_capacity(&plan); + let arena = super::platform::materialize_process_arena_with_environment( + plan, + Some(include), + Some(libraries), + ) + .unwrap(); + assert_eq!( + super::platform::prepared_process_arena_owned_capacity(&arena), + required + ); + } + + #[cfg(unix)] + #[test] + fn sysroot_output_is_one_nonempty_absolute_utf8_line() { + assert_eq!( + super::platform::one_sysroot_line(b"/toolchain\n"), + Ok(&b"/toolchain"[..]) + ); + assert_eq!( + super::platform::one_sysroot_line(b"/toolchain\r\n"), + Ok(&b"/toolchain"[..]) + ); + for invalid in [ + &b""[..], + &b"/toolchain"[..], + &b"\n"[..], + &b"/one\n/two\n"[..], + &b"/one\0two\n"[..], + &[0xff, b'\n'], + ] { + assert_eq!( + super::platform::one_sysroot_line(invalid), + Err(Error::Invalid) + ); + } + let resolver = super::platform::prepare_tool_resolver("rustc", 32_768).unwrap(); + assert!(matches!( + super::platform::hold_rustc_discovery_prepared( + resolver, + std::ffi::OsStr::new("relative-rustc") + ), + Err(Error::Invalid) + )); + } + + #[test] + fn direct_rustc_and_windows_process_source_contract_is_closed() { + let source = include_str!("lib.rs"); + let discovery_symbol = ["pub fn hold_rustc_", "discovery_prepared"].concat(); + let direct_compile_symbol = ["pub fn compile_direct_", "rustc_prepared"].concat(); + let generic_worker = ["fn compile_rust_", "prepared_inner"].concat(); + assert_eq!(source.matches(&discovery_symbol).count(), 2); + assert_eq!(source.matches(&direct_compile_symbol).count(), 2); + assert_eq!(source.matches(&generic_worker).count(), 2); + let generic_public = ["pub fn compile_rust_", "prepared("].concat(); + let legacy_public = ["pub fn compile_rust_", "staticlib("].concat(); + let misplaced = ["misplaced_windows_", "direct_rustc"].concat(); + assert!(!source.contains(&generic_public)); + assert!(!source.contains(&legacy_public)); + assert!(!source.contains(&misplaced)); + + let windows_start = source.find("fn run_argv(\n executable: &Executable,\n cwd: &Directory,\n arguments: &[String]").unwrap(); + let windows_end = source[windows_start..] + .find("fn terminate_unassigned") + .map(|offset| windows_start + offset) + .unwrap(); + let windows = &source[windows_start..windows_end]; + for required in [ + "final_path_prepared(&executable.file.file, &mut process_arena.application)", + "final_path_prepared(&cwd.file, &mut process_arena.cwd)", + "process_arena.application.resize(PROCESS_PATH_UNITS, 0)", + "process_arena.environment.as_ptr().cast()", + "let mut attribute_bytes = process_arena.attribute_bytes", + "process_arena.attributes.resize(attribute_words, 0)", + "let null_name = [u16::from(b'N'), u16::from(b'U'), u16::from(b'L'), 0]", + "must_terminate_unassigned(process_handle.raw())", + "failed |= thread_handle.close().is_err()", + "failed |= job.close().is_err()", + ] { + assert!( + windows.contains(required), + "missing Windows process contract: {required}" + ); + } + for forbidden in [ + "final_path(&executable.file.file)", + "OpenOptions", + "vec![0_u8; attribute_bytes]", + "String::from_utf16", + "PathBuf::from", + "InitializeProcThreadAttributeList(std::ptr::null_mut()", + "let empty_environment", + ] { + assert!( + !windows.contains(forbidden), + "late Windows process allocation: {forbidden}" + ); + } + let obsolete_attribute_words = ["PROCESS_ATTRIBUTE_", "WORDS"].concat(); + assert!(!source.contains(&obsolete_attribute_words)); + assert!(source.contains("pub fn prepare_process_arena_plan(uses: usize)")); + assert!(source.contains( + "InitializeProcThreadAttributeList(std::ptr::null_mut(), 1, 0, &mut attribute_bytes)" + )); + } + + #[test] + fn windows_directory_identity_source_excludes_mutable_length_and_binds_all_rechecks() { + let source = include_str!("lib.rs"); + let start = source.find("struct DirectoryIdentity").unwrap(); + let end = source[start..] + .find("pub struct Directory") + .map(|offset| start + offset) + .unwrap(); + let identity = &source[start..end]; + for required in ["volume: u64", "file_id: [u8; 16]", "stable_attributes: u32"] { + assert!(identity.contains(required)); + } + assert!(!identity.contains("length:")); + + let windows_start = source.find("#[cfg(windows)]\nmod platform").unwrap(); + let windows = &source[windows_start..]; + for required in [ + "identity: DirectoryIdentity", + "directory_identity: Option", + "FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT", + "stable_attributes != FILE_ATTRIBUTE_DIRECTORY", + "directory_information(&directory.file)? != directory.identity", + "directory_information(&rebound)? == directory.identity", + "directory_information(&rebound)? != stage.identity", + "Result", + ] { + assert!( + windows.contains(required), + "missing stable Windows directory identity contract: {required}", + ); + } + } + + #[test] + fn linux_rust_staticlib_link_tail_is_frozen_for_prepared_and_legacy_paths() { + let source = include_str!("lib.rs"); + let unix_start = source.find("#[cfg(unix)]\nmod platform").unwrap(); + let windows_start = source.find("#[cfg(windows)]\nmod platform").unwrap(); + let unix = &source[unix_start..windows_start]; + let native_start = unix + .find("const LINUX_RUST_STATICLIB_NATIVE_LIBS: [&str; 7]") + .unwrap(); + let native_end = unix[native_start..] + .find("\n ];") + .map(|offset| native_start + offset + "\n ];".len()) + .unwrap(); + let native = &unix[native_start..native_end]; + let mut previous = 0usize; + for required in [ + "-lgcc_s", + "-lutil", + "-lrt", + "-lpthread", + "-lm", + "-ldl", + "-lc", + ] { + let offset = native.find(required).unwrap(); + assert!( + offset >= previous, + "Linux native-static library order changed" + ); + previous = offset; + } + assert_eq!( + unix.matches("LINUX_RUST_STATICLIB_NATIVE_LIBS") + .count(), + 3, + "the frozen Linux native-static library tail must have one definition and exactly two link consumers", + ); + assert_eq!( + unix.matches("LINUX_LINKER_ARGUMENT").count(), + 3, + "the absolute Linux linker argument must have one definition and exactly two link consumers", + ); + + let prepared_start = unix.find("pub fn prepare_link_invocation(").unwrap(); + let prepared_end = unix[prepared_start..] + .find("pub fn prepared_link_owned_capacity(") + .map(|offset| prepared_start + offset) + .unwrap(); + let prepared = &unix[prepared_start..prepared_end]; + let prepared_linker = prepared + .find("values[count] = LINUX_LINKER_ARGUMENT") + .unwrap(); + let prepared_archive = prepared.find("rust_archive.to_str()").unwrap(); + let prepared_output = prepared.find("output.to_str()").unwrap(); + let prepared_tail = prepared + .find("for value in LINUX_RUST_STATICLIB_NATIVE_LIBS") + .unwrap(); + assert!( + prepared_linker < prepared_archive + && prepared_archive < prepared_output + && prepared_output < prepared_tail + ); + + let legacy_start = unix.find("pub fn link_harness(").unwrap(); + let legacy_end = unix[legacy_start..] + .find("let mut process_arena = prepare_process_arena(1)?") + .map(|offset| legacy_start + offset) + .unwrap(); + let legacy = &unix[legacy_start..legacy_end]; + assert!(legacy.contains("arguments.insert(2, argument(LINUX_LINKER_ARGUMENT)?)")); + let legacy_archive = legacy.find("rust_archive.to_str()").unwrap(); + let legacy_output = legacy.find("output.to_str()").unwrap(); + let legacy_tail = legacy.find("LINUX_RUST_STATICLIB_NATIVE_LIBS").unwrap(); + assert!(legacy_archive < legacy_output && legacy_output < legacy_tail); + } + + #[test] + fn linux_runner_uses_the_held_executable_path_as_argv0_before_fexecve() { + let source = include_str!("lib.rs"); + let start = source + .find("#[cfg(target_os = \"linux\")]\n fn run_argv(") + .unwrap(); + let end = source[start..] + .find("#[cfg(target_os = \"macos\")]\n fn run_argv(") + .map(|offset| start + offset) + .unwrap(); + let runner = &source[start..end]; + assert!(!runner.contains("semaprax-native-rust-interop-tool")); + for required in [ + "const EXECUTABLE_FD: libc::c_int = 1020", + "c\"/proc/self/fd/1020\"", + "libc::readlink(", + "argv[0] = argv0.as_ptr().cast()", + "fexecve(executable_fd, argv.as_ptr(), env.as_ptr())", + ] { + assert!( + runner.contains(required), + "missing Linux argv0 contract: {required}" + ); + } + let duplicated = runner.find("libc::F_DUPFD").unwrap(); + let readlink = runner.find("libc::readlink(").unwrap(); + let argv0 = runner.find("argv[0] = argv0.as_ptr().cast()").unwrap(); + let execute = runner + .find("fexecve(executable_fd, argv.as_ptr(), env.as_ptr())") + .unwrap(); + assert!(duplicated < readlink && readlink < argv0 && argv0 < execute); + } + + #[cfg(windows)] + #[test] + fn windows_directory_identity_survives_full_inventory_and_rejects_foreign_or_substituted_path() + { + use std::ffi::OsStr; + + let root = std::env::temp_dir().join(format!( + "semaprax-windows-directory-identity-{}", + std::process::id(), + )); + let stage_path = root.join("stage"); + let displaced_path = root.join("displaced"); + let foreign_path = root.join("foreign"); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir_all(&stage_path).unwrap(); + std::fs::create_dir(&foreign_path).unwrap(); + let stage = super::platform::hold_directory(&stage_path).unwrap(); + let names = super::platform::prepare_discard_names([ + OsStr::new("a"), + OsStr::new("b"), + OsStr::new("c"), + OsStr::new("d"), + OsStr::new("e"), + OsStr::new("f"), + OsStr::new("g"), + ]) + .unwrap(); + let files = (0..7) + .map(|index| { + super::platform::write_file_new_prepared( + &stage, + &names, + index, + &[u8::try_from(index).unwrap()], + 0, + ) + .unwrap() + }) + .collect::>(); + super::platform::recheck_directory(&stage).unwrap(); + let mut inventory = super::platform::prepare_inventory_exact(&names).unwrap(); + let attached = std::array::from_fn(|index| Some(&files[index])); + super::platform::inventory_exact_prepared(&mut inventory, &stage, &names, attached) + .unwrap(); + assert!(!super::platform::same_directory_path(&stage, &foreign_path).unwrap()); + + drop((inventory, files, names)); + std::fs::rename(&stage_path, &displaced_path).unwrap(); + std::fs::create_dir(&stage_path).unwrap(); + super::platform::recheck_directory(&stage).unwrap(); + assert!(!super::platform::same_directory_path(&stage, &stage_path).unwrap()); + drop(stage); + std::fs::remove_dir_all(&root).unwrap(); + } +} diff --git a/crates/semaprax-native-rust-interop-platform/Cargo.toml b/crates/semaprax-native-rust-interop-platform/Cargo.toml new file mode 100644 index 0000000..906ff46 --- /dev/null +++ b/crates/semaprax-native-rust-interop-platform/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "semaprax-native-rust-interop-platform" +version = "0.1.0" +edition = "2021" +rust-version = "1.85" +publish = false +description = "Safe held-handle facade for private SEMAPRAX Native Rust Interop builds" +license = "Apache-2.0" + +[dependencies] +semaprax-native-rust-interop-platform-sys = { version = "=0.1.0", path = "../semaprax-native-rust-interop-platform-sys" } + +[lints.rust] +unsafe_code = "forbid" diff --git a/crates/semaprax-native-rust-interop-platform/src/lib.rs b/crates/semaprax-native-rust-interop-platform/src/lib.rs new file mode 100644 index 0000000..25d3afd --- /dev/null +++ b/crates/semaprax-native-rust-interop-platform/src/lib.rs @@ -0,0 +1,1021 @@ +//! Safe held-handle facade for private Native Rust Interop bundle builds. + +#![forbid(unsafe_code)] + +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; + +pub use semaprax_native_rust_interop_platform_sys::Error; + +pub struct HeldDirectory(semaprax_native_rust_interop_platform_sys::Directory); +pub struct HeldRegularFile(semaprax_native_rust_interop_platform_sys::RegularFile); +pub struct HeldExecutable(semaprax_native_rust_interop_platform_sys::Executable); +pub struct HeldTool { + executable: HeldExecutable, + path: String, +} +pub struct HeldRustcDiscovery(semaprax_native_rust_interop_platform_sys::RustcDiscovery); +pub struct HeldDirectRustc(semaprax_native_rust_interop_platform_sys::DirectRustc); + +pub struct PreparedStageName(semaprax_native_rust_interop_platform_sys::PreparedRelativeNameArena); +pub struct PreparedVersionInvocation( + semaprax_native_rust_interop_platform_sys::PreparedVersionInvocation, +); +pub struct PreparedSysrootInvocation( + semaprax_native_rust_interop_platform_sys::PreparedSysrootInvocation, +); +pub struct PreparedRustcVersionInvocation( + semaprax_native_rust_interop_platform_sys::PreparedRustcVersionInvocation, +); +pub struct PreparedProcessArenaPlan( + semaprax_native_rust_interop_platform_sys::PreparedProcessArenaPlan, +); +pub struct PreparedProcessArena(semaprax_native_rust_interop_platform_sys::PreparedProcessArena); +pub struct PreparedToolResolver(semaprax_native_rust_interop_platform_sys::PreparedToolResolver); +pub struct PreparedCCompileInvocation( + semaprax_native_rust_interop_platform_sys::PreparedCCompileInvocation, +); +pub struct PreparedRustCompileInvocation( + semaprax_native_rust_interop_platform_sys::PreparedRustCompileInvocation, +); +pub struct PreparedLinkInvocation( + semaprax_native_rust_interop_platform_sys::PreparedLinkInvocation, +); +pub struct PreparedRunInvocation(semaprax_native_rust_interop_platform_sys::PreparedRunInvocation); +pub struct PreparedLinkOrCopy { + native: semaprax_native_rust_interop_platform_sys::PreparedLinkOrCopy, + source_name: &'static str, + destination_name: &'static str, + source_index: usize, + destination_index: usize, +} +pub struct PreparedInventoryExact( + semaprax_native_rust_interop_platform_sys::PreparedInventoryExact, +); +pub struct PreparedPublishDirectory( + semaprax_native_rust_interop_platform_sys::PreparedPublishDirectory, +); + +pub struct PreparedDiscardInventory { + names: [&'static OsStr; N], + native: semaprax_native_rust_interop_platform_sys::PreparedDiscardNames, + files: [Option; N], + attached: usize, + #[cfg(debug_assertions)] + failure_after_delete: Option, +} + +pub fn prepare_stage_name(name: &OsStr) -> Result { + let maximum = name.as_encoded_bytes().len(); + let mut arena = prepare_stage_name_arena(maximum)?; + arena.set(name)?; + Ok(arena) +} + +pub fn prepare_stage_name_arena(maximum: usize) -> Result { + semaprax_native_rust_interop_platform_sys::prepare_relative_name_arena(maximum) + .map(PreparedStageName) +} + +impl PreparedStageName { + pub fn set(&mut self, name: &OsStr) -> Result<(), Error> { + semaprax_native_rust_interop_platform_sys::set_relative_name_arena(&mut self.0, name) + } + + pub fn capacity(&self) -> usize { + semaprax_native_rust_interop_platform_sys::relative_name_arena_capacity(&self.0) + } +} + +pub fn prepare_discard_inventory( + names: [&'static OsStr; N], +) -> Result, Error> { + let native = semaprax_native_rust_interop_platform_sys::prepare_discard_names(names)?; + Ok(PreparedDiscardInventory { + names, + native, + files: [const { None }; N], + attached: 0, + #[cfg(debug_assertions)] + failure_after_delete: None, + }) +} + +pub fn prepare_discard_inventory_bounded( + names: [&'static OsStr; N], + maximum_native_bytes: usize, +) -> Result, Error> { + let inventory = prepare_discard_inventory(names)?; + if prepared_discard_inventory_owned_capacity(&inventory) > maximum_native_bytes { + return Err(Error::OutputLimit); + } + Ok(inventory) +} + +pub fn prepared_discard_inventory_owned_capacity( + inventory: &PreparedDiscardInventory, +) -> usize { + semaprax_native_rust_interop_platform_sys::prepared_discard_names_owned_capacity( + &inventory.native, + ) +} + +impl PreparedDiscardInventory { + fn planned_slot(&self, name: &str) -> Result { + self.names + .iter() + .position(|candidate| *candidate == OsStr::new(name)) + .ok_or(Error::Invalid) + } + + pub fn validate_next(&self, name: &str) -> Result { + let index = self.attached; + if index >= N || self.names[index] != OsStr::new(name) || self.files[index].is_some() { + return Err(Error::Invalid); + } + Ok(index) + } + + pub fn validate_slot(&self, name: &str) -> Result { + self.names[..self.attached] + .iter() + .position(|candidate| *candidate == OsStr::new(name)) + .filter(|index| self.files[*index].is_some()) + .ok_or(Error::Invalid) + } + + pub fn attach(&mut self, name: &str, file: HeldRegularFile) -> Result<(), Error> { + let index = self.validate_next(name)?; + self.files[index] = Some(file); + self.attached += 1; + Ok(()) + } + + pub fn file(&self, name: &str) -> Result<&HeldRegularFile, Error> { + self.names[..self.attached] + .iter() + .position(|candidate| *candidate == OsStr::new(name)) + .and_then(|index| self.files[index].as_ref()) + .ok_or(Error::Changed) + } + + pub fn recheck(&self, names: &[&str]) -> Result<(), Error> { + for name in names { + recheck_regular_file(self.file(name)?)?; + } + Ok(()) + } + + pub fn attached(&self) -> usize { + self.attached + } + + pub const fn capacity(&self) -> usize { + N + } + + #[cfg(debug_assertions)] + #[doc(hidden)] + pub fn inject_discard_failure_after_delete(&mut self, deleted: Option) { + self.failure_after_delete = deleted; + } +} + +pub fn prepare_link_or_copy( + source: &PreparedDiscardInventory, + source_name: &'static str, + destination: &PreparedDiscardInventory, + destination_name: &'static str, +) -> Result { + let source_index = source.planned_slot(source_name)?; + let destination_index = destination.planned_slot(destination_name)?; + let native = semaprax_native_rust_interop_platform_sys::prepare_link_or_copy( + &destination.native, + destination_index, + )?; + Ok(PreparedLinkOrCopy { + native, + source_name, + destination_name, + source_index, + destination_index, + }) +} + +pub fn link_or_copy_required_capacity( + source: &PreparedDiscardInventory, + source_name: &str, + destination: &PreparedDiscardInventory, + destination_name: &str, +) -> Result { + let _ = source.planned_slot(source_name)?; + let destination_index = destination.planned_slot(destination_name)?; + semaprax_native_rust_interop_platform_sys::link_or_copy_required_capacity( + &destination.native, + destination_index, + ) +} + +pub fn prepared_link_or_copy_owned_capacity(prepared: &PreparedLinkOrCopy) -> usize { + semaprax_native_rust_interop_platform_sys::prepared_link_or_copy_owned_capacity( + &prepared.native, + ) +} + +#[cfg(debug_assertions)] +#[doc(hidden)] +pub fn inject_link_or_copy_failure_before_authentication(prepared: &mut PreparedLinkOrCopy) { + semaprax_native_rust_interop_platform_sys::inject_link_or_copy_failure_before_authentication( + &mut prepared.native, + ); +} + +pub fn link_or_copy_new_prepared( + prepared: PreparedLinkOrCopy, + source: &PreparedDiscardInventory, + destination_directory: &HeldDirectory, + destination: &mut PreparedDiscardInventory, + source_bytes: &[u8], +) -> Result<(), Error> { + let source_index = source.validate_slot(prepared.source_name)?; + let destination_index = destination.validate_next(prepared.destination_name)?; + if source_index != prepared.source_index || destination_index != prepared.destination_index { + return Err(Error::Invalid); + } + let source_file = source.files[source_index].as_ref().ok_or(Error::Invalid)?; + let destination_file = semaprax_native_rust_interop_platform_sys::link_or_copy_new_prepared( + prepared.native, + &source_file.0, + &destination_directory.0, + &destination.native, + destination_index, + source_bytes, + )?; + destination.files[destination_index] = Some(HeldRegularFile(destination_file)); + destination.attached += 1; + Ok(()) +} + +pub fn inventory_exact_required_capacity( + inventory: &PreparedDiscardInventory, +) -> Result { + semaprax_native_rust_interop_platform_sys::inventory_exact_required_capacity(&inventory.native) +} + +pub fn prepare_inventory_exact( + inventory: &PreparedDiscardInventory, +) -> Result, Error> { + semaprax_native_rust_interop_platform_sys::prepare_inventory_exact(&inventory.native) + .map(PreparedInventoryExact) +} + +pub fn prepared_inventory_exact_owned_capacity( + prepared: &PreparedInventoryExact, +) -> usize { + semaprax_native_rust_interop_platform_sys::prepared_inventory_exact_owned_capacity(&prepared.0) +} + +pub fn prepared_inventory_exact_remaining( + prepared: &PreparedInventoryExact, +) -> u8 { + semaprax_native_rust_interop_platform_sys::prepared_inventory_exact_remaining(&prepared.0) +} + +pub fn inventory_exact_prepared( + prepared: &mut PreparedInventoryExact, + directory: &HeldDirectory, + inventory: &PreparedDiscardInventory, +) -> Result<(), Error> { + if inventory.attached != N || inventory.files.iter().any(Option::is_none) { + return Err(Error::Invalid); + } + let files = std::array::from_fn(|index| inventory.files[index].as_ref().map(|file| &file.0)); + semaprax_native_rust_interop_platform_sys::inventory_exact_prepared( + &mut prepared.0, + &directory.0, + &inventory.native, + files, + ) +} + +pub fn write_file_new_prepared( + directory: &HeldDirectory, + inventory: &mut PreparedDiscardInventory, + name: &str, + bytes: &[u8], + mode: u32, +) -> Result<(), Error> { + let index = inventory.validate_next(name)?; + let file = semaprax_native_rust_interop_platform_sys::write_file_new_prepared( + &directory.0, + &inventory.native, + index, + bytes, + mode, + )?; + inventory.files[index] = Some(HeldRegularFile(file)); + inventory.attached += 1; + Ok(()) +} + +pub fn hold_regular_file_prepared( + directory: &HeldDirectory, + inventory: &PreparedDiscardInventory, + name: &str, +) -> Result { + let index = inventory.validate_slot(name)?; + let tracked = inventory.files[index].as_ref().ok_or(Error::Invalid)?; + semaprax_native_rust_interop_platform_sys::hold_regular_file_prepared( + &directory.0, + &inventory.native, + index, + &tracked.0, + ) + .map(HeldRegularFile) +} + +pub struct ToolOutput { + bytes: Vec, +} + +impl ToolOutput { + pub fn bytes(&self) -> &[u8] { + &self.bytes + } + + pub fn into_bytes(self) -> Vec { + self.bytes + } + + pub fn capacity(&self) -> usize { + self.bytes.capacity() + } +} + +pub fn prepare_version_invocation( + argument: &str, + maximum: usize, +) -> Result { + semaprax_native_rust_interop_platform_sys::prepare_version_invocation(argument, maximum) + .map(PreparedVersionInvocation) +} + +pub fn prepared_version_owned_capacity(prepared: &PreparedVersionInvocation) -> usize { + semaprax_native_rust_interop_platform_sys::prepared_version_owned_capacity(&prepared.0) +} + +pub fn prepare_sysroot_invocation(maximum: usize) -> Result { + semaprax_native_rust_interop_platform_sys::prepare_sysroot_invocation(maximum) + .map(PreparedSysrootInvocation) +} + +pub fn prepare_rustc_version_invocation( + maximum: usize, +) -> Result { + semaprax_native_rust_interop_platform_sys::prepare_rustc_version_invocation(maximum) + .map(PreparedRustcVersionInvocation) +} + +pub fn prepared_sysroot_owned_capacity(prepared: &PreparedSysrootInvocation) -> usize { + semaprax_native_rust_interop_platform_sys::prepared_sysroot_owned_capacity(&prepared.0) +} + +pub fn prepared_rustc_version_owned_capacity(prepared: &PreparedRustcVersionInvocation) -> usize { + semaprax_native_rust_interop_platform_sys::prepared_rustc_version_owned_capacity(&prepared.0) +} + +pub fn prepare_process_arena_plan(uses: usize) -> Result { + semaprax_native_rust_interop_platform_sys::prepare_process_arena_plan(uses) + .map(PreparedProcessArenaPlan) +} + +pub fn prepare_process_arena_plan_with_environment( + uses: usize, + include: Option<&OsStr>, + libraries: Option<&OsStr>, +) -> Result { + semaprax_native_rust_interop_platform_sys::prepare_process_arena_plan_with_environment( + uses, include, libraries, + ) + .map(PreparedProcessArenaPlan) +} + +pub fn prepared_process_arena_plan_capacity(plan: &PreparedProcessArenaPlan) -> usize { + semaprax_native_rust_interop_platform_sys::prepared_process_arena_plan_capacity(&plan.0) +} + +pub fn materialize_process_arena( + plan: PreparedProcessArenaPlan, +) -> Result { + semaprax_native_rust_interop_platform_sys::materialize_process_arena(plan.0) + .map(PreparedProcessArena) +} + +pub fn materialize_process_arena_with_environment( + plan: PreparedProcessArenaPlan, + include: Option<&OsStr>, + libraries: Option<&OsStr>, +) -> Result { + semaprax_native_rust_interop_platform_sys::materialize_process_arena_with_environment( + plan.0, include, libraries, + ) + .map(PreparedProcessArena) +} + +pub fn prepared_process_arena_owned_capacity(prepared: &PreparedProcessArena) -> usize { + semaprax_native_rust_interop_platform_sys::prepared_process_arena_owned_capacity(&prepared.0) +} + +pub fn prepared_process_arena_remaining(prepared: &PreparedProcessArena) -> usize { + semaprax_native_rust_interop_platform_sys::prepared_process_arena_remaining(&prepared.0) +} + +pub fn prepare_tool_resolver( + fallback: &str, + maximum: usize, +) -> Result { + semaprax_native_rust_interop_platform_sys::prepare_tool_resolver(fallback, maximum) + .map(PreparedToolResolver) +} + +pub fn prepared_tool_resolver_owned_capacity(prepared: &PreparedToolResolver) -> usize { + semaprax_native_rust_interop_platform_sys::prepared_tool_resolver_owned_capacity(&prepared.0) +} + +pub fn hold_directory(path: &Path) -> Result { + semaprax_native_rust_interop_platform_sys::hold_directory(path).map(HeldDirectory) +} + +pub fn recheck_directory(directory: &HeldDirectory) -> Result<(), Error> { + semaprax_native_rust_interop_platform_sys::recheck_directory(&directory.0) +} + +pub fn same_directory_path(directory: &HeldDirectory, path: &Path) -> Result { + semaprax_native_rust_interop_platform_sys::same_directory_path(&directory.0, path) +} + +pub fn create_directory_new( + parent: &HeldDirectory, + name: &OsStr, + mode: u32, +) -> Result { + semaprax_native_rust_interop_platform_sys::create_directory_new(&parent.0, name, mode) + .map(HeldDirectory) +} + +pub fn create_directory_new_prepared( + parent: &HeldDirectory, + name: &PreparedStageName, + mode: u32, +) -> Result { + semaprax_native_rust_interop_platform_sys::create_directory_new_prepared( + &parent.0, &name.0, mode, + ) + .map(HeldDirectory) +} + +pub fn write_file_new( + directory: &HeldDirectory, + name: &OsStr, + bytes: &[u8], + mode: u32, +) -> Result { + semaprax_native_rust_interop_platform_sys::write_file_new(&directory.0, name, bytes, mode) + .map(HeldRegularFile) +} + +pub fn hold_regular_file( + directory: &HeldDirectory, + name: &OsStr, +) -> Result { + semaprax_native_rust_interop_platform_sys::hold_regular_file(&directory.0, name) + .map(HeldRegularFile) +} + +pub fn recheck_regular_file(file: &HeldRegularFile) -> Result<(), Error> { + semaprax_native_rust_interop_platform_sys::recheck_regular(&file.0) +} + +pub fn hold_executable(directory: &HeldDirectory, name: &OsStr) -> Result { + semaprax_native_rust_interop_platform_sys::hold_executable(&directory.0, name) + .map(HeldExecutable) +} + +pub fn executable_regular_file(executable: &HeldExecutable) -> Result { + semaprax_native_rust_interop_platform_sys::executable_regular_file(&executable.0) + .map(HeldRegularFile) +} + +pub fn hold_external_executable(path: &Path) -> Result { + semaprax_native_rust_interop_platform_sys::hold_external_executable(path).map(HeldExecutable) +} + +pub fn read_exact(file: &HeldRegularFile, maximum: usize) -> Result, Error> { + semaprax_native_rust_interop_platform_sys::read_exact(&file.0, maximum) +} + +pub const FILE_COMPARE_SCRATCH_BYTES: usize = 8192; + +pub fn compare_exact( + file: &HeldRegularFile, + expected: &[u8], + scratch: &mut [u8; FILE_COMPARE_SCRATCH_BYTES], +) -> Result { + semaprax_native_rust_interop_platform_sys::compare_exact(&file.0, expected, scratch) +} + +pub fn rustc_version( + executable: &HeldExecutable, + cwd: &HeldDirectory, +) -> Result { + rustc_version_bounded(executable, cwd, 65_536) +} + +pub fn rustc_version_bounded( + executable: &HeldExecutable, + cwd: &HeldDirectory, + maximum: usize, +) -> Result { + semaprax_native_rust_interop_platform_sys::rustc_version(&executable.0, &cwd.0, maximum) + .map(|bytes| ToolOutput { bytes }) +} + +pub fn clang_version( + executable: &HeldExecutable, + cwd: &HeldDirectory, +) -> Result { + clang_version_bounded(executable, cwd, 65_536) +} + +pub fn clang_version_bounded( + executable: &HeldExecutable, + cwd: &HeldDirectory, + maximum: usize, +) -> Result { + semaprax_native_rust_interop_platform_sys::clang_version(&executable.0, &cwd.0, maximum) + .map(|bytes| ToolOutput { bytes }) +} + +pub fn hold_configured_tool(variable: &str, fallback: &str) -> Result { + if variable.is_empty() || fallback.is_empty() { + return Err(Error::Invalid); + } + let path = if let Some(value) = std::env::var_os(variable) { + PathBuf::from(value) + } else { + let paths = std::env::var_os("PATH").ok_or(Error::Invalid)?; + std::env::split_paths(&paths) + .map(|directory| directory.join(fallback)) + .find(|candidate| candidate.is_file()) + .ok_or(Error::Invalid)? + }; + let path = path.canonicalize().map_err(|_| Error::Invalid)?; + let executable = hold_external_executable(&path)?; + let path = path.to_str().ok_or(Error::Invalid)?.to_owned(); + Ok(HeldTool { executable, path }) +} + +pub fn hold_prepared_tool(path: PathBuf) -> Result { + let executable = hold_external_executable(&path)?; + let path = path.to_str().ok_or(Error::Invalid)?.to_owned(); + Ok(HeldTool { executable, path }) +} + +pub fn resolve_and_hold_tool_prepared( + prepared: PreparedToolResolver, + configured: Option<&OsStr>, + paths: Option<&OsStr>, +) -> Result { + let (executable, path) = + semaprax_native_rust_interop_platform_sys::resolve_and_hold_tool_prepared( + prepared.0, configured, paths, + )?; + Ok(HeldTool { + executable: HeldExecutable(executable), + path, + }) +} + +pub fn hold_rustc_discovery_prepared( + prepared: PreparedToolResolver, + configured: &OsStr, +) -> Result { + semaprax_native_rust_interop_platform_sys::hold_rustc_discovery_prepared(prepared.0, configured) + .map(HeldRustcDiscovery) +} + +pub fn rustc_discovery_output_prepared( + discovery: &HeldRustcDiscovery, + cwd: &HeldDirectory, + prepared: PreparedSysrootInvocation, + process_arena: &mut PreparedProcessArena, +) -> Result { + semaprax_native_rust_interop_platform_sys::rustc_discovery_output_prepared( + &discovery.0, + &cwd.0, + prepared.0, + &mut process_arena.0, + ) + .map(|bytes| ToolOutput { bytes }) +} + +pub fn hold_direct_rustc_prepared( + prepared: PreparedToolResolver, + sysroot_output: &[u8], +) -> Result { + semaprax_native_rust_interop_platform_sys::hold_direct_rustc_prepared( + prepared.0, + sysroot_output, + ) + .map(HeldDirectRustc) +} + +pub fn direct_rustc_output_prepared( + direct: &HeldDirectRustc, + cwd: &HeldDirectory, + prepared: PreparedSysrootInvocation, + process_arena: &mut PreparedProcessArena, +) -> Result { + semaprax_native_rust_interop_platform_sys::direct_rustc_output_prepared( + &direct.0, + &cwd.0, + prepared.0, + &mut process_arena.0, + ) + .map(|bytes| ToolOutput { bytes }) +} + +pub fn direct_rustc_version_prepared( + direct: &HeldDirectRustc, + cwd: &HeldDirectory, + prepared: PreparedRustcVersionInvocation, + process_arena: &mut PreparedProcessArena, +) -> Result { + semaprax_native_rust_interop_platform_sys::direct_rustc_version_prepared( + &direct.0, + &cwd.0, + prepared.0, + &mut process_arena.0, + ) + .map(|bytes| ToolOutput { bytes }) +} + +pub fn direct_rustc_reproduces_sysroot( + direct: &HeldDirectRustc, + prepared: PreparedToolResolver, + sysroot_output: &[u8], +) -> Result<(), Error> { + semaprax_native_rust_interop_platform_sys::direct_rustc_reproduces_sysroot( + &direct.0, + prepared.0, + sysroot_output, + ) +} + +pub fn tool_path(tool: &HeldTool) -> &str { + &tool.path +} + +pub fn tool_path_capacity(tool: &HeldTool) -> usize { + tool.path.capacity() +} + +pub fn rustc_tool_version_bounded( + tool: &HeldTool, + cwd: &HeldDirectory, + maximum: usize, +) -> Result { + rustc_version_bounded(&tool.executable, cwd, maximum) +} + +pub fn clang_tool_version_bounded( + tool: &HeldTool, + cwd: &HeldDirectory, + maximum: usize, +) -> Result { + clang_version_bounded(&tool.executable, cwd, maximum) +} + +pub fn tool_version_prepared( + tool: &HeldTool, + cwd: &HeldDirectory, + prepared: PreparedVersionInvocation, + process_arena: &mut PreparedProcessArena, +) -> Result { + semaprax_native_rust_interop_platform_sys::version_prepared( + &tool.executable.0, + &cwd.0, + prepared.0, + &mut process_arena.0, + ) + .map(|bytes| ToolOutput { bytes }) +} + +pub fn compile_c_tool_to_stdout_bounded( + tool: &HeldTool, + cwd: &HeldDirectory, + target: &str, + input: &OsStr, + optimization: u8, + sanitizers: bool, + maximum: usize, +) -> Result { + compile_c_to_stdout_bounded( + &tool.executable, + cwd, + target, + input, + optimization, + sanitizers, + maximum, + ) +} + +pub fn prepare_c_compile_invocation( + target: &str, + input: &OsStr, + optimization: u8, + sanitizers: bool, + maximum: usize, +) -> Result { + semaprax_native_rust_interop_platform_sys::prepare_c_compile_invocation( + target, + input, + optimization, + sanitizers, + maximum, + ) + .map(PreparedCCompileInvocation) +} + +pub fn prepared_c_compile_owned_capacity(prepared: &PreparedCCompileInvocation) -> usize { + semaprax_native_rust_interop_platform_sys::prepared_c_compile_owned_capacity(&prepared.0) +} + +pub fn compile_c_tool_prepared( + tool: &HeldTool, + cwd: &HeldDirectory, + prepared: PreparedCCompileInvocation, + process_arena: &mut PreparedProcessArena, +) -> Result { + semaprax_native_rust_interop_platform_sys::compile_c_prepared( + &tool.executable.0, + &cwd.0, + prepared.0, + &mut process_arena.0, + ) + .map(|bytes| ToolOutput { bytes }) +} + +pub fn prepare_rust_compile_invocation( + target: &str, + source: &OsStr, + output: &OsStr, +) -> Result { + semaprax_native_rust_interop_platform_sys::prepare_rust_compile_invocation( + target, source, output, + ) + .map(PreparedRustCompileInvocation) +} + +pub fn prepared_rust_compile_owned_capacity(prepared: &PreparedRustCompileInvocation) -> usize { + semaprax_native_rust_interop_platform_sys::prepared_rust_compile_owned_capacity(&prepared.0) +} + +pub fn compile_rust_tool_prepared( + tool: &HeldDirectRustc, + cwd: &HeldDirectory, + prepared: PreparedRustCompileInvocation, + process_arena: &mut PreparedProcessArena, +) -> Result { + semaprax_native_rust_interop_platform_sys::compile_direct_rustc_prepared( + &tool.0, + &cwd.0, + prepared.0, + &mut process_arena.0, + ) + .map(HeldRegularFile) +} + +#[allow(clippy::too_many_arguments)] +pub fn prepare_link_invocation( + target: &str, + harness: &OsStr, + c_object: &OsStr, + rust_archive: &OsStr, + output: &OsStr, + sanitizers: bool, +) -> Result { + semaprax_native_rust_interop_platform_sys::prepare_link_invocation( + target, + harness, + c_object, + rust_archive, + output, + sanitizers, + ) + .map(PreparedLinkInvocation) +} + +pub fn prepared_link_owned_capacity(prepared: &PreparedLinkInvocation) -> usize { + semaprax_native_rust_interop_platform_sys::prepared_link_owned_capacity(&prepared.0) +} + +pub fn link_tool_prepared( + tool: &HeldTool, + cwd: &HeldDirectory, + prepared: PreparedLinkInvocation, + process_arena: &mut PreparedProcessArena, +) -> Result { + semaprax_native_rust_interop_platform_sys::link_prepared( + &tool.executable.0, + &cwd.0, + prepared.0, + &mut process_arena.0, + ) + .map(HeldExecutable) +} + +pub fn prepare_run_invocation() -> Result { + semaprax_native_rust_interop_platform_sys::prepare_run_invocation().map(PreparedRunInvocation) +} + +pub fn prepared_run_owned_capacity(prepared: &PreparedRunInvocation) -> usize { + semaprax_native_rust_interop_platform_sys::prepared_run_owned_capacity(&prepared.0) +} + +pub fn execute_tool_prepared( + executable: &HeldExecutable, + cwd: &HeldDirectory, + prepared: PreparedRunInvocation, + process_arena: &mut PreparedProcessArena, +) -> Result<(), Error> { + semaprax_native_rust_interop_platform_sys::run_prepared( + &executable.0, + &cwd.0, + prepared.0, + &mut process_arena.0, + ) +} + +#[allow( + clippy::too_many_arguments, + reason = "each held link input remains explicit" +)] +pub fn link_tool_harness( + tool: &HeldTool, + cwd: &HeldDirectory, + target: &str, + harness: &OsStr, + c_object: &OsStr, + rust_archive: &OsStr, + output: &OsStr, + sanitizers: bool, +) -> Result { + link_harness( + &tool.executable, + cwd, + target, + harness, + c_object, + rust_archive, + output, + sanitizers, + ) +} + +pub fn compile_c_to_stdout( + executable: &HeldExecutable, + cwd: &HeldDirectory, + target: &str, + input: &OsStr, + optimization: u8, + sanitizers: bool, +) -> Result { + compile_c_to_stdout_bounded( + executable, + cwd, + target, + input, + optimization, + sanitizers, + 33_554_432, + ) +} + +pub fn compile_c_to_stdout_bounded( + executable: &HeldExecutable, + cwd: &HeldDirectory, + target: &str, + input: &OsStr, + optimization: u8, + sanitizers: bool, + maximum: usize, +) -> Result { + semaprax_native_rust_interop_platform_sys::compile_c_to_stdout( + &executable.0, + &cwd.0, + target, + input, + optimization, + sanitizers, + maximum, + ) + .map(|bytes| ToolOutput { bytes }) +} + +pub fn execute_harness(executable: &HeldExecutable, cwd: &HeldDirectory) -> Result<(), Error> { + semaprax_native_rust_interop_platform_sys::execute_harness(&executable.0, &cwd.0) +} + +#[allow( + clippy::too_many_arguments, + reason = "each held link input remains explicit" +)] +pub fn link_harness( + clang: &HeldExecutable, + cwd: &HeldDirectory, + target: &str, + harness: &OsStr, + c_object: &OsStr, + rust_archive: &OsStr, + output: &OsStr, + sanitizers: bool, +) -> Result { + semaprax_native_rust_interop_platform_sys::link_harness( + &clang.0, + &cwd.0, + target, + harness, + c_object, + rust_archive, + output, + sanitizers, + ) + .map(HeldExecutable) +} + +pub fn publish_directory_required_capacity(name: &OsStr) -> Result { + semaprax_native_rust_interop_platform_sys::publish_directory_required_capacity(name) +} + +pub fn prepare_publish_directory(name: &OsStr) -> Result { + semaprax_native_rust_interop_platform_sys::prepare_publish_directory(name) + .map(PreparedPublishDirectory) +} + +pub fn prepared_publish_directory_owned_capacity(prepared: &PreparedPublishDirectory) -> usize { + semaprax_native_rust_interop_platform_sys::prepared_publish_directory_owned_capacity( + &prepared.0, + ) +} + +pub fn prepared_publish_directory_remaining(prepared: &PreparedPublishDirectory) -> u8 { + semaprax_native_rust_interop_platform_sys::prepared_publish_directory_remaining(&prepared.0) +} + +#[cfg(debug_assertions)] +#[doc(hidden)] +pub fn inject_publish_directory_failure( + prepared: &mut PreparedPublishDirectory, + point: u8, +) -> Result<(), Error> { + semaprax_native_rust_interop_platform_sys::inject_publish_directory_failure( + &mut prepared.0, + point, + ) +} + +pub fn publish_directory_new_prepared( + prepared: &mut PreparedPublishDirectory, + parent: &HeldDirectory, + stage: &HeldDirectory, + stage_name: &PreparedStageName, + output_name: &OsStr, +) -> Result<(), Error> { + semaprax_native_rust_interop_platform_sys::publish_directory_new_prepared( + &mut prepared.0, + &parent.0, + &stage.0, + &stage_name.0, + output_name, + ) +} + +pub fn discard_owned_stage_prepared( + parent: &HeldDirectory, + stage: &HeldDirectory, + stage_name: &PreparedStageName, + inventory: &PreparedDiscardInventory, +) -> Result<(), Error> { + let raw = std::array::from_fn(|index| inventory.files[index].as_ref().map(|file| &file.0)); + semaprax_native_rust_interop_platform_sys::discard_owned_stage_prepared( + &parent.0, + &stage.0, + &stage_name.0, + &inventory.native, + &raw, + #[cfg(debug_assertions)] + inventory.failure_after_delete, + ) +} diff --git a/crates/semaprax-native-rust-interop-platform/tests/unix_authority.rs b/crates/semaprax-native-rust-interop-platform/tests/unix_authority.rs new file mode 100644 index 0000000..fa9bde1 --- /dev/null +++ b/crates/semaprax-native-rust-interop-platform/tests/unix_authority.rs @@ -0,0 +1,500 @@ +#![cfg(unix)] + +#[cfg(target_os = "macos")] +use semaprax_native_rust_interop_platform::{ + clang_version, hold_external_executable, rustc_version, +}; +use semaprax_native_rust_interop_platform::{ + create_directory_new, discard_owned_stage_prepared, hold_directory, inventory_exact_prepared, + prepare_discard_inventory, prepare_inventory_exact, prepare_publish_directory, + prepare_stage_name, prepared_publish_directory_remaining, publish_directory_new_prepared, + read_exact, recheck_directory, write_file_new, write_file_new_prepared, Error, HeldDirectory, + HeldRegularFile, +}; +use semaprax_native_rust_interop_platform::{execute_harness, hold_executable}; +use std::ffi::OsStr; +use std::fmt::Write as _; +use std::fs::File; +use std::io::Read as _; +use std::ops::Deref; +use std::os::unix::fs::MetadataExt as _; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{Duration, Instant}; + +fn discard_one( + parent: &HeldDirectory, + stage: &HeldDirectory, + stage_name: &'static str, + file_name: &'static str, + file: HeldRegularFile, +) -> Result<(), Error> { + let stage_name = prepare_stage_name(OsStr::new(stage_name))?; + let mut inventory = prepare_discard_inventory([OsStr::new(file_name)])?; + inventory.attach(file_name, file)?; + discard_owned_stage_prepared(parent, stage, &stage_name, &inventory) +} + +struct OwnedRoot { + path: PathBuf, + dev: u64, + ino: u64, +} + +impl Deref for OwnedRoot { + type Target = Path; + + fn deref(&self) -> &Self::Target { + &self.path + } +} + +impl AsRef for OwnedRoot { + fn as_ref(&self) -> &Path { + &self.path + } +} + +impl Drop for OwnedRoot { + fn drop(&mut self) { + let Ok(metadata) = std::fs::symlink_metadata(&self.path) else { + return; + }; + if metadata.is_dir() + && !metadata.file_type().is_symlink() + && (metadata.dev(), metadata.ino()) == (self.dev, self.ino) + { + std::fs::remove_dir_all(&self.path).unwrap(); + } + } +} + +fn root(label: &str) -> OwnedRoot { + for _ in 0..32 { + let mut random = [0_u8; 16]; + File::open("/dev/urandom") + .unwrap() + .read_exact(&mut random) + .unwrap(); + let mut nonce = String::with_capacity(random.len() * 2); + for byte in random { + write!(&mut nonce, "{byte:02x}").unwrap(); + } + let path = std::fs::canonicalize(std::env::temp_dir()) + .unwrap() + .join(format!( + "semaprax-native-rust-platform-{label}-{}-{nonce}", + std::process::id() + )); + match std::fs::create_dir(&path) { + Ok(()) => { + let metadata = std::fs::symlink_metadata(&path).unwrap(); + return OwnedRoot { + path, + dev: metadata.dev(), + ino: metadata.ino(), + }; + } + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {} + Err(error) => panic!("create owned test directory: {error}"), + } + } + panic!("could not allocate an owned test directory") +} + +#[cfg(target_os = "macos")] +fn resolved_tool(name: &str) -> PathBuf { + std::env::split_paths(&std::env::var_os("PATH").expect("test PATH")) + .map(|directory| directory.join(name)) + .find_map(|candidate| std::fs::canonicalize(candidate).ok()) + .expect("installed test tool") +} + +#[cfg(target_os = "macos")] +#[test] +fn darwin_installed_tools_are_suspended_vnode_attested_before_resume() { + let root = root("darwin-attestation"); + let cwd = hold_directory(root.as_ref()).unwrap(); + let rustc_path = std::env::var_os("RUSTC") + .map(PathBuf::from) + .map(|path| std::fs::canonicalize(path).expect("resolved RUSTC image")) + .unwrap_or_else(|| resolved_tool("rustc")); + let rustc = hold_external_executable(&rustc_path).unwrap(); + let clang = hold_external_executable(&resolved_tool("clang")).unwrap(); + + let rustc_output = rustc_version(&rustc, &cwd).unwrap(); + assert!(rustc_output.bytes().starts_with(b"rustc ")); + let clang_output = clang_version(&clang, &cwd).unwrap(); + assert!(clang_output.bytes().starts_with(b"Apple clang version ")); +} + +fn compile_c(root: &Path, name: &str, source: &str) -> PathBuf { + let source_path = root.join(format!("{name}.c")); + let executable = root.join(name); + std::fs::write(&source_path, source).unwrap(); + let compiler = std::env::var_os("CC").unwrap_or_else(|| "cc".into()); + let output = Command::new(compiler) + .env_clear() + .env("TMPDIR", root) + .args(["-std=c11", "-Wall", "-Wextra", "-Werror", "-O2"]) + .arg(&source_path) + .arg("-o") + .arg(&executable) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + executable +} + +#[test] +fn intermediate_symlinks_and_directory_identity_or_permission_drift_are_rejected() { + use std::os::unix::fs::{symlink, PermissionsExt as _}; + + let root = root("directory-authority"); + let real = root.join("real"); + std::fs::create_dir(&real).unwrap(); + let link = root.join("link"); + symlink(&real, &link).unwrap(); + let error = match hold_directory(&link) { + Ok(_) => panic!("intermediate symlink was followed"), + Err(error) => error, + }; + assert_eq!(error, Error::Changed); + + let held = hold_directory(&real).unwrap(); + let original = std::fs::metadata(&real).unwrap().permissions(); + let mut changed = original.clone(); + changed.set_mode(0o700); + std::fs::set_permissions(&real, changed).unwrap(); + assert_eq!(recheck_directory(&held), Err(Error::Changed)); + std::fs::set_permissions(&real, original).unwrap(); + + let displaced = root.join("displaced"); + std::fs::rename(&real, &displaced).unwrap(); + std::fs::create_dir(&real).unwrap(); + recheck_directory(&held).unwrap(); + let parent = hold_directory(&root).unwrap(); + let stage_name = prepare_stage_name(OsStr::new("real")).unwrap(); + let mut publish = prepare_publish_directory(OsStr::new("output")).unwrap(); + let error = match publish_directory_new_prepared( + &mut publish, + &parent, + &held, + &stage_name, + OsStr::new("output"), + ) { + Ok(_) => panic!("substituted stage path was published"), + Err(error) => error, + }; + assert_eq!(error, Error::Changed); + assert!(displaced.is_dir()); + assert!(real.is_dir()); +} + +#[test] +fn handle_relative_create_inventory_and_publish_are_no_clobber() { + let root = root("publish"); + let parent = hold_directory(&root).unwrap(); + let stage = create_directory_new(&parent, OsStr::new("stage"), 0o700).unwrap(); + let mut inventory = prepare_discard_inventory([OsStr::new("artifact")]).unwrap(); + let mut exact = prepare_inventory_exact(&inventory).unwrap(); + write_file_new_prepared(&stage, &mut inventory, "artifact", b"authenticated", 0o600).unwrap(); + assert_eq!( + read_exact(inventory.file("artifact").unwrap(), 13).unwrap(), + b"authenticated" + ); + assert_eq!( + read_exact(inventory.file("artifact").unwrap(), 12), + Err(Error::OutputLimit) + ); + inventory_exact_prepared(&mut exact, &stage, &inventory).unwrap(); + let stage_name = prepare_stage_name(OsStr::new("stage")).unwrap(); + let mut mismatched_publish = prepare_publish_directory(OsStr::new("planned")).unwrap(); + assert_eq!( + publish_directory_new_prepared( + &mut mismatched_publish, + &parent, + &stage, + &stage_name, + OsStr::new("different"), + ), + Err(Error::Invalid) + ); + assert_eq!(prepared_publish_directory_remaining(&mismatched_publish), 1); + assert!(root.join("stage").is_dir()); + + let foreign = root.join("foreign"); + std::fs::create_dir(&foreign).unwrap(); + std::fs::write(foreign.join("sentinel"), b"foreign").unwrap(); + let mut foreign_publish = prepare_publish_directory(OsStr::new("foreign")).unwrap(); + let error = match publish_directory_new_prepared( + &mut foreign_publish, + &parent, + &stage, + &stage_name, + OsStr::new("foreign"), + ) { + Ok(_) => panic!("foreign output was replaced"), + Err(error) => error, + }; + assert_eq!(error, Error::Exists); + assert_eq!(prepared_publish_directory_remaining(&foreign_publish), 0); + assert_eq!(std::fs::read(foreign.join("sentinel")).unwrap(), b"foreign"); + assert_eq!( + std::fs::read(root.join("stage/artifact")).unwrap(), + b"authenticated" + ); + + let mut bundle_publish = prepare_publish_directory(OsStr::new("bundle")).unwrap(); + publish_directory_new_prepared( + &mut bundle_publish, + &parent, + &stage, + &stage_name, + OsStr::new("bundle"), + ) + .unwrap(); + assert_eq!(prepared_publish_directory_remaining(&bundle_publish), 0); + inventory_exact_prepared(&mut exact, &stage, &inventory).unwrap(); + assert!(!root.join("stage").exists()); + assert_eq!( + std::fs::read(root.join("bundle/artifact")).unwrap(), + b"authenticated" + ); +} + +#[test] +fn exact_owned_stage_discard_removes_only_the_authenticated_inventory() { + let root = root("discard-exact"); + let parent = hold_directory(&root).unwrap(); + let stage = create_directory_new(&parent, OsStr::new("stage"), 0o700).unwrap(); + let first = write_file_new(&stage, OsStr::new("first"), b"one", 0o600).unwrap(); + let second = write_file_new(&stage, OsStr::new("second"), b"two", 0o600).unwrap(); + + let stage_name = prepare_stage_name(OsStr::new("stage")).unwrap(); + let mut inventory = + prepare_discard_inventory([OsStr::new("first"), OsStr::new("second")]).unwrap(); + inventory.attach("first", first).unwrap(); + inventory.attach("second", second).unwrap(); + discard_owned_stage_prepared(&parent, &stage, &stage_name, &inventory).unwrap(); + + assert!(!root.join("stage").exists()); + assert!(root.is_dir()); +} + +#[test] +fn owned_stage_discard_stops_on_inventory_or_file_identity_drift() { + use std::os::unix::fs::symlink; + + let root = root("discard-hostile"); + let parent = hold_directory(&root).unwrap(); + + let inventory_stage = + create_directory_new(&parent, OsStr::new("inventory-stage"), 0o700).unwrap(); + let expected = write_file_new( + &inventory_stage, + OsStr::new("expected"), + b"authenticated", + 0o600, + ) + .unwrap(); + std::fs::write(root.join("inventory-stage/foreign-sentinel"), b"foreign").unwrap(); + assert_eq!( + discard_one( + &parent, + &inventory_stage, + "inventory-stage", + "expected", + expected, + ), + Err(Error::Changed) + ); + assert_eq!( + std::fs::read(root.join("inventory-stage/foreign-sentinel")).unwrap(), + b"foreign" + ); + assert_eq!( + std::fs::read(root.join("inventory-stage/expected")).unwrap(), + b"authenticated" + ); + + let file_stage = create_directory_new(&parent, OsStr::new("file-stage"), 0o700).unwrap(); + let held = + write_file_new(&file_stage, OsStr::new("artifact"), b"authenticated", 0o600).unwrap(); + std::fs::rename( + root.join("file-stage/artifact"), + root.join("displaced-artifact"), + ) + .unwrap(); + let foreign = root.join("foreign-target"); + std::fs::write(&foreign, b"foreign-sentinel").unwrap(); + symlink(&foreign, root.join("file-stage/artifact")).unwrap(); + assert_eq!( + discard_one(&parent, &file_stage, "file-stage", "artifact", held), + Err(Error::Changed) + ); + assert_eq!(std::fs::read(&foreign).unwrap(), b"foreign-sentinel"); + assert!(std::fs::symlink_metadata(root.join("file-stage/artifact")) + .unwrap() + .file_type() + .is_symlink()); + assert_eq!( + std::fs::read(root.join("displaced-artifact")).unwrap(), + b"authenticated" + ); +} + +#[test] +fn owned_stage_discard_stops_on_same_path_directory_substitution() { + let root = root("discard-stage-substitution"); + let parent = hold_directory(&root).unwrap(); + let stage = create_directory_new(&parent, OsStr::new("stage"), 0o700).unwrap(); + let held = write_file_new(&stage, OsStr::new("artifact"), b"authenticated", 0o600).unwrap(); + std::fs::rename(root.join("stage"), root.join("displaced-stage")).unwrap(); + std::fs::create_dir(root.join("stage")).unwrap(); + std::fs::write(root.join("stage/foreign-sentinel"), b"foreign").unwrap(); + + assert_eq!( + discard_one(&parent, &stage, "stage", "artifact", held), + Err(Error::Changed) + ); + assert_eq!( + std::fs::read(root.join("stage/foreign-sentinel")).unwrap(), + b"foreign" + ); + assert_eq!( + std::fs::read(root.join("displaced-stage/artifact")).unwrap(), + b"authenticated" + ); +} + +#[test] +fn held_executable_ignores_same_byte_path_substitution_and_clears_process_ambient_state() { + let root = root("held-executable"); + let executable = compile_c( + &root, + "probe", + "#include \n#include \nint main(void){if(getenv(\"PATH\")!=0)return 8;for(int fd=4;fd<1024;fd++){if(fcntl(fd,F_GETFD)!=-1)return 9;}return 0;}\n", + ); + let directory = hold_directory(&root).unwrap(); + let held = hold_executable(&directory, OsStr::new("probe")).unwrap(); + let original = std::fs::read(&executable).unwrap(); + std::fs::rename(&executable, root.join("displaced-probe")).unwrap(); + std::fs::write(&executable, &original).unwrap(); + let permissions = std::fs::metadata(root.join("displaced-probe")) + .unwrap() + .permissions(); + std::fs::set_permissions(&executable, permissions).unwrap(); + + execute_harness(&held, &directory).unwrap(); + assert_eq!(std::fs::read(&executable).unwrap(), original); + assert!(root.join("displaced-probe").is_file()); +} + +#[test] +fn output_overflow_kills_and_reaps_the_child_with_a_bounded_wait() { + let root = root("bounded-kill"); + compile_c( + &root, + "noisy", + "#include \nint main(void){(void)write(1,\"x\",1);sleep(30);return 0;}\n", + ); + let directory = hold_directory(&root).unwrap(); + let held = hold_executable(&directory, OsStr::new("noisy")).unwrap(); + let started = Instant::now(); + assert_eq!(execute_harness(&held, &directory), Err(Error::OutputLimit)); + assert!(started.elapsed() < Duration::from_secs(10)); +} + +#[test] +fn output_overflow_quiesces_the_owned_process_group_before_return() { + let root = root("bounded-process-group"); + compile_c( + &root, + "forking-noisy", + "#include \n#include \nint main(void){pid_t child=fork();if(child<0)return 2;if(child==0){FILE *file=fopen(\"descendant.pid\",\"w\");if(!file)_exit(3);fprintf(file,\"%ld\",(long)getpid());fclose(file);sleep(30);_exit(0);}while(access(\"descendant.pid\",F_OK)!=0)usleep(1000);(void)write(1,\"x\",1);sleep(30);return 0;}\n", + ); + let directory = hold_directory(&root).unwrap(); + let held = hold_executable(&directory, OsStr::new("forking-noisy")).unwrap(); + let started = Instant::now(); + assert_eq!(execute_harness(&held, &directory), Err(Error::OutputLimit)); + assert!(started.elapsed() < Duration::from_secs(10)); + + let descendant = std::fs::read_to_string(root.join("descendant.pid")) + .unwrap() + .parse::() + .unwrap(); + let deadline = Instant::now() + Duration::from_secs(2); + loop { + let pid = descendant.to_string(); + if !Command::new("/bin/kill") + .args(["-0", pid.as_str()]) + .output() + .unwrap() + .status + .success() + { + break; + } + if Instant::now() >= deadline { + let _ = Command::new("/bin/kill") + .args(["-KILL", pid.as_str()]) + .output(); + panic!("owned descendant remained observable after execute_harness returned"); + } + std::thread::sleep(Duration::from_millis(10)); + } +} + +#[test] +fn external_consumer_cannot_extract_handles_or_reach_the_sys_quarantine() { + let root = root("opacity"); + std::fs::create_dir(root.join("src")).unwrap(); + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + std::fs::write( + root.join("Cargo.toml"), + format!( + "[package]\nname='opacity-probe'\nversion='0.0.0'\nedition='2021'\n[dependencies]\nsemaprax-native-rust-interop-platform={{path={:?}}}\n", + manifest_dir + ), + ) + .unwrap(); + std::fs::write( + root.join("src/main.rs"), + r#"use semaprax_native_rust_interop_platform::{hold_directory,HeldDirectory}; +use std::os::fd::AsRawFd; +fn raw(directory:&HeldDirectory)->i32{directory.0.as_raw_fd()} +fn require_clone(){} +fn clone_it(){require_clone::();} +fn debug_it(directory:&HeldDirectory){let _=format!("{directory:?}");} +fn main(){let _=hold_directory(std::path::Path::new("/"));let _=semaprax_native_rust_interop_platform_sys::Error::Invalid;} +"#, + ) + .unwrap(); + let checked = Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into())) + .env("CARGO_TARGET_DIR", root.join("target")) + .args(["check", "--offline", "--quiet"]) + .current_dir(&root) + .output() + .unwrap(); + assert!(!checked.status.success()); + let stderr = String::from_utf8_lossy(&checked.stderr); + assert!( + stderr.contains("field `0` of struct `HeldDirectory` is private"), + "{stderr}" + ); + assert!(stderr.contains("Clone"), "{stderr}"); + assert!(stderr.contains("doesn't implement `Debug`"), "{stderr}"); + assert!( + stderr.contains("semaprax_native_rust_interop_platform_sys"), + "{stderr}" + ); + assert!( + stderr.contains("failed to resolve") || stderr.contains("cannot find module or crate"), + "{stderr}" + ); +} diff --git a/crates/semaprax-native-rust-interop-platform/tests/windows_authority.rs b/crates/semaprax-native-rust-interop-platform/tests/windows_authority.rs new file mode 100644 index 0000000..f260437 --- /dev/null +++ b/crates/semaprax-native-rust-interop-platform/tests/windows_authority.rs @@ -0,0 +1,457 @@ +#![cfg(windows)] + +use semaprax_native_rust_interop_platform::{ + clang_version_bounded, create_directory_new, discard_owned_stage_prepared, execute_harness, + hold_directory, hold_executable, hold_regular_file, inventory_exact_prepared, + prepare_discard_inventory, prepare_inventory_exact, prepare_publish_directory, + prepare_stage_name, publish_directory_new_prepared, read_exact, recheck_directory, + same_directory_path, write_file_new, Error, HeldDirectory, HeldRegularFile, +}; +use std::ffi::OsStr; +use std::fs::{self, File}; +use std::io::Read as _; +use std::ops::Deref; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{Duration, Instant}; + +fn discard_one( + parent: &HeldDirectory, + stage: &HeldDirectory, + stage_name: &'static str, + file_name: &'static str, + file: HeldRegularFile, +) -> Result<(), Error> { + let stage_name = prepare_stage_name(OsStr::new(stage_name))?; + let mut inventory = prepare_discard_inventory([OsStr::new(file_name)])?; + inventory.attach(file_name, file)?; + discard_owned_stage_prepared(parent, stage, &stage_name, &inventory) +} + +struct OwnedRoot { + path: PathBuf, + authority: Option, +} + +impl Deref for OwnedRoot { + type Target = Path; + + fn deref(&self) -> &Self::Target { + &self.path + } +} + +impl Drop for OwnedRoot { + fn drop(&mut self) { + let Some(authority) = self.authority.take() else { + return; + }; + let identity_matches = recheck_directory(&authority).is_ok() + && same_directory_path(&authority, &self.path) == Ok(true); + drop(authority); + let Ok(metadata) = fs::symlink_metadata(&self.path) else { + return; + }; + if metadata.is_dir() && !metadata.file_type().is_symlink() && identity_matches { + fs::remove_dir_all(&self.path).unwrap(); + } + } +} + +fn root(label: &str) -> OwnedRoot { + let parent = fs::canonicalize(std::env::temp_dir()).unwrap(); + for _ in 0..32 { + let mut random = [0_u8; 16]; + File::open("NUL") + .and_then(|mut file| file.read_exact(&mut random)) + .unwrap_or_else(|_| { + random[..8].copy_from_slice( + &std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + .to_le_bytes()[..8], + ); + }); + let nonce = random + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + let path = parent.join(format!( + "semaprax-native-rust-platform-{label}-{}-{nonce}", + std::process::id() + )); + match fs::create_dir(&path) { + Ok(()) => { + let authority = hold_directory(&path).unwrap(); + return OwnedRoot { + path, + authority: Some(authority), + }; + } + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {} + Err(error) => panic!("create owned Windows test root: {error}"), + } + } + panic!("could not allocate an owned Windows test root") +} + +fn compile_c(root: &Path, name: &str, source: &str) -> PathBuf { + let source_path = root.join(format!("{name}.c")); + let executable = root.join(format!("{name}.exe")); + fs::write(&source_path, source).unwrap(); + let compiler = std::env::var_os("CLANG").unwrap_or_else(|| "clang".into()); + let output = Command::new(compiler) + .env("TMP", root) + .env("TEMP", root) + .args(["-std=c11", "-Wall", "-Wextra", "-Werror", "-O2"]) + .arg(&source_path) + .arg("-o") + .arg(&executable) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + executable +} + +#[test] +fn windows_junctions_and_same_path_directory_substitution_are_rejected() { + let root = root("directory-authority"); + let real = root.join("real"); + fs::create_dir(&real).unwrap(); + let junction = root.join("junction"); + let linked = Command::new("cmd") + .args(["/d", "/c", "mklink", "/J"]) + .arg(&junction) + .arg(&real) + .output() + .unwrap(); + assert!(linked.status.success()); + assert_eq!(hold_directory(&junction).err(), Some(Error::Changed)); + + let held = hold_directory(&real).unwrap(); + let displaced = root.join("displaced"); + fs::rename(&real, &displaced).unwrap(); + fs::create_dir(&real).unwrap(); + recheck_directory(&held).unwrap(); + let parent = hold_directory(&root).unwrap(); + let stage_name = prepare_stage_name(OsStr::new("real")).unwrap(); + let mut publish = prepare_publish_directory(OsStr::new("output")).unwrap(); + assert_eq!( + publish_directory_new_prepared( + &mut publish, + &parent, + &held, + &stage_name, + OsStr::new("output") + ) + .err(), + Some(Error::Changed) + ); + assert!(displaced.is_dir()); + assert!(real.is_dir()); +} + +#[test] +fn windows_create_inventory_publish_and_exact_discard_are_no_clobber() { + let root = root("publish-discard"); + let parent = hold_directory(&root).unwrap(); + let stage = create_directory_new(&parent, OsStr::new("stage"), 0o700).unwrap(); + let file = write_file_new(&stage, OsStr::new("artifact"), b"authenticated", 0o600).unwrap(); + assert_eq!(read_exact(&file, 13).unwrap(), b"authenticated"); + assert_eq!(read_exact(&file, 12), Err(Error::OutputLimit)); + let mut inventory = prepare_discard_inventory([OsStr::new("artifact")]).unwrap(); + inventory + .attach( + "artifact", + hold_regular_file(&stage, OsStr::new("artifact")).unwrap(), + ) + .unwrap(); + let mut exact = prepare_inventory_exact(&inventory).unwrap(); + inventory_exact_prepared(&mut exact, &stage, &inventory).unwrap(); + + let foreign = root.join("foreign"); + fs::create_dir(&foreign).unwrap(); + fs::write(foreign.join("sentinel"), b"foreign").unwrap(); + let stage_name = prepare_stage_name(OsStr::new("stage")).unwrap(); + let mut publish = prepare_publish_directory(OsStr::new("foreign")).unwrap(); + assert_eq!( + publish_directory_new_prepared( + &mut publish, + &parent, + &stage, + &stage_name, + OsStr::new("foreign") + ) + .err(), + Some(Error::Exists) + ); + assert_eq!(fs::read(foreign.join("sentinel")).unwrap(), b"foreign"); + + discard_one(&parent, &stage, "stage", "artifact", file).unwrap(); + assert!(!root.join("stage").exists()); +} + +#[test] +fn windows_discard_stops_on_inventory_and_stage_identity_drift() { + let root = root("discard-hostile"); + let parent = hold_directory(&root).unwrap(); + let stage = create_directory_new(&parent, OsStr::new("stage"), 0o700).unwrap(); + let file = write_file_new(&stage, OsStr::new("artifact"), b"authenticated", 0o600).unwrap(); + fs::write(root.join("stage/foreign-sentinel"), b"foreign").unwrap(); + let stage_name = prepare_stage_name(OsStr::new("stage")).unwrap(); + let mut inventory = prepare_discard_inventory([OsStr::new("artifact")]).unwrap(); + inventory.attach("artifact", file).unwrap(); + assert_eq!( + discard_owned_stage_prepared(&parent, &stage, &stage_name, &inventory), + Err(Error::Changed) + ); + assert_eq!( + fs::read(root.join("stage/foreign-sentinel")).unwrap(), + b"foreign" + ); + + fs::rename(root.join("stage"), root.join("displaced-stage")).unwrap(); + fs::create_dir(root.join("stage")).unwrap(); + fs::write(root.join("stage/foreign-sentinel"), b"substitute").unwrap(); + assert_eq!( + discard_owned_stage_prepared(&parent, &stage, &stage_name, &inventory), + Err(Error::Changed) + ); + assert_eq!( + fs::read(root.join("stage/foreign-sentinel")).unwrap(), + b"substitute" + ); + assert_eq!( + fs::read(root.join("displaced-stage/artifact")).unwrap(), + b"authenticated" + ); + + let file_stage = create_directory_new(&parent, OsStr::new("file-stage"), 0o700).unwrap(); + let held = write_file_new( + &file_stage, + OsStr::new("artifact"), + b"authenticated-file", + 0o600, + ) + .unwrap(); + fs::rename( + root.join("file-stage/artifact"), + root.join("displaced-artifact"), + ) + .unwrap(); + fs::write(root.join("file-stage/artifact"), b"foreign-file").unwrap(); + assert_eq!( + discard_one(&parent, &file_stage, "file-stage", "artifact", held), + Err(Error::Changed) + ); + assert_eq!( + fs::read(root.join("file-stage/artifact")).unwrap(), + b"foreign-file" + ); + assert_eq!( + fs::read(root.join("displaced-artifact")).unwrap(), + b"authenticated-file" + ); +} + +#[test] +fn windows_held_executable_uses_held_identity_and_empty_environment() { + let root = root("held-executable"); + let good = compile_c( + &root, + "good", + "#include \nint main(void){return getenv(\"PATH\")!=0?8:0;}\n", + ); + let bad = compile_c(&root, "bad", "int main(void){return 77;}\n"); + let probe = root.join("probe.exe"); + fs::rename(&good, &probe).unwrap(); + let directory = hold_directory(&root).unwrap(); + let held = hold_executable(&directory, OsStr::new("probe.exe")).unwrap(); + fs::rename(&probe, root.join("displaced-probe.exe")).unwrap(); + fs::copy(&bad, &probe).unwrap(); + + execute_harness(&held, &directory).unwrap(); + assert!(root.join("displaced-probe.exe").is_file()); + assert!(probe.is_file()); +} + +#[test] +fn windows_run_argv_handles_zero_and_small_stdout_at_normal_eof() { + let root = root("normal-eof"); + let silent = compile_c(&root, "silent", "int main(void){return 0;}\n"); + let small = compile_c( + &root, + "small", + "#include \nint main(void){fputs(\"ok\",stdout);return 0;}\n", + ); + let directory = hold_directory(&root).unwrap(); + let silent = hold_executable(&directory, silent.file_name().unwrap()).unwrap(); + let small = hold_executable(&directory, small.file_name().unwrap()).unwrap(); + assert_eq!( + clang_version_bounded(&silent, &directory, 0) + .unwrap() + .bytes(), + b"" + ); + assert_eq!( + clang_version_bounded(&small, &directory, 2) + .unwrap() + .bytes(), + b"ok" + ); + assert_eq!( + clang_version_bounded(&small, &directory, 1).err(), + Some(Error::OutputLimit) + ); +} + +#[test] +fn windows_names_are_exact_ascii_non_dos_and_casefold_no_clobber() { + let root = root("names"); + let parent = hold_directory(&root).unwrap(); + let stage = create_directory_new(&parent, OsStr::new("s"), 0o700).unwrap(); + let one = write_file_new(&stage, OsStr::new("a"), b"one", 0o600).unwrap(); + assert_eq!( + write_file_new(&stage, OsStr::new("A"), b"foreign", 0o600).err(), + Some(Error::Exists) + ); + assert_eq!(read_exact(&one, 3).unwrap(), b"one"); + for reserved in [ + "CON", "con.txt", "PRN", "AUX", "NUL", "CLOCK$", "COM1", "com9.log", "LPT1", "lpt9.bin", + ] { + assert_eq!( + write_file_new(&stage, OsStr::new(reserved), b"x", 0o600).err(), + Some(Error::Invalid), + "reserved Windows name {reserved} was accepted" + ); + } + let mut inventory = prepare_discard_inventory([OsStr::new("a")]).unwrap(); + inventory + .attach("a", hold_regular_file(&stage, OsStr::new("a")).unwrap()) + .unwrap(); + let mut exact = prepare_inventory_exact(&inventory).unwrap(); + inventory_exact_prepared(&mut exact, &stage, &inventory).unwrap(); + discard_one(&parent, &stage, "s", "a", one).unwrap(); +} + +#[test] +fn windows_descendant_held_stdout_is_quiesced_without_output_overflow() { + let root = root("descendant-stdout"); + compile_c( + &root, + "quiet_tree", + "#include \n#include \n#include \nint main(int argc,char **argv){if(argc==2&&strcmp(argv[1],\"child\")==0){Sleep(30000);return 0;}char path[MAX_PATH];if(!GetModuleFileNameA(NULL,path,MAX_PATH))return 4;char command[MAX_PATH+16];if(sprintf_s(command,sizeof(command),\"\\\"%s\\\" child\",path)<0)return 5;STARTUPINFOA startup={0};startup.cb=sizeof(startup);PROCESS_INFORMATION process={0};if(!CreateProcessA(NULL,command,NULL,NULL,TRUE,0,NULL,NULL,&startup,&process))return 6;FILE *file=fopen(\"descendant.pid\",\"w\");if(!file)return 7;fprintf(file,\"%lu\",(unsigned long)process.dwProcessId);fclose(file);CloseHandle(process.hThread);CloseHandle(process.hProcess);return 0;}\n", + ); + let directory = hold_directory(&root).unwrap(); + let held = hold_executable(&directory, OsStr::new("quiet_tree.exe")).unwrap(); + let started = Instant::now(); + execute_harness(&held, &directory).unwrap(); + assert!(started.elapsed() < Duration::from_secs(10)); + let descendant = fs::read_to_string(root.join("descendant.pid")).unwrap(); + let listed = Command::new("tasklist") + .args(["/FI", &format!("PID eq {descendant}"), "/FO", "CSV", "/NH"]) + .output() + .unwrap(); + assert!(!String::from_utf8_lossy(&listed.stdout).contains(&format!("\"{descendant}\""))); +} + +#[test] +fn windows_silent_timeout_is_bounded_and_reaps_the_leader() { + let root = root("silent-timeout"); + compile_c( + &root, + "silent_timeout", + "#include \n#include \nint main(void){FILE *file=fopen(\"leader.pid\",\"w\");if(!file)return 3;fprintf(file,\"%lu\",(unsigned long)GetCurrentProcessId());fclose(file);Sleep(60000);return 0;}\n", + ); + let directory = hold_directory(&root).unwrap(); + let held = hold_executable(&directory, OsStr::new("silent_timeout.exe")).unwrap(); + let started = Instant::now(); + assert_eq!(execute_harness(&held, &directory), Err(Error::Spawn)); + let elapsed = started.elapsed(); + assert!(elapsed >= Duration::from_secs(30)); + assert!(elapsed < Duration::from_secs(40)); + let leader = fs::read_to_string(root.join("leader.pid")).unwrap(); + let listed = Command::new("tasklist") + .args(["/FI", &format!("PID eq {leader}"), "/FO", "CSV", "/NH"]) + .output() + .unwrap(); + assert!(!String::from_utf8_lossy(&listed.stdout).contains(&format!("\"{leader}\""))); +} + +#[test] +fn windows_output_overflow_kills_and_reaps_the_process_tree_with_a_bounded_wait() { + let root = root("bounded-kill"); + compile_c( + &root, + "noisy", + "#include \n#include \n#include \nint main(int argc,char **argv){if(argc==2&&strcmp(argv[1],\"child\")==0){FILE *file=fopen(\"descendant.pid\",\"w\");if(!file)return 3;fprintf(file,\"%lu\",(unsigned long)GetCurrentProcessId());fclose(file);Sleep(30000);return 0;}char path[MAX_PATH];if(!GetModuleFileNameA(NULL,path,MAX_PATH))return 4;char command[MAX_PATH+16];if(sprintf_s(command,sizeof(command),\"\\\"%s\\\" child\",path)<0)return 5;STARTUPINFOA startup={0};startup.cb=sizeof(startup);PROCESS_INFORMATION process={0};if(!CreateProcessA(NULL,command,NULL,NULL,FALSE,0,NULL,NULL,&startup,&process))return 6;CloseHandle(process.hThread);CloseHandle(process.hProcess);while(GetFileAttributesA(\"descendant.pid\")==INVALID_FILE_ATTRIBUTES)Sleep(1);fputs(\"x\",stdout);fflush(stdout);Sleep(30000);return 0;}\n", + ); + let directory = hold_directory(&root).unwrap(); + let held = hold_executable(&directory, OsStr::new("noisy.exe")).unwrap(); + let started = Instant::now(); + assert_eq!(execute_harness(&held, &directory), Err(Error::OutputLimit)); + assert!(started.elapsed() < Duration::from_secs(10)); + + let descendant = fs::read_to_string(root.join("descendant.pid")).unwrap(); + let deadline = Instant::now() + Duration::from_secs(2); + loop { + let listed = Command::new("tasklist") + .args(["/FI", &format!("PID eq {descendant}"), "/FO", "CSV", "/NH"]) + .output() + .unwrap(); + let output = String::from_utf8_lossy(&listed.stdout); + if !output.contains(&format!("\"{descendant}\"")) { + break; + } + if Instant::now() >= deadline { + let _ = Command::new("taskkill") + .args(["/PID", &descendant, "/F"]) + .output(); + panic!("owned Windows descendant remained observable after harness return"); + } + std::thread::sleep(Duration::from_millis(10)); + } +} + +#[test] +fn windows_external_consumer_cannot_extract_handles_or_reach_sys_quarantine() { + let root = root("opacity"); + fs::create_dir(root.join("src")).unwrap(); + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + fs::write( + root.join("Cargo.toml"), + format!( + "[package]\nname='windows-opacity-probe'\nversion='0.0.0'\nedition='2021'\n[dependencies]\nsemaprax-native-rust-interop-platform={{path={manifest_dir:?}}}\n" + ), + ) + .unwrap(); + fs::write( + root.join("src/main.rs"), + r#"use semaprax_native_rust_interop_platform::{hold_directory,HeldDirectory}; +use std::os::windows::io::AsRawHandle; +fn raw(directory:&HeldDirectory)->*mut core::ffi::c_void{directory.0.as_raw_handle()} +fn require_clone(){} +fn clone_it(){require_clone::();} +fn debug_it(directory:&HeldDirectory){let _=format!("{directory:?}");} +fn main(){let _=hold_directory(std::path::Path::new("C:\\"));let _=semaprax_native_rust_interop_platform_sys::Error::Invalid;} +"#, + ) + .unwrap(); + let checked = Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into())) + .env("CARGO_TARGET_DIR", root.join("target")) + .args(["check", "--offline", "--quiet"]) + .current_dir(&*root) + .output() + .unwrap(); + assert!(!checked.status.success()); + let stderr = String::from_utf8_lossy(&checked.stderr); + assert!(stderr.contains("field `0` of struct `HeldDirectory` is private")); + assert!(stderr.contains("Clone")); + assert!(stderr.contains("Debug")); + assert!(stderr.contains("semaprax_native_rust_interop_platform_sys")); +} diff --git a/docs/AGENT-RUNTIME-V1.md b/docs/AGENT-RUNTIME-V1.md index 95b36a2..5c91789 100644 --- a/docs/AGENT-RUNTIME-V1.md +++ b/docs/AGENT-RUNTIME-V1.md @@ -1,7 +1,7 @@ # Bounded Native Agent Runtime v1 -Status: the A+B private proof is hosted green; C1 exposes an additive injected- -host Rust API whose new exact-head hosted promotion gate remains pending. It does +Status: the A+B private proof and additive C1 injected-host Rust API are hosted +green. It does not add language syntax, compiler semantics, a provider transport, a CLI, or a backend. @@ -67,11 +67,7 @@ retry; tool schema/effect/capability/policy/result failures; cancellation and deadline boundaries; replay mutations; secret-sentinel absence; no-write inventory; 240-byte identities and JSON escaping; and cumulative builder limits. CI is configured to run the fake-host corpus on Ubuntu, macOS, and -Windows. The exact `cd2f6393bb84657f7ef4f0094e1136eb5a401355` A+B matrix is -hosted green in [run 31585682213](https://github.com/wavect/semaprax/actions/runs/31585682213); -all 12 jobs passed, including the deterministic fake-host gate on Ubuntu, -macOS, and Windows. C1 public integration is locally green 4/4 and must pass a -fresh exact-head hosted matrix before promotion. There is no live- +Windows. Public Agent Runtime v1 is hosted GREEN at 8cf29aff8d1be3ccf74c36bc8c837f0c666ca067 (run 31591039261, 12/12 jobs, private and public deterministic fake-host gates on Ubuntu, macOS, and Windows). There is no live- provider or provider-quality claim. ## Public C1 surface diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f5d5832..ca1832b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -56,6 +56,65 @@ The declaration index is the single current source of target-independent type fa The native and Wasm emitters now consume only validated HIR for semantic lowering; their parsed-AST entry points are compatibility wrappers that resolve first. A centralized HIR validator rejects duplicate/non-canonical identities, invalid declarations and nominal types, lexical-scope violations, inconsistent expression/call types, definite or conditional resource reuse, contract transfers, undeclared or unpermitted effects, effectful contracts, invalid result bindings, and an invalid entrypoint before either backend emits an artifact. Only after those checks pass, `cleanup` independently rebuilds the structural storage inventory; `cleanup_plan` then rebuilds and exact-compares the complete target-neutral plan. A hostile direct-HIR transform therefore cannot remove, retarget, reorder, or forge cleanup meaning before reaching Graph or a backend. +The private Native Rust Interoperability v1 A+B lane is a separate unpublished +current-host bridge, not another public backend. Its additive `import rust fn` +declarations require an explicit `failure infallible;` or status-domain clause +and resolve to a distinct scalar-only HIR call kind. Rust imports alone may +return unit; selected Rust-facing exports return `i64` or `bool`. A pure preflight +selects explicit-ID exports/imports and their bounded acyclic closure, derives +canonical Spec and Descriptor documents, and emits the generated C +header/source plus safe Rust facade and private unsafe FFI sibling. Descriptor +and Manifest, header, C, safe Rust, and private FFI replay now use independently +structured ordered exact-byte consumers. Exhaustive byte-edit tests cover the +canonical Spec input and all six outputs, and a portable fixed-target fixture +freezes each output's byte length and raw SHA-256 plus the existing Descriptor +and Manifest protocol-domain digests. This freezes those wire identities without +claiming hosted execution or public promotion. +Preparation reserves its cumulative authority before each semantic phase. The +local capacity corpus separates persistent HIR/facts/artifact storage from +sequential scratch, iteratively traverses admitted depth, transfers the Spec +allocation once, and exercises named post-HIR fact/render/replay high waters and +minimum-minus-one zero-entry boundaries. This is evidence for the private +preparation route only, not a general compiler allocation claim. +The build stage requires an explicitly configured absolute Rust launcher and +uses it only for one bounded sysroot discovery. It independently holds the +direct compiler at that sysroot, requires the direct compiler to reproduce the +same held sysroot, validates its version, and restricts every Rust artifact +operation to the distinct direct-compiler authority. Rust discovery/version, +Clang version, and the eight build/link/run operations consume one exact +pre-effect 12-use process arena. Windows queries and bounds the attribute-list +size before reserving and materializing that arena. The four retained `rustc -vV` fields occupy one +fixed-capacity no-growth store, and prepared target arguments admit the host's +underscore-bearing components without widening the closed punctuation grammar. +Prepared native names, inventories, artifact +comparisons, and the final no-clobber rename leave no allocation or new local +budget failure after the final inventory scan. The build uses statically linked +generated objects, executes a generated round trip, and publishes one +create-new exact inventory through held platform authority. Windows directory +authority binds volume, full file identity, and reparse state rather than +mutable directory length. Each private build +stage is required to retain its create-returned directory authority until +settlement and attempt one exact-inventory cleanup on success or failure. +Cleanup must stop on identity, reparse/symlink, or inventory disagreement, +preserve foreign sentinels, and expose no generic recursive-delete primitive. +Local builder 99/99, platform-system 22/22, platform 10/10, source-contract +6/6, strict-Clippy, formatting, and security evidence are green. Exact-head +hosted cleanup/process authority and sanitizer evidence on Ubuntu, macOS, and +Windows remain held; Windows process-arena runtime and capacity-minus-one +evidence remain hosted-only. Compiler sysroot/dynamic-library descendant +provenance is not claimed. Graph +and Wasm +reject programs containing the private declaration kind with `SPX-G218` and +`SPX-W114`; callable v2/v3, the loader/host, and ordinary native/Wasm bytes are +unchanged. The scalar C ABI carries caller-owned result storage, a +capability-digest-bound context, a typed callback table, and closed canonical +status words; generated safe Rust contains panic, thread, reentry, and call +budgets around the private unsafe quarantine. This lane grants no dynamic +loading, allocator, resource, pointer, async, cross-thread, network, custody, +public CLI/API, general Rust ABI, or production-readiness claim. See +[`NATIVE-RUST-INTEROP-V1.md`](NATIVE-RUST-INTEROP-V1.md); public C remains held +until exact-head Ubuntu/macOS/Windows and Linux sanitizer evidence is green. + This remains staged groundwork rather than the sole compiler IR: the current verifier still establishes meaning from parsed AST before HIR resolution. Explicit trivial/imported resource lifecycles, declaration-only interface/import contracts, record declarations/updates, bounded explicitly instantiated generic Copy records, bounded copy-variant templates/construction/exhaustive matching, typed ordinary-`Result` and ordinary-`Option` propagation, stable type/member/case identities, recursive resource/type facts, and by-value recursion rejection now reach validated HIR and the semantic graph. Generic parameters are owner/index-stable and the admitted concrete arguments are direct `i64`/`bool`; generic record fields are restricted to direct scalars or parameters owned by that record, and every construction/update/projection substitutes the exact ordered concrete instance. The compiler-owned `semaprax.prelude.v1` injects ordinary `Option` and `Result` variants before checking. The bounded postfix `?` form accepts only direct-scalar Copy instances: `Result` requires an enclosing `Result`, while `Option` requires an enclosing `Option`. It evaluates its operand once, reconstructs the exact outer `Err` or payload-free `None`, and routes both ordinary-body and propagated results through shared postconditions and publication. The source checker and HIR validator independently replay lifecycle compatibility, lifecycle-effect authority, prefix-aware partial-place availability, exact generic substitution, exact construction, copy-match exhaustiveness, and every compiler-owned carrier/member/source/target identity. `aggregate_layout` computes checked deterministic Native64 and Wasm32 record layouts keyed by the full record ID plus ordered arguments; its digest and native symbol bind the same exact instance even when two instances have identical physical fields. `variant_layout` computes independently reconstructable per-concrete-instance internal layouts with declaration-order `u32` tags, an aligned maximum-payload area, and one inert byte for an empty payload. Its v2 digest authenticates the full concrete instance and both template and substituted field types; physical tags and representation are unchanged from v1. `CleanupInventory` remains a structural discovery boundary. Every `ResolvedFunction` carries a cleanup plan: v2 remains canonical unless authenticated Option propagation is present, which requires v3. Both schemas include typed blocks, edges, lexical regions, entry liveness, storage/leaf flags, atomic call commits, sticky status sources, guarded finalizers, scalar/owned result publication, and exact body-versus-propagated Copy-result staging; v3 adds an authenticated payload-free Option-None source. Generic records add no cleanup action because the admitted instances are direct-scalar Copy values; canonical replay remains bound to exact HIR types. Immutable update consumes its base first, evaluates replacements in authored order, transfers untouched fields, and cleans displaced live fields exactly once in reverse order. Copy matches branch on an exact scrutinee expression and stable case IDs without inventing droppable payload leaves; distinct concrete instances therefore cannot share a cleanup decision. Propagation uses complementary predicates on the authenticated success case and cannot be confused with physical failure selection. The builder covers every current HIR expression and normal/checked-failure path; the validator reconstructs the plan from core HIR rather than trusting attached metadata. The bounded record-pattern tranche is irrefutable and Copy-only. One explicit @@ -1273,11 +1332,8 @@ runtime-owned sinks, closed provider attempt values, cancellation handle, and opaque run getters; the unsealed host is injected and trusted for its declared transport/tools. The module has no built-in transport, process/environment/home access, filesystem mutation, durable memory, wallet, payment, signing, language, Graph, cleanup, or backend surface. -The exact `cd2f6393bb84657f7ef4f0094e1136eb5a401355` A+B matrix is hosted green in -[run 31585682213](https://github.com/wavect/semaprax/actions/runs/31585682213), -including three-OS fake-host evidence. C1 public integration is locally green -4/4; hosted promotion remains pending. -This private proof changes none of the 38 Partial/18 Missing totals. +Public Agent Runtime v1 is hosted GREEN at 8cf29aff8d1be3ccf74c36bc8c837f0c666ca067 (run 31591039261, 12/12 jobs, private and public deterministic fake-host gates on Ubuntu, macOS, and Windows). Private Economic Agent v1 A+B is exact-head hosted green at fe75c38d898b71e3ed5c57411fb46d0dbd4fc34b in run 31611748969, including both Economic gates on Ubuntu, macOS, and Windows. Public Economic Agent v1 C is exact-head hosted green at 03f1f2736de23d03b298f265f93409de89a6be95 in run 31616168124 (12/12 jobs), including the private, process-termination, and public Economic gates on Ubuntu, macOS, and Windows. +This additive injected-host surface changes none of the 38 Partial/18 Missing totals. ## Trust boundaries diff --git a/docs/COMPLETION-MATRIX.md b/docs/COMPLETION-MATRIX.md index d890b28..504abdc 100644 --- a/docs/COMPLETION-MATRIX.md +++ b/docs/COMPLETION-MATRIX.md @@ -10,10 +10,7 @@ deterministic fake-host parser/router/tool-loop and Trace/Evidence coverage plus a narrow injected-host C1 Rust API. It does not change any completion row or the 38 Partial/18 Missing totals, and makes no public provider, language/backend, durable-memory, wallet, payment, signing, or economic-authority claim. -The exact `cd2f6393bb84657f7ef4f0094e1136eb5a401355` A+B matrix is hosted green in -[run 31585682213](https://github.com/wavect/semaprax/actions/runs/31585682213), -including the three-OS fake-host gate. C1 public integration is locally green -4/4 and hosted promotion remains pending. +Public Agent Runtime v1 is hosted GREEN at 8cf29aff8d1be3ccf74c36bc8c837f0c666ca067 (run 31591039261, 12/12 jobs, private and public deterministic fake-host gates on Ubuntu, macOS, and Windows). Private Economic Agent v1 A+B is exact-head hosted green at fe75c38d898b71e3ed5c57411fb46d0dbd4fc34b in run 31611748969, including both Economic gates on Ubuntu, macOS, and Windows. Public Economic Agent v1 C is exact-head hosted green at 03f1f2736de23d03b298f265f93409de89a6be95 in run 31616168124 (12/12 jobs), including the private, process-termination, and public Economic gates on Ubuntu, macOS, and Windows. Totals remain 38 Partial/18 Missing. Status values: @@ -49,7 +46,7 @@ provenance, or automatic identity selection. Exact replay inside apply mints | Ownership and cleanup meaning | Partial | Move/partial-place checks plus independently rebuilt and replayed CleanupPlan v2 plans, and feature-minimal v3 plans for bounded Option propagation, are executable, including exact body/Result-residual/Option-None Copy-result staging and shared postcondition/publication joins; general lifetimes, aliases, concurrency, FFI, and public physical cleanup remain open | | Aggregate records v1 bounded execution | Partial | Construction/projection/update, stable IDs, checked Native64/Wasm32 layouts, frozen one-byte/alignment-one empty records, and cleanup are executable; nested public scalar records and exact-instance generic Copy records with ordered direct `i64`/`bool` arguments run through native C11 O0/O2 and Node/Wasm, with the generic-record gate hosted green in [run 31365363898, Ubuntu job 93383304995](https://github.com/wavect/semaprax/actions/runs/31365363898/job/93383304995). Bounded irrefutable Copy-record matches now destructure exact nested/generic instances with scalar or whole-record bindings, ignored fields, one evaluation, scalar arms, program-wide Graph v13, unchanged straight-line CleanupPlan v2/v3, Native O0/O2, and 4,096-entry Node/Wasm; the Ubuntu gate is hosted green in [run 31373317800, job 93406925130](https://github.com/wavect/semaprax/actions/runs/31373317800/job/93406925130), and independent security review is green. One private shared-plan resource harness separately proves an exact cross-backend cleanup trace and zero liveness. Stable public aggregate ABIs, public resource-record execution, nested/resource/non-Copy generic breadth, refutable or ownership-aware matching, and general aggregate execution remain open | | Copy variants + bounded generics/prelude/`?` | Partial | Nominal variants with explicit direct `i64`/`bool` arguments, ordinary compiler-owned `Option`/`Result`, exhaustive copy match, exact-instance layouts, and Native O0/O2 plus Node/Wasm are hosted green in [run 31347109201](https://github.com/wavect/semaprax/actions/runs/31347109201). Bounded postfix `?` for direct-scalar Copy `Result` is hosted green in [run 31353051690](https://github.com/wavect/semaprax/actions/runs/31353051690), and the analogous Option tranche is hosted green in [run 31360176398, job 93367728277](https://github.com/wavect/semaprax/actions/runs/31360176398/job/93367728277). Bounded explicitly instantiated effect-free generic Copy functions now have exact source/HIR/Graph-v14/native/Wasm evidence, including unused-template validation, concrete-instance separation, failure order, poison, and 4,096-entry Node re-entry; independent security review is green and the hosted matrix is green in [run 31385406865, Ubuntu job 93445428338](https://github.com/wavect/semaprax/actions/runs/31385406865/job/93445428338). Inference, constraints, aggregate/resource/non-Copy generic signatures or arguments, generic-function `?`, non-copy propagation/matching, residual conversion, stable public aggregate ABI, callable/component signatures, and public resource admission remain open | -| Native code and interop | Partial | Scalar C11/Clang and bounded private callable/resource evidence exist; public general native execution and C/Objective-C/Swift/Kotlin ecosystem import remain open | +| Native code and interop | Partial | Scalar C11/Clang and bounded private callable/resource evidence exist. Private Native Rust Interoperability v1 A+B now has a locally green scalar, current-host static-link implementation: an absolute configured launcher may only discover one bounded sysroot, the direct compiler is independently held and fixed-point checked, artifact compilation is direct-image-only, and one pre-effect 12-use process arena plus prepared filesystem/publication authority covers Phase B. Windows attribute-list storage is queried once before effects, bounded, reserved before allocation, and checked for no growth through all 12 uses. Named pre-HIR and post-HIR retained/scratch envelopes, iterative render/replay machines, exact transfer boundaries, no-growth prepared invocations, and minimum-minus-one zero-entry gates cover the local private preparation path. The prepared Linux link plan also binds the frozen native-static library tail required by Rust's standard library. Local builder 99/99, platform-system 22/22, platform 10/10, source-contract 6/6, strict-Clippy, formatting, and security gates are green. Exact-head Ubuntu/macOS/Windows, Windows runtime/capacity settlement, and Linux sanitizer evidence remain pending, compiler sysroot/dynamic-library descendant provenance is not claimed, and public C remains held. Resources, aggregates, borrowing, async, general native execution, and C/Objective-C/Swift/Kotlin ecosystem import remain open. See [NATIVE-RUST-INTEROP-V1](NATIVE-RUST-INTEROP-V1.md) | | Web and portable components | Partial | Scalar Core Wasm, bounded generic Copy functions, public scalar/nested and generic Copy records, bounded Copy-record patterns, bounded generic/prelude/typed-`?` copy-variant Core Wasm, narrow owned-resource Wasm, and private WIT/component evidence exist. V3-v7 hosted evidence remains green, including Generic Record Component v7 in [run 31373317800, job 93406924922](https://github.com/wavect/semaprax/actions/runs/31373317800/job/93406924922). Private Record-Pattern Projection Component v8 freezes four exact monomorphic preserve/invert exports over distinct same-layout `Phantom`/`Phantom` records under `semaprax:private@0.6.0`; local exact/upstream, hostile, Node/core, source-lock, strict, and security gates are green, and its pinned Rust 1.97.1/Wasmtime 47 hosted execution is green in [run 31385406865, job 93445428268](https://github.com/wavect/semaprax/actions/runs/31385406865/job/93445428268). Private Generic-Function Instance Component v9 fixes `semaprax:private@0.7.0`, three phantom Copy templates, six exact ordered Graph-v14 `FunctionInstanceId` exports with identical scalar WIT signatures, no record/layout roots, and source/Graph/core/plan/profile/raw/DAG KATs; its pinned Rust 1.97.1/Wasmtime 47 hosted runtime is green in [run 31392541096, job 93467490492](https://github.com/wavect/semaprax/actions/runs/31392541096/job/93467490492). Private Source-Option Propagation Component v10 fixes `semaprax:private@0.8.0`, one exact compiler-owned `Option` through postfix-`?` to `Option` export, Graph v11, CleanupPlan v3, both layout-v2 instances, and source/Graph/prelude/layout/plan/core/profile/raw/DAG KATs; local core 5/5, component 4/4, CI-lock 4/4, full, hostile, and security gates are green, and its pinned v3-v10 Wasmtime runtime is hosted green in [run 31396483313, job 93481068502](https://github.com/wavect/semaprax/actions/runs/31396483313/job/93481068502). General source selection or algebraic mapping, general/empty/nested/resource/non-Copy component records or carriers, general generic-function components, imports/capabilities, callable/FFI aggregate signatures, browser/multi-engine conformance, and public API/ABI remain open | | Desktop and mobile applications | Partial | Private macOS engine/AppKit ([job 93309086230](https://github.com/wavect/semaprax/actions/runs/31338834586/job/93309086230)), Windows engine/Win32 UI ([job 93322134480](https://github.com/wavect/semaprax/actions/runs/31343897595/job/93322134480)), Swift/iOS XCFramework/app ([job 93309086228](https://github.com/wavect/semaprax/actions/runs/31338834586/job/93309086228)), and Android JNI/Kotlin app ([job 93309086206](https://github.com/wavect/semaprax/actions/runs/31338834586/job/93309086206)) gates are green. Public SDKs, UI language, lifecycle breadth, signing/distribution, and device breadth remain open | | Full SEMAPRAX product objective | Partial | No single lane proves native mobile + desktop + web + broad interop + full ownership/lifetime safety together; the global goal is not complete | diff --git a/docs/ECONOMIC-AGENT-V1.md b/docs/ECONOMIC-AGENT-V1.md new file mode 100644 index 0000000..e9bf9f0 --- /dev/null +++ b/docs/ECONOMIC-AGENT-V1.md @@ -0,0 +1,279 @@ +# Economic Agent v1 + +Private Economic Agent v1 A+B is exact-head hosted green at fe75c38d898b71e3ed5c57411fb46d0dbd4fc34b in run 31611748969, including both Economic gates on Ubuntu, macOS, and Windows. Public Economic Agent v1 C is exact-head hosted green at 03f1f2736de23d03b298f265f93409de89a6be95 in run 31616168124 (12/12 jobs), including the private, process-termination, and public Economic gates on Ubuntu, macOS, and Windows. +This changes none of the 38 Partial/18 Missing totals. + +## Authority boundary + +Economic Agent v1 consumes only a completed, already-replayed Agent Runtime +result whose untrusted final message is a canonical Payment Intent. The model +cannot approve, sign, broadcast, widen policy, or mint wallet authority. A +separate injected approver binds the exact Policy, Intent, Plan, Simulation, +and Approval Request. Opaque injected custody receives only the approved +unsigned transaction and digest bindings; keys and credentials never enter the +runtime, Trace, Evidence, or diagnostics. + +The public injected-host API admits only native assets on Sepolia EIP-1559 type-2, +Solana devnet System Program transfers, Bitcoin regtest P2WPKH PSBT v2, and an +x402 invoice overlay over one of those rails. It includes no built-in HTTP, +DNS, chain node, journal, approver, custody, signing key, filesystem, process, +environment, mainnet, token, contract, arbitrary program/script, swap, bridge, +refund, or automatic rebroadcast authority. + +## Canonical state and execution + +The exact v1 documents are Policy, Payment Intent, x402 Invoice, Chain +Snapshot, Payment Plan, Simulation, Approval Request, Approval, Journal, +Broadcast Receipt, Reconciliation, Trace, and Evidence. Each is compact +canonical UTF-8 JSON with one terminal LF, exact key order, closed types, +bounded depth, checked decimal integers, exact domain-separated digests, and +independent replay. Journal broadcast and reconciliation fields retain bounded +`schema,digest,bytes,document` capsules so restart reconciliation can recover +the exact transaction identity without another authority read. + +Execution loads the journal once, atomically reserves the exact rolling-24h +policy window with the first Journal CAS, obtains a frozen rail snapshot, +builds and independently decodes the unsigned transaction, simulates, obtains +separate approval, signs once, persists the signed binding before broadcast, +broadcasts at most once, and reconciles one observation. Uncertain broadcast is +persisted and is never retried automatically. Standalone reconciliation +requires the same sealed Agent result and can never sign or broadcast. + +The one decreasing 64 MiB builder budget covers canonical inputs, retained +state, adapter sinks, journal candidates, Trace, Evidence, and replay. Before +each injected authority call, cancellation, deadline, policy, output, +continuation, Journal, Trace, Evidence, and builder capacity are required to +fit. Once an external effect is attempted, every operational exit must return +canonical replayed Trace/Evidence and the admitted Journal transition is +attempted according to the state machine. + +## Evidence gates + +The configured focused gate is: + +```sh +cargo test --locked -p semaprax --lib economic_agent::tests -- --nocapture +``` + +It must pass with deterministic fake journals, adapters, approvers, and custody +on Ubuntu, macOS, and Windows before the private hosted claim. Required gates +also include every document KAT and mutation, independent chain byte vectors, +x402 SSRF/path hostiles, exact/+1 limits, rolling-window concurrency, every +adapter disposition, cancellation/deadline boundaries, no-retry restart and +process-termination evidence, replay mutation, secret/no-write inventory, +full workspace tests, strict Clippy, rustdoc, formatting, and package/external +consumer checks. Test-network names are encoding namespaces; CI performs no +live node, faucet, credential, or external-network request. Process-kill gates +prove OS process termination and journal replay, not power-loss durability. + +The additive public C surface now exposes only the opaque injected-host dialect +documented below. Its local evidence is green; hosted promotion remains pending +an exact-head 12/12 run with the public gate on all three host operating systems. + +## Exact canonical wire ledger + +All documents use compact UTF-8 JSON followed by exactly one LF. Objects are +closed and preserve the following key order; arrays have the documented +semantic order and decimal integers are JSON `u64` without signs, fractions, +or exponents. + +- Policy, `semaprax.economic-agent-policy.v1`: + `schema,economic_agent_id,wallet_id,network_policies,x402_origins,limits,nonclaims`. +- Payment Intent, `semaprax.economic-agent-payment-intent.v1`: + `schema,intent_id,wallet_id,rail,idempotency_key,created_at_ms,expires_at_ms,memo,payment`. + EVM payment keys are `kind,network,asset,recipient,amount_atomic,max_fee_atomic`; + Solana adds `max_compute_units,max_priority_fee_atomic`; Bitcoin adds + `confirmation_target`; x402 uses + `kind,origin,method,resource,invoice_digest,payee,settlement_rail,network,asset,amount_atomic,max_fee_atomic,invoice_expires_at_ms,invoice_nonce`. +- x402 Invoice: `schema,origin,method,resource,invoice_id,payee,settlement_rail,network,asset,amount_atomic,max_fee_atomic,expires_at_ms,nonce,idempotency_key`. +- Chain Snapshot: `schema,rail,network,observed_at_ms,expires_at_ms,state`. + The EVM state is `chain_id,from,nonce,base_fee_per_gas,max_priority_fee_per_gas,gas_limit`; + Solana is `fee_payer,recent_blockhash,last_valid_block_height,lamports_per_signature`; + Bitcoin is `wallet_script_pubkey,height,fee_rate_sat_vbyte,utxos`, with each + sorted UTXO `txid,vout,value_atomic,script_pubkey,confirmations`. +- Payment Plan: `schema,run_id,source_agent_evidence,policy,intent,x402_invoice,chain_snapshot,rail,network,asset,wallet_id,recipient,amount_atomic,max_fee_atomic,unsigned_transaction,expires_at_ms`. +- Simulation: `schema,plan,success,fee_atomic,balance_before_atomic,balance_after_atomic,allowance_atomic,units,expires_at_ms`. +- Approval Request: `schema,run_id,wallet_id,rail,network,asset,recipient,amount_atomic,max_fee_atomic,origin,method,resource,policy,intent,plan,simulation,expires_at_ms`. +- Approval: `schema,approval_id,approver_id,policy,intent,plan,simulation,approval_request,decision,approved_amount_atomic,approved_fee_atomic,expires_at_ms`. +- Journal: `schema,idempotency_key,version,policy,intent,run_id,state,reserved_amount_atomic,reserved_fee_atomic,plan,simulation,approval,unsigned_transaction,signed_transaction,broadcast,reconciliation,updated_at_ms`. +- Broadcast Receipt: `schema,rail,network,signed_transaction_digest,transaction_id,disposition,observed_at_ms`. +- Reconciliation: `schema,rail,network,transaction_id,status,observed_at_ms,observed_height,confirmations,canonical_block_id`. +- Trace: `schema,run_id,source_agent_evidence_digest,policy_digest,intent_digest,events,result,nonclaims`. + Events are `index,kind,rail,input_digest,output_digest,status,usage`; usage is + `journal_reads,journal_writes,invoice_reads,snapshot_reads,simulations,approvals,signatures,broadcasts,reconciliations,input_bytes,output_bytes,elapsed_ms`. +- Evidence: `schema,run_id,source_agent,policy,intent,x402_invoice,plan,simulation,approval,journal,broadcast,reconciliation,trace,result,limits,budget,nonclaims`. + +Document digest domains are +`semaprax.economic-agent.{policy|payment-intent|x402-invoice|chain-snapshot|payment-plan|simulation|approval-request|approval|journal|broadcast-receipt|reconciliation|trace|evidence}-digest.v1\0`. +Unsigned and signed transaction domains end in +`unsigned-transaction-digest.v1\0` and `signed-transaction-digest.v1\0`. +The run-ID domain is `semaprax.economic-agent.run-id.v1\0` and binds the +source Agent Evidence digest, Policy digest, Intent digest, and exact +idempotency bytes. + +## Exact limits, budget, and durable topology + +Limits occur in this order: +`max_policy_bytes,max_intent_bytes,max_invoice_bytes,max_snapshot_bytes,max_plan_bytes,max_simulation_bytes,max_approval_request_bytes,max_approval_bytes,max_journal_bytes,max_unsigned_transaction_bytes,max_signed_transaction_bytes,max_broadcast_receipt_bytes,max_reconciliation_bytes,max_trace_events,max_trace_bytes,max_evidence_bytes,max_builder_bytes,max_json_depth,max_identifier_bytes,max_memo_bytes,max_recipients,max_network_policies,max_x402_origins,max_utxos,max_reconciliations,max_elapsed_ms,max_amount_atomic,max_fee_atomic,max_compute_units,max_confirmation_target,max_concurrency,max_unexpected_authority_calls`. +Production maxima are respectively +`1048576,1048576,1048576,1048576,1048576,1048576,1048576,65536,8388608,1048576,2097152,1048576,1048576,1024,8388608,16777216,67108864,16,128,1024,128,16,32,100,64,600000,1000000000000000000,1000000000000000,200000,144,1,0`. + +Budget keys are +`used_policy_bytes,used_intent_bytes,used_invoice_bytes,used_snapshot_bytes,used_plan_bytes,used_simulation_bytes,used_approval_request_bytes,used_approval_bytes,used_journal_bytes,used_unsigned_transaction_bytes,used_signed_transaction_bytes,used_broadcast_receipt_bytes,used_reconciliation_bytes,used_trace_events,used_trace_bytes,used_evidence_bytes,used_builder_bytes,used_recipients,used_network_policies,used_x402_origins,used_utxos,used_reconciliations,used_elapsed_ms,used_concurrency,used_unexpected_authority_calls`. + +Fresh durable versions are exact: v1 Reserved, v2 Prepared, v3 Approved, +v4 Approved as the durable sign-attempt marker, v5 Signed, v6 +BroadcastUnknown with the runtime provisional receipt (`unknown`, observed +time zero), and v7 an actual adapter receipt with positive observed time. +Custody and broadcast are never retried after their markers. Reconciliation +uses base B=6 for a provisional receipt and B=7 for an actual receipt. For +`offset=version-B`, persisted attempts are `(offset+1)/2`; even offsets are +between attempts and odd offsets are durable attempt markers. An odd marker +surviving termination consumes its attempt. v1 may resume without reserving +again; v2-v5 fail closed without further authority; v6 and later are +reconcile-only. The configured maximum of 64 observations is cumulative over +restarts. + +## Exact nonclaims + +Policy, Trace, and Evidence carry this ordered list: + +1. `no_model_output_payment_authority` +2. `no_model_self_approval_or_policy_expansion` +3. `no_seed_private_key_credential_or_signing_material_input` +4. `no_secret_prompt_trace_evidence_log_or_diagnostic_exposure` +5. `no_builtin_network_http_dns_custody_or_chain_authority` +6. `no_mainnet_authority` +7. `no_wildcard_network_asset_recipient_origin_or_resource` +8. `no_token_contract_program_script_swap_bridge_or_unlimited_approval` +9. `no_raw_signing_or_signed_transaction_export` +10. `no_exactly_once_signing_broadcast_or_payment` +11. `no_automatic_uncertain_broadcast_retry` +12. `no_guaranteed_confirmation_finality_or_reorg_freedom` +13. `no_compromised_wallet_approver_adapter_provider_or_chain_recovery` +14. `no_power_loss_durability_without_host_journal_contract` +15. `no_cross_process_or_distributed_concurrency_guarantee` +16. `no_live_price_exchange_rate_fee_or_cost_accuracy` +17. `no_balance_allowance_or_simulation_truth_beyond_adapter` +18. `no_human_identity_intent_approval_provenance_or_nonrepudiation` +19. `no_signature_attestation_or_custody_provenance` +20. `no_tax_accounting_legal_regulatory_sanctions_or_compliance_correctness` +21. `no_privacy_data_residency_or_unlinkability_guarantee` +22. `no_x402_redirect_ssrf_private_network_or_server_honesty_guarantee_beyond_admitted_adapter_contract` +23. `no_automatic_refund_chargeback_replacement_or_fee_bumping` +24. `no_wallet_recovery_rotation_backup_or_inheritance` +25. `no_general_payment_sdk_or_production_readiness` +26. `no_language_graph_cleanup_backend_or_workspace_atomicity_semantics` +27. `no_current_agent_runtime_schema_api_or_kat_modification` +28. `no_completion_matrix_status_promotion` + +## Schema literals and diagnostics + +The 13 schema literals, in document order, are: + +```text +semaprax.economic-agent-policy.v1 +semaprax.economic-agent-payment-intent.v1 +semaprax.economic-agent-x402-invoice.v1 +semaprax.economic-agent-chain-snapshot.v1 +semaprax.economic-agent-payment-plan.v1 +semaprax.economic-agent-simulation.v1 +semaprax.economic-agent-approval-request.v1 +semaprax.economic-agent-approval.v1 +semaprax.economic-agent-journal.v1 +semaprax.economic-agent-broadcast-receipt.v1 +semaprax.economic-agent-reconciliation.v1 +semaprax.economic-agent-trace.v1 +semaprax.economic-agent-evidence.v1 +``` + +Exact diagnostic ownership is: + +- `SPX-G210`: `Economic Agent {document} is not canonical {schema} JSON`. +- `SPX-G211`: `Economic Agent policy invariant failed: {field}`. +- `SPX-G212`: `Economic Agent payment intent was rejected: {reason}`, + where reason is one of `agent run not completed`, `wallet mismatch`, + `rail/network/asset not allowed`, `recipient not allowed`, + `origin/method/resource not allowed`, `expired`, + `amount or fee not allowed`, or `idempotency already bound`. +- `SPX-G213`: `Economic Agent prepared transaction or simulation disagrees with the admitted intent`. +- `SPX-G214`: `Economic Agent approval is absent, expired, rejected, or digest-mismatched`. +- `SPX-G215`: `Economic Agent journal state or idempotency replay disagrees with the admitted operation`. +- `SPX-G216`: `{field} exceeds {maximum}`. +- `SPX-G217`: `Economic Agent Trace or Evidence disagrees with the replayed state machine`. +- `SPX-I222`: `Economic Agent journal adapter failed`. +- `SPX-I223`: `Economic Agent chain adapter failed`. +- `SPX-I224`: `Economic Agent approval adapter failed`. +- `SPX-I225`: `Economic Agent custody adapter failed`. +- `SPX-I226`: `Economic Agent broadcast outcome is uncertain`. +- `SPX-I227`: `Economic Agent reconciliation adapter failed`. +- `SPX-I228`: `Economic Agent run was cancelled`. +- `SPX-I229`: `Economic Agent deadline was exceeded`. + +## Public injected contract + +The public C surface is a visibility-only promotion over the hosted A+B core. +Its exact entry points are +`EconomicAgent::new(policy:&str,host:H,cancellation:AgentCancellation)`, +`execute(&mut self,source:&AgentRun)`, and +`reconcile(&mut self,idempotency_key:&str,source:&AgentRun)`. +`EconomicAgentHost` is the supertrait of `PaymentJournal`, +`X402InvoiceAdapter`, the EVM/Solana/Bitcoin payment adapters, +`PaymentApprover`, and `WalletCustody`, and supplies the pure +`boundary_probe()->Box` observation. + +`PaymentJournal::load(idempotency_key,sink)` returns exactly `Missing`, +`Present`, `DefinitelyNotStarted`, or `FailedUncertain`. +`compare_and_swap(idempotency_key,expected_version,journal,rolling)` and all +other adapters return `Succeeded`, `DefinitelyNotStarted`, `FailedUncertain`, +or `PolicyRejected`. Rolling is exactly `Reserve(&reservation)`, `Retain`, or +`Release`; the reservation getters expose wallet, rail, network, asset, +requested time, amount, and maximum rolling-24h amount. The journal host +atomically samples its trusted nondecreasing clock, validates requested-time +freshness, expires rows at `now-admitted_at >= 86400000`, checked-sums the exact +wallet/rail/network/asset tuple, and binds the admitted row to the idempotency +key. Release is legal only before any possible custody or broadcast attempt. + +Each rail adapter has exact methods +`{rail}_snapshot(intent,sink)`, +`{rail}_simulate(plan,unsigned_transaction,sink)`, +`{rail}_broadcast(signed_transaction,sink)`, and +`{rail}_reconcile(transaction_id,sink)`. +The invoice adapter receives only `origin,method,resource`; the approver only +the canonical Approval Request; custody receives only +`wallet_id,rail,unsigned_transaction_digest,unsigned_transaction,approval_digest`. +Document and byte sinks expose only sticky `push`; they have no public +constructor, content readback, or rejection-reason channel. + +The authority sequence is sealed Agent replay, Policy/Intent admission, +one Journal load, v1 rolling Reserve CAS, optional invoice, snapshot, core +build plus independent unsigned decode, simulation and v2 CAS, approval and v3 +CAS, v4 sign-attempt CAS, one custody call, v5 Signed CAS, v6 provisional +broadcast CAS, one broadcast call, optional v7 actual-receipt CAS, durable odd +reconcile-attempt CAS, one reconcile call, even completion CAS, and independent +Trace/Evidence replay. Capacity, cancellation, deadline, freshness, and policy +are rechecked immediately before every external call and after every durable +pre-attempt marker. Definitely-not-started is never automatically retried; +uncertainty seals the corresponding boundary. A killed process may leave only +the exact authenticated old state or admitted next state; restart never signs +or broadcasts twice. + +## Frozen 13-document fixture ledger + +Each row is `document | SHA-256(raw canonical bytes) | domain digest | bytes`: + +```text +policy | 57ce4d3844f49c9102eb1a2c17f1946305c623e587d4f52a31744ba96ff6114a | sha256:ee623062817928e0088f24b8215705f9aad8e19a52861db6d6051679889c0b53 | 2987 +intent | bfe0695c7e2a5bdfd545b264fb79777cfdadaa449d9089c59753ae3739e36d86 | sha256:2a13c2a14cfafba6b4087e647de9e5609c8bb65ddad25c305aa8f5bc28091e2c | 670 +invoice | 38b5b00511f2e461f8df0fe1a830e89109376c893e3c52cea5d23a8d36d8733b | sha256:24cb1025c6beb2a081a05ab504f7d7f6cbb37b27e003da35cbbea003a52ac095 | 417 +snapshot | d005d0f573f337d804d80b8489b63a9f6b03099837b230af69e18a4692b4b9eb | sha256:4123e22e7449e4bbcef812af71337f2e3e5390b4cce20e59f7080b74eeb727d0 | 309 +plan | 75418fad0967fa4791d9f146f6997af67b3e76f51dcb2320bfbe2211814bde45 | sha256:81dad7aa8e82bdf8ef7b02e2b5a94b899715c86e7d82895c7cdb18c5e7ed28d8 | 1391 +simulation | b3508d24fd29028a9fad89703ba72ade9f4e620eec30f0dd2017b711b96db483 | sha256:3a1ac9d741be20bf0d5e35a78e475369a5ccad5c3662bbc5f5365f123df81f1d | 369 +approval_request | fc932dcef1eb518ba05f463df9e3dd7193ce96df408edb60f3aaa9214a3f19b9 | sha256:0833d896f4be4e4feb08d0558e43e7512d589a6c96c368c6ce73ef3a8435adf1 | 1056 +approval | 48f716162ae5ec67b28303c5e5c09b641a16a1711b7b83bd4d16a1be6094a56c | sha256:63f3e81facdc9e0ece43b28cbd47310b1a7898662cd4afe3abaae24d06eab8db | 1022 +journal | f65a8f115c405b086d9a6edb1366a594c87b9f295be8739ea2a56724297f69c9 | sha256:9f3d1f568a090c280cfe645536e912d4eb0c18c740bb7309d434ebfd1d1cb169 | 2394 +broadcast | 51479da80d60c4e4c363302963010a6675278e53958ce563dafb2892da3c537f | sha256:64882648d5bac5fb58a7408d38e5fa737ba314d27decba34e2106c2310c651a6 | 335 +reconciliation | b1b449375018c27465332384d67205438bea8a2660d3144c417e5de5d5198ba1 | sha256:e26e7655d758b53867228950de241c3265675f18687110550fc89ecdc46f2b4a | 301 +trace | a388543ab6c1a57a0b7798fbd0c5d721bb33c0ab7f7123f3bb8f24c4c965db58 | sha256:f28c44894b93948068381bb9047fedade3b855cdd1831a992466a08fa97f6f11 | 11023 +evidence | 2d4d4164476bd4fdd037f138b264d0a72728b125d6819baae87da165242788b0 | sha256:9dd80e5a13aaaa02b5b854cee0f68870ac22dfdabca14e79d32857ae35980cc6 | 17399 +``` diff --git a/docs/MIGRATIONS.md b/docs/MIGRATIONS.md index 601ea41..033a5ba 100644 --- a/docs/MIGRATIONS.md +++ b/docs/MIGRATIONS.md @@ -6,6 +6,10 @@ Bounded Native Agent Runtime v1 C1 is additive. Existing consumers require no migration; callers opt into the injected `AgentHost` API explicitly, and no CLI, language, Graph, backend, provider transport, write, or wallet surface changed. +Economic Agent v1 is additive and requires no migration. Callers may opt into +the injected-host API; it adds no CLI, built-in transport, custody, wallet, +language, Graph, cleanup, or backend authority. + ## Graph-v6 CLI context to agent-context v1 `semaprax context` now emits `semaprax.agent-context.v1` instead of a Graph-v6 diff --git a/docs/NATIVE-RUST-INTEROP-V1.md b/docs/NATIVE-RUST-INTEROP-V1.md new file mode 100644 index 0000000..fc6b9fe --- /dev/null +++ b/docs/NATIVE-RUST-INTEROP-V1.md @@ -0,0 +1,194 @@ +# Native Rust Interoperability v1 + +Status: private A+B design and implementation are locally green. Public C and +hosted promotion remain held. The six output artifacts have frozen +whole-byte known-answer identities after independent exact replay and exhaustive +byte-edit rejection; this wire freeze is not an A+B runtime or platform GO. + +Native Rust Interoperability v1 is an additive, current-host, scalar bridge. It +does not change callable v2/v3, the native loader or host, Graph schemas, Wasm, +`SPX-B104`, or any existing wire/KAT. Its admitted round trip is safe generated +Rust caller → selected SEMAPRAX export → selected Rust-import callback → scalar +result. It never detours through Wasm or a dynamic library. + +## Source and semantic admission + +The only new source form is an explicitly identified Rust import: + +```spx +@id("host.add") +import rust fn add(left: i64, right: i64) -> i64 + effects { host.math } + failure status "host.math.v1"; +``` + +Every native Rust import must end with an explicit `failure status "domain";` +or `failure infallible;` clause; omission rejects rather than silently choosing +a failure model. Parameters are 0–8 value-mode `i64`/`bool`; results are unit, +`i64`, or `bool`. IDs are explicit, effects are sorted and selected, failure +domains are closed, and calls retain the distinct HIR kind +`NativeRustImportCall`. Selected exports are 1–32 explicit-ID, +non-entry, monomorphic scalar functions whose result is `i64` or `bool`; `unit` +is admitted only as a Rust-import result. Their acyclic transitive closure is at +most 256 functions and may reach only selected Rust imports. Calls from a +contract, including through a helper, are rejected. Graph-derived routes reject +`SPX-G218`; Wasm rejects `SPX-W114`; ordinary callable routes remain closed by +`SPX-B104`. + +## Canonical documents and digests + +Spec schema is `semaprax.native-rust-interop-spec.v1`, compact JSON plus one LF, +with ordered keys `schema,module,source_revision,target,exports,imports, +capabilities,limits,nonclaims`. Descriptor schema is +`semaprax.native-rust-interop-descriptor.v1`, with ordered keys `schema,module, +source_revision,hir_digest,target,status_domains,abi,exports,imports,limits, +nonclaims`. Bundle schema is `semaprax.native-rust-interop-bundle.v1`, with +ordered keys `schema,descriptor,files,toolchain,limits,nonclaims`. + +Digest domains are: + +- `semaprax.native-rust-interop.source-revision.v1\0` +- `semaprax.native-rust-interop.hir-digest.v1\0` +- `semaprax.native-rust-interop.spec-digest.v1\0` +- `semaprax.native-rust-interop.descriptor-digest.v1\0` +- `semaprax.native-rust-interop.call-contract.v1\0` +- `semaprax.native-rust-interop.capabilities.v1\0` +- `semaprax.native-rust-interop.bundle-digest.v1\0` + +The target row is `triple,pointer_width,endian,panic_strategy,thread_policy` and +admits only the exact current host, 64-bit little-endian, unwind, same-thread +profile. Call contracts use u64-BE length framing and bind direction, persistent +ID, source parameter names and scalar types, result, sorted effects and +capabilities, exact status domains/ordinals including semantic 65533, host +65534, and adapter 65535 where required, complete ABI row, and target. + +Limits are fixed: exports 32, imports 32, parameters 8, closure functions 256, +status domains 64, effects 64, identifier bytes 128, source bytes 16,777,216, +spec bytes 1,048,576, descriptor bytes 1,048,576, generated C bytes 4,194,304, +generated header bytes 1,048,576, combined generated Rust bytes 4,194,304, +manifest bytes 1,048,576, cumulative builder bytes 33,554,432, JSON depth 8, +semantic expression depth 512, call depth 32, bridge crossings 4,096, and +unexpected inventory entries 0. + +## ABI and status + +The generated C ABI is version 1, calling convention C. `spxnr_status_v1` is a +u64: code bits 0–31, class 32–39, retry bit 40, reserved zero bits 41–47, and +domain ordinal 48–63. Zero is success. Ordinal 65533 is +`semaprax.native-rust-semantics.v1`, 65534 is host, and 65535 is adapter; +selected status domains occupy sorted ordinals 1..N. Semantic codes are neg/add/ +sub/mul/div/rem = 1..6. Contract pre/post codes are 1/2. Results are caller-owned, +uninitialized, and written only after complete success. + +Context `SPXNRCTX1` stores ABI version, size, userdata, imports-table pointer, +capability digest, call depth, and zero reserved word. `SPXNRIMP1` stores version, +size, and callbacks in descriptor order. C validates pointer alignment, +versions, sizes, bool 0/1, callback presence, capability digest, depth, result +pointer, and status canonicality. The safe Rust wrapper enforces the call budget, +same-thread ownership, and non-reentrant use before effects. The generated bridge never formats, returns, or +stores a caught panic payload and forgets it before the FFI return; no unwind +crosses FFI. Output from a caller-installed process-global panic hook is outside +the bridge's authority and is neither suppressed nor claimed. No allocator +crosses the boundary. + +Generated safe Rust defines `NativeRustImports`, `NativeRustImportResult`, +`NativeRustStatusClass`, `NativeRustCapabilities`, `NativeRustBridge`, and the +closed call errors. The bridge is opaque, non-Clone/non-Debug, !Send/!Sync, +same-thread, and has no host/raw-context escape. A private sibling FFI module is +the only generated unsafe quarantine. + +## Build and publication authority + +Private A is pure: +`prepare_native_rust_interop(&Program,&[u8]) -> PreparedNativeRustInterop`. +Its cumulative authority is reserved before phase entry. Pre-resolution HIR, +cleanup inventory/plan, TypeFacts, post-HIR fact construction, the five +renderers, Descriptor replay, and the independent C-expression replay have +named retained-versus-scratch envelopes, iterative depth-bounded traversals, +observed high-water gates, and exact/minus-one entry tests. Persistent facts +and final artifact sinks are charged separately from sequential scratch; the +Spec allocation is transferred rather than charged twice. These are local +bounded-memory facts for this private preparation path, not a general compiler +allocation or no-allocation claim. + +Private B calls A once. `RUSTC` must name an explicit absolute discovery +executable; that executable may only run the frozen bounded sysroot query and +produces no accepted artifact. B independently opens the reported sysroot and +its exact `bin/rustc`/`bin/rustc.exe`, rejects path indirection, requires that +direct compiler to reproduce the same held sysroot, validates its version, and +admits Rust artifacts only through the distinct held-direct-rustc authority. +Clang is independently held. One exact pre-effect process arena is consumed by +the four discovery/version operations and eight build/link/run operations. On +Windows its attribute-list size is queried once before effects, capped, +aligned, reserved before allocation, and rechecked on every use. Private B +exactly replays Descriptor and Manifest bytes, and generates +header/C/safe-Rust/private-FFI artifacts with independent ordered exact-byte +consumers. Prepared invocations bind the admitted current-host target spelling, +including underscore-bearing target components, while rejecting other +punctuation. The four required `rustc -vV` fields share one preallocated +65,536-byte fixed-capacity store; parsing is no-growth and its retained capacity +is transferred exactly into Phase B rather than reserving four independent +maximum strings. The canonical Spec input and all six outputs reject every-byte +substitution, deletion, insertion, and truncation. One fixed-target fixture pins +the byte length and independently recomputed raw SHA-256 of Descriptor, Manifest, +header, C, safe Rust, and private FFI; it additionally pins the existing protocol +domain digests for Descriptor and Manifest. It compiles strict C and Rust, +statically links the object with the frozen Linux native-static library tail +when applicable, executes the round trip, then publishes a create-new exact +inventory. +There is no dylib, loader, symbol lookup, network, CLI, or public execution +surface. + +This direct-image policy closes ordinary rustup-launcher indirection. It does +not claim provenance for the selected compiler sysroot, dynamically loaded +libraries or backends, or arbitrary descendants; the explicitly configured +discovery executable is trusted only to nominate the direct compiler that is +then independently held and exercised. + +Every owned build stage is continuously represented by the directory authority +returned when it was created. Settlement uses only the opaque exact-inventory +discard operation. Identity, reparse/symlink, or inventory disagreement stops +deletion, preserves any foreign sentinel, and leaves inert residue for external +recovery. Exact success/failure-path settlement evidence remains a promotion +gate. The safe facade and system quarantine expose no generic or recursive +path-delete operation. + +Windows promotion additionally requires executable tests for zero and small +stdout at normal EOF, silent deadline expiry, descendant-held stdout without +overflow, one-character/reserved-DOS/case-folded names, and injected image, +Job assignment, resume, terminate, wait/query, pipe-peek, and pipe-read +failures. Every ordinary error must retain its sticky code only after proven +leader-and-Job quiescence. An unprovable settlement must fail-stop before any +later tool or publication action. Source inspection and non-Windows cfg-off +compilation do not satisfy this hosted gate. + +The six manifest file rows are `descriptor.json`, `module.c`, +`semaprax_native_rust_interop.h`, `semaprax_native_rust_interop.rs`, +`semaprax_native_rust_interop_ffi.rs`, and `module.o`/`module.obj`, sorted. The +directory additionally contains `semaprax.native-rust-interop.json`. The +manifest never hashes itself. + +## Diagnostics and nonclaims + +The exact owned diagnostics are B106 noncanonical spec; B107 closed declaration +reason; B108 descriptor disagreement; B109 limit; B110 target/toolchain; B111 +generated replay; I230 Clang; I231 Rust link/run; I232 publication; G218 Graph; +and W114 Wasm. Diagnostics never echo source, paths, tool output, secrets, panic +payloads, or pointers. + +The ordered nonclaims in every document deny resource/aggregate/pointer ABI, +cross-boundary allocation, Wasm detours, dynamic loading, public execution, +changes to callable/Graph/Agent/Economic/Workspace/Patch wires, sandboxing, +same-UID process signaling or task-port isolation, +cross-target reuse, unwind, abort/OOM/signal/process recovery, power-loss +durability, async/reentrant/cross-thread use, provenance or ambient authority, +error text/payload evidence, exactly-once effects, other ecosystem bindings, +dynamic dependency identity or filesystem-race isolation, +stable Rust ABI, public CLI/registry/network, general interop readiness, and a +completion-matrix promotion. + +Public C remains held until this private A+B surface is committed and its exact +head is green on Ubuntu, macOS, and Windows, including the required Windows +runtime/capacity settlement and Linux sanitizer lanes. Local runs qualify only +when `RUSTC` and `CLANG` explicitly select the admitted absolute tools; ambient +launcher or proxy discovery is intentionally not equivalent evidence. diff --git a/docs/QUALITY-GATES.md b/docs/QUALITY-GATES.md index bcf0bdd..8625632 100644 --- a/docs/QUALITY-GATES.md +++ b/docs/QUALITY-GATES.md @@ -35,12 +35,64 @@ provider, transport, quality, public API/CLI, wallet, or economic-authority claim. C1 additionally requires an external-crate host, exact public surface and opacity locks, cancellation/retry/cap/secret/no-write checks, package/rustdoc, and an explicit Ubuntu/macOS/Windows public integration gate. See [Bounded Native -Agent Runtime v1](AGENT-RUNTIME-V1.md). The exact -`cd2f6393bb84657f7ef4f0094e1136eb5a401355` A+B matrix is hosted green in -[run 31585682213](https://github.com/wavect/semaprax/actions/runs/31585682213); -C1 public integration is locally green 4/4; hosted promotion remains pending. +Agent Runtime v1](AGENT-RUNTIME-V1.md). Public Agent Runtime v1 is hosted GREEN at 8cf29aff8d1be3ccf74c36bc8c837f0c666ca067 (run 31591039261, 12/12 jobs, private and public deterministic fake-host gates on Ubuntu, macOS, and Windows). +Private Economic Agent v1 additionally configures `cargo test --locked -p semaprax --lib economic_agent::tests -- --nocapture` and `cargo test --locked -p semaprax --lib economic_agent::tests::economic_process_kill_markers_never_repeat_sign_or_broadcast -- --exact --nocapture` on Ubuntu, macOS, and Windows. Private Economic Agent v1 A+B is exact-head hosted green at fe75c38d898b71e3ed5c57411fb46d0dbd4fc34b in run 31611748969, including both Economic gates on Ubuntu, macOS, and Windows. Public Economic Agent v1 C is exact-head hosted green at 03f1f2736de23d03b298f265f93409de89a6be95 in run 31616168124 (12/12 jobs), including the private, process-termination, and public Economic gates on Ubuntu, macOS, and Windows. +Public C additionally runs `cargo test --locked -p semaprax --test economic_agent_v1 -- --nocapture` on Ubuntu, macOS, and Windows. Totals remain 38 Partial/18 Missing. +Private Native Rust Interoperability v1 A+B additionally requires three named +gates on Ubuntu, macOS, and Windows: + +```sh +cargo test --locked -p semaprax --test native_rust_interop_v1 -- --nocapture +cargo test --locked -p semaprax --test native_rust_interop_ci_contract -- --nocapture +cargo test --locked -p semaprax-native-rust-interop -- --nocapture +cargo test --locked -p semaprax-native-rust-interop-platform --all-targets -- --nocapture +``` + +The first gate freezes the additive syntax, distinct HIR call kind, exact +diagnostics, and explicit Graph/Wasm exclusions. The source-contract gate keeps +the private crates unpublished and quarantined and rejects a public builder +surface. The private builder suite must independently replay the canonical +Spec, Descriptor, generated sources, and Manifest; reject every-byte +substitution, deletion, insertion, and truncation; freeze byte length and raw +SHA-256 for Descriptor, Manifest, header, C, safe Rust, and private FFI plus the +protocol-domain digests for Descriptor and Manifest; prove the cumulative builder +cap with named pre-HIR/post-HIR retained-versus-scratch high waters, iterative +render/replay traversal, exact persistent transfers, no-growth final sinks, and +minimum-minus-one zero-entry rejection; prove one fixed-capacity aggregate +`rustc -vV` parse and every prepared invocation without geometric growth; prove +the create-new inventory; compile and statically link generated C and Rust +at both `-O0` and `-O2`; and execute +Rust-to-SEMAPRAX, SEMAPRAX-to-Rust, and round-trip success/failure cases. Its +hostile corpus covers ABI/version/size/alignment/bool/status/capability/result +publication, panic containment, same-thread/non-reentrant use, depth and call +budgets, tool and artifact substitution, race hooks, and safe-facade opacity. +The platform gate must prove held directory/file/executable identity, +no-clobber publication, reparse/symlink rejection, empty child environment, +ambient FD/handle closure, bounded output kill-and-reap, and the corresponding +Windows handle/job/process behavior rather than treating an unsupported stub as +Windows evidence. Windows must also cover zero/small stdout EOF, silent timeout, +descendant-held stdout without overflow, exact reserved/case-folded name +handling, and injected image/assign/resume/terminate/wait/query/peek/read +failures. Ordinary errors publish their sticky code only after proven leader and +Job quiescence; settlement-proof failure fail-stops before later build or +publication actions. Every created build stage remains bound to its create-returned +directory authority through settlement. Success and every failure path attempt +one exact-inventory cleanup; identity, reparse/symlink, or inventory mismatch +must stop cleanup, preserve the foreign sentinel, and leave only inert residue. +Neither the facade nor its system quarantine may expose generic or recursive +path deletion. Linux additionally requires the generated boundary under Clang +ASan+UBSan. Package/source locks must keep the three unpublished private crates +out of the public `semaprax` package, reject dynamic loading/link lookup, and +preserve existing Graph, Wasm, callable-v2/v3, Agent, Economic, Workspace, +Patch, CLI, API, and KAT bytes. Exact-head hosted promotion requires the whole +matrix; one host, a compile-only lane, or a platform-disabled test is not +evidence for the other hosts. Qualifying local runs set explicit absolute +`RUSTC` and `CLANG`; an ambient launcher or proxy is deliberately rejected and +is not a regression in the direct-image policy. Public C remains held until +that gate is green. + Quality gates are executable evidence, not a checklist substitute for reasoning. Every pull request must pass the baseline and the gates for each changed semantic layer. ## Baseline diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 8620c50..76660ed 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -8,10 +8,7 @@ Its C1 injected-host Rust API remains deliberately narrow: provider transports, durable memory, language/backend integration, approval, target execution, wallets, payments, signing, and economic authority remain held. See [the private runtime contract](AGENT-RUNTIME-V1.md). -The exact `cd2f6393bb84657f7ef4f0094e1136eb5a401355` A+B matrix is hosted green in -[run 31585682213](https://github.com/wavect/semaprax/actions/runs/31585682213), -including the fake-host gate on all three host OSes. C1 public integration is -locally green 4/4 and hosted promotion remains pending. +Public Agent Runtime v1 is hosted GREEN at 8cf29aff8d1be3ccf74c36bc8c837f0c666ca067 (run 31591039261, 12/12 jobs, private and public deterministic fake-host gates on Ubuntu, macOS, and Windows). Private Economic Agent v1 A+B is exact-head hosted green at fe75c38d898b71e3ed5c57411fb46d0dbd4fc34b in run 31611748969, including both Economic gates on Ubuntu, macOS, and Windows. Public Economic Agent v1 C is exact-head hosted green at 03f1f2736de23d03b298f265f93409de89a6be95 in run 31616168124 (12/12 jobs), including the private, process-termination, and public Economic gates on Ubuntu, macOS, and Windows. Totals remain 38 Partial/18 Missing. ## 0.1 — Executable semantic seed @@ -612,13 +609,42 @@ Exit criterion: implement a zero-copy parser and server without a tracing GC. - WIT import/export and WebAssembly Component output. - Portable canonical ABI plus native fast ABI. - Generated C headers and safe wrapper annotations. +- Bidirectional native Rust interoperability on compatible targets. Rust crates + must be able to call exported SEMAPRAX declarations through generated safe + wrappers, and SEMAPRAX packages must be able to import explicitly admitted + Rust declarations through generated adapters, without routing either + direction through Wasm. +- Freeze the Rust boundary as a versioned interface/ABI contract covering exact + symbol and type identity, layouts, ownership and borrowing, allocation and + deallocation ownership, panic/unwind containment, thread affinity, + capabilities/effects, error translation, and toolchain/target compatibility. + Start with direct scalar calls, then admit records, variants, borrowed + slices/strings, owned resources, callbacks, and async only as separately + evidenced shapes; unsupported shapes fail at build time rather than falling + back to an unchecked C ABI. +- Generate Cargo/link metadata and a narrow Rust crate facade for SEMAPRAX + exports, plus a quarantined native shim for admitted Rust imports. Require + round-trip conformance, ownership/failure hostility, sanitizer coverage, and + Ubuntu/macOS/Windows evidence in both call directions before describing a + shape as supported. Rust `unsafe` remains confined to reviewed boundary code + and never becomes ambient SEMAPRAX authority. +- The private scalar A+B precursor is locally green with direct-rustc + fixed-point authority, named pre-HIR/post-HIR capacity envelopes and exact + transfers, a pre-effect 12-use process arena, fixed-capacity tool-version + storage, prepared filesystem/publication authority, and fail-stop settlement. + Exact-head Ubuntu/macOS/Windows and Linux sanitizer evidence remain the + promotion gate; + compiler sysroot/dynamic-library descendant provenance remains a nonclaim. - Capability-sandboxed reproducible package builds. - Provenance, SBOM, license, and unsafe-code metadata. - Capability-sandboxed agent-tool components, a non-exporting secret-store interface, signed/versioned model catalogs, and canonical audit events. Network, home-directory, credential, and ambient tool access remain denied by default. -Exit criterion: compose SEMAPRAX, Rust, and JavaScript components behind one interface contract. +Exit criterion: compose SEMAPRAX, Rust, and JavaScript components behind one +interface contract, including hosted native Rust-to-SEMAPRAX and +SEMAPRAX-to-Rust calls with matching ownership, failure, and capability +semantics. ## 0.5 — Concurrency and applications diff --git a/src/agent_runtime.rs b/src/agent_runtime.rs index 0e8c025..dcc9d01 100644 --- a/src/agent_runtime.rs +++ b/src/agent_runtime.rs @@ -711,6 +711,24 @@ impl AgentRun { pub fn evidence_digest(&self) -> &str { &self.evidence_digest } + + pub(crate) fn economic_binding(&self) -> EconomicAgentBinding<'_> { + EconomicAgentBinding { + status: self.status, + final_message: self.replay.final_message(), + run_id: self.replay.run_id(), + evidence: &self.evidence, + evidence_digest: &self.evidence_digest, + } + } +} + +pub(crate) struct EconomicAgentBinding<'a> { + pub(crate) status: AgentRunStatus, + pub(crate) final_message: Option<&'a str>, + pub(crate) run_id: &'a str, + pub(crate) evidence: &'a str, + pub(crate) evidence_digest: &'a str, } type RunStatus = AgentRunStatus; @@ -1099,5 +1117,8 @@ fn render_effective_limits(limits: EffectiveLimits) -> String { // reviewable. mod private; +#[cfg(test)] +pub(crate) use private::completed_run_for_economic_test; + #[cfg(test)] mod tests; diff --git a/src/agent_runtime/private.rs b/src/agent_runtime/private.rs index 92ff9d3..3536b09 100644 --- a/src/agent_runtime/private.rs +++ b/src/agent_runtime/private.rs @@ -50,6 +50,10 @@ impl EvidenceReplay { pub(super) fn final_message(&self) -> Option<&str> { self.state.final_message.as_deref() } + + pub(super) fn run_id(&self) -> &str { + &self.state.run_id + } } struct Route { @@ -749,6 +753,121 @@ pub(super) fn new_agent(profile_source: &str, host: H) -> TestAgen TestAgent(Agent::new(profile_source, host, AgentCancellation::new()).unwrap()) } +#[cfg(test)] +pub(crate) fn completed_run_for_economic_test(message: &str) -> AgentRun { + struct Probe; + impl AgentBoundaryProbe for Probe { + fn policy_epoch(&self) -> u64 { + 1 + } + fn elapsed_ms(&self) -> u64 { + 0 + } + } + struct Host { + response: Vec, + } + impl AgentHost for Host { + fn policy_epoch(&self) -> u64 { + 1 + } + fn elapsed_ms(&self) -> u64 { + 0 + } + fn boundary_probe(&self) -> Box { + Box::new(Probe) + } + fn tokenize(&mut self, _: &str, request: &str) -> Option { + Some(request.len() as u64) + } + fn attempt_provider( + &mut self, + _: &str, + _: &str, + request: &str, + _: u64, + sink: &mut AgentProviderSink, + ) -> AgentProviderAttempt { + assert!(sink.push(&self.response)); + AgentProviderAttempt::new( + AgentProviderDisposition::Succeeded, + AgentProviderUsage::new(request.len() as u64, self.response.len() as u64, 0), + ) + } + fn invoke_tool(&mut self, _: &str, _: &str, _: &str, _: &mut AgentToolResultSink) -> bool { + false + } + } + let profile = Profile { + agent_id: "economic.fixture.agent".to_owned(), + models: vec![Model { + provider_id: "fixture.local".to_owned(), + model_id: "fixture-economic".to_owned(), + locality: Locality::Local, + quality_tier: QualityTier::Basic, + tokenizer_id: "fixture.bytes-v1".to_owned(), + max_context_tokens: 1_048_576, + input_price: 0, + output_price: 0, + capabilities: vec!["text".to_owned()], + }], + tools: vec![], + policy: Policy { + allowed_provider_ids: vec!["fixture.local".to_owned()], + allowed_model_ids: vec!["fixture-economic".to_owned()], + required_locality: RequiredLocality::LocalOnly, + minimum_quality_tier: QualityTier::Basic, + required_model_capabilities: vec!["text".to_owned()], + granted_capabilities: vec![], + allowed_tool_ids: vec![], + }, + limits: EffectiveLimits { + max_turns: 1, + max_provider_attempts: 1, + max_retries_per_turn: 0, + max_concurrency: 1, + max_elapsed_ms: 10_000, + max_provider_request_bytes: 2_097_152, + max_provider_response_bytes: 1_048_576, + max_stream_chunks: 4, + max_total_provider_input_bytes: 2_097_152, + max_total_provider_output_bytes: 1_048_576, + max_reported_model_input_tokens: 2_097_152, + max_reported_model_output_tokens: 262_144, + max_usd_microunits: 0, + max_tool_calls: 0, + max_tool_arguments_bytes: 1, + max_tool_result_bytes: 1, + max_total_tool_bytes: 1, + max_retained_state_bytes: 2_097_152, + max_trace_events: 32, + max_trace_bytes: 262_144, + max_evidence_bytes: 2_097_152, + max_builder_bytes: 67_108_864, + }, + source: String::new(), + digest: String::new(), + }; + let profile_source = render_profile(&profile); + let task = Task { + nonce: "0".repeat(64), + objective: "Return the exact economic proposal.".to_owned(), + context: vec![], + source: String::new(), + digest: String::new(), + }; + let task_source = render_task(&task); + let response = format!( + "{{\"schema\":\"{ACTION_SCHEMA}\",\"kind\":\"final\",\"message\":{}}}\n", + quote_json(message) + ) + .into_bytes(); + Agent::new(&profile_source, Host { response }, AgentCancellation::new()) + .unwrap() + .run(&task_source) + .unwrap() +} + impl Agent { /// Parses and owns one canonical Agent Runtime Profile before observing the host. pub fn new( diff --git a/src/aggregate_layout.rs b/src/aggregate_layout.rs index 4a9a103..f780cff 100644 --- a/src/aggregate_layout.rs +++ b/src/aggregate_layout.rs @@ -214,6 +214,7 @@ fn layout_type( visiting: &mut BTreeSet, ) -> Result { match ty { + ResolvedType::Unit => Err(layout_error("unit has no aggregate value layout")), ResolvedType::I64 | ResolvedType::Bool => { let (size, align) = scalar_size_align(target, ty)?; scalar_layout(target, ty, size, align) @@ -446,6 +447,11 @@ fn collect_expr_record_types( collect_expr_record_types(program, argument, instances)?; } } + ResolvedExprKind::NativeRustImportCall(call) => { + for argument in &call.args { + collect_expr_record_types(program, argument, instances)?; + } + } ResolvedExprKind::Unary { value, .. } | ResolvedExprKind::Project { base: value, .. } => { collect_expr_record_types(program, value, instances)?; } diff --git a/src/ast.rs b/src/ast.rs index ae3d397..18d06e9 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -28,24 +28,36 @@ pub enum Type { impl fmt::Display for Type { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Type::I64 => write!(f, "i64"), - Type::Bool => write!(f, "bool"), - Type::Named { name, arguments } => { - write!(f, "{name}")?; - if !arguments.is_empty() { - write!(f, "<")?; - for (index, argument) in arguments.iter().enumerate() { + enum Frame<'a> { + Type(&'a Type), + Arguments(&'a [Type], usize), + } + let mut frames = vec![Frame::Type(self)]; + while let Some(frame) = frames.pop() { + match frame { + Frame::Type(Type::I64) => f.write_str("i64")?, + Frame::Type(Type::Bool) => f.write_str("bool")?, + Frame::Type(Type::Named { name, arguments }) => { + f.write_str(name)?; + if !arguments.is_empty() { + f.write_str("<")?; + frames.push(Frame::Arguments(arguments, 0)); + } + } + Frame::Arguments(arguments, index) => { + if let Some(argument) = arguments.get(index) { if index != 0 { - write!(f, ", ")?; + f.write_str(", ")?; } - write!(f, "{argument}")?; + frames.push(Frame::Arguments(arguments, index + 1)); + frames.push(Frame::Type(argument)); + } else { + f.write_str(">")?; } - write!(f, ">")?; } - Ok(()) } } + Ok(()) } } @@ -180,7 +192,9 @@ pub struct ImportDeclaration { pub explicit_id: bool, pub name: String, pub name_span: Span, + pub native_rust: bool, pub params: Vec, + pub result: ImportResult, pub effects: Vec, pub failure: ImportFailure, pub consumes: String, @@ -188,6 +202,23 @@ pub struct ImportDeclaration { pub span: Span, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ImportResult { + Unit, + I64, + Bool, +} + +impl fmt::Display for ImportResult { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Unit => "unit", + Self::I64 => "i64", + Self::Bool => "bool", + }) + } +} + #[derive(Clone, Debug, Eq, PartialEq)] pub enum ImportFailure { Infallible, @@ -449,119 +480,321 @@ impl BinaryOp { } impl Expr { - pub fn visit_calls(&self, visit: &mut impl FnMut(&str, Span)) { - match &self.kind { - ExprKind::Call { name, args, .. } => { - visit(name, self.span); - for arg in args { - arg.visit_calls(visit); - } - } - ExprKind::Unary { value, .. } => value.visit_calls(visit), - ExprKind::Binary { left, right, .. } => { - left.visit_calls(visit); - right.visit_calls(visit); + fn visit_call_nodes(&self, mut visit: impl FnMut(&Expr)) { + const FIXED_DEPTH: usize = 513; + let mut stack = [None; FIXED_DEPTH]; + stack[0] = Some((self, 0usize)); + let mut len = 1usize; + let mut overflow = Vec::new(); + loop { + let state = if let Some(state) = overflow.pop() { + state + } else if len != 0 { + len -= 1; + stack[len].take().expect("call visitor frame retained") + } else { + break; + }; + let (expression, next_child) = state; + if next_child == 0 { + visit(expression); } - ExprKind::Block { statements, tail } => { - for statement in statements { - match statement { - Statement::Let { value, .. } => value.visit_calls(visit), + if let Some(child) = expression.child(next_child) { + let parent = (expression, next_child + 1); + if overflow.is_empty() && len + 2 <= stack.len() { + stack[len] = Some(parent); + stack[len + 1] = Some((child, 0)); + len += 2; + } else { + if overflow.is_empty() { + overflow.extend(stack[..len].iter_mut().filter_map(Option::take)); + len = 0; } - } - tail.visit_calls(visit); - } - ExprKind::If { - condition, - then_branch, - else_branch, - } => { - condition.visit_calls(visit); - then_branch.visit_calls(visit); - else_branch.visit_calls(visit); - } - ExprKind::ConstructRecord { fields, .. } => { - for field in fields { - field.value.visit_calls(visit); - } - } - ExprKind::ConstructVariant { fields, .. } => { - for field in fields { - field.value.visit_calls(visit); - } - } - ExprKind::Match { scrutinee, arms } => { - scrutinee.visit_calls(visit); - for arm in arms { - arm.value.visit_calls(visit); + overflow.push(parent); + overflow.push((child, 0)); } } - ExprKind::Try { operand } => operand.visit_calls(visit), - ExprKind::UpdateRecord { base, fields } => { - base.visit_calls(visit); - for field in fields { - field.value.visit_calls(visit); - } - } - ExprKind::Project { base, .. } => base.visit_calls(visit), - ExprKind::Int(_) | ExprKind::Bool(_) | ExprKind::Var(_) => {} } } - pub fn visit_call_instances(&self, visit: &mut impl FnMut(&str, &[Type], Span)) { + fn child(&self, index: usize) -> Option<&Expr> { match &self.kind { - ExprKind::Call { - name, - type_arguments, - args, - } => { - visit(name, type_arguments, self.span); - for arg in args { - arg.visit_call_instances(visit); - } - } - ExprKind::Unary { value, .. } => value.visit_call_instances(visit), + ExprKind::Call { args, .. } => args.get(index), + ExprKind::Unary { value, .. } + | ExprKind::Try { operand: value } + | ExprKind::Project { base: value, .. } => (index == 0).then_some(value), ExprKind::Binary { left, right, .. } => { - left.visit_call_instances(visit); - right.visit_call_instances(visit); - } - ExprKind::Block { statements, tail } => { - for statement in statements { - match statement { - Statement::Let { value, .. } => value.visit_call_instances(visit), - } - } - tail.visit_call_instances(visit); + [left.as_ref(), right.as_ref()].get(index).copied() } + ExprKind::Block { statements, tail } => statements + .get(index) + .map(|statement| match statement { + Statement::Let { value, .. } => value, + }) + .or_else(|| (index == statements.len()).then_some(tail)), ExprKind::If { condition, then_branch, else_branch, - } => { - condition.visit_call_instances(visit); - then_branch.visit_call_instances(visit); - else_branch.visit_call_instances(visit); - } + } => [ + condition.as_ref(), + then_branch.as_ref(), + else_branch.as_ref(), + ] + .get(index) + .copied(), ExprKind::ConstructRecord { fields, .. } | ExprKind::ConstructVariant { fields, .. } => { - for field in fields { - field.value.visit_call_instances(visit); - } + fields.get(index).map(|field| &field.value) } - ExprKind::Match { scrutinee, arms } => { - scrutinee.visit_call_instances(visit); - for arm in arms { - arm.value.visit_call_instances(visit); - } + ExprKind::Match { scrutinee, arms } => (index == 0) + .then_some(scrutinee.as_ref()) + .or_else(|| arms.get(index - 1).map(|arm| &arm.value)), + ExprKind::UpdateRecord { base, fields } => (index == 0) + .then_some(base.as_ref()) + .or_else(|| fields.get(index - 1).map(|field| &field.value)), + ExprKind::Int(_) | ExprKind::Bool(_) | ExprKind::Var(_) => None, + } + } + + pub fn visit_calls(&self, visit: &mut impl FnMut(&str, Span)) { + self.visit_call_nodes(|expression| { + if let ExprKind::Call { name, .. } = &expression.kind { + visit(name, expression.span); } - ExprKind::Try { operand } => operand.visit_call_instances(visit), - ExprKind::UpdateRecord { base, fields } => { - base.visit_call_instances(visit); - for field in fields { - field.value.visit_call_instances(visit); - } + }); + } + + pub fn visit_call_instances(&self, visit: &mut impl FnMut(&str, &[Type], Span)) { + self.visit_call_nodes(|expression| { + if let ExprKind::Call { + name, + type_arguments, + .. + } = &expression.kind + { + visit(name, type_arguments, expression.span); } - ExprKind::Project { base, .. } => base.visit_call_instances(visit), - ExprKind::Int(_) | ExprKind::Bool(_) | ExprKind::Var(_) => {} + }); + } +} + +#[cfg(test)] +mod call_visitor_tests { + use super::*; + + fn call(name: &str, marker: usize) -> Expr { + Expr { + span: Span { + start: marker, + end: marker + 1, + line: 1, + column: marker + 1, + }, + kind: ExprKind::Call { + name: name.to_owned(), + type_arguments: vec![Type::Named { + name: format!("T{marker}"), + arguments: Vec::new(), + }], + args: Vec::new(), + }, } } + + #[test] + fn iterative_call_visitors_preserve_preorder_and_authored_child_order() { + let span = Span::default(); + let expression = Expr { + span, + kind: ExprKind::Call { + name: "outer".to_owned(), + type_arguments: vec![Type::Named { + name: "T0".to_owned(), + arguments: Vec::new(), + }], + args: vec![ + Expr { + span, + kind: ExprKind::Block { + statements: vec![Statement::Let { + name: "value".to_owned(), + name_span: span, + value: call("first", 1), + span, + }], + tail: Box::new(Expr { + span, + kind: ExprKind::If { + condition: Box::new(call("second", 2)), + then_branch: Box::new(call("third", 3)), + else_branch: Box::new(call("fourth", 4)), + }, + }), + }, + }, + Expr { + span, + kind: ExprKind::ConstructRecord { + type_name: "Pair".to_owned(), + type_span: span, + type_arguments: Vec::new(), + fields: vec![ + FieldInitializer { + name: "left".to_owned(), + name_span: span, + value: call("fifth", 5), + span, + }, + FieldInitializer { + name: "right".to_owned(), + name_span: span, + value: call("sixth", 6), + span, + }, + ], + }, + }, + Expr { + span, + kind: ExprKind::Match { + scrutinee: Box::new(call("seventh", 7)), + arms: vec![ + MatchArm { + pattern: MatchPattern::Wildcard { span }, + value: call("eighth", 8), + span, + }, + MatchArm { + pattern: MatchPattern::Wildcard { span }, + value: call("ninth", 9), + span, + }, + ], + }, + }, + Expr { + span, + kind: ExprKind::UpdateRecord { + base: Box::new(call("tenth", 10)), + fields: vec![ + FieldInitializer { + name: "left".to_owned(), + name_span: span, + value: call("eleventh", 11), + span, + }, + FieldInitializer { + name: "right".to_owned(), + name_span: span, + value: call("twelfth", 12), + span, + }, + ], + }, + }, + Expr { + span, + kind: ExprKind::Try { + operand: Box::new(call("thirteenth", 13)), + }, + }, + Expr { + span, + kind: ExprKind::Project { + base: Box::new(call("fourteenth", 14)), + field: "value".to_owned(), + field_span: span, + }, + }, + Expr { + span, + kind: ExprKind::ConstructVariant { + type_name: "Choice".to_owned(), + type_span: span, + type_arguments: Vec::new(), + case_name: "Value".to_owned(), + case_span: span, + fields: vec![FieldInitializer { + name: "value".to_owned(), + name_span: span, + value: call("fifteenth", 15), + span, + }], + }, + }, + Expr { + span, + kind: ExprKind::Binary { + op: BinaryOp::Add, + left: Box::new(call("sixteenth", 16)), + right: Box::new(call("seventeenth", 17)), + }, + }, + ], + }, + }; + + let expected_names = [ + "outer", + "first", + "second", + "third", + "fourth", + "fifth", + "sixth", + "seventh", + "eighth", + "ninth", + "tenth", + "eleventh", + "twelfth", + "thirteenth", + "fourteenth", + "fifteenth", + "sixteenth", + "seventeenth", + ]; + let mut calls = Vec::new(); + expression.visit_calls(&mut |name, span| calls.push((name.to_owned(), span.start))); + assert_eq!( + calls + .iter() + .map(|(name, _)| name.as_str()) + .collect::>(), + expected_names + ); + assert_eq!( + calls.iter().map(|(_, marker)| *marker).collect::>(), + (0..=17).collect::>() + ); + + let mut instances = Vec::new(); + expression.visit_call_instances(&mut |name, arguments, span| { + instances.push((name.to_owned(), arguments[0].to_string(), span.start)); + }); + assert_eq!( + instances + .iter() + .map(|(name, _, _)| name.as_str()) + .collect::>(), + expected_names + ); + assert_eq!( + instances + .iter() + .map(|(_, ty, _)| ty.as_str()) + .collect::>(), + (0..=17) + .map(|marker| format!("T{marker}")) + .collect::>() + ); + assert_eq!( + instances + .iter() + .map(|(_, _, marker)| *marker) + .collect::>(), + (0..=17).collect::>() + ); + } } diff --git a/src/bounded_output.rs b/src/bounded_output.rs index 19a7ee4..216cd7e 100644 --- a/src/bounded_output.rs +++ b/src/bounded_output.rs @@ -4,7 +4,9 @@ use std::ops::Deref; use std::rc::Rc; struct Budget { + initial: usize, remaining: Cell, + floor: Cell, overflowed: Cell, } @@ -42,7 +44,9 @@ pub(crate) fn with_limit_usage(limit: usize, operation: impl FnOnce() -> T) - .as_ref() .map_or(limit, |budget| limit.min(budget.remaining.get())); let budget = Rc::new(Budget { + initial: effective_limit, remaining: Cell::new(effective_limit), + floor: Cell::new(0), overflowed: Cell::new(false), }); let previous = ACTIVE.with(|active| active.replace(Some(Rc::clone(&budget)))); @@ -71,6 +75,12 @@ fn reserve(budget: Option<&Budget>, length: usize) -> bool { budget.overflowed.set(true); return false; } + if length + .checked_add(budget.floor.get()) + .is_none_or(|required| required > remaining) + { + return false; + } budget.remaining.set(remaining - length); true } @@ -80,10 +90,46 @@ pub(crate) fn reserve_active(length: usize) -> bool { reserve(budget.as_deref(), length) } +pub(crate) fn reserve_active_preserving(length: usize, floor: usize) -> bool { + let budget = active(); + let Some(budget) = budget.as_deref() else { + return true; + }; + let floor = floor.max(budget.floor.get()); + let Some(required) = length.checked_add(floor) else { + return false; + }; + if required > budget.remaining.get() { + return false; + } + reserve(Some(budget), length) +} + +pub(crate) fn set_active_floor(floor: usize) -> bool { + let Some(budget) = active() else { + return true; + }; + if floor > budget.remaining.get() { + return false; + } + budget.floor.set(floor); + true +} + +pub(crate) fn clear_active_floor() { + if let Some(budget) = active() { + budget.floor.set(0); + } +} + pub(crate) fn active_remaining() -> Option { active().map(|budget| budget.remaining.get()) } +pub(crate) fn active_limit() -> Option { + active().map(|budget| budget.initial) +} + fn reserve_sink(captured: Option<&Rc>, length: usize) -> bool { let current = active(); reserve( @@ -179,6 +225,11 @@ impl CappedString { } } + #[cfg(test)] + pub(crate) fn allocated_capacity(&self) -> usize { + self.bytes.capacity() + } + pub(crate) fn push_str(&mut self, value: &str) { if reserve_sink(self.budget.as_ref(), value.len()) { self.bytes.push_str(value); diff --git a/src/call_index.rs b/src/call_index.rs index d111c34..7accc77 100644 --- a/src/call_index.rs +++ b/src/call_index.rs @@ -6,6 +6,23 @@ use crate::hir::{ ResolvedExprKind, ResolvedProgram, ResolvedStatement, ResolvedType, }; +#[cfg(test)] +thread_local! { + static CAPACITY_HIGH_WATER: std::cell::Cell = const { std::cell::Cell::new(0) }; +} +#[cfg(test)] +fn reset_capacity_high_water() { + CAPACITY_HIGH_WATER.with(|water| water.set(0)); +} +#[cfg(test)] +fn capacity_high_water() -> usize { + CAPACITY_HIGH_WATER.with(std::cell::Cell::get) +} +#[cfg(test)] +fn note_capacity_high_water(bytes: usize) { + CAPACITY_HIGH_WATER.with(|water| water.set(water.get().max(bytes))); +} + #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] pub(crate) enum PersistentCallableKind { Function, @@ -190,88 +207,128 @@ impl PersistentCallIndex { region: CallRegion, expression: &ResolvedExpr, ) -> Result<(), Diagnostic> { - if let ResolvedExprKind::Call { - callee, - type_arguments, - instance, - .. - } = &expression.kind - { - let site = PersistentCallSite { - expression: expression.id.clone(), - owner: owner.clone(), - owner_kind, - owner_origin, - region, - callee: callee.clone(), - type_arguments: type_arguments.clone(), - instance: instance.clone(), - }; - if self - .sites_by_expression - .insert(expression.id.as_str().to_owned(), site) - .is_some() - { - return Err(call_index_error(format!( - "call expression `{}` has multiple source owners", - expression.id - ))); - } - self.calls_by_owner - .get_mut(owner) - .expect("registered owner remains indexed") - .insert(callee.clone()); + enum Frame<'a> { + Enter(&'a ResolvedExpr), + Children(&'a ResolvedExpr, usize), } - - match &expression.kind { - ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) | ResolvedExprKind::Place(_) => {} - ResolvedExprKind::Call { args, .. } => { - for argument in args { - self.visit_expr(owner, owner_kind, owner_origin, region, argument)?; + const { assert!(std::mem::size_of::>() == 16) }; + fn child(expression: &ResolvedExpr, index: usize) -> Option<&ResolvedExpr> { + match &expression.kind { + ResolvedExprKind::Call { args, .. } => args.get(index), + ResolvedExprKind::NativeRustImportCall(call) => call.args.get(index), + ResolvedExprKind::Unary { value, .. } + | ResolvedExprKind::Project { base: value, .. } + | ResolvedExprKind::Try { operand: value, .. } + | ResolvedExprKind::TryOption { operand: value, .. } => { + (index == 0).then_some(value) } - } - ResolvedExprKind::Unary { value, .. } - | ResolvedExprKind::Project { base: value, .. } - | ResolvedExprKind::Try { operand: value, .. } - | ResolvedExprKind::TryOption { operand: value, .. } => { - self.visit_expr(owner, owner_kind, owner_origin, region, value)?; - } - ResolvedExprKind::Binary { left, right, .. } => { - self.visit_expr(owner, owner_kind, owner_origin, region, left)?; - self.visit_expr(owner, owner_kind, owner_origin, region, right)?; - } - ResolvedExprKind::Block { statements, tail } => { - for statement in statements { - let ResolvedStatement::Let { value, .. } = statement; - self.visit_expr(owner, owner_kind, owner_origin, region, value)?; + ResolvedExprKind::Binary { left, right, .. } => { + [left.as_ref(), right.as_ref()].get(index).copied() } - self.visit_expr(owner, owner_kind, owner_origin, region, tail)?; - } - ResolvedExprKind::If { - condition, - then_branch, - else_branch, - } => { - self.visit_expr(owner, owner_kind, owner_origin, region, condition)?; - self.visit_expr(owner, owner_kind, owner_origin, region, then_branch)?; - self.visit_expr(owner, owner_kind, owner_origin, region, else_branch)?; - } - ResolvedExprKind::ConstructRecord { fields, .. } - | ResolvedExprKind::ConstructVariant { fields, .. } => { - for initializer in fields { - self.visit_expr(owner, owner_kind, owner_origin, region, &initializer.value)?; + ResolvedExprKind::Block { statements, tail } => statements + .get(index) + .map(|statement| { + let ResolvedStatement::Let { value, .. } = statement; + value + }) + .or_else(|| (index == statements.len()).then_some(tail)), + ResolvedExprKind::If { + condition, + then_branch, + else_branch, + } => [ + condition.as_ref(), + then_branch.as_ref(), + else_branch.as_ref(), + ] + .get(index) + .copied(), + ResolvedExprKind::ConstructRecord { fields, .. } + | ResolvedExprKind::ConstructVariant { fields, .. } => { + fields.get(index).map(|field| &field.value) } - } - ResolvedExprKind::Match { scrutinee, arms } => { - self.visit_expr(owner, owner_kind, owner_origin, region, scrutinee)?; - for arm in arms { - self.visit_expr(owner, owner_kind, owner_origin, region, &arm.value)?; + ResolvedExprKind::Match { scrutinee, arms } => { + if index == 0 { + Some(scrutinee) + } else { + arms.get(index - 1).map(|arm| &arm.value) + } + } + ResolvedExprKind::UpdateRecord { base, fields, .. } => { + if index == 0 { + Some(base) + } else { + fields.get(index - 1).map(|field| &field.value) + } } + ResolvedExprKind::Int(_) + | ResolvedExprKind::Bool(_) + | ResolvedExprKind::Place(_) => None, } - ResolvedExprKind::UpdateRecord { base, fields, .. } => { - self.visit_expr(owner, owner_kind, owner_origin, region, base)?; - for initializer in fields { - self.visit_expr(owner, owner_kind, owner_origin, region, &initializer.value)?; + } + + let mut frames = vec![Frame::Enter(expression)]; + while let Some(frame) = frames.pop() { + #[cfg(test)] + note_capacity_high_water( + frames.capacity() * std::mem::size_of::>() + + self + .sites_by_expression + .iter() + .map(|(key, site)| { + std::mem::size_of::<(String, PersistentCallSite)>() + + key.capacity() + + site.type_arguments.capacity() + * std::mem::size_of::() + }) + .sum::() + + self + .calls_by_owner + .values() + .map(|values| values.len() * std::mem::size_of::()) + .sum::(), + ); + match frame { + Frame::Enter(expression) => { + if let ResolvedExprKind::Call { + callee, + type_arguments, + instance, + .. + } = &expression.kind + { + let site = PersistentCallSite { + expression: expression.id.clone(), + owner: owner.clone(), + owner_kind, + owner_origin, + region, + callee: callee.clone(), + type_arguments: type_arguments.clone(), + instance: instance.clone(), + }; + if self + .sites_by_expression + .insert(expression.id.as_str().to_owned(), site) + .is_some() + { + return Err(call_index_error(format!( + "call expression `{}` has multiple source owners", + expression.id + ))); + } + self.calls_by_owner + .get_mut(owner) + .expect("registered owner remains indexed") + .insert(callee.clone()); + } + frames.push(Frame::Children(expression, 0)); + } + Frame::Children(expression, index) => { + if let Some(next) = child(expression, index) { + frames.push(Frame::Children(expression, index + 1)); + frames.push(Frame::Enter(next)); + } } } } @@ -295,7 +352,9 @@ mod tests { "#; let program = crate::parse(source, std::path::Path::new("call-index.spx")).unwrap(); let resolved = hir::resolve(&program).unwrap(); + reset_capacity_high_water(); let index = PersistentCallIndex::build(&resolved).unwrap(); + assert!(capacity_high_water() > 0); let expression = index.sites_by_expression.keys().next().unwrap().clone(); assert_eq!( diff --git a/src/cleanup.rs b/src/cleanup.rs index c582a17..8869d12 100644 --- a/src/cleanup.rs +++ b/src/cleanup.rs @@ -8,10 +8,101 @@ use crate::diagnostic::Diagnostic; use crate::hir::{ - DeclarationId, ExpressionId, OwnershipMode, ResolvedExpr, ResolvedExprKind, ResolvedFunction, - ResolvedProgram, ResolvedStatement, ResolvedType, ResolvedTypeDeclarationKind, ValueId, + DeclarationId, ExpressionId, OwnershipMode, ResolvedBinding, ResolvedExpr, ResolvedExprKind, + ResolvedFunction, ResolvedProgram, ResolvedStatement, ResolvedType, + ResolvedTypeDeclarationKind, ValueId, }; +#[cfg(test)] +thread_local! { + static INVENTORY_CAPACITY_HIGH_WATER: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +#[cfg(test)] +pub(crate) fn reset_capacity_high_water() { + INVENTORY_CAPACITY_HIGH_WATER.with(|water| water.set(0)); +} + +#[cfg(test)] +pub(crate) fn capacity_high_water() -> usize { + INVENTORY_CAPACITY_HIGH_WATER.with(std::cell::Cell::get) +} + +#[cfg(test)] +fn note_capacity_high_water(bytes: usize) { + INVENTORY_CAPACITY_HIGH_WATER.with(|water| water.set(water.get().max(bytes))); +} + +#[cfg(test)] +fn resolved_type_owned_capacity(ty: &ResolvedType) -> usize { + match ty { + ResolvedType::Unit | ResolvedType::I64 | ResolvedType::Bool => 0, + ResolvedType::TypeParameter { owner, .. } => owner.as_str().len(), + ResolvedType::Nominal { + declaration, + arguments, + } => { + declaration.as_str().len() + + arguments.capacity() * std::mem::size_of::() + + arguments + .iter() + .map(resolved_type_owned_capacity) + .sum::() + } + } +} + +#[cfg(test)] +fn shape_owned_capacity(shape: &FieldLivenessShape) -> usize { + match shape { + FieldLivenessShape::NoDrop => 0, + FieldLivenessShape::Leaf { lifecycle, .. } => lifecycle.as_str().len(), + FieldLivenessShape::Record { + declaration, + fields, + } => { + declaration.as_str().len() + + fields.capacity() * std::mem::size_of::() + + fields + .iter() + .map(|field| field.field.as_str().len() + shape_owned_capacity(&field.shape)) + .sum::() + } + } +} + +#[cfg(test)] +fn slot_owned_capacity(slot: &CleanupStorageSlot) -> usize { + let origin = match &slot.origin { + CleanupStorageOrigin::Parameter { value, .. } + | CleanupStorageOrigin::Binding { value } + | CleanupStorageOrigin::ProvisionalResult { value } => value.as_str().len(), + CleanupStorageOrigin::Temporary { expression } => expression.as_str().len(), + }; + origin + resolved_type_owned_capacity(&slot.ty) + shape_owned_capacity(&slot.shape) +} + +#[cfg(test)] +fn flag_owned_capacity(flag: &CleanupFlag) -> usize { + flag.lifecycle.as_str().len() + + flag.place.projections.capacity() * std::mem::size_of::() + + flag + .place + .projections + .iter() + .map(|id| id.as_str().len()) + .sum::() +} + +#[cfg(test)] +fn inventory_builder_live_capacity(builder: &InventoryBuilder<'_>) -> usize { + builder.slots.capacity() * std::mem::size_of::() + + builder.slots.iter().map(slot_owned_capacity).sum::() + + builder.flags.capacity() * std::mem::size_of::() + + builder.flags.iter().map(flag_owned_capacity).sum::() + + builder.live_owned_parameters.capacity() * std::mem::size_of::() +} + pub const CLEANUP_INVENTORY_SCHEMA_V1: &str = "semaprax.cleanup-inventory.v1"; #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] @@ -149,6 +240,9 @@ pub(crate) fn build_inventory( builder.collect_expression(expression)?; } + #[cfg(test)] + note_capacity_high_water(inventory_builder_live_capacity(&builder)); + Ok(CleanupInventory { schema: CLEANUP_INVENTORY_SCHEMA_V1, entry_state: CleanupEntryState { @@ -207,7 +301,7 @@ impl InventoryBuilder<'_> { let index = u32::try_from(self.slots.len()) .map_err(|_| cleanup_error("too many cleanup storage candidates"))?; let storage = CleanupStorageId(index); - let shape = self.shape_for_type(&ty, storage, &mut Vec::new())?; + let shape = self.shape_for_type(&ty, storage)?; self.slots.push(CleanupStorageSlot { id: storage, discovery_index: index, @@ -222,158 +316,338 @@ impl InventoryBuilder<'_> { &mut self, ty: &ResolvedType, storage: CleanupStorageId, - projections: &mut Vec, ) -> Result { - if !self.needs_drop(ty)? { - return Ok(FieldLivenessShape::NoDrop); + enum Frame<'a> { + Enter(&'a ResolvedType), + Children( + &'a DeclarationId, + &'a [crate::hir::ResolvedFieldDeclaration], + usize, + ), + FinishField(&'a crate::hir::ResolvedFieldDeclaration), + FinishRecord(&'a DeclarationId, usize), } - let ResolvedType::Nominal { - declaration, - arguments, - } = ty - else { - return Err(cleanup_error(format!( - "droppable type `{}` is not nominal", - ty.identity_key() - ))); - }; - if !arguments.is_empty() { - return Err(cleanup_error(format!( - "droppable type `{}` has unsupported generic arguments", - ty.identity_key() - ))); - } - let declaration_item = self - .program - .types - .iter() - .find(|item| item.id == *declaration) - .ok_or_else(|| cleanup_error(format!("unknown cleanup type `{declaration}`")))?; - match &declaration_item.kind { - ResolvedTypeDeclarationKind::Resource { drop } => { - let flag_index = u32::try_from(self.flags.len()) - .map_err(|_| cleanup_error("too many cleanup liveness flags"))?; - let flag = LivenessFlagId(flag_index); - self.flags.push(CleanupFlag { - id: flag, - place: CleanupPlace { - storage, - projections: projections.clone(), - }, - lifecycle: drop.id.clone(), - }); - Ok(FieldLivenessShape::Leaf { - flag, - lifecycle: drop.id.clone(), - }) - } - ResolvedTypeDeclarationKind::Record { fields } => { - let mut shapes = Vec::with_capacity(fields.len()); - for field in fields { + + const { assert!(std::mem::size_of::>() == 40) }; + + let mut frames = vec![Frame::Enter(ty)]; + let mut projections = Vec::::new(); + let mut shapes = Vec::new(); + while let Some(frame) = frames.pop() { + #[cfg(test)] + note_capacity_high_water( + frames.capacity() * std::mem::size_of::>() + + projections.capacity() * std::mem::size_of::() + + projections + .iter() + .map(|id| id.as_str().len()) + .sum::() + + shapes.capacity() * std::mem::size_of::() + + self.slots.capacity() * std::mem::size_of::() + + self.flags.capacity() * std::mem::size_of::() + + self.live_owned_parameters.capacity() + * std::mem::size_of::() + + self.slots.iter().map(slot_owned_capacity).sum::() + + self.flags.iter().map(flag_owned_capacity).sum::() + + shapes.iter().map(shape_owned_capacity).sum::(), + ); + match frame { + Frame::Enter(ty) => { + if !self.needs_drop(ty)? { + shapes.push(FieldLivenessShape::NoDrop); + continue; + } + let ResolvedType::Nominal { + declaration, + arguments, + } = ty + else { + return Err(cleanup_error(format!( + "droppable type `{}` is not nominal", + ty.identity_key() + ))); + }; + if !arguments.is_empty() { + return Err(cleanup_error(format!( + "droppable type `{}` has unsupported generic arguments", + ty.identity_key() + ))); + } + let declaration_item = self + .program + .types + .iter() + .find(|item| item.id == *declaration) + .ok_or_else(|| { + cleanup_error(format!("unknown cleanup type `{declaration}`")) + })?; + match &declaration_item.kind { + ResolvedTypeDeclarationKind::Resource { drop } => { + let flag_index = u32::try_from(self.flags.len()) + .map_err(|_| cleanup_error("too many cleanup liveness flags"))?; + let flag = LivenessFlagId(flag_index); + self.flags.push(CleanupFlag { + id: flag, + place: CleanupPlace { + storage, + projections: projections.clone(), + }, + lifecycle: drop.id.clone(), + }); + shapes.push(FieldLivenessShape::Leaf { + flag, + lifecycle: drop.id.clone(), + }); + } + ResolvedTypeDeclarationKind::Record { fields } => { + frames.try_reserve(2).map_err(|_| { + cleanup_error("cleanup shape capacity exceeds address space") + })?; + frames.push(Frame::FinishRecord(declaration, fields.len())); + frames.push(Frame::Children(declaration, fields, 0)); + } + ResolvedTypeDeclarationKind::Variant { .. } => { + return Err(cleanup_error( + "droppable variant cleanup is outside the copy-only v1 slice", + )); + } + } + } + Frame::Children(declaration, fields, index) => { + let Some(field) = fields.get(index) else { + continue; + }; + frames.try_reserve(3).map_err(|_| { + cleanup_error("cleanup shape capacity exceeds address space") + })?; + frames.push(Frame::Children(declaration, fields, index + 1)); + frames.push(Frame::FinishField(field)); projections.push(field.id.clone()); - let shape = self.shape_for_type(&field.ty, storage, projections)?; - projections.pop(); - shapes.push(FieldLiveness { - field: field.id.clone(), - field_index: field.index, - shape, + frames.push(Frame::Enter(&field.ty)); + } + Frame::FinishField(field) => { + let shape = shapes + .pop() + .ok_or_else(|| cleanup_error("cleanup field shape is absent"))?; + if projections.pop().as_ref() != Some(&field.id) { + return Err(cleanup_error("cleanup projection stack is inconsistent")); + } + shapes.push(FieldLivenessShape::Record { + declaration: field.id.clone(), + fields: vec![FieldLiveness { + field: field.id.clone(), + field_index: field.index, + shape, + }], + }); + } + Frame::FinishRecord(declaration, field_count) => { + let split = shapes + .len() + .checked_sub(field_count) + .ok_or_else(|| cleanup_error("cleanup record shapes are incomplete"))?; + let mut fields = Vec::with_capacity(field_count); + if fields.capacity() != field_count { + return Err(cleanup_error("cleanup record field capacity is not exact")); + } + for shape in shapes.drain(split..) { + let field = match shape { + FieldLivenessShape::Record { + declaration: field, + mut fields, + } if fields.len() == 1 && fields[0].field == field => fields.remove(0), + _ => unreachable!("field wrapper is internal to shape construction"), + }; + if fields.len() == fields.capacity() { + return Err(cleanup_error( + "cleanup record field capacity was exhausted", + )); + } + fields.push(field); + } + if fields.len() != field_count || fields.capacity() != field_count { + return Err(cleanup_error( + "cleanup record field capacity disagrees with its shape", + )); + } + #[cfg(test)] + note_capacity_high_water( + frames.capacity() * std::mem::size_of::>() + + projections.capacity() * std::mem::size_of::() + + projections + .iter() + .map(|id| id.as_str().len()) + .sum::() + + shapes.capacity() * std::mem::size_of::() + + shapes.iter().map(shape_owned_capacity).sum::() + + fields.capacity() * std::mem::size_of::() + + fields + .iter() + .map(|field| { + field.field.as_str().len() + shape_owned_capacity(&field.shape) + }) + .sum::() + + declaration.as_str().len(), + ); + shapes.push(FieldLivenessShape::Record { + declaration: declaration.clone(), + fields, }); } - Ok(FieldLivenessShape::Record { - declaration: declaration.clone(), - fields: shapes, - }) } - ResolvedTypeDeclarationKind::Variant { .. } => Err(cleanup_error( - "droppable variant cleanup is outside the copy-only v1 slice", - )), } + if shapes.len() != 1 || !projections.is_empty() { + return Err(cleanup_error("cleanup shape traversal did not settle")); + } + Ok(shapes.pop().expect("shape count checked above")) } fn collect_expression(&mut self, expression: &ResolvedExpr) -> Result<(), Diagnostic> { - match &expression.kind { - ResolvedExprKind::Call { args, .. } => { - for argument in args { - self.collect_expression(argument)?; + enum Frame<'a> { + Enter(&'a ResolvedExpr), + Children(&'a ResolvedExpr, usize), + Finish(&'a ResolvedExpr), + AddBinding(&'a ResolvedBinding), + AddUpdateBase(&'a ResolvedExpr), + } + + const { assert!(std::mem::size_of::>() == 24) }; + + let mut frames = vec![Frame::Enter(expression)]; + while let Some(frame) = frames.pop() { + #[cfg(test)] + note_capacity_high_water( + frames.capacity() * std::mem::size_of::>() + + self.slots.capacity() * std::mem::size_of::() + + self.flags.capacity() * std::mem::size_of::() + + self.slots.iter().map(slot_owned_capacity).sum::() + + self.flags.iter().map(flag_owned_capacity).sum::(), + ); + match frame { + Frame::Enter(expression) => { + frames.try_reserve(2).map_err(|_| { + cleanup_error("cleanup traversal capacity exceeds address space") + })?; + frames.push(Frame::Finish(expression)); + frames.push(Frame::Children(expression, 0)); } - } - ResolvedExprKind::Unary { value, .. } => self.collect_expression(value)?, - ResolvedExprKind::Binary { left, right, .. } => { - self.collect_expression(left)?; - self.collect_expression(right)?; - } - ResolvedExprKind::Block { statements, tail } => { - for statement in statements { - match statement { - ResolvedStatement::Let { binding, value, .. } => { - self.collect_expression(value)?; - if binding.ownership == OwnershipMode::Own - && self.needs_drop(&binding.ty)? - { - self.add_slot( - CleanupStorageOrigin::Binding { - value: binding.id.clone(), - }, - binding.ty.clone(), - )?; + Frame::Children(expression, index) => { + frames.try_reserve(2).map_err(|_| { + cleanup_error("cleanup traversal capacity exceeds address space") + })?; + let mut enter = None; + let mut action = None; + match &expression.kind { + ResolvedExprKind::Call { args, .. } => { + enter = args.get(index); + } + ResolvedExprKind::NativeRustImportCall(call) => { + enter = call.args.get(index); + } + ResolvedExprKind::Unary { value, .. } + | ResolvedExprKind::Project { base: value, .. } + | ResolvedExprKind::Try { operand: value, .. } + | ResolvedExprKind::TryOption { operand: value, .. } => { + enter = (index == 0).then_some(value.as_ref()); + } + ResolvedExprKind::Binary { left, right, .. } => { + enter = match index { + 0 => Some(left), + 1 => Some(right), + _ => None, + }; + } + ResolvedExprKind::Block { statements, tail } => { + let statement_index = index / 2; + if let Some(statement) = statements.get(statement_index) { + let ResolvedStatement::Let { binding, value, .. } = statement; + if index % 2 == 0 { + enter = Some(value); + } else { + action = Some(Frame::AddBinding(binding)); + } + } else if index == statements.len() * 2 { + enter = Some(tail); } } + ResolvedExprKind::If { + condition, + then_branch, + else_branch, + } => { + enter = match index { + 0 => Some(condition), + 1 => Some(then_branch), + 2 => Some(else_branch), + _ => None, + }; + } + ResolvedExprKind::ConstructRecord { fields, .. } + | ResolvedExprKind::ConstructVariant { fields, .. } => { + enter = fields.get(index).map(|field| &field.value); + } + ResolvedExprKind::Match { scrutinee, arms } => { + enter = if index == 0 { + Some(scrutinee) + } else { + arms.get(index - 1).map(|arm| &arm.value) + }; + } + ResolvedExprKind::UpdateRecord { base, fields, .. } => { + if index == 0 { + enter = Some(base); + } else if index == 1 { + action = Some(Frame::AddUpdateBase(base)); + } else { + enter = fields.get(index - 2).map(|field| &field.value); + } + } + ResolvedExprKind::Int(_) + | ResolvedExprKind::Bool(_) + | ResolvedExprKind::Place(_) => {} + } + if enter.is_some() || action.is_some() { + frames.push(Frame::Children(expression, index + 1)); + if let Some(action) = action { + frames.push(action); + } + if let Some(child) = enter { + frames.push(Frame::Enter(child)); + } } } - self.collect_expression(tail)?; - } - ResolvedExprKind::If { - condition, - then_branch, - else_branch, - } => { - self.collect_expression(condition)?; - self.collect_expression(then_branch)?; - self.collect_expression(else_branch)?; - } - ResolvedExprKind::ConstructRecord { fields, .. } => { - for field in fields { - self.collect_expression(&field.value)?; - } - } - ResolvedExprKind::ConstructVariant { fields, .. } => { - for field in fields { - self.collect_expression(&field.value)?; - } - } - ResolvedExprKind::Try { operand, .. } | ResolvedExprKind::TryOption { operand, .. } => { - self.collect_expression(operand)? - } - ResolvedExprKind::Match { scrutinee, arms } => { - self.collect_expression(scrutinee)?; - for arm in arms { - self.collect_expression(&arm.value)?; - } - } - ResolvedExprKind::UpdateRecord { base, fields, .. } => { - self.collect_expression(base)?; - // A place expression normally needs no temporary. Record - // update is the exception: consuming the whole base into a - // child-region epoch lets failures and successful - // replacement dispose the displaced fields uniformly. - if matches!(base.kind, ResolvedExprKind::Place(_)) - && base.ownership == OwnershipMode::Own - && self.needs_drop(&base.ty)? - { - self.add_slot( - CleanupStorageOrigin::Temporary { - expression: base.id.clone(), - }, - base.ty.clone(), - )?; + Frame::Finish(expression) => self.collect_owned_temporary(expression)?, + Frame::AddBinding(binding) => { + if binding.ownership == OwnershipMode::Own && self.needs_drop(&binding.ty)? { + self.add_slot( + CleanupStorageOrigin::Binding { + value: binding.id.clone(), + }, + binding.ty.clone(), + )?; + } } - for field in fields { - self.collect_expression(&field.value)?; + Frame::AddUpdateBase(base) => { + // A place expression normally needs no temporary. Record + // update deliberately materializes an owned base epoch. + if matches!(base.kind, ResolvedExprKind::Place(_)) + && base.ownership == OwnershipMode::Own + && self.needs_drop(&base.ty)? + { + self.add_slot( + CleanupStorageOrigin::Temporary { + expression: base.id.clone(), + }, + base.ty.clone(), + )?; + } } } - ResolvedExprKind::Project { base, .. } => self.collect_expression(base)?, - ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) | ResolvedExprKind::Place(_) => {} } + Ok(()) + } + + fn collect_owned_temporary(&mut self, expression: &ResolvedExpr) -> Result<(), Diagnostic> { if expression.ownership == OwnershipMode::Own && self.needs_drop(&expression.ty)? && !matches!(expression.kind, ResolvedExprKind::Place(_)) diff --git a/src/cleanup_plan.rs b/src/cleanup_plan.rs index ec18578..905cabd 100644 --- a/src/cleanup_plan.rs +++ b/src/cleanup_plan.rs @@ -346,4 +346,248 @@ impl CleanupPlan { exits: Vec::new(), } } + + #[cfg(test)] + #[allow(dead_code)] + fn owned_capacity_bytes(&self) -> Option { + fn storage_bytes(storage: &StorageId) -> usize { + match storage { + StorageId::Value(value) => value.as_str().len(), + StorageId::Temporary(expression) => expression.as_str().len(), + StorageId::CallArgument { + call, + value_expression, + .. + } => call + .as_str() + .len() + .saturating_add(value_expression.as_str().len()), + StorageId::ProvisionalResult => 0, + } + } + fn place_bytes(place: &CleanupPlace) -> Option { + place + .projections + .iter() + .try_fold(storage_bytes(&place.storage), |bytes, projection| { + bytes.checked_add(projection.as_str().len()) + })? + .checked_add(place.projections.capacity() * std::mem::size_of::()) + } + fn status_id_bytes(status: &StatusSourceId) -> usize { + status.expression.as_str().len() + } + let mut total = self + .entry_state + .live_owned_parameters + .capacity() + .checked_mul(std::mem::size_of::())? + .checked_add(self.slots.capacity() * std::mem::size_of::())? + .checked_add(self.status_sources.capacity() * std::mem::size_of::())? + .checked_add(self.blocks.capacity() * std::mem::size_of::())? + .checked_add(self.edges.capacity() * std::mem::size_of::())? + .checked_add(self.regions.capacity() * std::mem::size_of::())? + .checked_add(self.exits.capacity() * std::mem::size_of::())?; + for place in &self.entry_state.live_owned_parameters { + total = total.checked_add(place_bytes(place)?)?; + } + for slot in &self.slots { + total = total + .checked_add(storage_bytes(&slot.storage))? + .checked_add(resolved_type_owned_capacity(&slot.ty)?)? + .checked_add(field_shape_bytes(&slot.field_liveness_shape)?)?; + } + for status in &self.status_sources { + total = total.checked_add(status_id_bytes(&status.id))?; + match &status.producer { + StatusProducer::PropagatedCall { callee } => { + total = total.checked_add(callee.as_str().len())?; + } + StatusProducer::CheckedArithmetic { + normalized_cases, .. + } => { + total = total.checked_add( + normalized_cases.capacity() * std::mem::size_of::(), + )?; + } + StatusProducer::ContractFalse { .. } => {} + } + } + for block in &self.blocks { + total = total.checked_add( + block.transitions.capacity() * std::mem::size_of::(), + )?; + for transition in &block.transitions { + match transition { + CleanupTransition::Initialize { at, destination } => { + total = total + .checked_add(at.as_str().len())? + .checked_add(place_bytes(destination)?)?; + } + CleanupTransition::Transfer { + at, + source, + destination, + } => { + total = total + .checked_add(at.as_str().len())? + .checked_add(place_bytes(source)?)? + .checked_add(place_bytes(destination)?)?; + } + CleanupTransition::CallCommit { call, arguments } => { + total = total.checked_add(call.as_str().len())?.checked_add( + arguments.capacity() * std::mem::size_of::(), + )?; + for argument in arguments { + total = total.checked_add(place_bytes(&argument.source)?)?; + } + } + CleanupTransition::SelectFailure { source } => { + total = total.checked_add(status_id_bytes(source))?; + } + CleanupTransition::StageCopyResult { source } => { + total = total.checked_add(staged_result_bytes(source)?)?; + } + } + } + if let CleanupTerminator::Branch(edges) = &block.terminator { + total = total.checked_add(edges.capacity() * std::mem::size_of::())?; + } + } + for edge in &self.edges { + total = total.checked_add(match &edge.condition { + EdgeCondition::Always => 0, + EdgeCondition::BooleanResult(expression, _) => expression.as_str().len(), + EdgeCondition::VariantCase { + scrutinee, case, .. + } => scrutinee.as_str().len().saturating_add(case.as_str().len()), + EdgeCondition::StatusZero(status) | EdgeCondition::StatusNonzero(status) => { + status_id_bytes(status) + } + })?; + } + for region in &self.regions { + total = + total.checked_add(region.slots.capacity() * std::mem::size_of::())?; + for storage in ®ion.slots { + total = total.checked_add(storage_bytes(storage))?; + } + } + for exit in &self.exits { + total = total + .checked_add( + exit.leaves_regions.capacity() * std::mem::size_of::(), + )? + .checked_add( + exit.finalize_in_order.capacity() * std::mem::size_of::(), + )?; + for action in &exit.finalize_in_order { + total = total + .checked_add(place_bytes(&action.source)?)? + .checked_add(action.lifecycle_id.as_str().len())?; + } + total = total.checked_add(match &exit.continuation { + ExitContinuation::Continue(_) | ExitContinuation::ReturnUnit => 0, + ExitContinuation::CommitResult { source } => match source { + CleanupResultSource::Scalar { expression } => expression.as_str().len(), + CleanupResultSource::Owned { storage } => place_bytes(storage)?, + }, + ExitContinuation::ReturnFailure { source } => status_id_bytes(source), + })?; + } + Some(total) + } +} + +#[allow(dead_code)] +fn resolved_type_owned_capacity(ty: &ResolvedType) -> Option { + match ty { + ResolvedType::Unit | ResolvedType::I64 | ResolvedType::Bool => Some(0), + ResolvedType::TypeParameter { owner, .. } => Some(owner.as_str().len()), + ResolvedType::Nominal { + declaration, + arguments, + } => arguments + .iter() + .try_fold(declaration.as_str().len(), |bytes, argument| { + bytes.checked_add(resolved_type_owned_capacity(argument)?) + })? + .checked_add(arguments.capacity() * std::mem::size_of::()), + } +} + +#[allow(dead_code)] +fn field_shape_bytes(shape: &FieldLivenessShape) -> Option { + match shape { + FieldLivenessShape::NoDrop => Some(0), + FieldLivenessShape::Leaf { lifecycle, .. } => Some(lifecycle.as_str().len()), + FieldLivenessShape::Record { + declaration, + fields, + } => fields + .iter() + .try_fold(declaration.as_str().len(), |bytes, field| { + bytes + .checked_add(field.field.as_str().len())? + .checked_add(field_shape_bytes(&field.shape)?) + })? + .checked_add(fields.capacity() * std::mem::size_of::()), + } +} + +#[allow(dead_code)] +fn staged_result_bytes(source: &StagedCopyResultSource) -> Option { + match source { + StagedCopyResultSource::Body { + expression, + instance, + } => expression + .as_str() + .len() + .checked_add(resolved_type_owned_capacity(instance)?), + StagedCopyResultSource::TryResidual { + expression, + operand, + source_instance, + target_instance, + result, + ok_case, + ok_field, + err_case, + err_field, + } => [ + expression.as_str().len(), + operand.as_str().len(), + result.as_str().len(), + ok_case.as_str().len(), + ok_field.as_str().len(), + err_case.as_str().len(), + err_field.as_str().len(), + ] + .into_iter() + .try_fold(0usize, usize::checked_add)? + .checked_add(resolved_type_owned_capacity(source_instance)?)? + .checked_add(resolved_type_owned_capacity(target_instance)?), + StagedCopyResultSource::TryOptionNone { + expression, + operand, + source_instance, + target_instance, + option, + some_case, + some_field, + none_case, + } => [ + expression.as_str().len(), + operand.as_str().len(), + option.as_str().len(), + some_case.as_str().len(), + some_field.as_str().len(), + none_case.as_str().len(), + ] + .into_iter() + .try_fold(0usize, usize::checked_add)? + .checked_add(resolved_type_owned_capacity(source_instance)?)? + .checked_add(resolved_type_owned_capacity(target_instance)?), + } } diff --git a/src/cleanup_plan/build.rs b/src/cleanup_plan/build.rs index 7a7b72b..0b02d11 100644 --- a/src/cleanup_plan/build.rs +++ b/src/cleanup_plan/build.rs @@ -25,6 +25,335 @@ use super::{ }; const UNRESOLVED_EXIT: ExitTargetId = ExitTargetId(u32::MAX); +#[cfg(test)] +const CLEANUP_EVAL_RESULT_SIZE_CEILING: usize = 128; + +#[cfg(test)] +thread_local! { + static LOWER_CAPACITY_HIGH_WATER: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +#[cfg(test)] +pub(crate) fn reset_lower_capacity_high_water() { + LOWER_CAPACITY_HIGH_WATER.with(|water| water.set(0)); +} + +#[cfg(test)] +pub(crate) fn lower_capacity_high_water() -> usize { + LOWER_CAPACITY_HIGH_WATER.with(std::cell::Cell::get) +} + +#[cfg(test)] +fn note_lower_capacity_high_water(bytes: usize) { + LOWER_CAPACITY_HIGH_WATER.with(|water| water.set(water.get().max(bytes))); +} + +#[cfg(test)] +fn storage_id_owned_capacity(storage: &StorageId) -> usize { + match storage { + StorageId::Value(id) => id.as_str().len(), + StorageId::Temporary(id) => id.as_str().len(), + StorageId::CallArgument { + call, + value_expression, + .. + } => call.as_str().len() + value_expression.as_str().len(), + StorageId::ProvisionalResult => 0, + } +} + +#[cfg(test)] +fn cleanup_place_owned_capacity(place: &CleanupPlace) -> usize { + storage_id_owned_capacity(&place.storage) + + place.projections.capacity() * std::mem::size_of::() + + place + .projections + .iter() + .map(|projection| projection.as_str().len()) + .sum::() +} + +#[cfg(test)] +fn status_source_id_owned_capacity(source: &StatusSourceId) -> usize { + source.expression.as_str().len() +} + +#[cfg(test)] +fn field_shape_owned_capacity(shape: &FieldLivenessShape) -> usize { + match shape { + FieldLivenessShape::NoDrop => 0, + FieldLivenessShape::Leaf { lifecycle, .. } => lifecycle.as_str().len(), + FieldLivenessShape::Record { + declaration, + fields, + } => { + declaration.as_str().len() + + fields.capacity() * std::mem::size_of::() + + fields + .iter() + .map(|field| { + field.field.as_str().len() + field_shape_owned_capacity(&field.shape) + }) + .sum::() + } + } +} + +#[cfg(test)] +fn cleanup_slot_owned_capacity(slot: &CleanupSlot) -> usize { + storage_id_owned_capacity(&slot.storage) + + resolved_type_owned_capacity(&slot.ty) + + field_shape_owned_capacity(&slot.field_liveness_shape) +} + +#[cfg(test)] +fn transition_owned_capacity(transition: &CleanupTransition) -> usize { + match transition { + CleanupTransition::Initialize { at, destination } => { + at.as_str().len() + cleanup_place_owned_capacity(destination) + } + CleanupTransition::Transfer { + at, + source, + destination, + } => { + at.as_str().len() + + cleanup_place_owned_capacity(source) + + cleanup_place_owned_capacity(destination) + } + CleanupTransition::CallCommit { call, arguments } => { + call.as_str().len() + + arguments.capacity() * std::mem::size_of::() + + arguments + .iter() + .map(|argument| cleanup_place_owned_capacity(&argument.source)) + .sum::() + } + CleanupTransition::SelectFailure { source } => status_source_id_owned_capacity(source), + CleanupTransition::StageCopyResult { source } => match source { + StagedCopyResultSource::Body { + expression, + instance, + } => expression.as_str().len() + resolved_type_owned_capacity(instance), + StagedCopyResultSource::TryResidual { + expression, + operand, + source_instance, + target_instance, + result, + ok_case, + ok_field, + err_case, + err_field, + } => { + expression.as_str().len() + + operand.as_str().len() + + resolved_type_owned_capacity(source_instance) + + resolved_type_owned_capacity(target_instance) + + result.as_str().len() + + ok_case.as_str().len() + + ok_field.as_str().len() + + err_case.as_str().len() + + err_field.as_str().len() + } + StagedCopyResultSource::TryOptionNone { + expression, + operand, + source_instance, + target_instance, + option, + some_case, + some_field, + none_case, + } => { + expression.as_str().len() + + operand.as_str().len() + + resolved_type_owned_capacity(source_instance) + + resolved_type_owned_capacity(target_instance) + + option.as_str().len() + + some_case.as_str().len() + + some_field.as_str().len() + + none_case.as_str().len() + } + }, + } +} + +#[cfg(test)] +fn edge_condition_owned_capacity(condition: &EdgeCondition) -> usize { + match condition { + EdgeCondition::Always => 0, + EdgeCondition::BooleanResult(expression, _) => expression.as_str().len(), + EdgeCondition::VariantCase { + scrutinee, case, .. + } => scrutinee.as_str().len() + case.as_str().len(), + EdgeCondition::StatusZero(source) | EdgeCondition::StatusNonzero(source) => { + status_source_id_owned_capacity(source) + } + } +} + +#[cfg(test)] +fn exit_continuation_owned_capacity(continuation: &ExitContinuation) -> usize { + match continuation { + ExitContinuation::CommitResult { + source: CleanupResultSource::Scalar { expression }, + } => expression.as_str().len(), + ExitContinuation::CommitResult { + source: CleanupResultSource::Owned { storage }, + } => cleanup_place_owned_capacity(storage), + ExitContinuation::ReturnFailure { source } => status_source_id_owned_capacity(source), + ExitContinuation::Continue(_) | ExitContinuation::ReturnUnit => 0, + } +} + +#[cfg(test)] +fn builder_nested_capacity(builder: &PlanBuilder<'_>) -> usize { + let block_payload = builder.blocks.iter().fold(0usize, |bytes, block| { + bytes + + block.transitions.capacity() * std::mem::size_of::() + + block + .transitions + .iter() + .map(transition_owned_capacity) + .sum::() + + match &block.terminator { + Some(CleanupTerminator::Branch(edges)) => { + edges.capacity() * std::mem::size_of::() + } + Some(CleanupTerminator::Goto(_) | CleanupTerminator::Exit(_)) | None => 0, + } + }); + let region_payload = builder.regions.iter().fold(0usize, |bytes, region| { + bytes + + region.slots.capacity() * std::mem::size_of::() + + region + .slots + .iter() + .map(storage_id_owned_capacity) + .sum::() + }); + let exit_payload = builder.exits.iter().fold(0usize, |bytes, exit| { + bytes + + exit.leaves_regions.capacity() * std::mem::size_of::() + + exit.finalize_in_order.capacity() * std::mem::size_of::() + + exit + .finalize_in_order + .iter() + .map(|action| { + cleanup_place_owned_capacity(&action.source) + + action.lifecycle_id.as_str().len() + }) + .sum::() + + exit_continuation_owned_capacity(&exit.continuation) + }); + let status_payload = builder.status_sources.iter().fold(0usize, |bytes, source| { + bytes + + match &source.producer { + StatusProducer::CheckedArithmetic { + normalized_cases, .. + } => normalized_cases.capacity() * std::mem::size_of::(), + StatusProducer::PropagatedCall { callee } => callee.as_str().len(), + StatusProducer::ContractFalse { .. } => 0, + } + + status_source_id_owned_capacity(&source.id) + }); + let edge_payload = builder + .edges + .iter() + .map(|edge| edge_condition_owned_capacity(&edge.condition)) + .sum::(); + block_payload + + region_payload + + exit_payload + + status_payload + + edge_payload + + builder.initial_state.live_order.capacity() * std::mem::size_of::() + + builder.pending_try_residuals.capacity() * std::mem::size_of::() + + builder + .pending_try_residuals + .iter() + .map(|pending| { + pending.state.live_order.capacity() * std::mem::size_of::() + }) + .sum::() + + builder.slots.capacity() * std::mem::size_of::() + + builder + .slots + .iter() + .map(cleanup_slot_owned_capacity) + .sum::() + + builder.storage_to_slot.len() + * (std::mem::size_of::<(StorageId, CleanupSlotId)>() + + std::mem::size_of::>()) + + builder + .storage_to_slot + .keys() + .map(storage_id_owned_capacity) + .sum::() + + builder.inventory_storage.len() + * (std::mem::size_of::<(InventoryStorageId, StorageId)>() + + std::mem::size_of::>()) + + builder + .inventory_storage + .values() + .map(storage_id_owned_capacity) + .sum::() + + builder.leaves.len() + * (std::mem::size_of::<(LivenessFlagId, LeafMetadata)>() + + std::mem::size_of::>()) + + builder + .leaves + .values() + .map(|leaf| cleanup_place_owned_capacity(&leaf.place) + leaf.lifecycle.as_str().len()) + .sum::() + + builder.entry_state.live_owned_parameters.capacity() * std::mem::size_of::() + + builder + .entry_state + .live_owned_parameters + .iter() + .map(cleanup_place_owned_capacity) + .sum::() +} + +#[cfg(test)] +fn flow_state_owned_capacity(state: &FlowState) -> usize { + state.live_order.capacity() * std::mem::size_of::() +} + +#[cfg(test)] +fn eval_result_owned_capacity(result: &EvalResult) -> usize { + flow_state_owned_capacity(&result.state).saturating_add( + result + .owned_source + .as_ref() + .map_or(0, cleanup_place_owned_capacity), + ) +} + +#[cfg(test)] +fn resolved_type_owned_capacity(ty: &ResolvedType) -> usize { + match ty { + ResolvedType::Unit | ResolvedType::I64 | ResolvedType::Bool => 0, + ResolvedType::TypeParameter { owner, .. } => owner.as_str().len(), + ResolvedType::Nominal { + declaration, + arguments, + } => { + declaration.as_str().len() + + arguments.capacity() * std::mem::size_of::() + + arguments + .iter() + .map(resolved_type_owned_capacity) + .sum::() + } + } +} + +#[cfg(test)] +fn resolved_param_owned_capacity(param: &crate::hir::ResolvedParam) -> usize { + param.id.as_str().len() + param.name.capacity() + resolved_type_owned_capacity(¶m.ty) +} /// Build the one canonical cleanup plan for a validated resolved function. /// @@ -37,6 +366,22 @@ pub(crate) fn build_plan( PlanBuilder::new(program, function)?.build() } +#[cfg(test)] +pub(super) fn assert_expression_lowering_oracle( + program: &ResolvedProgram, + function: &ResolvedFunction, + expression: &ResolvedExpr, +) { + let mut iterative = PlanBuilder::new(program, function).unwrap(); + let mut recursive = iterative.clone(); + let state = iterative.initial_state.clone(); + let region = CleanupRegionId(0); + let block = BlockId(0); + let actual = iterative.lower_expr_iterative(expression, block, state.clone(), region); + let expected = recursive.lower_expr_recursive_reference(expression, block, state, region); + PlanBuilder::assert_lowering_oracle(&iterative, &recursive, &actual, &expected, expression); +} + #[derive(Clone, Debug, Eq, PartialEq)] struct FlowState { /// Live flags in semantic initialization order. A whole-aggregate @@ -63,18 +408,21 @@ impl FlowState { } } +#[derive(Clone, Debug, Eq, PartialEq)] struct EvalResult { block: BlockId, state: FlowState, owned_source: Option, } +#[derive(Clone, Debug, Eq, PartialEq)] struct PendingTryResidual { block: BlockId, state: FlowState, region: CleanupRegionId, } +#[derive(Clone, Debug, Eq, PartialEq)] struct OpenBlock { id: BlockId, region: CleanupRegionId, @@ -82,12 +430,13 @@ struct OpenBlock { terminator: Option, } -#[derive(Clone)] +#[derive(Clone, Debug, Eq, PartialEq)] struct LeafMetadata { place: CleanupPlace, lifecycle: DeclarationId, } +#[derive(Clone)] struct PlanBuilder<'a> { program: &'a ResolvedProgram, function: &'a ResolvedFunction, @@ -345,6 +694,16 @@ impl<'a> PlanBuilder<'a> { }; self.finish_success(current, state, root, result)?; + #[cfg(test)] + note_lower_capacity_high_water( + self.blocks.capacity() * std::mem::size_of::() + + self.edges.capacity() * std::mem::size_of::() + + self.regions.capacity() * std::mem::size_of::() + + self.exits.capacity() * std::mem::size_of::() + + self.status_sources.capacity() * std::mem::size_of::() + + builder_nested_capacity(&self), + ); + let blocks = self .blocks .into_iter() @@ -1148,80 +1507,1833 @@ impl<'a> PlanBuilder<'a> { state: FlowState, region: CleanupRegionId, ) -> Result { - match &expression.kind { - ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) => Ok(EvalResult { - block, - state, - owned_source: None, - }), - ResolvedExprKind::Place(place) => { - let owned_source = if expression.ownership == OwnershipMode::Own - && self.needs_drop(&expression.ty)? - { - Some(self.place_from_hir(place)?) - } else { - None - }; - Ok(EvalResult { - block, - state, - owned_source, - }) + self.lower_expr_iterative(expression, block, state, region) + } + + #[cfg(test)] + fn assert_lowering_oracle( + actual_builder: &Self, + expected_builder: &Self, + actual: &Result, + expected: &Result, + expression: &ResolvedExpr, + ) { + match (actual, expected) { + (Ok(actual), Ok(expected)) => assert_eq!( + actual, expected, + "cleanup lowering result differs at {}", + expression.id + ), + (Err(actual), Err(expected)) => { + assert_eq!(actual.code, expected.code); + assert_eq!(actual.severity, expected.severity); + assert_eq!(actual.message, expected.message); + assert_eq!(actual.path, expected.path); + assert_eq!(actual.span, expected.span); + assert_eq!(actual.help, expected.help); } - ResolvedExprKind::Call { - callee, - instance, - args, - .. - } => self.lower_call( - expression, - callee, - instance.as_ref(), - args, - (block, state, region), + (actual, expected) => panic!( + "cleanup lowering outcome differs at {}: actual={actual:?} expected={expected:?}", + expression.id ), - ResolvedExprKind::Unary { op, value } => { - let evaluated = self.lower_expr(value, block, state, region)?; - let (block, state) = match op { - UnaryOp::Not => (evaluated.block, evaluated.state), - UnaryOp::Neg => { + } + assert_eq!(actual_builder.slots, expected_builder.slots); + assert_eq!( + actual_builder.storage_to_slot, + expected_builder.storage_to_slot + ); + assert_eq!( + actual_builder.inventory_storage, + expected_builder.inventory_storage + ); + assert_eq!(actual_builder.leaves, expected_builder.leaves); + assert_eq!(actual_builder.next_flag, expected_builder.next_flag); + assert_eq!( + actual_builder.status_sources, + expected_builder.status_sources + ); + assert_eq!(actual_builder.blocks, expected_builder.blocks); + assert_eq!(actual_builder.edges, expected_builder.edges); + assert_eq!(actual_builder.regions, expected_builder.regions); + assert_eq!(actual_builder.exits, expected_builder.exits); + assert_eq!(actual_builder.entry_state, expected_builder.entry_state); + assert_eq!(actual_builder.initial_state, expected_builder.initial_state); + assert_eq!( + actual_builder.pending_try_residuals, + expected_builder.pending_try_residuals + ); + assert_eq!(actual_builder.schema, expected_builder.schema); + } + + fn lower_expr_iterative( + &mut self, + expression: &ResolvedExpr, + block: BlockId, + state: FlowState, + region: CleanupRegionId, + ) -> Result { + enum Frame<'e> { + RestoreRegion(CleanupRegionId), + Enter { + expression: &'e ResolvedExpr, + block: BlockId, + state: FlowState, + }, + Unary { + expression: &'e ResolvedExpr, + op: UnaryOp, + }, + BinaryLeft { + expression: &'e ResolvedExpr, + op: BinaryOp, + right: &'e ResolvedExpr, + }, + BinaryRight { + expression: &'e ResolvedExpr, + op: BinaryOp, + }, + LazyAfterLeft { + operation: BinaryOp, + left_id: ExpressionId, + right: &'e ResolvedExpr, + }, + LazyAfterRight { + left_state: FlowState, + skip: BlockId, + }, + IfAfterCondition { + expression: &'e ResolvedExpr, + then_branch: &'e ResolvedExpr, + else_branch: &'e ResolvedExpr, + }, + IfAfterThen { + expression: &'e ResolvedExpr, + else_branch: &'e ResolvedExpr, + else_entry: BlockId, + condition_state: FlowState, + destination: Option, + }, + IfAfterElse { + expression: &'e ResolvedExpr, + then_result: EvalResult, + destination: Option, + }, + Project { + expression: &'e ResolvedExpr, + field: &'e DeclarationId, + }, + NativeNext { + args: &'e [ResolvedExpr], + index: usize, + flow: EvalResult, + }, + NativeAfterArg { + args: &'e [ResolvedExpr], + index: usize, + }, + CallNext { + expression: &'e ResolvedExpr, + callee: &'e DeclarationId, + args: &'e [ResolvedExpr], + params: Vec, + index: usize, + flow: EvalResult, + commits: Vec, + }, + CallAfterArg { + expression: &'e ResolvedExpr, + callee: &'e DeclarationId, + args: &'e [ResolvedExpr], + params: Vec, + index: usize, + commits: Vec, + }, + BlockNext { + expression: &'e ResolvedExpr, + statements: &'e [ResolvedStatement], + tail: &'e ResolvedExpr, + index: usize, + flow: EvalResult, + child_region: CleanupRegionId, + destination: Option, + }, + BlockAfterStatement { + expression: &'e ResolvedExpr, + statements: &'e [ResolvedStatement], + tail: &'e ResolvedExpr, + index: usize, + child_region: CleanupRegionId, + destination: Option, + }, + BlockAfterTail { + expression: &'e ResolvedExpr, + child_region: CleanupRegionId, + destination: Option, + }, + RecordNext { + expression: &'e ResolvedExpr, + fields: &'e [crate::hir::ResolvedFieldInitializer], + index: usize, + flow: EvalResult, + destination: Option, + }, + RecordAfterField { + expression: &'e ResolvedExpr, + fields: &'e [crate::hir::ResolvedFieldInitializer], + index: usize, + destination: Option, + }, + VariantNext { + fields: &'e [crate::hir::ResolvedFieldInitializer], + index: usize, + flow: EvalResult, + }, + VariantAfterField { + fields: &'e [crate::hir::ResolvedFieldInitializer], + index: usize, + }, + TryAfterOperand { + expression: &'e ResolvedExpr, + operand: &'e ResolvedExpr, + result: &'e DeclarationId, + ok_case: &'e DeclarationId, + ok_field: &'e DeclarationId, + err_case: &'e DeclarationId, + err_field: &'e DeclarationId, + residual_type: &'e ResolvedType, + }, + TryOptionAfterOperand { + expression: &'e ResolvedExpr, + operand: &'e ResolvedExpr, + option: &'e DeclarationId, + some_case: &'e DeclarationId, + some_field: &'e DeclarationId, + none_case: &'e DeclarationId, + residual_type: &'e ResolvedType, + }, + MatchAfterScrutinee { + scrutinee: &'e ResolvedExpr, + arms: &'e [ResolvedMatchArm], + }, + MatchRecordAfterArm, + MatchNext { + scrutinee: &'e ResolvedExpr, + arms: &'e [ResolvedMatchArm], + index: usize, + decision: BlockId, + branch_state: FlowState, + arm_results: Vec, + }, + MatchAfterArm { + scrutinee: &'e ResolvedExpr, + arms: &'e [ResolvedMatchArm], + index: usize, + decision: BlockId, + branch_state: FlowState, + arm_results: Vec, + }, + UpdateAfterBase { + expression: &'e ResolvedExpr, + record: &'e DeclarationId, + fields: &'e [crate::hir::ResolvedFieldInitializer], + destination: Option, + update_region: CleanupRegionId, + }, + UpdateNext { + expression: &'e ResolvedExpr, + record: &'e DeclarationId, + fields: &'e [crate::hir::ResolvedFieldInitializer], + index: usize, + flow: EvalResult, + destination: Option, + update_region: CleanupRegionId, + staged_base: Option, + replaced: BTreeSet, + }, + UpdateAfterField { + expression: &'e ResolvedExpr, + record: &'e DeclarationId, + fields: &'e [crate::hir::ResolvedFieldInitializer], + index: usize, + destination: Option, + update_region: CleanupRegionId, + staged_base: Option, + replaced: BTreeSet, + }, + } + const { assert!(std::mem::size_of::>() == 344) }; + #[cfg(test)] + fn frame_owned_capacity(frame: &Frame<'_>) -> usize { + let destination = |place: &Option| { + place.as_ref().map_or(0, cleanup_place_owned_capacity) + }; + let results = |values: &Vec| { + values.capacity() * std::mem::size_of::() + + values.iter().map(eval_result_owned_capacity).sum::() + }; + match frame { + Frame::Enter { state, .. } + | Frame::LazyAfterRight { + left_state: state, .. + } => flow_state_owned_capacity(state), + Frame::LazyAfterLeft { left_id, .. } => left_id.as_str().len(), + Frame::IfAfterThen { + condition_state, + destination: place, + .. + } => flow_state_owned_capacity(condition_state) + destination(place), + Frame::IfAfterElse { + then_result, + destination: place, + .. + } => eval_result_owned_capacity(then_result) + destination(place), + Frame::NativeNext { flow, .. } | Frame::VariantNext { flow, .. } => { + eval_result_owned_capacity(flow) + } + Frame::BlockNext { + flow, + destination: place, + .. + } + | Frame::RecordNext { + flow, + destination: place, + .. + } => eval_result_owned_capacity(flow) + destination(place), + Frame::CallNext { + params, + flow, + commits, + .. + } => { + params.capacity() * std::mem::size_of::() + + params + .iter() + .map(resolved_param_owned_capacity) + .sum::() + + eval_result_owned_capacity(flow) + + commits.capacity() * std::mem::size_of::() + + commits + .iter() + .map(|commit| cleanup_place_owned_capacity(&commit.source)) + .sum::() + } + Frame::CallAfterArg { + params, commits, .. + } => { + params.capacity() * std::mem::size_of::() + + params + .iter() + .map(resolved_param_owned_capacity) + .sum::() + + commits.capacity() * std::mem::size_of::() + + commits + .iter() + .map(|commit| cleanup_place_owned_capacity(&commit.source)) + .sum::() + } + Frame::MatchNext { + branch_state, + arm_results, + .. + } + | Frame::MatchAfterArm { + branch_state, + arm_results, + .. + } => flow_state_owned_capacity(branch_state) + results(arm_results), + Frame::UpdateNext { + flow, + destination: place, + staged_base, + replaced, + .. + } => { + eval_result_owned_capacity(flow) + + destination(place) + + destination(staged_base) + + replaced.len() + * (std::mem::size_of::() + + std::mem::size_of::>()) + + replaced.iter().map(|id| id.as_str().len()).sum::() + } + Frame::UpdateAfterField { + destination: place, + staged_base, + replaced, + .. + } => { + destination(place) + + destination(staged_base) + + replaced.len() + * (std::mem::size_of::() + + std::mem::size_of::>()) + + replaced.iter().map(|id| id.as_str().len()).sum::() + } + Frame::BlockAfterStatement { + destination: place, .. + } + | Frame::BlockAfterTail { + destination: place, .. + } + | Frame::RecordAfterField { + destination: place, .. + } + | Frame::UpdateAfterBase { + destination: place, .. + } => destination(place), + _ => 0, + } + } + let mut frames = vec![Frame::Enter { + expression, + block, + state, + }]; + let mut results = Vec::new(); + let mut active_region = region; + while let Some(frame) = frames.pop() { + #[cfg(test)] + note_lower_capacity_high_water( + frames.capacity() * std::mem::size_of::>() + + results.capacity() * std::mem::size_of::() + + self.blocks.capacity() * std::mem::size_of::() + + self.edges.capacity() * std::mem::size_of::() + + self.regions.capacity() * std::mem::size_of::() + + self.exits.capacity() * std::mem::size_of::() + + self.status_sources.capacity() * std::mem::size_of::() + + builder_nested_capacity(self) + + frames.iter().map(frame_owned_capacity).sum::() + + frame_owned_capacity(&frame) + + results + .iter() + .map(eval_result_owned_capacity) + .sum::(), + ); + match frame { + Frame::RestoreRegion(restored) => active_region = restored, + Frame::Enter { + expression, + block, + state, + } => match &expression.kind { + ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) => { + results.push(EvalResult { + block, + state, + owned_source: None, + }) + } + ResolvedExprKind::Place(place) => { + let owned_source = if expression.ownership == OwnershipMode::Own + && self.needs_drop(&expression.ty)? + { + Some(self.place_from_hir(place)?) + } else { + None + }; + results.push(EvalResult { + block, + state, + owned_source, + }); + } + ResolvedExprKind::Unary { op, value } => { + frames.push(Frame::Unary { + expression, + op: *op, + }); + frames.push(Frame::Enter { + expression: value, + block, + state, + }); + } + ResolvedExprKind::Binary { op, left, right } + if !matches!(op, BinaryOp::And | BinaryOp::Or) => + { + frames.push(Frame::BinaryLeft { + expression, + op: *op, + right, + }); + frames.push(Frame::Enter { + expression: left, + block, + state, + }); + } + ResolvedExprKind::Binary { op, left, right } + if matches!(op, BinaryOp::And | BinaryOp::Or) => + { + frames.push(Frame::LazyAfterLeft { + operation: *op, + left_id: left.id.clone(), + right, + }); + frames.push(Frame::Enter { + expression: left, + block, + state, + }); + } + ResolvedExprKind::Binary { .. } => { + return Err(plan_error( + "cleanup lowering received an unknown binary operator", + )); + } + ResolvedExprKind::If { + condition, + then_branch, + else_branch, + } => { + frames.push(Frame::IfAfterCondition { + expression, + then_branch, + else_branch, + }); + frames.push(Frame::Enter { + expression: condition, + block, + state, + }); + } + ResolvedExprKind::Project { base, field } => { + frames.push(Frame::Project { expression, field }); + frames.push(Frame::Enter { + expression: base, + block, + state, + }); + } + ResolvedExprKind::NativeRustImportCall(call) => { + frames.push(Frame::NativeNext { + args: &call.args, + index: 0, + flow: EvalResult { + block, + state, + owned_source: None, + }, + }) + } + ResolvedExprKind::Call { + callee, + instance, + args, + .. + } => { + let target = self + .program + .resolve_call_target(callee, instance.as_ref()) + .ok_or_else(|| { + plan_error(format!("unknown cleanup call target `{callee}`")) + })?; + if target.params.len() != args.len() { + return Err(plan_error(format!( + "cleanup call `{}` has inconsistent arity", + expression.id + ))); + } + frames.push(Frame::CallNext { + expression, + callee, + args, + params: target.params.clone(), + index: 0, + flow: EvalResult { + block, + state, + owned_source: None, + }, + commits: Vec::with_capacity(args.len()), + }); + } + ResolvedExprKind::Block { statements, tail } => { + let destination = self.expression_slot(expression, active_region)?; + let child_region = self.new_region(active_region)?; + let entry = self.new_block(child_region)?; + let edge = self.new_edge(block, entry, EdgeCondition::Always)?; + self.terminate(block, CleanupTerminator::Goto(edge))?; + frames.push(Frame::BlockNext { + expression, + statements, + tail, + index: 0, + flow: EvalResult { + block: entry, + state, + owned_source: None, + }, + child_region, + destination, + }); + } + ResolvedExprKind::ConstructRecord { fields, .. } => { + let destination = self.expression_slot(expression, active_region)?; + frames.push(Frame::RecordNext { + expression, + fields, + index: 0, + flow: EvalResult { + block, + state, + owned_source: None, + }, + destination, + }); + } + ResolvedExprKind::ConstructVariant { fields, .. } => { + frames.push(Frame::VariantNext { + fields, + index: 0, + flow: EvalResult { + block, + state, + owned_source: None, + }, + }); + } + ResolvedExprKind::Try { + operand, + result, + ok_case, + ok_field, + err_case, + err_field, + residual_type, + } => { + self.check_try_metadata( + expression, + operand, + result, + ok_case, + ok_field, + err_case, + err_field, + residual_type, + )?; + frames.push(Frame::TryAfterOperand { + expression, + operand, + result, + ok_case, + ok_field, + err_case, + err_field, + residual_type, + }); + frames.push(Frame::Enter { + expression: operand, + block, + state, + }); + } + ResolvedExprKind::TryOption { + operand, + option, + some_case, + some_field, + none_case, + residual_type, + } => { + self.check_try_option_metadata( + expression, + operand, + option, + some_case, + some_field, + none_case, + residual_type, + )?; + frames.push(Frame::TryOptionAfterOperand { + expression, + operand, + option, + some_case, + some_field, + none_case, + residual_type, + }); + frames.push(Frame::Enter { + expression: operand, + block, + state, + }); + } + ResolvedExprKind::Match { scrutinee, arms } => { + if arms.is_empty() { + return Err(plan_error("copy-variant match has no arms")); + } + if self.needs_drop(&arms[0].value.ty)? { + return Err(plan_error( + "droppable match result reached the copy-only cleanup slice", + )); + } + frames.push(Frame::MatchAfterScrutinee { scrutinee, arms }); + frames.push(Frame::Enter { + expression: scrutinee, + block, + state, + }); + } + ResolvedExprKind::UpdateRecord { + base, + record, + fields, + } => { + let destination = self.expression_slot(expression, active_region)?; + let (entry, update_region) = if destination.is_some() { + let update_region = self.new_region(active_region)?; + let entry = self.new_block(update_region)?; + let edge = self.new_edge(block, entry, EdgeCondition::Always)?; + self.terminate(block, CleanupTerminator::Goto(edge))?; + (entry, update_region) + } else { + (block, active_region) + }; + frames.push(Frame::UpdateAfterBase { + expression, + record, + fields, + destination, + update_region, + }); + if active_region != update_region { + frames.push(Frame::RestoreRegion(active_region)); + active_region = update_region; + } + frames.push(Frame::Enter { + expression: base, + block: entry, + state, + }); + } + }, + Frame::Unary { expression, op } => { + let mut evaluated = results.pop().expect("unary result retained"); + if op == UnaryOp::Neg { let source = self.checked_source( expression, CheckedOperation::Neg, vec![StatusCase::NegationOverflow], )?; - self.split_status(evaluated.block, evaluated.state, region, source)? + let (block, state) = self.split_status( + evaluated.block, + evaluated.state, + active_region, + source, + )?; + evaluated = EvalResult { + block, + state, + owned_source: None, + }; + } else { + evaluated.owned_source = None; } - }; - Ok(EvalResult { - block, - state, - owned_source: None, - }) - } - ResolvedExprKind::Binary { op, left, right } - if matches!(op, BinaryOp::And | BinaryOp::Or) => - { - self.lower_lazy(expression, *op, left, right, block, state, region) - } - ResolvedExprKind::Binary { op, left, right } => { - let left = self.lower_expr(left, block, state, region)?; - let right = self.lower_expr(right, left.block, left.state, region)?; - let checked = match op { - BinaryOp::Add => Some((CheckedOperation::Add, vec![StatusCase::AddOverflow])), - BinaryOp::Sub => Some((CheckedOperation::Sub, vec![StatusCase::SubOverflow])), - BinaryOp::Mul => Some((CheckedOperation::Mul, vec![StatusCase::MulOverflow])), - BinaryOp::Div => Some(( - CheckedOperation::Div, - vec![StatusCase::DivisionByZero, StatusCase::DivisionOverflow], - )), - BinaryOp::Rem => Some(( - CheckedOperation::Rem, - vec![StatusCase::RemainderByZero, StatusCase::RemainderOverflow], - )), - BinaryOp::Eq - | BinaryOp::Ne - | BinaryOp::Lt + results.push(evaluated); + } + Frame::BinaryLeft { + expression, + op, + right, + } => { + let left = results.pop().expect("binary left retained"); + frames.push(Frame::BinaryRight { expression, op }); + frames.push(Frame::Enter { + expression: right, + block: left.block, + state: left.state, + }); + } + Frame::BinaryRight { expression, op } => { + let right = results.pop().expect("binary right retained"); + let checked = match op { + BinaryOp::Add => { + Some((CheckedOperation::Add, vec![StatusCase::AddOverflow])) + } + BinaryOp::Sub => { + Some((CheckedOperation::Sub, vec![StatusCase::SubOverflow])) + } + BinaryOp::Mul => { + Some((CheckedOperation::Mul, vec![StatusCase::MulOverflow])) + } + BinaryOp::Div => Some(( + CheckedOperation::Div, + vec![StatusCase::DivisionByZero, StatusCase::DivisionOverflow], + )), + BinaryOp::Rem => Some(( + CheckedOperation::Rem, + vec![StatusCase::RemainderByZero, StatusCase::RemainderOverflow], + )), + _ => None, + }; + let (block, state) = if let Some((operation, cases)) = checked { + let source = self.checked_source(expression, operation, cases)?; + self.split_status(right.block, right.state, active_region, source)? + } else { + (right.block, right.state) + }; + results.push(EvalResult { + block, + state, + owned_source: None, + }); + } + Frame::LazyAfterLeft { + operation, + left_id, + right, + } => { + let left = results.pop().expect("lazy left result retained"); + let evaluate_right_when = operation == BinaryOp::And; + let evaluate = self.new_block(active_region)?; + let skip = self.new_block(active_region)?; + let evaluate_edge = self.new_edge( + left.block, + evaluate, + EdgeCondition::BooleanResult(left_id.clone(), evaluate_right_when), + )?; + let skip_edge = self.new_edge( + left.block, + skip, + EdgeCondition::BooleanResult(left_id, !evaluate_right_when), + )?; + self.terminate( + left.block, + CleanupTerminator::Branch(vec![evaluate_edge, skip_edge]), + )?; + frames.push(Frame::LazyAfterRight { + left_state: left.state.clone(), + skip, + }); + frames.push(Frame::Enter { + expression: right, + block: evaluate, + state: left.state, + }); + } + Frame::LazyAfterRight { left_state, skip } => { + let right = results.pop().expect("lazy right result retained"); + let state = self.merge_states(&right.state, &left_state)?; + let join = self.new_block(active_region)?; + let evaluated_edge = self.new_edge(right.block, join, EdgeCondition::Always)?; + self.terminate(right.block, CleanupTerminator::Goto(evaluated_edge))?; + let skipped_edge = self.new_edge(skip, join, EdgeCondition::Always)?; + self.terminate(skip, CleanupTerminator::Goto(skipped_edge))?; + results.push(EvalResult { + block: join, + state, + owned_source: None, + }); + } + Frame::IfAfterCondition { + expression, + then_branch, + else_branch, + } => { + let condition = results.pop().expect("if condition result retained"); + let destination = self.expression_slot(expression, active_region)?; + let then_entry = self.new_block(active_region)?; + let else_entry = self.new_block(active_region)?; + let then_edge = self.new_edge( + condition.block, + then_entry, + EdgeCondition::BooleanResult(condition_id(expression)?, true), + )?; + let else_edge = self.new_edge( + condition.block, + else_entry, + EdgeCondition::BooleanResult(condition_id(expression)?, false), + )?; + self.terminate( + condition.block, + CleanupTerminator::Branch(vec![then_edge, else_edge]), + )?; + frames.push(Frame::IfAfterThen { + expression, + else_branch, + else_entry, + condition_state: condition.state.clone(), + destination, + }); + frames.push(Frame::Enter { + expression: then_branch, + block: then_entry, + state: condition.state, + }); + } + Frame::IfAfterThen { + expression, + else_branch, + else_entry, + condition_state, + destination, + } => { + let mut then_result = results.pop().expect("then result retained"); + if let Some(destination) = destination.clone() { + let source = then_result + .owned_source + .take() + .ok_or_else(|| plan_error("owned then branch has no cleanup source"))?; + self.transfer( + then_result.block, + expression.id.clone(), + source, + destination, + &mut then_result.state, + true, + )?; + } + frames.push(Frame::IfAfterElse { + expression, + then_result, + destination, + }); + frames.push(Frame::Enter { + expression: else_branch, + block: else_entry, + state: condition_state, + }); + } + Frame::IfAfterElse { + expression, + then_result, + destination, + } => { + let mut else_result = results.pop().expect("else result retained"); + if let Some(destination) = destination.clone() { + let source = else_result + .owned_source + .take() + .ok_or_else(|| plan_error("owned else branch has no cleanup source"))?; + self.transfer( + else_result.block, + expression.id.clone(), + source, + destination, + &mut else_result.state, + true, + )?; + } + let state = self.merge_states(&then_result.state, &else_result.state)?; + let join = self.new_block(active_region)?; + let then_join = + self.new_edge(then_result.block, join, EdgeCondition::Always)?; + self.terminate(then_result.block, CleanupTerminator::Goto(then_join))?; + let else_join = + self.new_edge(else_result.block, join, EdgeCondition::Always)?; + self.terminate(else_result.block, CleanupTerminator::Goto(else_join))?; + results.push(EvalResult { + block: join, + state, + owned_source: destination, + }); + } + Frame::Project { expression, field } => { + let base = results.pop().expect("projection base retained"); + let destination = self.expression_slot(expression, active_region)?; + let mut state = base.state; + if let Some(destination) = destination.clone() { + let source = base + .owned_source + .ok_or_else(|| { + plan_error("owned projection base has no cleanup source") + })? + .projected(field.clone()); + self.transfer( + base.block, + expression.id.clone(), + source, + destination, + &mut state, + true, + )?; + } + results.push(EvalResult { + block: base.block, + state, + owned_source: destination, + }); + } + Frame::NativeNext { args, index, flow } => { + if index == args.len() { + results.push(flow); + } else { + frames.push(Frame::NativeAfterArg { args, index }); + frames.push(Frame::Enter { + expression: &args[index], + block: flow.block, + state: flow.state, + }); + } + } + Frame::NativeAfterArg { args, index } => { + let evaluated = results.pop().expect("native argument result retained"); + if evaluated.owned_source.is_some() { + return Err(plan_error( + "native Rust import received a non-scalar argument", + )); + } + frames.push(Frame::NativeNext { + args, + index: index + 1, + flow: evaluated, + }); + } + Frame::CallNext { + expression, + callee, + args, + params, + index, + flow, + commits, + } => { + if index == args.len() { + let mut state = flow.state; + for commit in &commits { + self.consume_place(&commit.source, &mut state, &expression.id)?; + } + self.push_transition( + flow.block, + CleanupTransition::CallCommit { + call: expression.id.clone(), + arguments: commits, + }, + ); + let source = StatusSourceId { + expression: expression.id.clone(), + lane: StatusLane::OperationFailure, + }; + self.add_status_source( + source.clone(), + StatusProducer::PropagatedCall { + callee: callee.clone(), + }, + )?; + let (success, mut success_state) = + self.split_status(flow.block, state, active_region, source)?; + let destination = self.expression_slot(expression, active_region)?; + if let Some(destination) = destination.clone() { + self.initialize( + success, + expression.id.clone(), + destination, + &mut success_state, + )?; + } + results.push(EvalResult { + block: success, + state: success_state, + owned_source: destination, + }); + } else { + let argument = &args[index]; + frames.push(Frame::CallAfterArg { + expression, + callee, + args, + params, + index, + commits, + }); + frames.push(Frame::Enter { + expression: argument, + block: flow.block, + state: flow.state, + }); + } + } + Frame::CallAfterArg { + expression, + callee, + args, + params, + index, + mut commits, + } => { + let argument = &args[index]; + let evaluated = results.pop().expect("call argument result retained"); + let mut state = evaluated.state; + if params[index].ownership == OwnershipMode::Own + && self.needs_drop(¶ms[index].ty)? + { + let source = evaluated.owned_source.ok_or_else(|| { + plan_error(format!( + "owned call argument {} at `{}` has no cleanup source", + index, expression.id + )) + })?; + let epoch = + self.call_argument_slot(expression, index, argument, active_region)?; + self.transfer( + evaluated.block, + argument.id.clone(), + source, + epoch.clone(), + &mut state, + true, + )?; + commits.push(CallArgumentTransfer { + parameter_index: u32::try_from(index) + .map_err(|_| plan_error("too many call arguments"))?, + source: epoch, + }); + } + frames.push(Frame::CallNext { + expression, + callee, + args, + params, + index: index + 1, + flow: EvalResult { + block: evaluated.block, + state, + owned_source: None, + }, + commits, + }); + } + Frame::BlockNext { + expression, + statements, + tail, + index, + flow, + child_region, + destination, + } => { + if index < statements.len() { + let ResolvedStatement::Let { value, .. } = &statements[index]; + frames.push(Frame::BlockAfterStatement { + expression, + statements, + tail, + index, + child_region, + destination, + }); + if active_region != child_region { + frames.push(Frame::RestoreRegion(active_region)); + active_region = child_region; + } + frames.push(Frame::Enter { + expression: value, + block: flow.block, + state: flow.state, + }); + } else { + frames.push(Frame::BlockAfterTail { + expression, + child_region, + destination, + }); + if active_region != child_region { + frames.push(Frame::RestoreRegion(active_region)); + active_region = child_region; + } + frames.push(Frame::Enter { + expression: tail, + block: flow.block, + state: flow.state, + }); + } + } + Frame::BlockAfterStatement { + expression, + statements, + tail, + index, + child_region, + destination, + } => { + let ResolvedStatement::Let { binding, value, .. } = &statements[index]; + let evaluated = results.pop().expect("block statement result retained"); + let mut state = evaluated.state; + if let Some(binding_place) = self.binding_slot(binding, child_region)? { + let source = evaluated.owned_source.ok_or_else(|| { + plan_error(format!( + "owned binding `{}` has no cleanup source", + binding.id + )) + })?; + self.transfer( + evaluated.block, + value.id.clone(), + source, + binding_place, + &mut state, + true, + )?; + } + frames.push(Frame::BlockNext { + expression, + statements, + tail, + index: index + 1, + flow: EvalResult { + block: evaluated.block, + state, + owned_source: None, + }, + child_region, + destination, + }); + } + Frame::BlockAfterTail { + expression, + child_region, + destination, + } => { + let evaluated = results.pop().expect("block tail result retained"); + let mut state = evaluated.state; + if let Some(destination) = destination.clone() { + let source = evaluated + .owned_source + .ok_or_else(|| plan_error("owned block tail has no cleanup source"))?; + self.transfer( + evaluated.block, + expression.id.clone(), + source, + destination, + &mut state, + true, + )?; + } + let (block, state) = self.exit_scope(evaluated.block, state, child_region)?; + results.push(EvalResult { + block, + state, + owned_source: destination, + }); + } + Frame::RecordNext { + expression, + fields, + index, + flow, + destination, + } => { + if index == fields.len() { + let mut state = flow.state; + if let Some(destination) = &destination { + self.canonicalize_complete_aggregate(destination, &mut state)?; + } + results.push(EvalResult { + block: flow.block, + state, + owned_source: destination, + }); + } else { + frames.push(Frame::RecordAfterField { + expression, + fields, + index, + destination, + }); + frames.push(Frame::Enter { + expression: &fields[index].value, + block: flow.block, + state: flow.state, + }); + } + } + Frame::RecordAfterField { + expression, + fields, + index, + destination, + } => { + let initializer = &fields[index]; + let evaluated = results.pop().expect("record field result retained"); + let mut state = evaluated.state; + if initializer.value.ownership == OwnershipMode::Own + && self.needs_drop(&initializer.value.ty)? + { + let source = evaluated.owned_source.ok_or_else(|| { + plan_error(format!( + "record field `{}` has no cleanup source", + initializer.field + )) + })?; + let field_destination = destination + .as_ref() + .ok_or_else(|| { + plan_error("droppable record constructor has no cleanup slot") + })? + .projected(initializer.field.clone()); + self.transfer( + evaluated.block, + initializer.value.id.clone(), + source, + field_destination, + &mut state, + false, + )?; + } + frames.push(Frame::RecordNext { + expression, + fields, + index: index + 1, + flow: EvalResult { + block: evaluated.block, + state, + owned_source: None, + }, + destination, + }); + } + Frame::VariantNext { + fields, + index, + flow, + } => { + if index == fields.len() { + results.push(EvalResult { + block: flow.block, + state: flow.state, + owned_source: None, + }); + } else { + frames.push(Frame::VariantAfterField { fields, index }); + frames.push(Frame::Enter { + expression: &fields[index].value, + block: flow.block, + state: flow.state, + }); + } + } + Frame::VariantAfterField { fields, index } => { + let evaluated = results.pop().expect("variant field result retained"); + let field = &fields[index]; + if field.value.ownership == OwnershipMode::Own + && self.needs_drop(&field.value.ty)? + { + return Err(plan_error( + "droppable variant payload reached the copy-only cleanup slice", + )); + } + frames.push(Frame::VariantNext { + fields, + index: index + 1, + flow: EvalResult { + block: evaluated.block, + state: evaluated.state, + owned_source: None, + }, + }); + } + Frame::TryAfterOperand { + expression, + operand, + result, + ok_case, + ok_field, + err_case, + err_field, + residual_type, + } => { + let evaluated = results.pop().expect("try operand result retained"); + results.push(self.finish_try( + expression, + operand, + result, + ok_case, + ok_field, + err_case, + err_field, + residual_type, + evaluated, + active_region, + )?); + } + Frame::TryOptionAfterOperand { + expression, + operand, + option, + some_case, + some_field, + none_case, + residual_type, + } => { + let evaluated = results.pop().expect("option try operand result retained"); + results.push(self.finish_try_option( + expression, + operand, + option, + some_case, + some_field, + none_case, + residual_type, + evaluated, + active_region, + )?); + } + Frame::MatchAfterScrutinee { scrutinee, arms } => { + let scrutinee_result = results.pop().expect("match scrutinee result retained"); + if scrutinee_result.owned_source.is_some() { + return Err(plan_error( + "droppable match scrutinee reached the copy-only cleanup slice", + )); + } + let is_record = match &scrutinee.ty { + ResolvedType::Nominal { declaration, .. } => self + .program + .declarations + .declaration(declaration) + .is_some_and(|item| item.kind == DeclarationKind::Record), + ResolvedType::Unit + | ResolvedType::I64 + | ResolvedType::Bool + | ResolvedType::TypeParameter { .. } => false, + }; + if is_record { + let [arm] = arms else { + return Err(plan_error( + "irrefutable record match must have exactly one arm", + )); + }; + if matches!(&arm.pattern, ResolvedMatchPattern::Variant { .. }) { + return Err(plan_error("variant pattern has a record match scrutinee")); + } + frames.push(Frame::MatchRecordAfterArm); + frames.push(Frame::Enter { + expression: &arm.value, + block: scrutinee_result.block, + state: scrutinee_result.state, + }); + } else { + frames.push(Frame::MatchNext { + scrutinee, + arms, + index: 0, + decision: scrutinee_result.block, + branch_state: scrutinee_result.state, + arm_results: Vec::with_capacity(arms.len()), + }); + } + } + Frame::MatchRecordAfterArm => { + let result = results.pop().expect("record match arm result retained"); + if result.owned_source.is_some() { + return Err(plan_error( + "droppable record match arm reached the copy-only cleanup slice", + )); + } + results.push(result); + } + Frame::MatchNext { + scrutinee, + arms, + index, + mut decision, + branch_state, + arm_results, + } => { + if index == arms.len() { + let mut arm_results = arm_results.into_iter(); + let first = arm_results.next().ok_or_else(|| { + plan_error("copy-variant match produced no arm result") + })?; + let mut merged_state = first.state.clone(); + let mut completed = vec![first]; + for result in arm_results { + merged_state = self.merge_states(&merged_state, &result.state)?; + completed.push(result); + } + let join = self.new_block(active_region)?; + for result in completed { + let edge = self.new_edge(result.block, join, EdgeCondition::Always)?; + self.terminate(result.block, CleanupTerminator::Goto(edge))?; + } + results.push(EvalResult { + block: join, + state: merged_state, + owned_source: None, + }); + } else { + let arm = &arms[index]; + let final_arm = index + 1 == arms.len(); + let arm_entry = self.new_block(active_region)?; + if final_arm { + let edge = self.new_edge(decision, arm_entry, EdgeCondition::Always)?; + self.terminate(decision, CleanupTerminator::Goto(edge))?; + } else { + let ResolvedMatchPattern::Variant { case, .. } = &arm.pattern else { + return Err(plan_error( + "wildcard match arm must be the final exhaustive arm", + )); + }; + let next_decision = self.new_block(active_region)?; + let selected = self.new_edge( + decision, + arm_entry, + EdgeCondition::VariantCase { + scrutinee: scrutinee.id.clone(), + case: case.clone(), + matches: true, + }, + )?; + let rejected = self.new_edge( + decision, + next_decision, + EdgeCondition::VariantCase { + scrutinee: scrutinee.id.clone(), + case: case.clone(), + matches: false, + }, + )?; + self.terminate( + decision, + CleanupTerminator::Branch(vec![selected, rejected]), + )?; + decision = next_decision; + } + frames.push(Frame::MatchAfterArm { + scrutinee, + arms, + index, + decision, + branch_state: branch_state.clone(), + arm_results, + }); + frames.push(Frame::Enter { + expression: &arm.value, + block: arm_entry, + state: branch_state, + }); + } + } + Frame::MatchAfterArm { + scrutinee, + arms, + index, + decision, + branch_state, + mut arm_results, + } => { + let result = results.pop().expect("match arm result retained"); + if result.owned_source.is_some() { + return Err(plan_error( + "droppable match arm reached the copy-only cleanup slice", + )); + } + arm_results.push(result); + frames.push(Frame::MatchNext { + scrutinee, + arms, + index: index + 1, + decision, + branch_state, + arm_results, + }); + } + Frame::UpdateAfterBase { + expression, + record, + fields, + destination, + update_region, + } => { + let mut evaluated = results.pop().expect("update base result retained"); + let staged_base = if destination.is_some() { + let ResolvedExprKind::UpdateRecord { base, .. } = &expression.kind else { + unreachable!("update continuation retains update expression"); + }; + let staged_base = + CleanupPlace::whole(StorageId::Temporary(base.id.clone())); + self.assign_slot(&staged_base.storage, update_region)?; + let base_source = evaluated.owned_source.clone().ok_or_else(|| { + plan_error("owned record update base has no cleanup source") + })?; + if base_source != staged_base { + self.transfer( + evaluated.block, + base.id.clone(), + base_source, + staged_base.clone(), + &mut evaluated.state, + true, + )?; + } + Some(staged_base) + } else { + None + }; + frames.push(Frame::UpdateNext { + expression, + record, + fields, + index: 0, + flow: evaluated, + destination, + update_region, + staged_base, + replaced: BTreeSet::new(), + }); + } + Frame::UpdateNext { + expression, + record, + fields, + index, + flow, + destination, + update_region, + staged_base, + replaced, + } => { + if index == fields.len() { + if let Some(destination) = destination { + let staged_base = + staged_base.expect("droppable update staged its base"); + let declarations = self + .program + .declarations + .record_fields(record) + .ok_or_else(|| { + plan_error(format!( + "record update has unknown record `{record}`" + )) + })? + .to_vec(); + let mut state = flow.state; + for field in declarations { + if replaced.contains(&field.id) || !self.needs_drop(&field.ty)? { + continue; + } + self.transfer( + flow.block, + expression.id.clone(), + staged_base.projected(field.id.clone()), + destination.projected(field.id), + &mut state, + false, + )?; + } + let (block, mut state) = + self.exit_scope(flow.block, state, update_region)?; + self.canonicalize_complete_aggregate(&destination, &mut state)?; + results.push(EvalResult { + block, + state, + owned_source: Some(destination), + }); + } else { + results.push(EvalResult { + block: flow.block, + state: flow.state, + owned_source: None, + }); + } + } else { + if replaced.contains(&fields[index].field) { + return Err(plan_error(format!( + "record update repeats field `{}`", + fields[index].field + ))); + } + frames.push(Frame::UpdateAfterField { + expression, + record, + fields, + index, + destination, + update_region, + staged_base, + replaced, + }); + if active_region != update_region { + frames.push(Frame::RestoreRegion(active_region)); + active_region = update_region; + } + frames.push(Frame::Enter { + expression: &fields[index].value, + block: flow.block, + state: flow.state, + }); + } + } + Frame::UpdateAfterField { + expression, + record, + fields, + index, + destination, + update_region, + staged_base, + mut replaced, + } => { + let initializer = &fields[index]; + let inserted = replaced.insert(initializer.field.clone()); + debug_assert!(inserted); + let mut evaluated = results.pop().expect("update field result retained"); + if let Some(destination) = &destination { + if initializer.value.ownership == OwnershipMode::Own + && self.needs_drop(&initializer.value.ty)? + { + let source = evaluated.owned_source.clone().ok_or_else(|| { + plan_error(format!( + "record replacement field `{}` has no cleanup source", + initializer.field + )) + })?; + self.transfer( + evaluated.block, + initializer.value.id.clone(), + source, + destination.projected(initializer.field.clone()), + &mut evaluated.state, + false, + )?; + } + } + frames.push(Frame::UpdateNext { + expression, + record, + fields, + index: index + 1, + flow: evaluated, + destination, + update_region, + staged_base, + replaced, + }); + } + } + } + if results.len() != 1 { + return Err(plan_error( + "iterative cleanup lowering lost its root result", + )); + } + results + .pop() + .ok_or_else(|| plan_error("iterative cleanup lowering produced no result")) + } + + #[cfg(test)] + fn lower_expr_recursive_reference( + &mut self, + expression: &ResolvedExpr, + block: BlockId, + state: FlowState, + region: CleanupRegionId, + ) -> Result { + if matches!(expression.kind, ResolvedExprKind::Unary { .. }) { + let mut unary = Vec::new(); + let mut leaf = expression; + while let ResolvedExprKind::Unary { value, .. } = &leaf.kind { + unary.push(leaf); + leaf = value; + } + let mut evaluated = self.lower_expr_recursive_reference(leaf, block, state, region)?; + for expression in unary.into_iter().rev() { + let ResolvedExprKind::Unary { op, .. } = &expression.kind else { + unreachable!("unary chain contains only unary expressions"); + }; + if *op == UnaryOp::Neg { + let source = self.checked_source( + expression, + CheckedOperation::Neg, + vec![StatusCase::NegationOverflow], + )?; + let (block, state) = + self.split_status(evaluated.block, evaluated.state, region, source)?; + evaluated = EvalResult { + block, + state, + owned_source: None, + }; + } else { + evaluated.owned_source = None; + } + } + return Ok(evaluated); + } + match &expression.kind { + ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) => Ok(EvalResult { + block, + state, + owned_source: None, + }), + ResolvedExprKind::Place(place) => { + let owned_source = if expression.ownership == OwnershipMode::Own + && self.needs_drop(&expression.ty)? + { + Some(self.place_from_hir(place)?) + } else { + None + }; + Ok(EvalResult { + block, + state, + owned_source, + }) + } + ResolvedExprKind::Call { + callee, + instance, + args, + .. + } => self.lower_call( + expression, + callee, + instance.as_ref(), + args, + (block, state, region), + ), + ResolvedExprKind::NativeRustImportCall(call) => { + let mut current_block = block; + let mut current_state = state; + for argument in &call.args { + let evaluated = self.lower_expr_recursive_reference( + argument, + current_block, + current_state, + region, + )?; + if evaluated.owned_source.is_some() { + return Err(plan_error( + "native Rust import received a non-scalar argument", + )); + } + current_block = evaluated.block; + current_state = evaluated.state; + } + Ok(EvalResult { + block: current_block, + state: current_state, + owned_source: None, + }) + } + ResolvedExprKind::Unary { .. } => unreachable!("unary chain handled above"), + ResolvedExprKind::Binary { op, left, right } + if matches!(op, BinaryOp::And | BinaryOp::Or) => + { + self.lower_lazy(expression, *op, left, right, block, state, region) + } + ResolvedExprKind::Binary { op, left, right } => { + let left = self.lower_expr_recursive_reference(left, block, state, region)?; + let right = + self.lower_expr_recursive_reference(right, left.block, left.state, region)?; + let checked = match op { + BinaryOp::Add => Some((CheckedOperation::Add, vec![StatusCase::AddOverflow])), + BinaryOp::Sub => Some((CheckedOperation::Sub, vec![StatusCase::SubOverflow])), + BinaryOp::Mul => Some((CheckedOperation::Mul, vec![StatusCase::MulOverflow])), + BinaryOp::Div => Some(( + CheckedOperation::Div, + vec![StatusCase::DivisionByZero, StatusCase::DivisionOverflow], + )), + BinaryOp::Rem => Some(( + CheckedOperation::Rem, + vec![StatusCase::RemainderByZero, StatusCase::RemainderOverflow], + )), + BinaryOp::Eq + | BinaryOp::Ne + | BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge @@ -1309,7 +3421,7 @@ impl<'a> PlanBuilder<'a> { self.lower_update_record(expression, block, state, region) } ResolvedExprKind::Project { base, field } => { - let base = self.lower_expr(base, block, state, region)?; + let base = self.lower_expr_recursive_reference(base, block, state, region)?; let destination = self.expression_slot(expression, region)?; let mut state = base.state; if let Some(destination) = destination.clone() { @@ -1335,6 +3447,7 @@ impl<'a> PlanBuilder<'a> { } } + #[cfg(test)] fn lower_call( &mut self, expression: &ResolvedExpr, @@ -1360,7 +3473,8 @@ impl<'a> PlanBuilder<'a> { let mut commits = Vec::new(); for (index, (argument, parameter)) in args.iter().zip(¶ms).enumerate() { - let evaluated = self.lower_expr(argument, current, current_state, region)?; + let evaluated = + self.lower_expr_recursive_reference(argument, current, current_state, region)?; current = evaluated.block; current_state = evaluated.state; if parameter.ownership == OwnershipMode::Own && self.needs_drop(¶meter.ty)? { @@ -1432,6 +3546,7 @@ impl<'a> PlanBuilder<'a> { } #[allow(clippy::too_many_arguments)] + #[cfg(test)] fn lower_lazy( &mut self, _expression: &ResolvedExpr, @@ -1443,7 +3558,7 @@ impl<'a> PlanBuilder<'a> { region: CleanupRegionId, ) -> Result { let left_expression_id = left.id.clone(); - let left = self.lower_expr(left, block, state, region)?; + let left = self.lower_expr_recursive_reference(left, block, state, region)?; let evaluate_right_when = operation == BinaryOp::And; let evaluate = self.new_block(region)?; let skip = self.new_block(region)?; @@ -1462,7 +3577,8 @@ impl<'a> PlanBuilder<'a> { CleanupTerminator::Branch(vec![evaluate_edge, skip_edge]), )?; - let evaluated_right = self.lower_expr(right, evaluate, left.state.clone(), region)?; + let evaluated_right = + self.lower_expr_recursive_reference(right, evaluate, left.state.clone(), region)?; let joined_state = self.merge_states(&evaluated_right.state, &left.state)?; let join = self.new_block(region)?; let evaluated_edge = self.new_edge(evaluated_right.block, join, EdgeCondition::Always)?; @@ -1479,6 +3595,7 @@ impl<'a> PlanBuilder<'a> { }) } + #[cfg(test)] fn lower_block( &mut self, expression: &ResolvedExpr, @@ -1500,7 +3617,8 @@ impl<'a> PlanBuilder<'a> { let mut current_state = state; for statement in statements { let ResolvedStatement::Let { binding, value, .. } = statement; - let evaluated = self.lower_expr(value, current, current_state, region)?; + let evaluated = + self.lower_expr_recursive_reference(value, current, current_state, region)?; current = evaluated.block; current_state = evaluated.state; if let Some(binding_place) = self.binding_slot(binding, region)? { @@ -1520,7 +3638,8 @@ impl<'a> PlanBuilder<'a> { )?; } } - let evaluated_tail = self.lower_expr(tail, current, current_state, region)?; + let evaluated_tail = + self.lower_expr_recursive_reference(tail, current, current_state, region)?; current = evaluated_tail.block; current_state = evaluated_tail.state; if let Some(destination) = destination.clone() { @@ -1545,6 +3664,7 @@ impl<'a> PlanBuilder<'a> { } #[allow(clippy::too_many_arguments)] + #[cfg(test)] fn lower_if( &mut self, expression: &ResolvedExpr, @@ -1556,7 +3676,7 @@ impl<'a> PlanBuilder<'a> { region: CleanupRegionId, ) -> Result { let destination = self.expression_slot(expression, region)?; - let condition = self.lower_expr(condition, block, state, region)?; + let condition = self.lower_expr_recursive_reference(condition, block, state, region)?; let then_entry = self.new_block(region)?; let else_entry = self.new_block(region)?; let then_edge = self.new_edge( @@ -1574,8 +3694,12 @@ impl<'a> PlanBuilder<'a> { CleanupTerminator::Branch(vec![then_edge, else_edge]), )?; - let mut then_result = - self.lower_expr(then_branch, then_entry, condition.state.clone(), region)?; + let mut then_result = self.lower_expr_recursive_reference( + then_branch, + then_entry, + condition.state.clone(), + region, + )?; if let Some(destination) = destination.clone() { let source = then_result .owned_source @@ -1591,7 +3715,8 @@ impl<'a> PlanBuilder<'a> { )?; } - let mut else_result = self.lower_expr(else_branch, else_entry, condition.state, region)?; + let mut else_result = + self.lower_expr_recursive_reference(else_branch, else_entry, condition.state, region)?; if let Some(destination) = destination.clone() { let source = else_result .owned_source @@ -1620,6 +3745,7 @@ impl<'a> PlanBuilder<'a> { }) } + #[cfg(test)] fn lower_record( &mut self, expression: &ResolvedExpr, @@ -1632,7 +3758,12 @@ impl<'a> PlanBuilder<'a> { let mut current = block; let mut current_state = state; for initializer in fields { - let evaluated = self.lower_expr(&initializer.value, current, current_state, region)?; + let evaluated = self.lower_expr_recursive_reference( + &initializer.value, + current, + current_state, + region, + )?; current = evaluated.block; current_state = evaluated.state; if initializer.value.ownership == OwnershipMode::Own @@ -1673,6 +3804,7 @@ impl<'a> PlanBuilder<'a> { }) } + #[cfg(test)] fn lower_copy_variant( &mut self, fields: &[crate::hir::ResolvedFieldInitializer], @@ -1686,7 +3818,12 @@ impl<'a> PlanBuilder<'a> { owned_source: None, }; for field in fields { - evaluated = self.lower_expr(&field.value, evaluated.block, evaluated.state, region)?; + evaluated = self.lower_expr_recursive_reference( + &field.value, + evaluated.block, + evaluated.state, + region, + )?; if field.value.ownership == OwnershipMode::Own && self.needs_drop(&field.value.ty)? { return Err(plan_error( "droppable variant payload reached the copy-only cleanup slice", @@ -1698,8 +3835,8 @@ impl<'a> PlanBuilder<'a> { } #[allow(clippy::too_many_arguments)] - fn lower_try( - &mut self, + fn check_try_metadata( + &self, expression: &ResolvedExpr, operand: &ResolvedExpr, result: &DeclarationId, @@ -1708,10 +3845,7 @@ impl<'a> PlanBuilder<'a> { err_case: &DeclarationId, err_field: &DeclarationId, residual_type: &ResolvedType, - block: BlockId, - state: FlowState, - region: CleanupRegionId, - ) -> Result { + ) -> Result<(), Diagnostic> { if result.as_str() != prelude::RESULT_ID || ok_case.as_str() != prelude::RESULT_OK_ID || ok_field.as_str() != prelude::RESULT_OK_VALUE_ID @@ -1762,8 +3896,23 @@ impl<'a> PlanBuilder<'a> { )); } } + Ok(()) + } - let evaluated = self.lower_expr(operand, block, state, region)?; + #[allow(clippy::too_many_arguments)] + fn finish_try( + &mut self, + expression: &ResolvedExpr, + operand: &ResolvedExpr, + result: &DeclarationId, + ok_case: &DeclarationId, + ok_field: &DeclarationId, + err_case: &DeclarationId, + err_field: &DeclarationId, + residual_type: &ResolvedType, + evaluated: EvalResult, + region: CleanupRegionId, + ) -> Result { if evaluated.owned_source.is_some() { return Err(plan_error( "postfix `?` operand reached the Copy slice with cleanup storage", @@ -1822,7 +3971,7 @@ impl<'a> PlanBuilder<'a> { } #[allow(clippy::too_many_arguments)] - fn lower_try_option( + fn check_try_option_metadata( &mut self, expression: &ResolvedExpr, operand: &ResolvedExpr, @@ -1831,10 +3980,7 @@ impl<'a> PlanBuilder<'a> { some_field: &DeclarationId, none_case: &DeclarationId, residual_type: &ResolvedType, - block: BlockId, - state: FlowState, - region: CleanupRegionId, - ) -> Result { + ) -> Result<(), Diagnostic> { self.schema = CLEANUP_PLAN_SCHEMA_V3; if option.as_str() != prelude::OPTION_ID || some_case.as_str() != prelude::OPTION_SOME_ID @@ -1882,8 +4028,22 @@ impl<'a> PlanBuilder<'a> { )); } } + Ok(()) + } - let evaluated = self.lower_expr(operand, block, state, region)?; + #[allow(clippy::too_many_arguments)] + fn finish_try_option( + &mut self, + expression: &ResolvedExpr, + operand: &ResolvedExpr, + option: &DeclarationId, + some_case: &DeclarationId, + some_field: &DeclarationId, + none_case: &DeclarationId, + residual_type: &ResolvedType, + evaluated: EvalResult, + region: CleanupRegionId, + ) -> Result { if evaluated.owned_source.is_some() { return Err(plan_error( "Option postfix `?` operand reached the Copy slice with cleanup storage", @@ -1940,15 +4100,261 @@ impl<'a> PlanBuilder<'a> { }) } - fn lower_match( + #[allow(clippy::too_many_arguments)] + #[cfg(test)] + fn lower_try( &mut self, - scrutinee: &ResolvedExpr, - arms: &[ResolvedMatchArm], + expression: &ResolvedExpr, + operand: &ResolvedExpr, + result: &DeclarationId, + ok_case: &DeclarationId, + ok_field: &DeclarationId, + err_case: &DeclarationId, + err_field: &DeclarationId, + residual_type: &ResolvedType, block: BlockId, state: FlowState, region: CleanupRegionId, ) -> Result { - if arms.is_empty() { + if result.as_str() != prelude::RESULT_ID + || ok_case.as_str() != prelude::RESULT_OK_ID + || ok_field.as_str() != prelude::RESULT_OK_VALUE_ID + || err_case.as_str() != prelude::RESULT_ERR_ID + || err_field.as_str() != prelude::RESULT_ERR_ERROR_ID + { + return Err(plan_error( + "postfix `?` does not authenticate the ordinary Result prelude", + )); + } + for id in [result, ok_case, ok_field, err_case, err_field] { + let declaration = self + .program + .declarations + .declaration(id) + .ok_or_else(|| plan_error(format!("postfix `?` references unknown `{id}`")))?; + if declaration.identity_origin != IdentityOrigin::CompilerOwned { + return Err(plan_error(format!( + "postfix `?` reference `{id}` is not compiler-owned" + ))); + } + } + let source_arguments = result_arguments(&operand.ty, result)?; + let target_arguments = result_arguments(residual_type, result)?; + if source_arguments.len() != 2 + || target_arguments.len() != 2 + || source_arguments + .iter() + .chain(target_arguments.iter()) + .any(|argument| !matches!(argument, ResolvedType::I64 | ResolvedType::Bool)) + || expression.ty != source_arguments[0] + || source_arguments[1] != target_arguments[1] + || residual_type != &self.function.return_type + { + return Err(plan_error( + "postfix `?` has inconsistent source, value, residual, or function types", + )); + } + for ty in [&operand.ty, residual_type] { + let facts = self + .program + .declarations + .type_facts(ty) + .ok_or_else(|| plan_error("postfix `?` Result instance has no type facts"))?; + if !facts.copy || !facts.sized || facts.contains_resource || facts.needs_drop { + return Err(plan_error( + "postfix `?` reached cleanup planning outside the Copy Result slice", + )); + } + } + + let evaluated = self.lower_expr_recursive_reference(operand, block, state, region)?; + if evaluated.owned_source.is_some() { + return Err(plan_error( + "postfix `?` operand reached the Copy slice with cleanup storage", + )); + } + let success = self.new_block(region)?; + let residual = self.new_block(region)?; + let success_edge = self.new_edge( + evaluated.block, + success, + EdgeCondition::VariantCase { + scrutinee: operand.id.clone(), + case: ok_case.clone(), + matches: true, + }, + )?; + let residual_edge = self.new_edge( + evaluated.block, + residual, + EdgeCondition::VariantCase { + scrutinee: operand.id.clone(), + case: ok_case.clone(), + matches: false, + }, + )?; + self.terminate( + evaluated.block, + CleanupTerminator::Branch(vec![success_edge, residual_edge]), + )?; + self.push_transition( + residual, + CleanupTransition::StageCopyResult { + source: StagedCopyResultSource::TryResidual { + expression: expression.id.clone(), + operand: operand.id.clone(), + source_instance: operand.ty.clone(), + target_instance: residual_type.clone(), + result: result.clone(), + ok_case: ok_case.clone(), + ok_field: ok_field.clone(), + err_case: err_case.clone(), + err_field: err_field.clone(), + }, + }, + ); + self.pending_try_residuals.push(PendingTryResidual { + block: residual, + state: evaluated.state.clone(), + region, + }); + Ok(EvalResult { + block: success, + state: evaluated.state, + owned_source: None, + }) + } + + #[allow(clippy::too_many_arguments)] + #[cfg(test)] + fn lower_try_option( + &mut self, + expression: &ResolvedExpr, + operand: &ResolvedExpr, + option: &DeclarationId, + some_case: &DeclarationId, + some_field: &DeclarationId, + none_case: &DeclarationId, + residual_type: &ResolvedType, + block: BlockId, + state: FlowState, + region: CleanupRegionId, + ) -> Result { + self.schema = CLEANUP_PLAN_SCHEMA_V3; + if option.as_str() != prelude::OPTION_ID + || some_case.as_str() != prelude::OPTION_SOME_ID + || some_field.as_str() != prelude::OPTION_SOME_VALUE_ID + || none_case.as_str() != prelude::OPTION_NONE_ID + { + return Err(plan_error( + "Option postfix `?` does not authenticate the ordinary Option prelude", + )); + } + for id in [option, some_case, some_field, none_case] { + let declaration = self.program.declarations.declaration(id).ok_or_else(|| { + plan_error(format!("Option postfix `?` references unknown `{id}`")) + })?; + if declaration.identity_origin != IdentityOrigin::CompilerOwned { + return Err(plan_error(format!( + "Option postfix `?` reference `{id}` is not compiler-owned" + ))); + } + } + let source_arguments = option_arguments(&operand.ty, option)?; + let target_arguments = option_arguments(residual_type, option)?; + if source_arguments.len() != 1 + || target_arguments.len() != 1 + || source_arguments + .iter() + .chain(target_arguments.iter()) + .any(|argument| !matches!(argument, ResolvedType::I64 | ResolvedType::Bool)) + || expression.ty != source_arguments[0] + || residual_type != &self.function.return_type + { + return Err(plan_error( + "Option postfix `?` has inconsistent source, value, residual, or function types", + )); + } + for ty in [&operand.ty, residual_type] { + let facts = self + .program + .declarations + .type_facts(ty) + .ok_or_else(|| plan_error("Option postfix `?` instance has no type facts"))?; + if !facts.copy || !facts.sized || facts.contains_resource || facts.needs_drop { + return Err(plan_error( + "Option postfix `?` reached cleanup planning outside the Copy Option slice", + )); + } + } + + let evaluated = self.lower_expr_recursive_reference(operand, block, state, region)?; + if evaluated.owned_source.is_some() { + return Err(plan_error( + "Option postfix `?` operand reached the Copy slice with cleanup storage", + )); + } + let success = self.new_block(region)?; + let residual = self.new_block(region)?; + let success_edge = self.new_edge( + evaluated.block, + success, + EdgeCondition::VariantCase { + scrutinee: operand.id.clone(), + case: some_case.clone(), + matches: true, + }, + )?; + let residual_edge = self.new_edge( + evaluated.block, + residual, + EdgeCondition::VariantCase { + scrutinee: operand.id.clone(), + case: some_case.clone(), + matches: false, + }, + )?; + self.terminate( + evaluated.block, + CleanupTerminator::Branch(vec![success_edge, residual_edge]), + )?; + self.push_transition( + residual, + CleanupTransition::StageCopyResult { + source: StagedCopyResultSource::TryOptionNone { + expression: expression.id.clone(), + operand: operand.id.clone(), + source_instance: operand.ty.clone(), + target_instance: residual_type.clone(), + option: option.clone(), + some_case: some_case.clone(), + some_field: some_field.clone(), + none_case: none_case.clone(), + }, + }, + ); + self.pending_try_residuals.push(PendingTryResidual { + block: residual, + state: evaluated.state.clone(), + region, + }); + Ok(EvalResult { + block: success, + state: evaluated.state, + owned_source: None, + }) + } + + #[cfg(test)] + fn lower_match( + &mut self, + scrutinee: &ResolvedExpr, + arms: &[ResolvedMatchArm], + block: BlockId, + state: FlowState, + region: CleanupRegionId, + ) -> Result { + if arms.is_empty() { return Err(plan_error("copy-variant match has no arms")); } if self.needs_drop(&arms[0].value.ty)? { @@ -1957,7 +4363,8 @@ impl<'a> PlanBuilder<'a> { )); } - let scrutinee_result = self.lower_expr(scrutinee, block, state, region)?; + let scrutinee_result = + self.lower_expr_recursive_reference(scrutinee, block, state, region)?; if scrutinee_result.owned_source.is_some() { return Err(plan_error( "droppable match scrutinee reached the copy-only cleanup slice", @@ -1970,7 +4377,10 @@ impl<'a> PlanBuilder<'a> { .declarations .declaration(declaration) .is_some_and(|item| item.kind == DeclarationKind::Record), - ResolvedType::I64 | ResolvedType::Bool | ResolvedType::TypeParameter { .. } => false, + ResolvedType::Unit + | ResolvedType::I64 + | ResolvedType::Bool + | ResolvedType::TypeParameter { .. } => false, }; if is_record { let [arm] = arms else { @@ -1981,7 +4391,7 @@ impl<'a> PlanBuilder<'a> { if matches!(&arm.pattern, ResolvedMatchPattern::Variant { .. }) { return Err(plan_error("variant pattern has a record match scrutinee")); } - let result = self.lower_expr( + let result = self.lower_expr_recursive_reference( &arm.value, scrutinee_result.block, scrutinee_result.state, @@ -2036,7 +4446,12 @@ impl<'a> PlanBuilder<'a> { decision = next_decision; } - let result = self.lower_expr(&arm.value, arm_entry, branch_state.clone(), region)?; + let result = self.lower_expr_recursive_reference( + &arm.value, + arm_entry, + branch_state.clone(), + region, + )?; if result.owned_source.is_some() { return Err(plan_error( "droppable match arm reached the copy-only cleanup slice", @@ -2061,6 +4476,7 @@ impl<'a> PlanBuilder<'a> { }) } + #[cfg(test)] fn lower_update_record( &mut self, expression: &ResolvedExpr, @@ -2084,10 +4500,14 @@ impl<'a> PlanBuilder<'a> { // order still matters, so walk base then replacements in their authored // order and leave physical value movement to the backend layout lane. let Some(destination) = destination else { - let mut evaluated = self.lower_expr(base, block, state, region)?; + let mut evaluated = self.lower_expr_recursive_reference(base, block, state, region)?; for initializer in fields { - evaluated = - self.lower_expr(&initializer.value, evaluated.block, evaluated.state, region)?; + evaluated = self.lower_expr_recursive_reference( + &initializer.value, + evaluated.block, + evaluated.state, + region, + )?; } return Ok(EvalResult { block: evaluated.block, @@ -2105,7 +4525,8 @@ impl<'a> PlanBuilder<'a> { let edge = self.new_edge(block, entry, EdgeCondition::Always)?; self.terminate(block, CleanupTerminator::Goto(edge))?; - let mut evaluated = self.lower_expr(base, entry, state, update_region)?; + let mut evaluated = + self.lower_expr_recursive_reference(base, entry, state, update_region)?; let staged_base = CleanupPlace::whole(StorageId::Temporary(base.id.clone())); self.assign_slot(&staged_base.storage, update_region)?; let base_source = evaluated @@ -2131,7 +4552,7 @@ impl<'a> PlanBuilder<'a> { initializer.field ))); } - evaluated = self.lower_expr( + evaluated = self.lower_expr_recursive_reference( &initializer.value, evaluated.block, evaluated.state, @@ -2240,3 +4661,271 @@ fn condition_id(expression: &ResolvedExpr) -> Result { fn plan_error(message: impl Into) -> Diagnostic { Diagnostic::io("SPX-H006", format!("cleanup plan: {}", message.into())) } + +#[cfg(test)] +mod iterative_lowering_tests { + use std::path::Path; + + use sha2::{Digest, Sha256}; + + use super::assert_expression_lowering_oracle; + use crate::{hir, parse}; + + #[test] + fn iterative_lowering_private_frame_sizes_stay_within_capacity_formula() { + assert!( + std::mem::size_of::() <= super::CLEANUP_EVAL_RESULT_SIZE_CEILING + ); + } + + #[test] + fn iterative_lowering_matches_recursive_reference_for_every_resolved_body() { + let source = r#" +module test.cleanup_lowering_oracle; +permit { host.echo } +@id("choice") +variant Choice { + @id("choice.a") A { @id("choice.a.v") v: i64, }, + @id("choice.b") B, +} +@id("pair") +record Pair { + @id("pair.a") a: i64, + @id("pair.b") b: i64, +} +@id("host.echo.interface") +interface HostEcho permits { host.echo } { + @id("host.echo") import rust fn host_echo(value: i64) -> i64 + effects { host.echo } + failure status "host.echo.v1"; +} +@id("callee") fn callee(a: i64, b: i64) -> i64 { a + b } +@id("identity") fn identity(value: T) -> T { value } +@id("option_use") fn option_use(value: Option) -> Option { + let checked = value?; + Option::Some { value: checked > 0 } +} +@id("result_use") fn result_use(value: Result) -> Result { + let checked = value?; + Result::Ok { value: checked > 0 } +} +@id("exercise") fn exercise(flag: bool, choice: Choice, pair: Pair) -> i64 + uses { host.echo } +{ + let x = callee(1, 2); + let native = host_echo(identity(x)); + let rebuilt = if flag && true { Choice::A { v: Pair { a: native, b: 3 }.a } } else { choice }; + let y = pair with { b: 4 }.b; + match rebuilt { Choice::A { v } => y + v, Choice::B {} => -y, } +} +@id("main") fn main() -> i64 { 0 } +"#; + crate::cleanup::reset_capacity_high_water(); + let program = + hir::resolve(&parse(source, Path::new("cleanup-lowering-oracle.spx")).unwrap()) + .unwrap(); + super::reset_lower_capacity_high_water(); + for function in &program.functions { + assert_expression_lowering_oracle(&program, function, &function.body); + } + assert!(super::lower_capacity_high_water() > 0); + assert!(crate::cleanup::capacity_high_water() > 0); + } + + #[test] + fn inventory_and_cleanup_capacity_cover_owned_hostile_families() { + let source = include_str!("../../tests/fixtures/native_rust_hir_capacity.spx"); + let parsed = parse(source, Path::new("native-rust-hir-capacity.spx")).unwrap(); + let canonical = crate::format::canonical(&parsed); + assert_eq!( + format!("sha256:{:x}", Sha256::digest(canonical.as_bytes())), + "sha256:2a012464bb1bdb624a79972d558fe837f6d55a9cd9f40d2ead16bfbba615f316", + "shared canonical hostile identity drifted" + ); + crate::cleanup::reset_capacity_high_water(); + super::reset_lower_capacity_high_water(); + let resolved = hir::resolve(&parsed).unwrap(); + assert!(resolved.functions.iter().any(|function| { + function.cleanup.slots.iter().any(|slot| { + matches!( + slot.origin, + crate::cleanup::CleanupStorageOrigin::ProvisionalResult { .. } + ) + }) + })); + assert!(resolved + .functions + .iter() + .any(|function| function.params.len() == 8)); + for identity in [ + "choice.nested", + "scalar.nested-wide", + "generic.calls", + "pair.update", + "token.call", + "option.use", + "result.use", + "host.use", + ] { + assert!( + resolved + .functions + .iter() + .any(|function| function.id.as_str() == identity), + "hostile family `{identity}` was not resolved" + ); + } + assert_eq!(source.matches("generic_identity<").count(), 7); + assert_eq!(resolved.function_instances.len(), 2); + assert!( + resolved + .functions + .iter() + .find(|function| function.id.as_str() == "choice.nested") + .unwrap() + .cleanup + .flags + .len() + >= 8 + ); + assert!(resolved.functions.iter().any(|function| { + function + .cleanup_plan + .blocks + .iter() + .any(|block| matches!(block.terminator, crate::cleanup_plan::CleanupTerminator::Branch(ref edges) if edges.len() >= 2)) + })); + let transitions = resolved + .functions + .iter() + .flat_map(|function| &function.cleanup_plan.blocks) + .flat_map(|block| &block.transitions) + .collect::>(); + assert!(transitions.iter().any(|transition| matches!( + transition, + crate::cleanup_plan::CleanupTransition::CallCommit { arguments, .. } + if arguments.len() >= 2 + ))); + assert!(transitions.iter().any(|transition| matches!( + transition, + crate::cleanup_plan::CleanupTransition::Transfer { source, destination, .. } + if !source.projections.is_empty() || !destination.projections.is_empty() + ))); + assert!(transitions.iter().any(|transition| matches!( + transition, + crate::cleanup_plan::CleanupTransition::StageCopyResult { + source: crate::cleanup_plan::StagedCopyResultSource::TryResidual { .. }, + } + ))); + assert!(transitions.iter().any(|transition| matches!( + transition, + crate::cleanup_plan::CleanupTransition::StageCopyResult { + source: crate::cleanup_plan::StagedCopyResultSource::TryOptionNone { .. }, + } + ))); + assert!(resolved + .functions + .iter() + .any(|function| !function.cleanup_plan.status_sources.is_empty())); + assert!(resolved.functions.iter().any(|function| { + !function.cleanup_plan.regions.is_empty() + && !function.cleanup_plan.edges.is_empty() + && !function.cleanup_plan.exits.is_empty() + })); + let actual = [ + crate::cleanup::capacity_high_water(), + super::lower_capacity_high_water(), + ]; + assert_eq!( + actual, + [8_968, 137_286], + "inventory/lowering owned-capacity high-water pins drifted" + ); + assert!(actual[0] <= 6_492_084); + assert!(actual[1] <= 6_821_908); + } + + #[test] + fn long_identity_cleanup_dag_owned_census_covers_many_deep_roots() { + use std::fmt::Write as _; + + fn long_id(family: &str, index: usize) -> String { + format!("{family}.{index:03}.{}", "x".repeat(160)) + } + + let mut source = String::from("module cleanup.long_ids;\n"); + writeln!( + source, + "@id(\"{}\") resource R0 {{ @id(\"{}\") drop trivial; }}", + long_id("resource", 0), + long_id("lifecycle", 0) + ) + .unwrap(); + for index in 1..514 { + writeln!( + source, + "@id(\"{}\") record R{index} {{ @id(\"{}\") value: R{}, }}", + long_id("record", index), + long_id("field", index), + index - 1 + ) + .unwrap(); + } + let parameters = (0..8) + .map(|index| format!("p{index}: own R513")) + .collect::>() + .join(", "); + writeln!( + source, + "@id(\"{}\") fn consume({parameters}) -> i64 {{ 0 }}", + long_id("consume", 0) + ) + .unwrap(); + source.push_str("@id(\"app.main\") fn main() -> i64 { 0 }\n"); + + let parsed = parse(&source, Path::new("cleanup-long-ids.spx")).unwrap(); + let canonical = crate::format::canonical(&parsed); + let canonical_digest = format!("sha256:{:x}", Sha256::digest(canonical.as_bytes())); + assert!(canonical.len() < 1_048_576); + crate::cleanup::reset_capacity_high_water(); + super::reset_lower_capacity_high_water(); + let resolved = hir::resolve(&parsed).unwrap(); + let function = resolved + .functions + .iter() + .find(|function| function.name == "consume") + .unwrap(); + assert_eq!(function.params.len(), 8); + assert_eq!(function.cleanup.slots.len(), 8); + assert_eq!(function.cleanup_plan.slots.len(), 8); + + let mut maximum_shape_depth = 0usize; + let mut pending = function + .cleanup_plan + .slots + .iter() + .map(|slot| (&slot.field_liveness_shape, 1usize)) + .collect::>(); + while let Some((shape, depth)) = pending.pop() { + maximum_shape_depth = maximum_shape_depth.max(depth); + if let crate::cleanup::FieldLivenessShape::Record { fields, .. } = shape { + pending.extend(fields.iter().map(|field| (&field.shape, depth + 1))); + } + } + assert_eq!(maximum_shape_depth, 514); + + let inventory_owned = + crate::private_capacity_contract::cleanup_inventory_owned_capacity(&function.cleanup) + .unwrap(); + let plan_owned = + crate::private_capacity_contract::cleanup_plan_owned_capacity(&function.cleanup_plan) + .unwrap(); + let inventory_water = crate::cleanup::capacity_high_water(); + let lower_water = super::lower_capacity_high_water(); + assert!(inventory_owned > 8 * 514 * 160); + assert!(plan_owned > 8 * 514 * 160); + assert!(inventory_water >= inventory_owned); + assert!(lower_water >= plan_owned); + assert_eq!(canonical_digest.len(), 71); + } +} diff --git a/src/cleanup_plan/execute.rs b/src/cleanup_plan/execute.rs index 163848c..9f35fad 100644 --- a/src/cleanup_plan/execute.rs +++ b/src/cleanup_plan/execute.rs @@ -265,6 +265,11 @@ fn collect_variant_domains( visit(program, argument, domains)?; } } + hir::ResolvedExprKind::NativeRustImportCall(call) => { + for argument in &call.args { + visit(program, argument, domains)?; + } + } hir::ResolvedExprKind::Unary { value, .. } | hir::ResolvedExprKind::Project { base: value, .. } => { visit(program, value, domains)?; @@ -604,6 +609,10 @@ fn find_expression_by<'a>( hir::ResolvedExprKind::Call { args, .. } => args .iter() .find_map(|argument| find_expression_by(argument, predicate)), + hir::ResolvedExprKind::NativeRustImportCall(call) => call + .args + .iter() + .find_map(|argument| find_expression_by(argument, predicate)), hir::ResolvedExprKind::Unary { value, .. } | hir::ResolvedExprKind::Project { base: value, .. } | hir::ResolvedExprKind::Try { operand: value, .. } @@ -1216,6 +1225,7 @@ impl<'a> Executor<'a> { result: &TraceResult, ) -> Result<(), CleanupExecutionError> { let matches_type = match (&self.function.return_type, result) { + (ResolvedType::Unit, _) => false, (ResolvedType::I64, TraceResult::I64(_)) | (ResolvedType::Bool, TraceResult::Bool(_)) => true, (ResolvedType::Nominal { declaration, .. }, TraceResult::Owned { type_id }) => { @@ -1229,6 +1239,8 @@ impl<'a> Executor<'a> { storage.storage == StorageId::ProvisionalResult && storage.projections.is_empty() } (CleanupResultSource::Scalar { .. }, ResolvedType::Nominal { .. }) + | (CleanupResultSource::Scalar { .. }, ResolvedType::Unit) + | (CleanupResultSource::Owned { .. }, ResolvedType::Unit) | (CleanupResultSource::Owned { .. }, ResolvedType::I64 | ResolvedType::Bool) | (_, ResolvedType::TypeParameter { .. }) => false, }; diff --git a/src/cleanup_plan/replay.rs b/src/cleanup_plan/replay.rs index 2392c14..b128ac5 100644 --- a/src/cleanup_plan/replay.rs +++ b/src/cleanup_plan/replay.rs @@ -7,6 +7,9 @@ use std::collections::{BTreeMap, BTreeSet, VecDeque}; +#[cfg(test)] +use std::cell::Cell; + use crate::ast::{BinaryOp, UnaryOp}; use crate::cleanup::{CleanupStorageOrigin, FieldLiveness, FieldLivenessShape, LivenessFlagId}; use crate::diagnostic::Diagnostic; @@ -26,16 +29,29 @@ use super::{ }; const MAX_REPLAY_PATHS: usize = 65_536; -const MAX_REPLAY_WORK_UNITS: usize = 1_000_000; +// Independent fail-closed work cap. Valid admitted shapes are preflighted +// before path materialization; depth alone is not a work bound because wide +// calls and blocks can emit several observations per node. +const MAX_REPLAY_WORK_UNITS: usize = 8_000_000; struct ReplayBudget { remaining: usize, + skeleton_remaining: usize, } impl ReplayBudget { fn new() -> Self { Self { remaining: MAX_REPLAY_WORK_UNITS, + skeleton_remaining: 0, + } + } + + #[cfg(test)] + fn with_skeleton_limit(limit: usize) -> Self { + Self { + remaining: MAX_REPLAY_WORK_UNITS, + skeleton_remaining: limit, } } @@ -53,6 +69,93 @@ impl ReplayBudget { })?; Ok(()) } + + fn reserve_skeleton( + &mut self, + function: &ResolvedFunction, + units: usize, + ) -> Result<(), Diagnostic> { + self.remaining = self.remaining.checked_sub(units).ok_or_else(|| { + replay_error( + function, + "cleanup replay skeleton-work preflight exceeds the global budget", + ) + })?; + self.skeleton_remaining = units; + Ok(()) + } + + fn charge_skeleton( + &mut self, + function: &ResolvedFunction, + units: usize, + phase: &str, + ) -> Result<(), Diagnostic> { + self.skeleton_remaining = self.skeleton_remaining.checked_sub(units).ok_or_else(|| { + replay_error( + function, + format!("cleanup replay work budget exhausted during {phase}"), + ) + })?; + Ok(()) + } +} + +#[cfg(test)] +thread_local! { + static SKELETON_MATERIALIZATIONS: Cell = const { Cell::new(0) }; +} + +fn note_skeleton_materialization() { + #[cfg(test)] + SKELETON_MATERIALIZATIONS.with(|count| count.set(count.get().saturating_add(1))); +} + +#[cfg(test)] +fn reset_skeleton_materializations() { + SKELETON_MATERIALIZATIONS.with(|count| count.set(0)); +} + +#[cfg(test)] +fn skeleton_materializations() -> usize { + SKELETON_MATERIALIZATIONS.with(Cell::get) +} + +fn skeleton_clone( + budget: &mut ReplayBudget, + function: &ResolvedFunction, + value: &T, + phase: &str, +) -> Result { + budget.charge_skeleton(function, 1, phase)?; + note_skeleton_materialization(); + Ok(value.clone()) +} + +fn skeleton_push( + budget: &mut ReplayBudget, + function: &ResolvedFunction, + target: &mut Vec, + value: T, + phase: &str, +) -> Result<(), Diagnostic> { + budget.charge_skeleton(function, 1, phase)?; + note_skeleton_materialization(); + target.push(value); + Ok(()) +} + +fn skeleton_queue_push( + budget: &mut ReplayBudget, + function: &ResolvedFunction, + target: &mut VecDeque, + value: T, + phase: &str, +) -> Result<(), Diagnostic> { + budget.charge_skeleton(function, 1, phase)?; + note_skeleton_materialization(); + target.push_back(value); + Ok(()) } #[derive(Clone)] @@ -128,6 +231,13 @@ struct ExprSkeletonPath { residual: bool, } +type BooleanSkeletonSplit = ( + Vec, + Vec, + Vec, +); +type CallSkeletonState = (ExprSkeletonPath, Vec<(u32, CleanupPlace)>); + /// Validate the structure of the cleanup plan attached to `function` without /// rebuilding it from HIR. #[cfg(test)] @@ -136,11 +246,22 @@ fn validate_structure( function: &ResolvedFunction, ) -> Result<(), Diagnostic> { let mut budget = ReplayBudget::new(); + reserve_program_skeleton_work(program, std::iter::once(function), &mut budget)?; validate_structure_with_budget(program, function, &mut budget) } pub(super) fn validate_program(program: &ResolvedProgram) -> Result<(), Diagnostic> { let mut budget = ReplayBudget::new(); + reserve_program_skeleton_work( + program, + program.functions.iter().chain( + program + .function_instances + .iter() + .map(|instance| &instance.function), + ), + &mut budget, + )?; for function in &program.functions { validate_structure_with_budget(program, function, &mut budget)?; } @@ -150,6 +271,35 @@ pub(super) fn validate_program(program: &ResolvedProgram) -> Result<(), Diagnost Ok(()) } +fn reserve_program_skeleton_work<'a>( + program: &ResolvedProgram, + functions: impl IntoIterator, + budget: &mut ReplayBudget, +) -> Result { + let mut total = 0usize; + let mut first = None; + for function in functions { + first.get_or_insert(function); + let function_upper = skeleton_work_upper(program, function)?; + total = total.checked_add(function_upper).ok_or_else(|| { + replay_error( + function, + "cleanup replay program-wide skeleton-work preflight overflowed", + ) + })?; + if total > MAX_REPLAY_WORK_UNITS { + return Err(replay_error( + function, + "cleanup replay program-wide skeleton-work preflight exceeds the global budget", + )); + } + } + if let Some(function) = first { + budget.reserve_skeleton(function, total)?; + } + Ok(total) +} + fn validate_structure_with_budget( program: &ResolvedProgram, function: &ResolvedFunction, @@ -226,45 +376,22 @@ fn validate_replay_size_budget(function: &ResolvedFunction) -> Result<(), Diagno "cleanup replay structure exceeds the global work budget", )); } - let plan_boolean_splits = function - .cleanup_plan - .edges - .iter() - .filter(|edge| { - matches!( - edge.condition, - EdgeCondition::BooleanResult(_, true) - | EdgeCondition::VariantCase { matches: true, .. } - ) - }) - .count(); - let hir_boolean_splits = function - .requires - .iter() - .chain(std::iter::once(&function.body)) - .chain(&function.ensures) - .fold( - function - .requires - .len() - .saturating_add(function.ensures.len()), - |total, expression| total.saturating_add(expression_boolean_splits(expression)), - ); - let boolean_splits = plan_boolean_splits.max(hir_boolean_splits); - let status_splits = function.cleanup_plan.status_sources.len(); - let boolean_paths = 1_usize - .checked_shl(u32::try_from(boolean_splits).unwrap_or(u32::MAX)) - .unwrap_or(usize::MAX); - let path_bound = boolean_paths.saturating_mul(status_splits.saturating_add(1)); - if path_bound > MAX_REPLAY_PATHS { + let cfg = branch_sensitive_cfg_bounds(function)?; + if cfg.terminal_paths > MAX_REPLAY_PATHS { return Err(replay_error( function, "cleanup replay path bound exceeds the global path budget", )); } + let semantic_paths = hir_terminal_path_bound(function)?; + if semantic_paths > MAX_REPLAY_PATHS { + return Err(replay_error( + function, + "cleanup replay semantic path bound exceeds the global path budget", + )); + } let expression_units = expression_facts(function)?.len(); - let per_path_units = structure_units.max(expression_units).max(1); - if per_path_units.saturating_mul(path_bound) > MAX_REPLAY_WORK_UNITS { + if cfg.work.saturating_add(expression_units) > MAX_REPLAY_WORK_UNITS { return Err(replay_error( function, "cleanup replay combined path/work bound exceeds the global budget", @@ -273,59 +400,547 @@ fn validate_replay_size_budget(function: &ResolvedFunction) -> Result<(), Diagno Ok(()) } -fn expression_boolean_splits(expression: &ResolvedExpr) -> usize { - match &expression.kind { - ResolvedExprKind::Call { args, .. } => args.iter().fold(0_usize, |total, argument| { - total.saturating_add(expression_boolean_splits(argument)) - }), - ResolvedExprKind::Unary { value, .. } | ResolvedExprKind::Project { base: value, .. } => { - expression_boolean_splits(value) - } - ResolvedExprKind::Binary { op, left, right } => { - let own = usize::from(matches!(op, BinaryOp::And | BinaryOp::Or)); - own.saturating_add(expression_boolean_splits(left)) - .saturating_add(expression_boolean_splits(right)) - } - ResolvedExprKind::Block { statements, tail } => { - statements +struct CfgReplayBounds { + terminal_paths: usize, + work: usize, +} + +/// Bound actual CFG traversal work without multiplying every structure item by +/// every terminal path. The cleanup graph is authenticated as acyclic later; +/// this independent saturating propagation is deliberately cycle-safe and +/// fails closed if a forged graph keeps increasing multiplicity. +fn branch_sensitive_cfg_bounds(function: &ResolvedFunction) -> Result { + let plan = &function.cleanup_plan; + let invalid = || { + replay_error( + function, + "cleanup replay preflight references an unknown id", + ) + }; + let entry = plan + .blocks + .get(plan.entry.0 as usize) + .map(|_| plan.entry.0 as usize) + .ok_or_else(invalid)?; + let mut successors = vec![Vec::::new(); plan.blocks.len()]; + let mut terminal = vec![false; plan.blocks.len()]; + for (index, block) in plan.blocks.iter().enumerate() { + let targets = match &block.terminator { + CleanupTerminator::Goto(edge) => plan + .edges + .get(edge.0 as usize) + .map(|edge| vec![edge.to.0 as usize]) + .ok_or_else(invalid)?, + CleanupTerminator::Branch(edges) => edges .iter() - .fold(expression_boolean_splits(tail), |total, statement| { - let ResolvedStatement::Let { value, .. } = statement; - total.saturating_add(expression_boolean_splits(value)) + .map(|edge| { + plan.edges + .get(edge.0 as usize) + .map(|edge| edge.to.0 as usize) + .ok_or_else(invalid) }) + .collect::, _>>()?, + CleanupTerminator::Exit(exit) => { + match &plan + .exits + .get(exit.0 as usize) + .ok_or_else(invalid)? + .continuation + { + ExitContinuation::Continue(edge) => plan + .edges + .get(edge.0 as usize) + .map(|edge| vec![edge.to.0 as usize]) + .ok_or_else(invalid)?, + ExitContinuation::CommitResult { .. } + | ExitContinuation::ReturnUnit + | ExitContinuation::ReturnFailure { .. } => { + terminal[index] = true; + Vec::new() + } + } + } + }; + if targets.iter().any(|target| *target >= plan.blocks.len()) { + return Err(invalid()); } - ResolvedExprKind::If { - condition, - then_branch, - else_branch, - } => 1_usize - .saturating_add(expression_boolean_splits(condition)) - .saturating_add(expression_boolean_splits(then_branch)) - .saturating_add(expression_boolean_splits(else_branch)), - ResolvedExprKind::ConstructRecord { fields, .. } => { - fields.iter().fold(0_usize, |total, field| { - total.saturating_add(expression_boolean_splits(&field.value)) - }) + successors[index] = targets; + } + + let mut reachable = vec![false; plan.blocks.len()]; + let mut discover = vec![entry]; + while let Some(index) = discover.pop() { + if std::mem::replace(&mut reachable[index], true) { + continue; } - ResolvedExprKind::ConstructVariant { fields, .. } => { - fields.iter().fold(0_usize, |total, field| { - total.saturating_add(expression_boolean_splits(&field.value)) - }) + discover.extend(successors[index].iter().copied()); + } + let mut indegree = vec![0_usize; plan.blocks.len()]; + for (index, targets) in successors.iter().enumerate() { + if reachable[index] { + for target in targets { + indegree[*target] = indegree[*target].saturating_add(1); + } + } + } + let mut pending = VecDeque::from([entry]); + let mut incoming = vec![0_usize; plan.blocks.len()]; + incoming[entry] = 1; + let mut total = 0_usize; + let mut terminal_paths = 0_usize; + let mut visited = 0_usize; + let ceiling = MAX_REPLAY_WORK_UNITS.saturating_add(1); + + while let Some(index) = pending.pop_front() { + visited = visited.saturating_add(1); + let paths = incoming[index]; + let block = &plan.blocks[index]; + let local = block.transitions.len().saturating_add(1); + total = total + .saturating_add(local.saturating_mul(paths)) + .min(ceiling); + if terminal[index] { + terminal_paths = terminal_paths.saturating_add(paths); + } + for successor in &successors[index] { + incoming[*successor] = incoming[*successor].saturating_add(paths).min(ceiling); + indegree[*successor] = indegree[*successor].saturating_sub(1); + if indegree[*successor] == 0 { + pending.push_back(*successor); + } } - ResolvedExprKind::Try { operand, .. } | ResolvedExprKind::TryOption { operand, .. } => { - expression_boolean_splits(operand).saturating_add(1) + if total >= ceiling { + return Ok(CfgReplayBounds { + terminal_paths: MAX_REPLAY_PATHS.saturating_add(1), + work: ceiling, + }); } - ResolvedExprKind::Match { scrutinee, arms } => arms.iter().fold( - expression_boolean_splits(scrutinee).saturating_add(arms.len().saturating_sub(1)), - |total, arm| total.saturating_add(expression_boolean_splits(&arm.value)), - ), - ResolvedExprKind::UpdateRecord { base, fields, .. } => fields + } + if visited != reachable.iter().filter(|reachable| **reachable).count() { + return Err(replay_error(function, "cleanup CFG contains a cycle")); + } + Ok(CfgReplayBounds { + terminal_paths, + work: total, + }) +} + +#[derive(Clone, Copy, Default)] +struct HirPathCounts { + normal: usize, + failed: usize, + residual: usize, +} + +impl HirPathCounts { + const ONE: Self = Self { + normal: 1, + failed: 0, + residual: 0, + }; + + fn total(self) -> usize { + self.normal + .saturating_add(self.failed) + .saturating_add(self.residual) + } +} + +fn sequence_path_counts(left: HirPathCounts, right: HirPathCounts) -> HirPathCounts { + HirPathCounts { + normal: left.normal.saturating_mul(right.normal), + failed: left + .failed + .saturating_add(left.normal.saturating_mul(right.failed)), + residual: left + .residual + .saturating_add(left.normal.saturating_mul(right.residual)), + } +} + +fn hir_terminal_path_bound(function: &ResolvedFunction) -> Result { + let mut paths = HirPathCounts::ONE; + for contract in &function.requires { + paths = sequence_path_counts(paths, expression_path_counts(function, contract)?); + paths.failed = paths.failed.saturating_add(paths.normal); + } + paths = sequence_path_counts(paths, expression_path_counts(function, &function.body)?); + // Residual paths terminate at the function boundary; successful body paths + // continue through postconditions. + for contract in &function.ensures { + paths = sequence_path_counts(paths, expression_path_counts(function, contract)?); + paths.failed = paths.failed.saturating_add(paths.normal); + } + Ok(paths.total()) +} + +fn skeleton_work_upper( + program: &ResolvedProgram, + function: &ResolvedFunction, +) -> Result { + let semantic_paths = hir_terminal_path_bound(function)?.max(1); + if semantic_paths > MAX_REPLAY_PATHS { + return Ok(0); + } + let mut hir_upper = checked_skeleton_mul(function, 4, semantic_paths)?; + for expression in function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures) + { + hir_upper = checked_skeleton_add( + function, + hir_upper, + expression_skeleton_work_upper(program, function, expression)?, + )?; + } + hir_upper = checked_skeleton_add( + function, + hir_upper, + checked_skeleton_mul( + function, + function + .requires + .len() + .checked_add(function.ensures.len()) + .and_then(|contracts| contracts.checked_mul(6)) + .ok_or_else(|| skeleton_preflight_overflow(function))?, + semantic_paths, + )?, + )?; + + let cfg = match branch_sensitive_cfg_bounds(function) { + Ok(cfg) if cfg.terminal_paths <= MAX_REPLAY_PATHS => cfg, + Ok(_) | Err(_) => return Ok(0), + }; + let mut max_unit_weight = 10usize; + for block in &function.cleanup_plan.blocks { + for transition in &block.transitions { + let weight = match transition { + CleanupTransition::Initialize { .. } => 4, + CleanupTransition::Transfer { .. } => 5, + CleanupTransition::CallCommit { arguments, .. } => arguments + .len() + .checked_mul(2) + .and_then(|arguments| arguments.checked_add(4)) + .ok_or_else(|| skeleton_preflight_overflow(function))?, + CleanupTransition::SelectFailure { .. } => 1, + CleanupTransition::StageCopyResult { .. } => 3, + }; + max_unit_weight = max_unit_weight.max(weight); + } + } + let plan_expansion = checked_skeleton_mul(function, cfg.work.max(1), max_unit_weight)?; + let plan_terminals = checked_skeleton_mul(function, cfg.terminal_paths.max(1), 4)?; + let comparison = checked_skeleton_mul(function, semantic_paths.max(cfg.terminal_paths), 2)?; + checked_skeleton_add( + function, + checked_skeleton_add(function, hir_upper, plan_expansion)?, + checked_skeleton_add(function, plan_terminals, comparison)?, + ) +} + +fn expression_skeleton_work_upper( + program: &ResolvedProgram, + function: &ResolvedFunction, + root: &ResolvedExpr, +) -> Result { + let mut stack = [None; 515]; + stack[0] = Some((root, 0usize)); + let mut len = 1usize; + let mut weight = 0usize; + while len != 0 { + let (expression, next) = stack[len - 1] + .as_mut() + .expect("skeleton census frame retained"); + if *next == 0 { + let local = match &expression.kind { + ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) => 2, + ResolvedExprKind::Place(place) => place.projections.len().saturating_mul(2) + 8, + ResolvedExprKind::Unary { .. } => 8, + ResolvedExprKind::Binary { .. } => 12, + ResolvedExprKind::Call { args, .. } => args.len().saturating_mul(6) + 14, + ResolvedExprKind::NativeRustImportCall(call) => { + call.args.len().saturating_mul(4) + 8 + } + ResolvedExprKind::Block { statements, .. } => { + statements.len().saturating_mul(4) + 5 + } + ResolvedExprKind::ConstructVariant { fields, .. } + | ResolvedExprKind::ConstructRecord { fields, .. } => { + fields.len().saturating_mul(4) + 6 + } + ResolvedExprKind::UpdateRecord { record, fields, .. } => checked_skeleton_add( + function, + fields + .len() + .checked_mul(5) + .and_then(|fields| fields.checked_add(8)) + .ok_or_else(|| skeleton_preflight_overflow(function))?, + untouched_update_field_work_upper( + program, function, expression, record, fields, + )?, + )?, + ResolvedExprKind::Try { .. } | ResolvedExprKind::TryOption { .. } => 10, + ResolvedExprKind::Project { .. } => 6, + ResolvedExprKind::If { .. } => 10, + ResolvedExprKind::Match { arms, .. } => arms.len().saturating_mul(4) + 8, + }; + let paths = expression_path_counts(function, expression)?.total().max(1); + if paths > MAX_REPLAY_PATHS { + return Ok(0); + } + weight = checked_skeleton_add( + function, + weight, + checked_skeleton_mul(function, local, paths)?, + )?; + } + if let Some(child) = replay_expression_child(expression, *next) { + *next += 1; + if len == stack.len() { + return Err(replay_error( + function, + "typed-HIR skeleton-work census exceeds the admitted expression depth", + )); + } + stack[len] = Some((child, 0)); + len += 1; + } else { + len -= 1; + stack[len] = None; + } + } + Ok(weight) +} + +fn untouched_update_field_work_upper( + program: &ResolvedProgram, + function: &ResolvedFunction, + expression: &ResolvedExpr, + record: &DeclarationId, + replacements: &[crate::hir::ResolvedFieldInitializer], +) -> Result { + if expression.ownership != OwnershipMode::Own + || !type_needs_drop(program, function, &expression.ty)? + { + return Ok(0); + } + let declarations = program.declarations.record_fields(record).ok_or_else(|| { + replay_error( + function, + format!("record update has unknown record `{record}`"), + ) + })?; + let mut untouched_droppable = 0usize; + for field in declarations { + if replacements .iter() - .fold(expression_boolean_splits(base), |total, field| { - total.saturating_add(expression_boolean_splits(&field.value)) - }), - ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) | ResolvedExprKind::Place(_) => 0, + .any(|replacement| replacement.field == field.id) + || !type_needs_drop(program, function, &field.ty)? + { + continue; + } + untouched_droppable = untouched_droppable + .checked_add(1) + .ok_or_else(|| skeleton_preflight_overflow(function))?; + } + let active_paths = expression_path_counts(function, expression)?.normal; + checked_skeleton_mul( + function, + checked_skeleton_mul(function, untouched_droppable, active_paths)?, + 8, + ) +} + +fn checked_skeleton_add( + function: &ResolvedFunction, + left: usize, + right: usize, +) -> Result { + left.checked_add(right) + .ok_or_else(|| skeleton_preflight_overflow(function)) +} + +fn checked_skeleton_mul( + function: &ResolvedFunction, + left: usize, + right: usize, +) -> Result { + left.checked_mul(right) + .ok_or_else(|| skeleton_preflight_overflow(function)) +} + +fn skeleton_preflight_overflow(function: &ResolvedFunction) -> Diagnostic { + replay_error( + function, + "cleanup replay program-wide skeleton-work preflight overflowed", + ) +} + +fn expression_path_counts( + function: &ResolvedFunction, + expression: &ResolvedExpr, +) -> Result { + #[derive(Clone, Copy)] + struct Frame<'a> { + expression: &'a ResolvedExpr, + next: usize, + accumulator: HirPathCounts, + first: HirPathCounts, + } + let mut stack = [None; 514]; + stack[0] = Some(Frame { + expression, + next: 0, + accumulator: HirPathCounts::ONE, + first: HirPathCounts::default(), + }); + let mut len = 1usize; + let mut result = HirPathCounts::ONE; + while len != 0 { + len -= 1; + let mut frame = stack[len].take().expect("path-count frame retained"); + if frame.next != 0 { + let child_index = frame.next - 1; + match &frame.expression.kind { + ResolvedExprKind::If { .. } => match child_index { + 0 => frame.first = result, + 1 => frame.accumulator = result, + 2 => { + let condition = frame.first; + frame.accumulator = HirPathCounts { + normal: condition.normal.saturating_mul( + frame.accumulator.normal.saturating_add(result.normal), + ), + failed: condition.failed.saturating_add( + condition.normal.saturating_mul( + frame.accumulator.failed.saturating_add(result.failed), + ), + ), + residual: condition.residual.saturating_add( + condition.normal.saturating_mul( + frame.accumulator.residual.saturating_add(result.residual), + ), + ), + }; + } + _ => unreachable!(), + }, + ResolvedExprKind::Binary { + op: BinaryOp::And | BinaryOp::Or, + .. + } => { + if child_index == 0 { + frame.first = result; + } else { + let left = frame.first; + frame.accumulator = HirPathCounts { + normal: left.normal.saturating_mul(result.normal.saturating_add(1)), + failed: left + .failed + .saturating_add(left.normal.saturating_mul(result.failed)), + residual: left + .residual + .saturating_add(left.normal.saturating_mul(result.residual)), + }; + } + } + ResolvedExprKind::Match { .. } => { + if child_index == 0 { + frame.first = result; + frame.accumulator = HirPathCounts::default(); + } else { + frame.accumulator.normal = + frame.accumulator.normal.saturating_add(result.normal); + frame.accumulator.failed = + frame.accumulator.failed.saturating_add(result.failed); + frame.accumulator.residual = + frame.accumulator.residual.saturating_add(result.residual); + } + } + _ => frame.accumulator = sequence_path_counts(frame.accumulator, result), + } + } + if frame.accumulator.total() > MAX_REPLAY_PATHS || frame.first.total() > MAX_REPLAY_PATHS { + return Ok(HirPathCounts { + normal: MAX_REPLAY_PATHS + 1, + failed: 0, + residual: 0, + }); + } + if let Some(child) = replay_expression_child(frame.expression, frame.next) { + if len + 2 > stack.len() { + return Err(replay_error( + function, + "typed-HIR path census exceeds the admitted expression depth", + )); + } + frame.next += 1; + stack[len] = Some(frame); + stack[len + 1] = Some(Frame { + expression: child, + next: 0, + accumulator: HirPathCounts::ONE, + first: HirPathCounts::default(), + }); + len += 2; + continue; + } + result = match &frame.expression.kind { + ResolvedExprKind::Call { .. } => HirPathCounts { + normal: frame.accumulator.normal, + failed: frame + .accumulator + .failed + .saturating_add(frame.accumulator.normal), + residual: frame.accumulator.residual, + }, + ResolvedExprKind::Unary { + op: UnaryOp::Neg, .. + } + | ResolvedExprKind::Binary { + op: BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Rem, + .. + } => HirPathCounts { + normal: frame.accumulator.normal, + failed: frame + .accumulator + .failed + .saturating_add(frame.accumulator.normal), + residual: frame.accumulator.residual, + }, + ResolvedExprKind::Try { .. } | ResolvedExprKind::TryOption { .. } => HirPathCounts { + normal: frame.accumulator.normal, + failed: frame.accumulator.failed, + residual: frame + .accumulator + .residual + .saturating_add(frame.accumulator.normal), + }, + ResolvedExprKind::If { .. } + | ResolvedExprKind::Binary { + op: BinaryOp::And | BinaryOp::Or, + .. + } => frame.accumulator, + ResolvedExprKind::Match { .. } => HirPathCounts { + normal: frame.first.normal.saturating_mul(frame.accumulator.normal), + failed: frame + .first + .failed + .saturating_add(frame.first.normal.saturating_mul(frame.accumulator.failed)), + residual: frame.first.residual.saturating_add( + frame + .first + .normal + .saturating_mul(frame.accumulator.residual), + ), + }, + _ => frame.accumulator, + }; } + Ok(result) } fn validate_inventory_coverage( @@ -462,32 +1077,76 @@ fn collect_supplemental_slots( next_flag: &mut u32, slots: &mut Vec, ) -> Result<(), Diagnostic> { - match &expression.kind { - ResolvedExprKind::Call { - callee, - instance, - args, - .. - } => { - let target = program - .resolve_call_target(callee, instance.as_ref()) - .ok_or_else(|| { - replay_error( - function, - format!( - "cleanup call `{}` has unknown callee `{callee}`", - expression.id - ), - ) - })?; - if target.params.len() != args.len() { - return Err(replay_error( - function, - format!("cleanup call `{}` has inconsistent arity", expression.id), - )); + enum Frame<'a> { + Expr(&'a ResolvedExpr, usize), + CallArgument(&'a ResolvedExpr, usize), + } + let mut frames = Vec::with_capacity(1028); + frames.push(Frame::Expr(expression, 0)); + while let Some(frame) = frames.pop() { + match frame { + Frame::Expr(expression, next) => { + if let ResolvedExprKind::Call { + callee, + instance, + args, + .. + } = &expression.kind + { + let target = program + .resolve_call_target(callee, instance.as_ref()) + .ok_or_else(|| { + replay_error( + function, + format!( + "cleanup call `{}` has unknown callee `{callee}`", + expression.id + ), + ) + })?; + if target.params.len() != args.len() { + return Err(replay_error( + function, + format!("cleanup call `{}` has inconsistent arity", expression.id), + )); + } + if let Some(argument) = args.get(next) { + if frames.len() + 3 > frames.capacity() { + return Err(replay_error( + function, + "supplemental-slot traversal exceeds the admitted depth", + )); + } + frames.push(Frame::Expr(expression, next + 1)); + frames.push(Frame::CallArgument(expression, next)); + frames.push(Frame::Expr(argument, 0)); + } + } else if let Some(child) = replay_expression_child(expression, next) { + if frames.len() + 2 > frames.capacity() { + return Err(replay_error( + function, + "supplemental-slot traversal exceeds the admitted depth", + )); + } + frames.push(Frame::Expr(expression, next + 1)); + frames.push(Frame::Expr(child, 0)); + } } - for (index, (argument, parameter)) in args.iter().zip(&target.params).enumerate() { - collect_supplemental_slots(program, function, argument, next_flag, slots)?; + Frame::CallArgument(expression, index) => { + let ResolvedExprKind::Call { + callee, + instance, + args, + .. + } = &expression.kind + else { + unreachable!("call-argument continuation retains a call"); + }; + let target = program + .resolve_call_target(callee, instance.as_ref()) + .ok_or_else(|| replay_error(function, "cleanup call target disappeared"))?; + let argument = &args[index]; + let parameter = &target.params[index]; if parameter.ownership == OwnershipMode::Own && type_needs_drop(program, function, ¶meter.ty)? { @@ -508,55 +1167,6 @@ fn collect_supplemental_slots( } } } - ResolvedExprKind::Unary { value, .. } | ResolvedExprKind::Project { base: value, .. } => { - collect_supplemental_slots(program, function, value, next_flag, slots)?; - } - ResolvedExprKind::Binary { left, right, .. } => { - collect_supplemental_slots(program, function, left, next_flag, slots)?; - collect_supplemental_slots(program, function, right, next_flag, slots)?; - } - ResolvedExprKind::Block { statements, tail } => { - for statement in statements { - let crate::hir::ResolvedStatement::Let { value, .. } = statement; - collect_supplemental_slots(program, function, value, next_flag, slots)?; - } - collect_supplemental_slots(program, function, tail, next_flag, slots)?; - } - ResolvedExprKind::If { - condition, - then_branch, - else_branch, - } => { - collect_supplemental_slots(program, function, condition, next_flag, slots)?; - collect_supplemental_slots(program, function, then_branch, next_flag, slots)?; - collect_supplemental_slots(program, function, else_branch, next_flag, slots)?; - } - ResolvedExprKind::ConstructRecord { fields, .. } => { - for field in fields { - collect_supplemental_slots(program, function, &field.value, next_flag, slots)?; - } - } - ResolvedExprKind::ConstructVariant { fields, .. } => { - for field in fields { - collect_supplemental_slots(program, function, &field.value, next_flag, slots)?; - } - } - ResolvedExprKind::Try { operand, .. } | ResolvedExprKind::TryOption { operand, .. } => { - collect_supplemental_slots(program, function, operand, next_flag, slots)?; - } - ResolvedExprKind::Match { scrutinee, arms } => { - collect_supplemental_slots(program, function, scrutinee, next_flag, slots)?; - for arm in arms { - collect_supplemental_slots(program, function, &arm.value, next_flag, slots)?; - } - } - ResolvedExprKind::UpdateRecord { base, fields, .. } => { - collect_supplemental_slots(program, function, base, next_flag, slots)?; - for field in fields { - collect_supplemental_slots(program, function, &field.value, next_flag, slots)?; - } - } - ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) | ResolvedExprKind::Place(_) => {} } Ok(()) } @@ -689,124 +1299,103 @@ fn collect_expression_statuses( expression: &ResolvedExpr, statuses: &mut Vec, ) -> Result<(), Diagnostic> { - match &expression.kind { - ResolvedExprKind::Call { - callee, - instance, - args, - .. - } => { - for argument in args { - collect_expression_statuses(program, function, argument, statuses)?; - } - if program - .resolve_call_target(callee, instance.as_ref()) - .is_none() - { + let mut stack = [None; 514]; + stack[0] = Some((expression, 0usize)); + let mut len = 1usize; + while len != 0 { + len -= 1; + let (expression, next) = stack[len].take().expect("status frame retained"); + if let Some(child) = replay_expression_child(expression, next) { + if len + 2 > stack.len() { return Err(replay_error( function, - format!("status source call has unknown callee `{callee}`"), - )); - } - statuses.push(StatusSource { - id: StatusSourceId { - expression: expression.id.clone(), - lane: StatusLane::OperationFailure, - }, - producer: StatusProducer::PropagatedCall { - callee: callee.clone(), - }, - }); - } - ResolvedExprKind::Unary { op, value } => { - collect_expression_statuses(program, function, value, statuses)?; - if *op == UnaryOp::Neg { - statuses.push(checked_status( - expression, - super::CheckedOperation::Neg, - vec![StatusCase::NegationOverflow], + "cleanup status expression depth exceeds 512", )); } + stack[len] = Some((expression, next + 1)); + stack[len + 1] = Some((child, 0)); + len += 2; + continue; } - ResolvedExprKind::Binary { op, left, right } => { - collect_expression_statuses(program, function, left, statuses)?; - collect_expression_statuses(program, function, right, statuses)?; - let checked = match op { - BinaryOp::Add => { - Some((super::CheckedOperation::Add, vec![StatusCase::AddOverflow])) - } - BinaryOp::Sub => { - Some((super::CheckedOperation::Sub, vec![StatusCase::SubOverflow])) - } - BinaryOp::Mul => { - Some((super::CheckedOperation::Mul, vec![StatusCase::MulOverflow])) + match &expression.kind { + ResolvedExprKind::Call { + callee, instance, .. + } => { + if program + .resolve_call_target(callee, instance.as_ref()) + .is_none() + { + return Err(replay_error( + function, + format!("status source call has unknown callee `{callee}`"), + )); } - BinaryOp::Div => Some(( - super::CheckedOperation::Div, - vec![StatusCase::DivisionByZero, StatusCase::DivisionOverflow], - )), - BinaryOp::Rem => Some(( - super::CheckedOperation::Rem, - vec![StatusCase::RemainderByZero, StatusCase::RemainderOverflow], - )), - BinaryOp::Eq - | BinaryOp::Ne - | BinaryOp::Lt - | BinaryOp::Le - | BinaryOp::Gt - | BinaryOp::Ge - | BinaryOp::And - | BinaryOp::Or => None, - }; - if let Some((operation, cases)) = checked { - statuses.push(checked_status(expression, operation, cases)); - } - } - ResolvedExprKind::Block { statements, tail } => { - for statement in statements { - let crate::hir::ResolvedStatement::Let { value, .. } = statement; - collect_expression_statuses(program, function, value, statuses)?; - } - collect_expression_statuses(program, function, tail, statuses)?; - } - ResolvedExprKind::If { - condition, - then_branch, - else_branch, - } => { - collect_expression_statuses(program, function, condition, statuses)?; - collect_expression_statuses(program, function, then_branch, statuses)?; - collect_expression_statuses(program, function, else_branch, statuses)?; - } - ResolvedExprKind::ConstructRecord { fields, .. } => { - for field in fields { - collect_expression_statuses(program, function, &field.value, statuses)?; - } - } - ResolvedExprKind::ConstructVariant { fields, .. } => { - for field in fields { - collect_expression_statuses(program, function, &field.value, statuses)?; - } - } - ResolvedExprKind::Try { operand, .. } | ResolvedExprKind::TryOption { operand, .. } => { - collect_expression_statuses(program, function, operand, statuses)?; - } - ResolvedExprKind::Match { scrutinee, arms } => { - collect_expression_statuses(program, function, scrutinee, statuses)?; - for arm in arms { - collect_expression_statuses(program, function, &arm.value, statuses)?; + statuses.push(StatusSource { + id: StatusSourceId { + expression: expression.id.clone(), + lane: StatusLane::OperationFailure, + }, + producer: StatusProducer::PropagatedCall { + callee: callee.clone(), + }, + }); } - } - ResolvedExprKind::UpdateRecord { base, fields, .. } => { - collect_expression_statuses(program, function, base, statuses)?; - for field in fields { - collect_expression_statuses(program, function, &field.value, statuses)?; + ResolvedExprKind::Unary { + op: UnaryOp::Neg, .. + } => statuses.push(checked_status( + expression, + super::CheckedOperation::Neg, + vec![StatusCase::NegationOverflow], + )), + ResolvedExprKind::Unary { + op: UnaryOp::Not, .. + } => {} + ResolvedExprKind::Binary { op, .. } => { + let checked = match op { + BinaryOp::Add => { + Some((super::CheckedOperation::Add, vec![StatusCase::AddOverflow])) + } + BinaryOp::Sub => { + Some((super::CheckedOperation::Sub, vec![StatusCase::SubOverflow])) + } + BinaryOp::Mul => { + Some((super::CheckedOperation::Mul, vec![StatusCase::MulOverflow])) + } + BinaryOp::Div => Some(( + super::CheckedOperation::Div, + vec![StatusCase::DivisionByZero, StatusCase::DivisionOverflow], + )), + BinaryOp::Rem => Some(( + super::CheckedOperation::Rem, + vec![StatusCase::RemainderByZero, StatusCase::RemainderOverflow], + )), + BinaryOp::Eq + | BinaryOp::Ne + | BinaryOp::Lt + | BinaryOp::Le + | BinaryOp::Gt + | BinaryOp::Ge + | BinaryOp::And + | BinaryOp::Or => None, + }; + if let Some((operation, cases)) = checked { + statuses.push(checked_status(expression, operation, cases)); + } } + ResolvedExprKind::NativeRustImportCall(_) + | ResolvedExprKind::Block { .. } + | ResolvedExprKind::If { .. } + | ResolvedExprKind::ConstructRecord { .. } + | ResolvedExprKind::ConstructVariant { .. } + | ResolvedExprKind::Try { .. } + | ResolvedExprKind::TryOption { .. } + | ResolvedExprKind::Match { .. } + | ResolvedExprKind::UpdateRecord { .. } + | ResolvedExprKind::Project { .. } + | ResolvedExprKind::Int(_) + | ResolvedExprKind::Bool(_) + | ResolvedExprKind::Place(_) => {} } - ResolvedExprKind::Project { base, .. } => { - collect_expression_statuses(program, function, base, statuses)?; - } - ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) | ResolvedExprKind::Place(_) => {} } Ok(()) } @@ -1545,11 +2134,7 @@ fn validate_typed_control_skeleton( function: &ResolvedFunction, budget: &mut ReplayBudget, ) -> Result<(), Diagnostic> { - let mut expected = hir_skeleton_paths(program, function)?; - let expected_units = expected.iter().fold(0_usize, |total, path| { - total.saturating_add(path.observations.len().saturating_add(1)) - }); - budget.charge(function, expected_units, "typed-HIR skeleton expansion")?; + let mut expected = hir_skeleton_paths(program, function, budget)?; let mut actual = plan_skeleton_paths(function, budget)?; expected.sort(); actual.sort(); @@ -1565,13 +2150,15 @@ fn validate_typed_control_skeleton( fn hir_skeleton_paths( program: &ResolvedProgram, function: &ResolvedFunction, + budget: &mut ReplayBudget, ) -> Result, Diagnostic> { - let mut paths = vec![empty_expr_path()]; + let mut work = SkeletonWork { function, budget }; + let mut paths = work.singleton_path(empty_expr_path(), "HIR root path")?; for contract in &function.requires { - paths = sequence_expression(program, function, paths, contract)?; - paths = split_contract(paths, contract); + paths = sequence_expression(program, function, paths, contract, &mut work)?; + paths = split_contract(paths, contract, &mut work)?; } - paths = sequence_expression(program, function, paths, &function.body)?; + paths = sequence_expression(program, function, paths, &function.body, &mut work)?; if paths.iter().any(|path| path.residual) { if !function.cleanup_plan.slots.is_empty() { return Err(replay_error( @@ -1584,43 +2171,150 @@ fn hir_skeleton_paths( continue; } if !path.residual { - path.observations.push(SkeletonObservation::StageCopyResult( - StagedCopyResultSource::Body { - expression: function.body.id.clone(), - instance: function.return_type.clone(), - }, - )); + let expression = + work.clone_owned(&function.body.id, "body result expression clone")?; + let instance = work.clone_owned(&function.return_type, "body result type clone")?; + work.push_observation( + path, + SkeletonObservation::StageCopyResult(StagedCopyResultSource::Body { + expression, + instance, + }), + "body result staging", + )?; } path.residual = false; } } if type_needs_drop(program, function, &function.return_type)? { + let body_id = work.clone_owned(&function.body.id, "owned result expression clone")?; paths = transfer_completed_paths( function, paths, - function.body.id.clone(), + body_id, CleanupPlace { storage: StorageId::ProvisionalResult, projections: Vec::new(), }, "owned function result", + &mut work, )?; } for contract in &function.ensures { - paths = sequence_expression(program, function, paths, contract)?; - paths = split_contract(paths, contract); - } - Ok(paths - .into_iter() - .map(|path| SkeletonPath { - observations: path.observations, - terminal: if path.failed { - SkeletonTerminal::Failure - } else { - SkeletonTerminal::Success + paths = sequence_expression(program, function, paths, contract, &mut work)?; + paths = split_contract(paths, contract, &mut work)?; + } + let mut completed = Vec::new(); + for path in paths { + work.push_skeleton_path( + &mut completed, + SkeletonPath { + observations: path.observations, + terminal: if path.failed { + SkeletonTerminal::Failure + } else { + SkeletonTerminal::Success + }, }, - }) - .collect()) + "completed HIR skeleton path", + )?; + } + Ok(completed) +} + +struct SkeletonWork<'a, 'b> { + function: &'a ResolvedFunction, + budget: &'b mut ReplayBudget, +} + +impl SkeletonWork<'_, '_> { + fn charge(&mut self, units: usize, phase: &str) -> Result<(), Diagnostic> { + self.budget.charge_skeleton(self.function, units, phase) + } + + fn clone_owned(&mut self, value: &T, phase: &str) -> Result { + self.charge(1, phase)?; + note_skeleton_materialization(); + Ok(value.clone()) + } + + fn push_expr_path( + &mut self, + paths: &mut Vec, + path: ExprSkeletonPath, + phase: &str, + ) -> Result<(), Diagnostic> { + self.charge(1, phase)?; + note_skeleton_materialization(); + paths.push(path); + Ok(()) + } + + fn push_skeleton_path( + &mut self, + paths: &mut Vec, + path: SkeletonPath, + phase: &str, + ) -> Result<(), Diagnostic> { + self.charge(1, phase)?; + note_skeleton_materialization(); + paths.push(path); + Ok(()) + } + + fn singleton_path( + &mut self, + path: ExprSkeletonPath, + phase: &str, + ) -> Result, Diagnostic> { + let mut paths = Vec::new(); + self.push_expr_path(&mut paths, path, phase)?; + Ok(paths) + } + + fn clone_expr_path( + &mut self, + path: &ExprSkeletonPath, + phase: &str, + ) -> Result { + self.charge(1, phase)?; + note_skeleton_materialization(); + Ok(path.clone()) + } + + fn clone_observations( + &mut self, + observations: &[SkeletonObservation], + phase: &str, + ) -> Result, Diagnostic> { + self.charge(1, phase)?; + note_skeleton_materialization(); + Ok(observations.to_vec()) + } + + fn extend_observations( + &mut self, + target: &mut Vec, + observations: &[SkeletonObservation], + phase: &str, + ) -> Result<(), Diagnostic> { + self.charge(1, phase)?; + note_skeleton_materialization(); + target.extend_from_slice(observations); + Ok(()) + } + + fn push_observation( + &mut self, + path: &mut ExprSkeletonPath, + observation: SkeletonObservation, + phase: &str, + ) -> Result<(), Diagnostic> { + self.charge(1, phase)?; + note_skeleton_materialization(); + path.observations.push(observation); + Ok(()) + } } fn empty_expr_path() -> ExprSkeletonPath { @@ -1637,416 +2331,1040 @@ fn sequence_expression( function: &ResolvedFunction, prefixes: Vec, expression: &ResolvedExpr, + work: &mut SkeletonWork<'_, '_>, +) -> Result, Diagnostic> { + if !has_active_paths(&prefixes) { + return Ok(prefixes); + } + let suffixes = expression_skeleton(program, function, expression, work)?; + sequence_skeleton_paths(prefixes, &suffixes, work) +} + +fn sequence_skeleton_paths( + prefixes: Vec, + suffixes: &[ExprSkeletonPath], + work: &mut SkeletonWork<'_, '_>, ) -> Result, Diagnostic> { - let suffixes = expression_skeleton(program, function, expression)?; let mut combined = Vec::new(); for prefix in prefixes { if prefix.failed || prefix.residual { - combined.push(prefix); + work.push_expr_path(&mut combined, prefix, "short-circuited skeleton path")?; continue; } - for suffix in &suffixes { - let mut observations = prefix.observations.clone(); - observations.extend(suffix.observations.clone()); - combined.push(ExprSkeletonPath { - observations, - owned_source: suffix.owned_source.clone(), - failed: suffix.failed, - residual: suffix.residual, - }); + for suffix in suffixes { + let mut observations = + work.clone_observations(&prefix.observations, "skeleton prefix clone")?; + work.extend_observations( + &mut observations, + &suffix.observations, + "skeleton suffix clone", + )?; + let owned_source = work.clone_owned( + &suffix.owned_source, + "sequenced skeleton owned-source clone", + )?; + work.push_expr_path( + &mut combined, + ExprSkeletonPath { + observations, + owned_source, + failed: suffix.failed, + residual: suffix.residual, + }, + "sequenced skeleton path", + )?; } } Ok(combined) } +fn has_active_paths(paths: &[ExprSkeletonPath]) -> bool { + paths.iter().any(|path| !path.failed && !path.residual) +} + fn expression_skeleton( program: &ResolvedProgram, function: &ResolvedFunction, expression: &ResolvedExpr, + work: &mut SkeletonWork<'_, '_>, ) -> Result, Diagnostic> { - match &expression.kind { - ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) => Ok(vec![empty_expr_path()]), - ResolvedExprKind::Place(place) => { - let owned_source = if expression.ownership == OwnershipMode::Own - && type_needs_drop(program, function, &expression.ty)? - { - Some(cleanup_place_from_hir(function, place)?) - } else { - None - }; - Ok(vec![ExprSkeletonPath { - observations: Vec::new(), - owned_source, - failed: false, - residual: false, - }]) - } - ResolvedExprKind::Call { - callee, - instance, - args, - .. - } => call_skeleton( - program, - function, - expression, - callee, - instance.as_ref(), - args, - ), - ResolvedExprKind::Unary { op, value } => { - let paths = sequence_expression(program, function, vec![empty_expr_path()], value)?; - if *op == UnaryOp::Neg { - Ok(split_status_paths( - paths, - StatusSourceId { - expression: expression.id.clone(), - lane: StatusLane::OperationFailure, - }, - )) - } else { - Ok(paths) - } - } - ResolvedExprKind::Binary { - op: BinaryOp::And | BinaryOp::Or, - left, - right, - } => lazy_skeleton( - program, - function, - expression.clone(), - *left.clone(), - *right.clone(), - ), - ResolvedExprKind::Binary { op, left, right } => { - let paths = sequence_expression(program, function, vec![empty_expr_path()], left)?; - let paths = sequence_expression(program, function, paths, right)?; - if matches!( - op, - BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Rem - ) { - Ok(split_status_paths( - paths, - StatusSourceId { - expression: expression.id.clone(), - lane: StatusLane::OperationFailure, - }, - )) - } else { - Ok(paths) - } - } - ResolvedExprKind::Block { statements, tail } => { - let mut paths = vec![empty_expr_path()]; - for statement in statements { - let ResolvedStatement::Let { binding, value, .. } = statement; - paths = sequence_expression(program, function, paths, value)?; - if binding.ownership == OwnershipMode::Own - && type_needs_drop(program, function, &binding.ty)? - { - paths = transfer_completed_paths( - function, - paths, - value.id.clone(), - CleanupPlace { - storage: StorageId::Value(binding.id.clone()), - projections: Vec::new(), - }, - "owned binding", - )?; - } - } - paths = sequence_expression(program, function, paths, tail)?; - if expression.ownership == OwnershipMode::Own - && type_needs_drop(program, function, &expression.ty)? - { - paths = transfer_completed_paths( + enum Frame<'a> { + Eval(&'a ResolvedExpr), + Unary { + expression: &'a ResolvedExpr, + op: UnaryOp, + }, + BinaryLeft { + expression: &'a ResolvedExpr, + op: BinaryOp, + right: &'a ResolvedExpr, + }, + BinaryRight { + expression: &'a ResolvedExpr, + op: BinaryOp, + left_paths: Vec, + }, + LazyRight { + expression: &'a ResolvedExpr, + op: BinaryOp, + left: &'a ResolvedExpr, + left_paths: Vec, + }, + CallArgument { + expression: &'a ResolvedExpr, + target: &'a ResolvedFunction, + args: &'a [ResolvedExpr], + index: usize, + states: Vec, + }, + NativeArgument { + args: &'a [ResolvedExpr], + index: usize, + paths: Vec, + }, + BlockValue { + expression: &'a ResolvedExpr, + statements: &'a [ResolvedStatement], + tail: &'a ResolvedExpr, + index: usize, + paths: Vec, + }, + BlockTail { + expression: &'a ResolvedExpr, + paths: Vec, + }, + VariantField { + fields: &'a [crate::hir::ResolvedFieldInitializer], + index: usize, + paths: Vec, + }, + RecordField { + expression: &'a ResolvedExpr, + fields: &'a [crate::hir::ResolvedFieldInitializer], + index: usize, + paths: Vec, + }, + UpdateBase { + expression: &'a ResolvedExpr, + base: &'a ResolvedExpr, + record: &'a DeclarationId, + fields: &'a [crate::hir::ResolvedFieldInitializer], + }, + UpdateField { + expression: &'a ResolvedExpr, + base: &'a ResolvedExpr, + record: &'a DeclarationId, + fields: &'a [crate::hir::ResolvedFieldInitializer], + index: usize, + paths: Vec, + replaced: BTreeSet, + needs_cleanup: bool, + }, + Try { + expression: &'a ResolvedExpr, + }, + TryOption { + expression: &'a ResolvedExpr, + }, + Project { + expression: &'a ResolvedExpr, + field: &'a DeclarationId, + }, + IfCondition { + expression: &'a ResolvedExpr, + condition: &'a ResolvedExpr, + then_branch: &'a ResolvedExpr, + else_branch: &'a ResolvedExpr, + }, + IfThen { + expression: &'a ResolvedExpr, + else_branch: &'a ResolvedExpr, + true_prefixes: Vec, + false_prefixes: Vec, + results: Vec, + }, + IfElse { + expression: &'a ResolvedExpr, + false_prefixes: Vec, + results: Vec, + }, + MatchScrutinee { + expression: &'a ResolvedExpr, + scrutinee: &'a ResolvedExpr, + arms: &'a [ResolvedMatchArm], + }, + MatchArm { + expression: &'a ResolvedExpr, + scrutinee: &'a ResolvedExpr, + arms: &'a [ResolvedMatchArm], + index: usize, + remaining: Vec, + results: Vec, + is_record: bool, + }, + } + + macro_rules! push_frame { + ($frames:expr, $frame:expr) => {{ + if $frames.len() == $frames.capacity() { + return Err(replay_error( function, - paths, - expression.id.clone(), - temporary_place(expression), - "owned block result", - )?; + "typed-HIR skeleton traversal exceeds the admitted depth", + )); } - Ok(paths) - } - ResolvedExprKind::ConstructVariant { fields, .. } => { - let mut paths = vec![empty_expr_path()]; - for field in fields { - paths = sequence_expression(program, function, paths, &field.value)?; - if field.value.ownership == OwnershipMode::Own - && type_needs_drop(program, function, &field.value.ty)? - { - return Err(replay_error( - function, - "droppable variant payload reached the copy-only cleanup skeleton", - )); + work.charge(1, "typed-HIR skeleton continuation push")?; + note_skeleton_materialization(); + $frames.push($frame); + }}; + } + + // The semantic depth ceiling excludes the function-body block. The + // continuation machine also holds the currently evaluated child beside + // that block and the 512 authored expression ancestors. + let mut frames = Vec::with_capacity(515); + push_frame!(frames, Frame::Eval(expression)); + let mut produced = None; + while let Some(frame) = frames.pop() { + match frame { + Frame::Eval(expression) => { + debug_assert!(produced.is_none()); + match &expression.kind { + ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) => { + produced = + Some(work.singleton_path(empty_expr_path(), "literal skeleton path")?); + } + ResolvedExprKind::Place(place) => { + let owned_source = if expression.ownership == OwnershipMode::Own + && type_needs_drop(program, function, &expression.ty)? + { + Some(cleanup_place_from_hir(function, place, work)?) + } else { + None + }; + produced = Some(work.singleton_path( + ExprSkeletonPath { + observations: Vec::new(), + owned_source, + failed: false, + residual: false, + }, + "place skeleton path", + )?); + } + ResolvedExprKind::Unary { op, value } => { + push_frame!( + frames, + Frame::Unary { + expression, + op: *op + } + ); + push_frame!(frames, Frame::Eval(value)); + } + ResolvedExprKind::Try { operand, .. } => { + push_frame!(frames, Frame::Try { expression }); + push_frame!(frames, Frame::Eval(operand)); + } + ResolvedExprKind::TryOption { operand, .. } => { + push_frame!(frames, Frame::TryOption { expression }); + push_frame!(frames, Frame::Eval(operand)); + } + ResolvedExprKind::Project { base, field } => { + push_frame!(frames, Frame::Project { expression, field }); + push_frame!(frames, Frame::Eval(base)); + } + ResolvedExprKind::Binary { op, left, right } => { + push_frame!( + frames, + Frame::BinaryLeft { + expression, + op: *op, + right, + } + ); + push_frame!(frames, Frame::Eval(left)); + } + ResolvedExprKind::Call { + callee, + instance, + args, + .. + } => { + let target = program + .resolve_call_target(callee, instance.as_ref()) + .ok_or_else(|| { + replay_error( + function, + format!("unknown skeleton callee `{callee}`"), + ) + })?; + work.charge(1, "call skeleton root state")?; + let states = vec![(empty_expr_path(), Vec::new())]; + if let Some(argument) = args.first() { + push_frame!( + frames, + Frame::CallArgument { + expression, + target, + args, + index: 0, + states, + } + ); + push_frame!(frames, Frame::Eval(argument)); + } else { + produced = Some(finish_call_states( + program, function, expression, states, work, + )?); + } + } + ResolvedExprKind::NativeRustImportCall(call) => { + let paths = + work.singleton_path(empty_expr_path(), "native-call root path")?; + if let Some(argument) = call.args.first() { + push_frame!( + frames, + Frame::NativeArgument { + args: &call.args, + index: 0, + paths, + } + ); + push_frame!(frames, Frame::Eval(argument)); + } else { + produced = Some(paths); + } + } + ResolvedExprKind::Block { statements, tail } => { + let paths = work.singleton_path(empty_expr_path(), "block root path")?; + if let Some(ResolvedStatement::Let { value, .. }) = statements.first() { + push_frame!( + frames, + Frame::BlockValue { + expression, + statements, + tail, + index: 0, + paths, + } + ); + push_frame!(frames, Frame::Eval(value)); + } else { + push_frame!(frames, Frame::BlockTail { expression, paths }); + push_frame!(frames, Frame::Eval(tail)); + } + } + ResolvedExprKind::ConstructVariant { fields, .. } => { + let paths = work + .singleton_path(empty_expr_path(), "variant-construction root path")?; + if let Some(field) = fields.first() { + push_frame!( + frames, + Frame::VariantField { + fields, + index: 0, + paths, + } + ); + push_frame!(frames, Frame::Eval(&field.value)); + } else { + produced = Some(paths); + } + } + ResolvedExprKind::ConstructRecord { fields, .. } => { + let paths = work + .singleton_path(empty_expr_path(), "record-construction root path")?; + if let Some(field) = fields.first() { + push_frame!( + frames, + Frame::RecordField { + expression, + fields, + index: 0, + paths, + } + ); + push_frame!(frames, Frame::Eval(&field.value)); + } else { + produced = Some(finish_record_paths(expression, paths, work)?); + } + } + ResolvedExprKind::UpdateRecord { + base, + record, + fields, + } => { + push_frame!( + frames, + Frame::UpdateBase { + expression, + base, + record, + fields, + } + ); + push_frame!(frames, Frame::Eval(base)); + } + ResolvedExprKind::If { + condition, + then_branch, + else_branch, + } => { + push_frame!( + frames, + Frame::IfCondition { + expression, + condition, + then_branch, + else_branch, + } + ); + push_frame!(frames, Frame::Eval(condition)); + } + ResolvedExprKind::Match { scrutinee, arms } => { + push_frame!( + frames, + Frame::MatchScrutinee { + expression, + scrutinee, + arms, + } + ); + push_frame!(frames, Frame::Eval(scrutinee)); + } } } - Ok(paths) - } - ResolvedExprKind::Try { - operand, - result, - ok_case, - ok_field, - err_case, - err_field, - residual_type, - } => { - let source = authenticated_try_stage_source( - program, - function, + Frame::Unary { expression, op } => { + let paths = produced.take().expect("unary operand path retained"); + produced = Some(if op == UnaryOp::Neg { + let expression_id = + work.clone_owned(&expression.id, "unary status expression clone")?; + split_status_paths( + paths, + StatusSourceId { + expression: expression_id, + lane: StatusLane::OperationFailure, + }, + work, + )? + } else { + paths + }); + } + Frame::BinaryLeft { expression, - operand, - result, - ok_case, - ok_field, - err_case, - err_field, - residual_type, - )?; - let operand_paths = expression_skeleton(program, function, operand)?; - let mut paths = Vec::with_capacity(operand_paths.len().saturating_mul(2)); - for path in operand_paths { - if path.failed || path.residual { - paths.push(path); - continue; + op, + right, + } => { + let left_paths = produced.take().expect("binary left path retained"); + if !has_active_paths(&left_paths) { + produced = Some(left_paths); + } else if matches!(op, BinaryOp::And | BinaryOp::Or) { + push_frame!( + frames, + Frame::LazyRight { + expression, + op, + left: match &expression.kind { + ResolvedExprKind::Binary { left, .. } => left, + _ => unreachable!(), + }, + left_paths, + } + ); + push_frame!(frames, Frame::Eval(right)); + } else { + push_frame!( + frames, + Frame::BinaryRight { + expression, + op, + left_paths, + } + ); + push_frame!(frames, Frame::Eval(right)); } - let mut success = path.clone(); - success.observations.push(SkeletonObservation::VariantCase { - scrutinee: operand.id.clone(), - case: ok_case.clone(), - matches: true, - }); - paths.push(success); - - let mut residual = path; - residual - .observations - .push(SkeletonObservation::VariantCase { - scrutinee: operand.id.clone(), - case: ok_case.clone(), - matches: false, - }); - residual - .observations - .push(SkeletonObservation::StageCopyResult(source.clone())); - residual.residual = true; - paths.push(residual); } - Ok(paths) - } - ResolvedExprKind::TryOption { - operand, - option, - some_case, - some_field, - none_case, - residual_type, - } => { - let source = authenticated_try_option_stage_source( - program, - function, + Frame::BinaryRight { expression, - operand, - option, - some_case, - some_field, - none_case, - residual_type, - )?; - let operand_paths = expression_skeleton(program, function, operand)?; - let mut paths = Vec::with_capacity(operand_paths.len().saturating_mul(2)); - for path in operand_paths { - if path.failed || path.residual { - paths.push(path); - continue; + op, + left_paths, + } => { + let right_paths = produced.take().expect("binary right path retained"); + let paths = sequence_skeleton_paths(left_paths, &right_paths, work)?; + produced = Some( + if matches!( + op, + BinaryOp::Add + | BinaryOp::Sub + | BinaryOp::Mul + | BinaryOp::Div + | BinaryOp::Rem + ) { + let expression_id = + work.clone_owned(&expression.id, "binary status expression clone")?; + split_status_paths( + paths, + StatusSourceId { + expression: expression_id, + lane: StatusLane::OperationFailure, + }, + work, + )? + } else { + paths + }, + ); + } + Frame::LazyRight { + expression, + op, + left, + left_paths, + } => { + let right_paths = produced.take().expect("lazy right path retained"); + produced = Some(finish_lazy_paths( + function, + expression, + op, + left, + left_paths, + &right_paths, + work, + )?); + } + Frame::NativeArgument { args, index, paths } => { + let suffixes = produced.take().expect("native argument path retained"); + let paths = sequence_skeleton_paths(paths, &suffixes, work)?; + let next = index + 1; + if has_active_paths(&paths) && next < args.len() { + push_frame!( + frames, + Frame::NativeArgument { + args, + index: next, + paths, + } + ); + push_frame!(frames, Frame::Eval(&args[next])); + } else { + produced = Some(paths); } - let mut success = path.clone(); - success.observations.push(SkeletonObservation::VariantCase { - scrutinee: operand.id.clone(), - case: some_case.clone(), - matches: true, - }); - paths.push(success); - - let mut residual = path; - residual - .observations - .push(SkeletonObservation::VariantCase { - scrutinee: operand.id.clone(), - case: some_case.clone(), - matches: false, - }); - residual - .observations - .push(SkeletonObservation::StageCopyResult(source.clone())); - residual.residual = true; - paths.push(residual); } - Ok(paths) - } - ResolvedExprKind::Match { scrutinee, arms } => { - match_skeleton(program, function, expression, scrutinee, arms) - } - ResolvedExprKind::If { - condition, - then_branch, - else_branch, - } => if_skeleton( - program, - function, - expression, - condition, - then_branch, - else_branch, - ), - ResolvedExprKind::ConstructRecord { fields, .. } => { - let mut paths = vec![empty_expr_path()]; - let destination = temporary_place(expression); - for field in fields { - paths = sequence_expression(program, function, paths, &field.value)?; - if field.value.ownership == OwnershipMode::Own - && type_needs_drop(program, function, &field.value.ty)? + Frame::CallArgument { + expression, + target, + args, + index, + states, + } => { + let suffixes = produced.take().expect("call argument path retained"); + let states = sequence_call_argument( + program, function, expression, target, args, index, states, &suffixes, work, + )?; + let next = index + 1; + if call_states_have_active(&states) && next < args.len() { + push_frame!( + frames, + Frame::CallArgument { + expression, + target, + args, + index: next, + states, + } + ); + push_frame!(frames, Frame::Eval(&args[next])); + } else { + produced = Some(finish_call_states( + program, function, expression, states, work, + )?); + } + } + Frame::BlockValue { + expression, + statements, + tail, + index, + paths, + } => { + let suffixes = produced.take().expect("binding path retained"); + let mut paths = sequence_skeleton_paths(paths, &suffixes, work)?; + let ResolvedStatement::Let { binding, value, .. } = &statements[index]; + if binding.ownership == OwnershipMode::Own + && type_needs_drop(program, function, &binding.ty)? { - let mut field_destination = destination.clone(); - field_destination.projections.push(field.field.clone()); + let value_id = work.clone_owned(&value.id, "binding value expression clone")?; + let binding_id = work.clone_owned(&binding.id, "binding storage clone")?; paths = transfer_completed_paths( function, paths, - field.value.id.clone(), - field_destination, - "owned record field", + value_id, + CleanupPlace { + storage: StorageId::Value(binding_id), + projections: Vec::new(), + }, + "owned binding", + work, )?; } + let next = index + 1; + if has_active_paths(&paths) && next < statements.len() { + let ResolvedStatement::Let { value, .. } = &statements[next]; + push_frame!( + frames, + Frame::BlockValue { + expression, + statements, + tail, + index: next, + paths, + } + ); + push_frame!(frames, Frame::Eval(value)); + } else if has_active_paths(&paths) { + push_frame!(frames, Frame::BlockTail { expression, paths }); + push_frame!(frames, Frame::Eval(tail)); + } else { + produced = Some(paths); + } } - for path in &mut paths { - if !path.failed && !path.residual { - path.owned_source = Some(destination.clone()); + Frame::BlockTail { expression, paths } => { + let suffixes = produced.take().expect("block tail path retained"); + let mut paths = sequence_skeleton_paths(paths, &suffixes, work)?; + if expression.ownership == OwnershipMode::Own + && type_needs_drop(program, function, &expression.ty)? + { + let expression_id = + work.clone_owned(&expression.id, "block result expression clone")?; + paths = transfer_completed_paths( + function, + paths, + expression_id, + temporary_place(expression, work)?, + "owned block result", + work, + )?; } + produced = Some(paths); } - Ok(paths) - } - ResolvedExprKind::UpdateRecord { - base, - record, - fields, - } => { - let mut paths = sequence_expression(program, function, vec![empty_expr_path()], base)?; - let needs_cleanup = expression.ownership == OwnershipMode::Own - && type_needs_drop(program, function, &expression.ty)?; - if !needs_cleanup { - for field in fields { - paths = sequence_expression(program, function, paths, &field.value)?; + Frame::VariantField { + fields, + index, + paths, + } => { + let suffixes = produced.take().expect("variant field path retained"); + let paths = sequence_skeleton_paths(paths, &suffixes, work)?; + let field = &fields[index]; + if has_active_paths(&paths) + && field.value.ownership == OwnershipMode::Own + && type_needs_drop(program, function, &field.value.ty)? + { + return Err(replay_error( + function, + "droppable variant payload reached the copy-only cleanup skeleton", + )); + } + let next = index + 1; + if has_active_paths(&paths) && next < fields.len() { + push_frame!( + frames, + Frame::VariantField { + fields, + index: next, + paths, + } + ); + push_frame!(frames, Frame::Eval(&fields[next].value)); + } else { + produced = Some(paths); } - return Ok(paths); } - - let staged_base = temporary_place(base); - for path in &mut paths { - if path.failed || path.residual { - continue; + Frame::RecordField { + expression, + fields, + index, + paths, + } => { + let suffixes = produced.take().expect("record field path retained"); + let mut paths = sequence_skeleton_paths(paths, &suffixes, work)?; + let field = &fields[index]; + if field.value.ownership == OwnershipMode::Own + && type_needs_drop(program, function, &field.value.ty)? + { + let mut destination = temporary_place(expression, work)?; + let field_id = + work.clone_owned(&field.field, "record field projection clone")?; + work.charge(1, "record field projection push")?; + note_skeleton_materialization(); + destination.projections.push(field_id); + let value_id = work.clone_owned(&field.value.id, "record field value clone")?; + paths = transfer_completed_paths( + function, + paths, + value_id, + destination, + "owned record field", + work, + )?; } - let source = path.owned_source.take().ok_or_else(|| { - replay_error( + let next = index + 1; + if has_active_paths(&paths) && next < fields.len() { + push_frame!( + frames, + Frame::RecordField { + expression, + fields, + index: next, + paths, + } + ); + push_frame!(frames, Frame::Eval(&fields[next].value)); + } else { + produced = Some(finish_record_paths(expression, paths, work)?); + } + } + Frame::UpdateBase { + expression, + base, + record, + fields, + } => { + let mut paths = produced.take().expect("update base path retained"); + let needs_cleanup = expression.ownership == OwnershipMode::Own + && type_needs_drop(program, function, &expression.ty)?; + if needs_cleanup { + let staged_base = temporary_place(base, work)?; + for path in &mut paths { + if path.failed || path.residual { + continue; + } + let source = path.owned_source.take().ok_or_else(|| { + replay_error( + function, + "owned record update base has no HIR cleanup source", + ) + })?; + if source != staged_base { + let at = work.clone_owned(&base.id, "update base expression clone")?; + let staged = + work.clone_owned(&staged_base, "update base destination clone")?; + work.push_observation( + path, + SkeletonObservation::Transfer { + at, + source, + destination: staged, + }, + "update base transfer", + )?; + } + path.owned_source = + Some(work.clone_owned(&staged_base, "update base source clone")?); + } + } + if has_active_paths(&paths) && !fields.is_empty() { + push_frame!( + frames, + Frame::UpdateField { + expression, + base, + record, + fields, + index: 0, + paths, + replaced: BTreeSet::new(), + needs_cleanup, + } + ); + push_frame!(frames, Frame::Eval(&fields[0].value)); + } else { + produced = Some(finish_update_paths( + program, function, - "owned record update base has no HIR cleanup source", - ) - })?; - if source != staged_base { - path.observations.push(SkeletonObservation::Transfer { - at: base.id.clone(), - source, - destination: staged_base.clone(), - }); + expression, + base, + record, + paths, + &BTreeSet::new(), + needs_cleanup, + work, + )?); } - path.owned_source = Some(staged_base.clone()); } - - let destination = temporary_place(expression); - let mut replaced = BTreeSet::new(); - for field in fields { - if !replaced.insert(field.field.clone()) { + Frame::UpdateField { + expression, + base, + record, + fields, + index, + paths, + mut replaced, + needs_cleanup, + } => { + let field = &fields[index]; + let field_id = work.clone_owned(&field.field, "updated-field set clone")?; + work.charge(1, "updated-field set insertion")?; + note_skeleton_materialization(); + if !replaced.insert(field_id) { return Err(replay_error( function, format!("record update repeats field `{}`", field.field), )); } - paths = sequence_expression(program, function, paths, &field.value)?; - if field.value.ownership == OwnershipMode::Own + let suffixes = produced.take().expect("update field path retained"); + let mut paths = sequence_skeleton_paths(paths, &suffixes, work)?; + if needs_cleanup + && field.value.ownership == OwnershipMode::Own && type_needs_drop(program, function, &field.value.ty)? { - let mut field_destination = destination.clone(); - field_destination.projections.push(field.field.clone()); + let mut destination = temporary_place(expression, work)?; + let field_id = + work.clone_owned(&field.field, "update field projection clone")?; + work.charge(1, "update field projection push")?; + note_skeleton_materialization(); + destination.projections.push(field_id); + let value_id = work.clone_owned(&field.value.id, "update field value clone")?; paths = transfer_completed_paths( function, paths, - field.value.id.clone(), - field_destination, + value_id, + destination, "owned record replacement", + work, )?; } + let next = index + 1; + if has_active_paths(&paths) && next < fields.len() { + push_frame!( + frames, + Frame::UpdateField { + expression, + base, + record, + fields, + index: next, + paths, + replaced, + needs_cleanup, + } + ); + push_frame!(frames, Frame::Eval(&fields[next].value)); + } else { + produced = Some(finish_update_paths( + program, + function, + expression, + base, + record, + paths, + &replaced, + needs_cleanup, + work, + )?); + } } - - let declarations = program.declarations.record_fields(record).ok_or_else(|| { - replay_error( + Frame::Try { expression } => { + let operand_paths = produced.take().expect("try operand path retained"); + produced = Some(finish_try_paths( + program, function, - format!("record update has unknown record `{record}`"), - ) - })?; - for field in declarations { - if replaced.contains(&field.id) || !type_needs_drop(program, function, &field.ty)? { - continue; - } - for path in &mut paths { - if path.failed || path.residual { - continue; + expression, + operand_paths, + false, + work, + )?); + } + Frame::TryOption { expression } => { + let operand_paths = produced.take().expect("Option try operand path retained"); + produced = Some(finish_try_paths( + program, + function, + expression, + operand_paths, + true, + work, + )?); + } + Frame::Project { expression, field } => { + let mut paths = produced.take().expect("projection base path retained"); + if expression.ownership == OwnershipMode::Own + && type_needs_drop(program, function, &expression.ty)? + { + for path in &mut paths { + if path.failed || path.residual { + continue; + } + let mut source = path.owned_source.take().ok_or_else(|| { + replay_error(function, "owned projection has no HIR cleanup source") + })?; + let field = work.clone_owned(field, "projection field clone")?; + work.charge(1, "projection field push")?; + note_skeleton_materialization(); + source.projections.push(field); + let destination = temporary_place(expression, work)?; + let at = work.clone_owned(&expression.id, "projection expression clone")?; + let transferred_destination = work + .clone_owned(&destination, "projection transfer destination clone")?; + work.push_observation( + path, + SkeletonObservation::Transfer { + at, + source, + destination: transferred_destination, + }, + "owned projection transfer", + )?; + path.owned_source = Some(destination); } - let mut source = staged_base.clone(); - source.projections.push(field.id.clone()); - let mut field_destination = destination.clone(); - field_destination.projections.push(field.id.clone()); - path.observations.push(SkeletonObservation::Transfer { - at: expression.id.clone(), - source, - destination: field_destination, - }); } + produced = Some(paths); } - for path in &mut paths { - if !path.failed && !path.residual { - path.owned_source = Some(destination.clone()); + Frame::IfCondition { + expression, + condition, + then_branch, + else_branch, + } => { + let condition_paths = produced.take().expect("if condition path retained"); + let (results, true_prefixes, false_prefixes) = + split_boolean_prefixes(condition_paths, &condition.id, work)?; + if true_prefixes.is_empty() && false_prefixes.is_empty() { + produced = Some(results); + } else if true_prefixes.is_empty() { + push_frame!( + frames, + Frame::IfElse { + expression, + false_prefixes, + results, + } + ); + push_frame!(frames, Frame::Eval(else_branch)); + } else { + push_frame!( + frames, + Frame::IfThen { + expression, + else_branch, + true_prefixes, + false_prefixes, + results, + } + ); + push_frame!(frames, Frame::Eval(then_branch)); } } - Ok(paths) - } - ResolvedExprKind::Project { base, field } => { - let mut paths = sequence_expression(program, function, vec![empty_expr_path()], base)?; - if expression.ownership == OwnershipMode::Own - && type_needs_drop(program, function, &expression.ty)? - { - for path in &mut paths { - if path.failed || path.residual { - continue; + Frame::IfThen { + expression, + else_branch, + true_prefixes, + false_prefixes, + mut results, + } => { + let then_paths = produced.take().expect("if then path retained"); + let mut selected = sequence_skeleton_paths(true_prefixes, &then_paths, work)?; + selected = + finish_conditional_result(program, function, expression, selected, work)?; + append_expr_paths(&mut results, selected, work, "if then result")?; + if false_prefixes.is_empty() { + produced = Some(results); + } else { + push_frame!( + frames, + Frame::IfElse { + expression, + false_prefixes, + results, + } + ); + push_frame!(frames, Frame::Eval(else_branch)); + } + } + Frame::IfElse { + expression, + false_prefixes, + mut results, + } => { + let else_paths = produced.take().expect("if else path retained"); + let mut selected = sequence_skeleton_paths(false_prefixes, &else_paths, work)?; + selected = + finish_conditional_result(program, function, expression, selected, work)?; + append_expr_paths(&mut results, selected, work, "if else result")?; + produced = Some(results); + } + Frame::MatchScrutinee { + expression, + scrutinee, + arms, + } => { + let scrutinee_paths = produced.take().expect("match scrutinee path retained"); + if !has_active_paths(&scrutinee_paths) { + produced = Some(scrutinee_paths); + continue; + } + let is_record = + validate_match_skeleton_shape(program, function, expression, scrutinee, arms)?; + push_frame!( + frames, + Frame::MatchArm { + expression, + scrutinee, + arms, + index: 0, + remaining: scrutinee_paths, + results: Vec::new(), + is_record, } - let mut source = path.owned_source.take().ok_or_else(|| { - replay_error(function, "owned projection has no HIR cleanup source") - })?; - source.projections.push(field.clone()); - let destination = temporary_place(expression); - path.observations.push(SkeletonObservation::Transfer { - at: expression.id.clone(), - source, - destination: destination.clone(), - }); - path.owned_source = Some(destination); + ); + push_frame!(frames, Frame::Eval(&arms[0].value)); + } + Frame::MatchArm { + expression, + scrutinee, + arms, + index, + remaining, + mut results, + is_record, + } => { + let arm_paths = produced.take().expect("match arm path retained"); + let next_remaining = finish_match_arm( + program, + function, + expression, + scrutinee, + arms, + index, + remaining, + &arm_paths, + &mut results, + is_record, + work, + )?; + let next = index + 1; + if !next_remaining.is_empty() && next < arms.len() { + push_frame!( + frames, + Frame::MatchArm { + expression, + scrutinee, + arms, + index: next, + remaining: next_remaining, + results, + is_record, + } + ); + push_frame!(frames, Frame::Eval(&arms[next].value)); + } else { + append_expr_paths(&mut results, next_remaining, work, "match remaining path")?; + produced = Some(results); } } - Ok(paths) } } + produced.ok_or_else(|| replay_error(function, "typed-HIR skeleton produced no root value")) } #[allow(clippy::too_many_arguments)] @@ -2061,6 +3379,7 @@ fn authenticated_try_stage_source( err_case: &DeclarationId, err_field: &DeclarationId, residual_type: &ResolvedType, + work: &mut SkeletonWork<'_, '_>, ) -> Result { if result.as_str() != prelude::RESULT_ID || ok_case.as_str() != prelude::RESULT_OK_ID @@ -2113,15 +3432,15 @@ fn authenticated_try_stage_source( } } Ok(StagedCopyResultSource::TryResidual { - expression: expression.id.clone(), - operand: operand.id.clone(), - source_instance: operand.ty.clone(), - target_instance: residual_type.clone(), - result: result.clone(), - ok_case: ok_case.clone(), - ok_field: ok_field.clone(), - err_case: err_case.clone(), - err_field: err_field.clone(), + expression: work.clone_owned(&expression.id, "try source expression clone")?, + operand: work.clone_owned(&operand.id, "try source operand clone")?, + source_instance: work.clone_owned(&operand.ty, "try source instance clone")?, + target_instance: work.clone_owned(residual_type, "try target instance clone")?, + result: work.clone_owned(result, "try Result identity clone")?, + ok_case: work.clone_owned(ok_case, "try Ok identity clone")?, + ok_field: work.clone_owned(ok_field, "try Ok field clone")?, + err_case: work.clone_owned(err_case, "try Err identity clone")?, + err_field: work.clone_owned(err_field, "try Err field clone")?, }) } @@ -2136,6 +3455,7 @@ fn authenticated_try_option_stage_source( some_field: &DeclarationId, none_case: &DeclarationId, residual_type: &ResolvedType, + work: &mut SkeletonWork<'_, '_>, ) -> Result { if option.as_str() != prelude::OPTION_ID || some_case.as_str() != prelude::OPTION_SOME_ID @@ -2189,14 +3509,14 @@ fn authenticated_try_option_stage_source( } } Ok(StagedCopyResultSource::TryOptionNone { - expression: expression.id.clone(), - operand: operand.id.clone(), - source_instance: operand.ty.clone(), - target_instance: residual_type.clone(), - option: option.clone(), - some_case: some_case.clone(), - some_field: some_field.clone(), - none_case: none_case.clone(), + expression: work.clone_owned(&expression.id, "Option try expression clone")?, + operand: work.clone_owned(&operand.id, "Option try operand clone")?, + source_instance: work.clone_owned(&operand.ty, "Option try source instance clone")?, + target_instance: work.clone_owned(residual_type, "Option try target instance clone")?, + option: work.clone_owned(option, "Option try identity clone")?, + some_case: work.clone_owned(some_case, "Option Some identity clone")?, + some_field: work.clone_owned(some_field, "Option Some field clone")?, + none_case: work.clone_owned(none_case, "Option None identity clone")?, }) } @@ -2245,13 +3565,13 @@ fn replay_option_arguments<'a>( Ok(arguments) } -fn match_skeleton( +fn validate_match_skeleton_shape( program: &ResolvedProgram, function: &ResolvedFunction, expression: &ResolvedExpr, scrutinee: &ResolvedExpr, arms: &[ResolvedMatchArm], -) -> Result, Diagnostic> { +) -> Result { if type_needs_drop(program, function, &expression.ty)? { return Err(replay_error( function, @@ -2268,13 +3588,15 @@ fn match_skeleton( return Err(replay_error(function, "copy-variant match has no arms")); } - let scrutinee_paths = expression_skeleton(program, function, scrutinee)?; let is_record = match &scrutinee.ty { ResolvedType::Nominal { declaration, .. } => program .declarations .declaration(declaration) .is_some_and(|item| item.kind == DeclarationKind::Record), - ResolvedType::I64 | ResolvedType::Bool | ResolvedType::TypeParameter { .. } => false, + ResolvedType::Unit + | ResolvedType::I64 + | ResolvedType::Bool + | ResolvedType::TypeParameter { .. } => false, }; if is_record { let [arm] = arms else { @@ -2289,252 +3611,531 @@ fn match_skeleton( "variant pattern has a record match scrutinee", )); } - let mut results = Vec::new(); - for mut path in scrutinee_paths { - if path.failed || path.residual { - results.push(path); - continue; - } - path.owned_source = None; - results.extend(sequence_expression( - program, - function, - vec![path], - &arm.value, - )?); - } - return Ok(results); } - let mut results = Vec::new(); - for mut path in scrutinee_paths { + Ok(is_record) +} + +#[allow(clippy::too_many_arguments)] +fn finish_match_arm( + program: &ResolvedProgram, + function: &ResolvedFunction, + expression: &ResolvedExpr, + scrutinee: &ResolvedExpr, + arms: &[ResolvedMatchArm], + index: usize, + remaining: Vec, + arm_paths: &[ExprSkeletonPath], + results: &mut Vec, + is_record: bool, + work: &mut SkeletonWork<'_, '_>, +) -> Result, Diagnostic> { + let mut next_remaining = Vec::new(); + for mut path in remaining { if path.failed || path.residual { - results.push(path); + work.push_expr_path(results, path, "match terminal scrutinee path")?; continue; } path.owned_source = None; - for (index, arm) in arms.iter().enumerate() { - let final_arm = index + 1 == arms.len(); - let mut selected = path.clone(); - if !final_arm { - let ResolvedMatchPattern::Variant { case, .. } = &arm.pattern else { - return Err(replay_error( - function, - "wildcard match arm must be the final exhaustive arm", - )); - }; - selected - .observations - .push(SkeletonObservation::VariantCase { - scrutinee: scrutinee.id.clone(), - case: case.clone(), - matches: true, - }); - path.observations.push(SkeletonObservation::VariantCase { - scrutinee: scrutinee.id.clone(), - case: case.clone(), - matches: false, - }); - } - results.extend(sequence_expression( - program, + if is_record || index + 1 == arms.len() { + let selected = sequence_skeleton_paths( + work.singleton_path(path, "match selected prefix")?, + arm_paths, + work, + )?; + let selected = + finish_conditional_result(program, function, expression, selected, work)?; + append_expr_paths(results, selected, work, "match selected result")?; + continue; + } + let ResolvedMatchPattern::Variant { case, .. } = &arms[index].pattern else { + return Err(replay_error( function, - vec![selected], - &arm.value, - )?); + "wildcard match arm must be the final exhaustive arm", + )); + }; + let mut selected = work.clone_expr_path(&path, "match selected path clone")?; + let selected_scrutinee = + work.clone_owned(&scrutinee.id, "match selected scrutinee clone")?; + let selected_case = work.clone_owned(case, "match selected case clone")?; + work.push_observation( + &mut selected, + SkeletonObservation::VariantCase { + scrutinee: selected_scrutinee, + case: selected_case, + matches: true, + }, + "match selected observation", + )?; + let selected = sequence_skeleton_paths( + work.singleton_path(selected, "match selected prefix")?, + arm_paths, + work, + )?; + let selected = finish_conditional_result(program, function, expression, selected, work)?; + append_expr_paths(results, selected, work, "match selected result")?; + let rejected_scrutinee = + work.clone_owned(&scrutinee.id, "match rejected scrutinee clone")?; + let rejected_case = work.clone_owned(case, "match rejected case clone")?; + work.push_observation( + &mut path, + SkeletonObservation::VariantCase { + scrutinee: rejected_scrutinee, + case: rejected_case, + matches: false, + }, + "match rejected observation", + )?; + work.push_expr_path(&mut next_remaining, path, "match remaining scrutinee path")?; + } + Ok(next_remaining) +} + +fn append_expr_paths( + target: &mut Vec, + paths: Vec, + work: &mut SkeletonWork<'_, '_>, + phase: &str, +) -> Result<(), Diagnostic> { + for path in paths { + work.push_expr_path(target, path, phase)?; + } + Ok(()) +} + +fn finish_record_paths( + expression: &ResolvedExpr, + mut paths: Vec, + work: &mut SkeletonWork<'_, '_>, +) -> Result, Diagnostic> { + let destination = temporary_place(expression, work)?; + for path in &mut paths { + if !path.failed && !path.residual { + path.owned_source = + Some(work.clone_owned(&destination, "record result destination clone")?); } } - Ok(results) + Ok(paths) } -fn call_skeleton( +#[allow(clippy::too_many_arguments)] +fn finish_update_paths( program: &ResolvedProgram, function: &ResolvedFunction, expression: &ResolvedExpr, - callee: &DeclarationId, - instance: Option<&FunctionInstanceId>, - args: &[ResolvedExpr], + base: &ResolvedExpr, + record: &DeclarationId, + mut paths: Vec, + replaced: &BTreeSet, + needs_cleanup: bool, + work: &mut SkeletonWork<'_, '_>, ) -> Result, Diagnostic> { - let target = program - .resolve_call_target(callee, instance) - .ok_or_else(|| replay_error(function, format!("unknown skeleton callee `{callee}`")))?; - let mut states = vec![(empty_expr_path(), Vec::<(u32, CleanupPlace)>::new())]; - for (index, (argument, parameter)) in args.iter().zip(&target.params).enumerate() { - let suffixes = expression_skeleton(program, function, argument)?; - let mut next = Vec::new(); - for (prefix, commits) in states { - if prefix.failed || prefix.residual { - next.push((prefix, commits)); + if !needs_cleanup { + return Ok(paths); + } + let staged_base = temporary_place(base, work)?; + let destination = temporary_place(expression, work)?; + let declarations = program.declarations.record_fields(record).ok_or_else(|| { + replay_error( + function, + format!("record update has unknown record `{record}`"), + ) + })?; + for field in declarations { + if replaced.contains(&field.id) || !type_needs_drop(program, function, &field.ty)? { + continue; + } + for path in &mut paths { + if path.failed || path.residual { continue; } - for suffix in &suffixes { - let mut observations = prefix.observations.clone(); - observations.extend(suffix.observations.clone()); - let mut path = ExprSkeletonPath { - observations, - owned_source: suffix.owned_source.clone(), - failed: suffix.failed, - residual: suffix.residual, - }; - let mut path_commits = commits.clone(); - if !path.failed - && !path.residual - && parameter.ownership == OwnershipMode::Own - && type_needs_drop(program, function, ¶meter.ty)? - { - let parameter_index = u32::try_from(index) - .map_err(|_| replay_error(function, "too many skeleton call arguments"))?; - let epoch = CleanupPlace { - storage: StorageId::CallArgument { - call: expression.id.clone(), - parameter_index, - value_expression: argument.id.clone(), - }, - projections: Vec::new(), - }; - let source = path.owned_source.take().ok_or_else(|| { - replay_error(function, "owned call argument has no HIR cleanup source") - })?; - path.observations.push(SkeletonObservation::Transfer { - at: argument.id.clone(), - source, - destination: epoch.clone(), - }); - path_commits.push((parameter_index, epoch)); - } - next.push((path, path_commits)); - } + let mut source = work.clone_owned(&staged_base, "update source place clone")?; + let source_field = work.clone_owned(&field.id, "update source field clone")?; + work.charge(1, "update source projection push")?; + note_skeleton_materialization(); + source.projections.push(source_field); + let mut field_destination = + work.clone_owned(&destination, "update destination place clone")?; + let destination_field = + work.clone_owned(&field.id, "update destination field clone")?; + work.charge(1, "update destination projection push")?; + note_skeleton_materialization(); + field_destination.projections.push(destination_field); + let at = work.clone_owned(&expression.id, "update transfer expression clone")?; + work.push_observation( + path, + SkeletonObservation::Transfer { + at, + source, + destination: field_destination, + }, + "untouched update-field transfer", + )?; } - states = next; } + finish_record_paths(expression, paths, work) +} - let source = StatusSourceId { - expression: expression.id.clone(), - lane: StatusLane::OperationFailure, +fn finish_try_paths( + program: &ResolvedProgram, + function: &ResolvedFunction, + expression: &ResolvedExpr, + operand_paths: Vec, + option: bool, + work: &mut SkeletonWork<'_, '_>, +) -> Result, Diagnostic> { + let (operand, success_case, source) = if option { + let ResolvedExprKind::TryOption { + operand, + option, + some_case, + some_field, + none_case, + residual_type, + } = &expression.kind + else { + unreachable!("Option try continuation retains Option try HIR") + }; + ( + operand.as_ref(), + some_case, + authenticated_try_option_stage_source( + program, + function, + expression, + operand, + option, + some_case, + some_field, + none_case, + residual_type, + work, + )?, + ) + } else { + let ResolvedExprKind::Try { + operand, + result, + ok_case, + ok_field, + err_case, + err_field, + residual_type, + } = &expression.kind + else { + unreachable!("try continuation retains Result try HIR") + }; + ( + operand.as_ref(), + ok_case, + authenticated_try_stage_source( + program, + function, + expression, + operand, + result, + ok_case, + ok_field, + err_case, + err_field, + residual_type, + work, + )?, + ) }; - let mut results = Vec::new(); - for (mut path, commits) in states { + let mut paths = Vec::new(); + for path in operand_paths { if path.failed || path.residual { - results.push(path); + work.push_expr_path(&mut paths, path, "short-circuited try path")?; continue; } - path.observations.push(SkeletonObservation::CallCommit { - call: expression.id.clone(), - arguments: commits, - }); - let mut failure = path.clone(); - failure.observations.push(SkeletonObservation::Status { - source: source.clone(), - success: false, - }); - failure.failed = true; - failure.owned_source = None; - results.push(failure); + let mut success = work.clone_expr_path(&path, "try success path clone")?; + let success_scrutinee = work.clone_owned(&operand.id, "try success scrutinee clone")?; + let selected_case = work.clone_owned(success_case, "try success case clone")?; + let residual_case = work.clone_owned(success_case, "try residual case clone")?; + work.push_observation( + &mut success, + SkeletonObservation::VariantCase { + scrutinee: success_scrutinee, + case: selected_case, + matches: true, + }, + "try success observation", + )?; + work.push_expr_path(&mut paths, success, "try success path")?; + + let mut residual = path; + let residual_scrutinee = work.clone_owned(&operand.id, "try residual scrutinee clone")?; + work.push_observation( + &mut residual, + SkeletonObservation::VariantCase { + scrutinee: residual_scrutinee, + case: residual_case, + matches: false, + }, + "try residual case observation", + )?; + let staged_source = work.clone_owned(&source, "try staged-result source clone")?; + work.push_observation( + &mut residual, + SkeletonObservation::StageCopyResult(staged_source), + "try residual staging observation", + )?; + residual.residual = true; + work.push_expr_path(&mut paths, residual, "try residual path")?; + } + Ok(paths) +} - path.observations.push(SkeletonObservation::Status { - source: source.clone(), - success: true, - }); - if expression.ownership == OwnershipMode::Own - && type_needs_drop(program, function, &expression.ty)? - { - let destination = temporary_place(expression); - path.observations.push(SkeletonObservation::Initialize { - at: expression.id.clone(), - destination: destination.clone(), - }); - path.owned_source = Some(destination); - } else { - path.owned_source = None; +fn split_boolean_prefixes( + paths: Vec, + expression: &ExpressionId, + work: &mut SkeletonWork<'_, '_>, +) -> Result { + let mut terminal = Vec::new(); + let mut true_paths = Vec::new(); + let mut false_paths = Vec::new(); + for mut path in paths { + if path.failed || path.residual { + work.push_expr_path(&mut terminal, path, "conditional terminal path")?; + continue; } - results.push(path); + let mut when_true = work.clone_expr_path(&path, "conditional true path clone")?; + let true_expression = work.clone_owned(expression, "conditional true expression clone")?; + work.push_observation( + &mut when_true, + SkeletonObservation::Boolean { + expression: true_expression, + value: true, + }, + "conditional true observation", + )?; + work.push_expr_path(&mut true_paths, when_true, "conditional true prefix")?; + let false_expression = + work.clone_owned(expression, "conditional false expression clone")?; + work.push_observation( + &mut path, + SkeletonObservation::Boolean { + expression: false_expression, + value: false, + }, + "conditional false observation", + )?; + work.push_expr_path(&mut false_paths, path, "conditional false prefix")?; } - Ok(results) + Ok((terminal, true_paths, false_paths)) } -fn lazy_skeleton( +fn finish_conditional_result( program: &ResolvedProgram, function: &ResolvedFunction, - expression: ResolvedExpr, - left: ResolvedExpr, - right: ResolvedExpr, + expression: &ResolvedExpr, + paths: Vec, + work: &mut SkeletonWork<'_, '_>, ) -> Result, Diagnostic> { - let ResolvedExprKind::Binary { op, .. } = expression.kind else { - return Err(replay_error( + if expression.ownership == OwnershipMode::Own + && type_needs_drop(program, function, &expression.ty)? + { + let expression_id = + work.clone_owned(&expression.id, "conditional result expression clone")?; + transfer_completed_paths( function, - "lazy skeleton received non-binary HIR", - )); + paths, + expression_id, + temporary_place(expression, work)?, + "owned conditional result", + work, + ) + } else { + Ok(paths) + } +} + +fn finish_lazy_paths( + function: &ResolvedFunction, + _expression: &ResolvedExpr, + op: BinaryOp, + left: &ResolvedExpr, + left_paths: Vec, + right_paths: &[ExprSkeletonPath], + work: &mut SkeletonWork<'_, '_>, +) -> Result, Diagnostic> { + if !matches!(op, BinaryOp::And | BinaryOp::Or) { + return Err(replay_error(function, "invalid lazy skeleton operation")); + } + let (mut terminal, true_paths, false_paths) = + split_boolean_prefixes(left_paths, &left.id, work)?; + let (evaluated, short) = if op == BinaryOp::And { + (true_paths, false_paths) + } else { + (false_paths, true_paths) }; - let left_paths = expression_skeleton(program, function, &left)?; - let mut results = Vec::new(); - for path in left_paths { - if path.failed || path.residual { - results.push(path); + for mut path in short { + path.owned_source = None; + work.push_expr_path(&mut terminal, path, "lazy short-circuit path")?; + } + let evaluated = sequence_skeleton_paths(evaluated, right_paths, work)?; + append_expr_paths(&mut terminal, evaluated, work, "lazy evaluated-right path")?; + Ok(terminal) +} + +fn call_states_have_active(states: &[CallSkeletonState]) -> bool { + states + .iter() + .any(|(path, _)| !path.failed && !path.residual) +} + +#[allow(clippy::too_many_arguments)] +fn sequence_call_argument( + program: &ResolvedProgram, + function: &ResolvedFunction, + expression: &ResolvedExpr, + target: &ResolvedFunction, + args: &[ResolvedExpr], + index: usize, + states: Vec, + suffixes: &[ExprSkeletonPath], + work: &mut SkeletonWork<'_, '_>, +) -> Result, Diagnostic> { + let argument = &args[index]; + let parameter = target + .params + .get(index) + .ok_or_else(|| replay_error(function, "skeleton call arity is inconsistent"))?; + let mut next = Vec::new(); + for (prefix, commits) in states { + if prefix.failed || prefix.residual { + work.charge(1, "short-circuited call state push")?; + note_skeleton_materialization(); + next.push((prefix, commits)); continue; } - for value in [true, false] { - let mut branch = path.clone(); - branch.observations.push(SkeletonObservation::Boolean { - expression: left.id.clone(), - value, - }); - let evaluates_right = match op { - BinaryOp::And => value, - BinaryOp::Or => !value, - _ => return Err(replay_error(function, "invalid lazy skeleton operation")), + for suffix in suffixes { + let mut observations = + work.clone_observations(&prefix.observations, "call prefix clone")?; + work.extend_observations(&mut observations, &suffix.observations, "call suffix clone")?; + let owned_source = + work.clone_owned(&suffix.owned_source, "call suffix owned-source clone")?; + let mut path = ExprSkeletonPath { + observations, + owned_source, + failed: suffix.failed, + residual: suffix.residual, }; - if evaluates_right { - results.extend(sequence_expression( - program, - function, - vec![branch], - &right, - )?); - } else { - branch.owned_source = None; - results.push(branch); + let mut path_commits = work.clone_owned(&commits, "call commit-state clone")?; + if !path.failed + && !path.residual + && parameter.ownership == OwnershipMode::Own + && type_needs_drop(program, function, ¶meter.ty)? + { + let parameter_index = u32::try_from(index) + .map_err(|_| replay_error(function, "too many skeleton call arguments"))?; + let call = work.clone_owned(&expression.id, "call-epoch call identity clone")?; + let value_expression = + work.clone_owned(&argument.id, "call-epoch value identity clone")?; + let epoch = CleanupPlace { + storage: StorageId::CallArgument { + call, + parameter_index, + value_expression, + }, + projections: Vec::new(), + }; + let source = path.owned_source.take().ok_or_else(|| { + replay_error(function, "owned call argument has no HIR cleanup source") + })?; + let at = work.clone_owned(&argument.id, "call-argument transfer identity clone")?; + let destination = work.clone_owned(&epoch, "call-argument epoch clone")?; + work.push_observation( + &mut path, + SkeletonObservation::Transfer { + at, + source, + destination, + }, + "owned call-argument transfer", + )?; + work.charge(1, "call-commit argument push")?; + note_skeleton_materialization(); + path_commits.push((parameter_index, epoch)); } + work.charge(1, "call state push")?; + note_skeleton_materialization(); + next.push((path, path_commits)); } } - Ok(results) + Ok(next) } -fn if_skeleton( +fn finish_call_states( program: &ResolvedProgram, function: &ResolvedFunction, expression: &ResolvedExpr, - condition: &ResolvedExpr, - then_branch: &ResolvedExpr, - else_branch: &ResolvedExpr, + states: Vec, + work: &mut SkeletonWork<'_, '_>, ) -> Result, Diagnostic> { - let condition_paths = expression_skeleton(program, function, condition)?; + let source_expression = + work.clone_owned(&expression.id, "call status source expression clone")?; + let source = StatusSourceId { + expression: source_expression, + lane: StatusLane::OperationFailure, + }; let mut results = Vec::new(); - for path in condition_paths { + for (mut path, commits) in states { if path.failed || path.residual { - results.push(path); + work.push_expr_path(&mut results, path, "short-circuited call path")?; continue; } - for (value, branch_expression) in [(true, then_branch), (false, else_branch)] { - let mut branch = path.clone(); - branch.observations.push(SkeletonObservation::Boolean { - expression: condition.id.clone(), - value, - }); - let branch_paths = - sequence_expression(program, function, vec![branch], branch_expression)?; - if expression.ownership == OwnershipMode::Own - && type_needs_drop(program, function, &expression.ty)? - { - results.extend(transfer_completed_paths( - function, - branch_paths, - expression.id.clone(), - temporary_place(expression), - "owned conditional result", - )?); - } else { - results.extend(branch_paths); - } + let call = work.clone_owned(&expression.id, "call-commit identity clone")?; + work.push_observation( + &mut path, + SkeletonObservation::CallCommit { + call, + arguments: commits, + }, + "call-commit observation", + )?; + let mut failure = work.clone_expr_path(&path, "call failure path clone")?; + let failure_source = work.clone_owned(&source, "call failure status-source clone")?; + work.push_observation( + &mut failure, + SkeletonObservation::Status { + source: failure_source, + success: false, + }, + "call failure observation", + )?; + failure.failed = true; + failure.owned_source = None; + work.push_expr_path(&mut results, failure, "call failure path")?; + + let success_source = work.clone_owned(&source, "call success status-source clone")?; + work.push_observation( + &mut path, + SkeletonObservation::Status { + source: success_source, + success: true, + }, + "call success observation", + )?; + if expression.ownership == OwnershipMode::Own + && type_needs_drop(program, function, &expression.ty)? + { + let destination = temporary_place(expression, work)?; + let at = work.clone_owned(&expression.id, "call result expression clone")?; + let initialized = work.clone_owned(&destination, "call result destination clone")?; + work.push_observation( + &mut path, + SkeletonObservation::Initialize { + at, + destination: initialized, + }, + "owned call result initialization", + )?; + path.owned_source = Some(destination); + } else { + path.owned_source = None; } + work.push_expr_path(&mut results, path, "call success path")?; } Ok(results) } @@ -2542,54 +4143,81 @@ fn if_skeleton( fn split_status_paths( paths: Vec, source: StatusSourceId, -) -> Vec { + work: &mut SkeletonWork<'_, '_>, +) -> Result, Diagnostic> { let mut results = Vec::new(); for mut path in paths { if path.failed || path.residual { - results.push(path); + work.push_expr_path(&mut results, path, "short-circuited status path")?; continue; } - let mut failure = path.clone(); - failure.observations.push(SkeletonObservation::Status { - source: source.clone(), - success: false, - }); + let mut failure = work.clone_expr_path(&path, "status failure path clone")?; + let failure_source = work.clone_owned(&source, "failure status-source clone")?; + work.push_observation( + &mut failure, + SkeletonObservation::Status { + source: failure_source, + success: false, + }, + "status failure observation", + )?; failure.failed = true; failure.owned_source = None; - results.push(failure); - path.observations.push(SkeletonObservation::Status { - source: source.clone(), - success: true, - }); + work.push_expr_path(&mut results, failure, "status failure path")?; + let success_source = work.clone_owned(&source, "success status-source clone")?; + work.push_observation( + &mut path, + SkeletonObservation::Status { + source: success_source, + success: true, + }, + "status success observation", + )?; path.owned_source = None; - results.push(path); + work.push_expr_path(&mut results, path, "status success path")?; } - results + Ok(results) } -fn split_contract(paths: Vec, contract: &ResolvedExpr) -> Vec { +fn split_contract( + paths: Vec, + contract: &ResolvedExpr, + work: &mut SkeletonWork<'_, '_>, +) -> Result, Diagnostic> { let mut results = Vec::new(); for mut path in paths { if path.failed || path.residual { - results.push(path); + work.push_expr_path(&mut results, path, "short-circuited contract path")?; continue; } - let mut failure = path.clone(); - failure.observations.push(SkeletonObservation::Boolean { - expression: contract.id.clone(), - value: false, - }); + let mut failure = work.clone_expr_path(&path, "contract failure path clone")?; + let failure_expression = + work.clone_owned(&contract.id, "contract failure expression clone")?; + work.push_observation( + &mut failure, + SkeletonObservation::Boolean { + expression: failure_expression, + value: false, + }, + "contract failure observation", + )?; failure.failed = true; failure.owned_source = None; - results.push(failure); - path.observations.push(SkeletonObservation::Boolean { - expression: contract.id.clone(), - value: true, - }); + work.push_expr_path(&mut results, failure, "contract failure path")?; + let success_expression = + work.clone_owned(&contract.id, "contract success expression clone")?; + work.push_observation( + &mut path, + SkeletonObservation::Boolean { + expression: success_expression, + value: true, + }, + "contract success observation", + )?; path.owned_source = None; - results.push(path); + work.push_expr_path(&mut results, path, "contract success path")?; } - results + Ok(results) } fn transfer_completed_paths( @@ -2598,6 +4226,7 @@ fn transfer_completed_paths( at: ExpressionId, destination: CleanupPlace, description: &str, + work: &mut SkeletonWork<'_, '_>, ) -> Result, Diagnostic> { for path in &mut paths { if path.failed || path.residual { @@ -2606,36 +4235,55 @@ fn transfer_completed_paths( let source = path.owned_source.take().ok_or_else(|| { replay_error(function, format!("{description} has no HIR cleanup source")) })?; - path.observations.push(SkeletonObservation::Transfer { - at: at.clone(), - source, - destination: destination.clone(), - }); - path.owned_source = Some(destination.clone()); + let transfer_at = work.clone_owned(&at, "completed transfer expression clone")?; + let transfer_destination = + work.clone_owned(&destination, "completed transfer destination clone")?; + work.push_observation( + path, + SkeletonObservation::Transfer { + at: transfer_at, + source, + destination: transfer_destination, + }, + "completed-path transfer observation", + )?; + path.owned_source = + Some(work.clone_owned(&destination, "completed transfer result-place clone")?); } Ok(paths) } -fn temporary_place(expression: &ResolvedExpr) -> CleanupPlace { - CleanupPlace { - storage: StorageId::Temporary(expression.id.clone()), +fn temporary_place( + expression: &ResolvedExpr, + work: &mut SkeletonWork<'_, '_>, +) -> Result { + Ok(CleanupPlace { + storage: StorageId::Temporary( + work.clone_owned(&expression.id, "temporary-place expression clone")?, + ), projections: Vec::new(), - } + }) } fn cleanup_place_from_hir( function: &ResolvedFunction, place: &crate::hir::Place, + work: &mut SkeletonWork<'_, '_>, ) -> Result { let storage = if place.root == function.result_id { StorageId::ProvisionalResult } else { - StorageId::Value(place.root.clone()) + StorageId::Value(work.clone_owned(&place.root, "place-root clone")?) }; let mut projections = Vec::new(); for projection in &place.projections { match projection { - PlaceProjection::Field(field) => projections.push(field.clone()), + PlaceProjection::Field(field) => { + let field = work.clone_owned(field, "place-projection clone")?; + work.charge(1, "place-projection push")?; + note_skeleton_materialization(); + projections.push(field); + } PlaceProjection::VariantField { .. } => { return Err(replay_error( function, @@ -2655,50 +4303,129 @@ fn plan_skeleton_paths( budget: &mut ReplayBudget, ) -> Result, Diagnostic> { let plan = &function.cleanup_plan; - let mut queue = VecDeque::from([(plan.entry, Vec::::new())]); + let mut queue = VecDeque::new(); + skeleton_queue_push( + budget, + function, + &mut queue, + (plan.entry, Vec::::new()), + "cleanup-plan root state push", + )?; let mut paths = Vec::new(); while let Some((block, mut observations)) = queue.pop_front() { - budget.charge( + let block = &plan.blocks[block.0 as usize]; + // Charge only work performed at this block. The previous charge used + // the entire accumulated observation length on every linear block, + // turning a depth-D skewed conditional into artificial O(D^3) work. + // Observation history is copied only at a real branch, charged below + // immediately before each clone. + budget.charge_skeleton( function, - observations.len().saturating_add(1), + block.transitions.len().saturating_add(1), "cleanup-plan skeleton expansion", )?; - let block = &plan.blocks[block.0 as usize]; for transition in &block.transitions { match transition { CleanupTransition::Initialize { at, destination } => { - observations.push(SkeletonObservation::Initialize { - at: at.clone(), - destination: destination.clone(), - }); + let at = + skeleton_clone(budget, function, at, "plan initialize expression clone")?; + let destination = skeleton_clone( + budget, + function, + destination, + "plan initialize destination clone", + )?; + skeleton_push( + budget, + function, + &mut observations, + SkeletonObservation::Initialize { at, destination }, + "plan initialize observation push", + )?; } CleanupTransition::Transfer { at, source, destination, - } => observations.push(SkeletonObservation::Transfer { - at: at.clone(), - source: source.clone(), - destination: destination.clone(), - }), + } => { + let at = + skeleton_clone(budget, function, at, "plan transfer expression clone")?; + let source = + skeleton_clone(budget, function, source, "plan transfer source clone")?; + let destination = skeleton_clone( + budget, + function, + destination, + "plan transfer destination clone", + )?; + skeleton_push( + budget, + function, + &mut observations, + SkeletonObservation::Transfer { + at, + source, + destination, + }, + "plan transfer observation push", + )?; + } CleanupTransition::CallCommit { call, arguments } => { - observations.push(SkeletonObservation::CallCommit { - call: call.clone(), - arguments: arguments - .iter() - .map(|argument| (argument.parameter_index, argument.source.clone())) - .collect(), - }); + let call = skeleton_clone(budget, function, call, "plan call identity clone")?; + let mut cloned_arguments = Vec::new(); + for argument in arguments { + let source = skeleton_clone( + budget, + function, + &argument.source, + "plan call argument source clone", + )?; + skeleton_push( + budget, + function, + &mut cloned_arguments, + (argument.parameter_index, source), + "plan call argument push", + )?; + } + skeleton_push( + budget, + function, + &mut observations, + SkeletonObservation::CallCommit { + call, + arguments: cloned_arguments, + }, + "plan call observation push", + )?; } CleanupTransition::SelectFailure { .. } => {} CleanupTransition::StageCopyResult { source } => { - observations.push(SkeletonObservation::StageCopyResult(source.clone())) + let source = skeleton_clone( + budget, + function, + source, + "plan staged-result source clone", + )?; + skeleton_push( + budget, + function, + &mut observations, + SkeletonObservation::StageCopyResult(source), + "plan staged-result observation push", + )?; } } } match &block.terminator { CleanupTerminator::Goto(edge) => { - queue.push_back((plan.edges[edge.0 as usize].to, observations)); + skeleton_queue_push( + budget, + function, + &mut queue, + (plan.edges[edge.0 as usize].to, observations), + "plan goto state push", + )?; } CleanupTerminator::Branch(edges) => { for edge in edges { @@ -2706,7 +4433,12 @@ fn plan_skeleton_paths( let observation = match &edge.condition { EdgeCondition::BooleanResult(expression, value) => { SkeletonObservation::Boolean { - expression: expression.clone(), + expression: skeleton_clone( + budget, + function, + expression, + "plan Boolean expression clone", + )?, value: *value, } } @@ -2715,16 +4447,36 @@ fn plan_skeleton_paths( case, matches, } => SkeletonObservation::VariantCase { - scrutinee: scrutinee.clone(), - case: case.clone(), + scrutinee: skeleton_clone( + budget, + function, + scrutinee, + "plan variant scrutinee clone", + )?, + case: skeleton_clone( + budget, + function, + case, + "plan variant case clone", + )?, matches: *matches, }, EdgeCondition::StatusZero(source) => SkeletonObservation::Status { - source: source.clone(), + source: skeleton_clone( + budget, + function, + source, + "plan zero status-source clone", + )?, success: true, }, EdgeCondition::StatusNonzero(source) => SkeletonObservation::Status { - source: source.clone(), + source: skeleton_clone( + budget, + function, + source, + "plan nonzero status-source clone", + )?, success: false, }, EdgeCondition::Always => { @@ -2734,27 +4486,62 @@ fn plan_skeleton_paths( )); } }; - let mut branch = observations.clone(); - branch.push(observation); - queue.push_back((edge.to, branch)); + let mut branch = skeleton_clone( + budget, + function, + &observations, + "cleanup-plan skeleton branch clone", + )?; + skeleton_push( + budget, + function, + &mut branch, + observation, + "plan branch observation push", + )?; + skeleton_queue_push( + budget, + function, + &mut queue, + (edge.to, branch), + "plan branch state push", + )?; } } CleanupTerminator::Exit(exit) => { let exit = &plan.exits[exit.0 as usize]; match exit.continuation { ExitContinuation::Continue(edge) => { - queue.push_back((plan.edges[edge.0 as usize].to, observations)); + skeleton_queue_push( + budget, + function, + &mut queue, + (plan.edges[edge.0 as usize].to, observations), + "plan continuation state push", + )?; } ExitContinuation::CommitResult { .. } | ExitContinuation::ReturnUnit => { - paths.push(SkeletonPath { - observations, - terminal: SkeletonTerminal::Success, - }); + skeleton_push( + budget, + function, + &mut paths, + SkeletonPath { + observations, + terminal: SkeletonTerminal::Success, + }, + "plan success path push", + )?; } - ExitContinuation::ReturnFailure { .. } => paths.push(SkeletonPath { - observations, - terminal: SkeletonTerminal::Failure, - }), + ExitContinuation::ReturnFailure { .. } => skeleton_push( + budget, + function, + &mut paths, + SkeletonPath { + observations, + terminal: SkeletonTerminal::Failure, + }, + "plan failure path push", + )?, } } } @@ -3364,86 +5151,87 @@ fn validate_staged_target( Ok(()) } -fn expression_has_try(expression: &ResolvedExpr) -> bool { +fn replay_expression_child(expression: &ResolvedExpr, index: usize) -> Option<&ResolvedExpr> { match &expression.kind { - ResolvedExprKind::Try { .. } | ResolvedExprKind::TryOption { .. } => true, - ResolvedExprKind::Call { args, .. } => args.iter().any(expression_has_try), - ResolvedExprKind::Unary { value, .. } | ResolvedExprKind::Project { base: value, .. } => { - expression_has_try(value) - } + ResolvedExprKind::Call { args, .. } => args.get(index), + ResolvedExprKind::NativeRustImportCall(call) => call.args.get(index), + ResolvedExprKind::Unary { value, .. } + | ResolvedExprKind::Try { operand: value, .. } + | ResolvedExprKind::TryOption { operand: value, .. } + | ResolvedExprKind::Project { base: value, .. } => (index == 0).then_some(value), ResolvedExprKind::Binary { left, right, .. } => { - expression_has_try(left) || expression_has_try(right) + [left.as_ref(), right.as_ref()].get(index).copied() } - ResolvedExprKind::Block { statements, tail } => { - statements.iter().any(|statement| { + ResolvedExprKind::Block { statements, tail } => statements + .get(index) + .map(|statement| { let ResolvedStatement::Let { value, .. } = statement; - expression_has_try(value) - }) || expression_has_try(tail) - } + value + }) + .or_else(|| (index == statements.len()).then_some(tail)), ResolvedExprKind::If { condition, then_branch, else_branch, - } => { - expression_has_try(condition) - || expression_has_try(then_branch) - || expression_has_try(else_branch) - } + } => [ + condition.as_ref(), + then_branch.as_ref(), + else_branch.as_ref(), + ] + .get(index) + .copied(), ResolvedExprKind::ConstructRecord { fields, .. } | ResolvedExprKind::ConstructVariant { fields, .. } => { - fields.iter().any(|field| expression_has_try(&field.value)) - } - ResolvedExprKind::Match { scrutinee, arms } => { - expression_has_try(scrutinee) || arms.iter().any(|arm| expression_has_try(&arm.value)) + fields.get(index).map(|field| &field.value) } - ResolvedExprKind::UpdateRecord { base, fields, .. } => { - expression_has_try(base) || fields.iter().any(|field| expression_has_try(&field.value)) - } - ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) | ResolvedExprKind::Place(_) => false, + ResolvedExprKind::Match { scrutinee, arms } => (index == 0) + .then_some(scrutinee.as_ref()) + .or_else(|| arms.get(index - 1).map(|arm| &arm.value)), + ResolvedExprKind::UpdateRecord { base, fields, .. } => (index == 0) + .then_some(base.as_ref()) + .or_else(|| fields.get(index - 1).map(|field| &field.value)), + ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) | ResolvedExprKind::Place(_) => None, } } -fn expression_has_option_try(expression: &ResolvedExpr) -> bool { - match &expression.kind { - ResolvedExprKind::TryOption { .. } => true, - ResolvedExprKind::Call { args, .. } => args.iter().any(expression_has_option_try), - ResolvedExprKind::Unary { value, .. } - | ResolvedExprKind::Try { operand: value, .. } - | ResolvedExprKind::Project { base: value, .. } => expression_has_option_try(value), - ResolvedExprKind::Binary { left, right, .. } => { - expression_has_option_try(left) || expression_has_option_try(right) - } - ResolvedExprKind::Block { statements, tail } => { - statements.iter().any(|statement| { - let ResolvedStatement::Let { value, .. } = statement; - expression_has_option_try(value) - }) || expression_has_option_try(tail) - } - ResolvedExprKind::If { - condition, - then_branch, - else_branch, - } => { - expression_has_option_try(condition) - || expression_has_option_try(then_branch) - || expression_has_option_try(else_branch) - } - ResolvedExprKind::ConstructRecord { fields, .. } - | ResolvedExprKind::ConstructVariant { fields, .. } => fields - .iter() - .any(|field| expression_has_option_try(&field.value)), - ResolvedExprKind::Match { scrutinee, arms } => { - expression_has_option_try(scrutinee) - || arms.iter().any(|arm| expression_has_option_try(&arm.value)) - } - ResolvedExprKind::UpdateRecord { base, fields, .. } => { - expression_has_option_try(base) - || fields - .iter() - .any(|field| expression_has_option_try(&field.value)) +fn expression_has_kind( + expression: &ResolvedExpr, + predicate: impl Fn(&ResolvedExprKind) -> bool, +) -> bool { + let mut stack = [None; 514]; + stack[0] = Some((expression, 0usize)); + let mut len = 1usize; + while len != 0 { + len -= 1; + let (expression, next) = stack[len].take().expect("expression frame retained"); + if next == 0 && predicate(&expression.kind) { + return true; + } + if let Some(child) = replay_expression_child(expression, next) { + if len + 2 > stack.len() { + return false; + } + stack[len] = Some((expression, next + 1)); + stack[len + 1] = Some((child, 0)); + len += 2; } - ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) | ResolvedExprKind::Place(_) => false, } + false +} + +fn expression_has_try(expression: &ResolvedExpr) -> bool { + expression_has_kind(expression, |kind| { + matches!( + kind, + ResolvedExprKind::Try { .. } | ResolvedExprKind::TryOption { .. } + ) + }) +} + +fn expression_has_option_try(expression: &ResolvedExpr) -> bool { + expression_has_kind(expression, |kind| { + matches!(kind, ResolvedExprKind::TryOption { .. }) + }) } fn validate_join_compatibility( @@ -3769,80 +5557,48 @@ fn collect_expression_facts( expression: &ResolvedExpr, facts: &mut BTreeMap>, ) -> Result<(), Diagnostic> { - let fact = match &expression.kind { - ResolvedExprKind::Call { - callee, - instance, - args, - .. - } => Some(CallFact { - callee: callee.clone(), - instance: instance.clone(), - arguments: args.iter().map(|argument| argument.id.clone()).collect(), - }), - _ => None, - }; - if facts.insert(expression.id.clone(), fact).is_some() { - return Err(replay_error( - function, - format!("HIR expression identity `{}` is repeated", expression.id), - )); - } - match &expression.kind { - ResolvedExprKind::Call { args, .. } => { - for argument in args { - collect_expression_facts(function, argument, facts)?; - } - } - ResolvedExprKind::Unary { value, .. } | ResolvedExprKind::Project { base: value, .. } => { - collect_expression_facts(function, value, facts)?; - } - ResolvedExprKind::Binary { left, right, .. } => { - collect_expression_facts(function, left, facts)?; - collect_expression_facts(function, right, facts)?; - } - ResolvedExprKind::Block { statements, tail } => { - for statement in statements { - let crate::hir::ResolvedStatement::Let { value, .. } = statement; - collect_expression_facts(function, value, facts)?; - } - collect_expression_facts(function, tail, facts)?; - } - ResolvedExprKind::If { - condition, - then_branch, - else_branch, - } => { - collect_expression_facts(function, condition, facts)?; - collect_expression_facts(function, then_branch, facts)?; - collect_expression_facts(function, else_branch, facts)?; - } - ResolvedExprKind::ConstructRecord { fields, .. } => { - for field in fields { - collect_expression_facts(function, &field.value, facts)?; - } - } - ResolvedExprKind::ConstructVariant { fields, .. } => { - for field in fields { - collect_expression_facts(function, &field.value, facts)?; - } - } - ResolvedExprKind::Try { operand, .. } | ResolvedExprKind::TryOption { operand, .. } => { - collect_expression_facts(function, operand, facts)?; - } - ResolvedExprKind::Match { scrutinee, arms } => { - collect_expression_facts(function, scrutinee, facts)?; - for arm in arms { - collect_expression_facts(function, &arm.value, facts)?; + // The private replay entry admits at most 512 semantic expression levels. + // Keep one indexed continuation per ancestor so wide calls, records, blocks, + // and matches never create a width-sized frontier and callback order stays + // identical to the former recursive pre-order walk. + let mut stack = [None; 514]; + stack[0] = Some((expression, 0usize)); + let mut len = 1usize; + while len != 0 { + len -= 1; + let (current, next_child) = stack[len].take().expect("expression-fact frame retained"); + if next_child == 0 { + let fact = match ¤t.kind { + ResolvedExprKind::Call { + callee, + instance, + args, + .. + } => Some(CallFact { + callee: callee.clone(), + instance: instance.clone(), + arguments: args.iter().map(|argument| argument.id.clone()).collect(), + }), + _ => None, + }; + if facts.insert(current.id.clone(), fact).is_some() { + return Err(replay_error( + function, + format!("HIR expression identity `{}` is repeated", current.id), + )); } } - ResolvedExprKind::UpdateRecord { base, fields, .. } => { - collect_expression_facts(function, base, facts)?; - for field in fields { - collect_expression_facts(function, &field.value, facts)?; + if let Some(child) = replay_expression_child(current, next_child) { + if len + 2 > stack.len() { + return Err(replay_error( + function, + "HIR expression fact traversal exceeds the admitted depth", + )); } + stack[len] = Some((current, next_child + 1)); + stack[len + 1] = Some((child, 0)); + len += 2; } - ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) | ResolvedExprKind::Place(_) => {} } Ok(()) } @@ -4729,16 +6485,397 @@ fn main() -> i64 { 0 } assert!(validate_reachable_acyclic_cfg(&function).is_ok()); } + #[test] + fn replay_preflight_rejects_every_invalid_cfg_target_and_cycles_without_panicking() { + fn assert_unknown(program: &ResolvedProgram, function: &ResolvedFunction) { + let diagnostic = validate_structure(program, function).unwrap_err(); + assert_eq!(diagnostic.code, "SPX-H006"); + assert_eq!( + diagnostic.message, + format!( + "cleanup plan for function `{}` failed independent replay: cleanup replay preflight references an unknown id", + function.id + ) + ); + } + + let program = program(); + let original = function(&program, "flow.regions"); + + let mut invalid_entry = original.clone(); + invalid_entry.cleanup_plan.entry = BlockId(u32::MAX); + assert_unknown(&program, &invalid_entry); + + let mut invalid_terminator_edge = original.clone(); + let edge = invalid_terminator_edge + .cleanup_plan + .blocks + .iter_mut() + .find_map(|block| match &mut block.terminator { + CleanupTerminator::Goto(edge) => Some(edge), + CleanupTerminator::Branch(edges) => edges.first_mut(), + CleanupTerminator::Exit(_) => None, + }) + .expect("fixture must contain a branch or goto edge"); + *edge = EdgeId(u32::MAX); + assert_unknown(&program, &invalid_terminator_edge); + + let mut invalid_edge_target = original.clone(); + invalid_edge_target + .cleanup_plan + .edges + .first_mut() + .expect("fixture must contain an edge") + .to = BlockId(u32::MAX); + assert_unknown(&program, &invalid_edge_target); + + let mut invalid_exit = original.clone(); + let exit = invalid_exit + .cleanup_plan + .blocks + .iter_mut() + .find_map(|block| match &mut block.terminator { + CleanupTerminator::Exit(exit) => Some(exit), + CleanupTerminator::Goto(_) | CleanupTerminator::Branch(_) => None, + }) + .expect("fixture must contain an exit terminator"); + *exit = crate::cleanup_plan::ExitTargetId(u32::MAX); + assert_unknown(&program, &invalid_exit); + + let mut invalid_continue = original.clone(); + let continuation = invalid_continue + .cleanup_plan + .exits + .iter_mut() + .find_map(|exit| match &mut exit.continuation { + ExitContinuation::Continue(edge) => Some(edge), + ExitContinuation::CommitResult { .. } + | ExitContinuation::ReturnFailure { .. } + | ExitContinuation::ReturnUnit => None, + }) + .expect("fixture must contain a continuing exit"); + *continuation = EdgeId(u32::MAX); + assert_unknown(&program, &invalid_continue); + + let mut cycle = function(&program, "flow.bool"); + let entry = cycle.cleanup_plan.entry; + let edge_id = match &cycle.cleanup_plan.blocks[entry.0 as usize].terminator { + CleanupTerminator::Goto(edge) => *edge, + CleanupTerminator::Branch(edges) => { + *edges.first().expect("entry branch must contain an edge") + } + CleanupTerminator::Exit(_) => panic!("fixture entry must have a successor"), + }; + cycle.cleanup_plan.edges[edge_id.0 as usize].to = entry; + let diagnostic = validate_structure(&program, &cycle).unwrap_err(); + assert_eq!(diagnostic.code, "SPX-H006"); + assert_eq!( + diagnostic.message, + "cleanup plan for function `flow.bool` failed independent replay: cleanup replay path bound exceeds the global path budget" + ); + } + #[test] fn replay_budget_exhaustion_is_a_deterministic_diagnostic() { let program = program(); let function = function(&program, "app.main"); - let mut budget = ReplayBudget { remaining: 1 }; + let mut budget = ReplayBudget { + remaining: 1, + skeleton_remaining: 0, + }; let diagnostic = budget.charge(&function, 2, "hostile test").unwrap_err(); assert_eq!(diagnostic.code, "SPX-H006"); assert!(diagnostic.message.contains("work budget exhausted")); } + fn assert_program_skeleton_authority(program: &ResolvedProgram) -> usize { + let functions = || { + program.functions.iter().chain( + program + .function_instances + .iter() + .map(|instance| &instance.function), + ) + }; + let independently_summed = functions() + .try_fold(0usize, |total, function| { + total + .checked_add(skeleton_work_upper(program, function)?) + .ok_or_else(|| skeleton_preflight_overflow(function)) + }) + .unwrap(); + assert!(independently_summed > 0); + + reset_skeleton_materializations(); + let mut exact = ReplayBudget { + remaining: independently_summed, + skeleton_remaining: 0, + }; + assert_eq!( + reserve_program_skeleton_work(program, functions(), &mut exact).unwrap(), + independently_summed + ); + assert_eq!(exact.remaining, 0); + assert_eq!(exact.skeleton_remaining, independently_summed); + assert_eq!(skeleton_materializations(), 0); + + reset_skeleton_materializations(); + let mut one_less = ReplayBudget { + remaining: independently_summed - 1, + skeleton_remaining: 0, + }; + let diagnostic = + reserve_program_skeleton_work(program, functions(), &mut one_less).unwrap_err(); + assert_eq!(diagnostic.code, "SPX-H006"); + assert!(diagnostic + .message + .contains("skeleton-work preflight exceeds")); + assert_eq!(skeleton_materializations(), 0); + + reset_skeleton_materializations(); + let mut actual = ReplayBudget::new(); + let derived = reserve_program_skeleton_work(program, functions(), &mut actual).unwrap(); + for function in functions() { + validate_structure_with_budget(program, function, &mut actual).unwrap(); + } + let charged = derived - actual.skeleton_remaining; + assert!(charged <= derived); + assert!(skeleton_materializations() > 0); + assert!(skeleton_materializations() <= charged); + independently_summed + } + + #[test] + fn program_wide_skeleton_preflight_sums_every_function_before_materialization() { + let program = program(); + let derived = assert_program_skeleton_authority(&program); + let largest_function = program + .functions + .iter() + .chain( + program + .function_instances + .iter() + .map(|instance| &instance.function), + ) + .map(|function| skeleton_work_upper(&program, function)) + .collect::, _>>() + .unwrap() + .into_iter() + .max() + .unwrap(); + assert!(derived > largest_function); + } + + #[test] + fn many_functions_and_deep_lazy_paths_share_one_exact_skeleton_authority() { + let mut source = String::from("module replay.aggregate;\n"); + for index in 0..48 { + source.push_str(&format!( + "@id(\"aggregate.f{index}\") fn f{index}(flag: bool) -> bool {{ flag && flag }}\n" + )); + } + let mut expression = String::from("flag"); + // Keep parser construction deliberately shallow; the private replay + // depth-512 gate uses a prebuilt Program in the builder crate. + for _ in 0..32 { + expression = format!("flag && ({expression})"); + } + source.push_str(&format!( + "@id(\"aggregate.deep\") fn deep(flag: bool) -> bool {{ {expression} }}\n" + )); + source.push_str("@id(\"app.main\") fn main() -> i64 { 0 }\n"); + let parsed = parse(&source, Path::new("cleanup-replay-aggregate.spx")).unwrap(); + let program = hir::resolve(&parsed).unwrap(); + assert_eq!(program.functions.len(), 50); + assert_program_skeleton_authority(&program); + } + + #[test] + fn wide_resource_update_untouched_fields_are_inside_charge_first_authority() { + let mut source = String::from( + "module replay.wide_update;\n\ + @id(\"wide.token\") resource Token { @id(\"wide.token.drop\") drop trivial; }\n\ + @id(\"wide.record\") record Wide {\n", + ); + for index in 0..32 { + source.push_str(&format!( + "@id(\"wide.field.{index}\") field_{index}: Token,\n" + )); + } + source.push_str( + "}\n\ + @id(\"wide.update\") fn update(value: own Wide, replacement: own Token) -> Wide {\n\ + value with { field_0: replacement }\n\ + }\n\ + @id(\"app.main\") fn main() -> i64 { 0 }\n", + ); + let parsed = parse(&source, Path::new("cleanup-replay-wide-update.spx")).unwrap(); + let program = hir::resolve(&parsed).unwrap(); + assert_program_skeleton_authority(&program); + + let function = program + .functions + .iter() + .find(|function| function.id.as_str() == "wide.update") + .unwrap(); + let ResolvedExprKind::Block { tail, .. } = &function.body.kind else { + panic!("wide update body remains a block") + }; + let ResolvedExprKind::UpdateRecord { record, fields, .. } = &tail.kind else { + panic!("wide update tail remains an update") + }; + assert_eq!(fields.len(), 1); + let untouched_droppable = program + .declarations + .record_fields(record) + .unwrap() + .iter() + .filter(|field| { + fields + .iter() + .all(|replacement| replacement.field != field.id) + }) + .filter(|field| type_needs_drop(&program, function, &field.ty).unwrap()) + .count(); + assert_eq!(untouched_droppable, 31); + let active_paths = expression_path_counts(function, tail).unwrap().normal; + let untouched_work = untouched_droppable + .checked_mul(active_paths) + .and_then(|units| units.checked_mul(8)) + .unwrap(); + let derived = skeleton_work_upper(&program, function).unwrap(); + assert!(derived >= untouched_work); + + reset_skeleton_materializations(); + let mut budget = ReplayBudget::with_skeleton_limit(derived); + validate_structure_with_budget(&program, function, &mut budget).unwrap(); + let charged = derived - budget.skeleton_remaining; + assert!(charged <= derived); + assert!(skeleton_materializations() <= charged); + } + + #[test] + fn terminated_prefix_skips_unreachable_invalid_lazy_if_and_match_children() { + fn unreachable_prefix( + program: &ResolvedProgram, + function: &ResolvedFunction, + expression: &ResolvedExpr, + ) -> Result, Diagnostic> { + let mut budget = ReplayBudget::with_skeleton_limit(MAX_REPLAY_WORK_UNITS); + let mut work = SkeletonWork { + function, + budget: &mut budget, + }; + let mut path = empty_expr_path(); + path.failed = true; + let prefixes = work.singleton_path(path, "unreachable hostile prefix")?; + sequence_expression(program, function, prefixes, expression, &mut work) + } + + fn poison(expression: &mut ResolvedExpr) { + expression.kind = ResolvedExprKind::Call { + callee: DeclarationId::new("hostile.unreachable.callee"), + type_arguments: Vec::new(), + instance: None, + args: Vec::new(), + }; + } + + let program = program(); + let mut if_function = function(&program, "flow.bool"); + let ResolvedExprKind::Block { tail, .. } = &mut if_function.body.kind else { + panic!("if fixture retains its body block") + }; + let ResolvedExprKind::If { then_branch, .. } = &mut tail.kind else { + panic!("if fixture retains its conditional tail") + }; + poison(then_branch); + let expression = (**tail).clone(); + let paths = unreachable_prefix(&program, &if_function, &expression).unwrap(); + assert_eq!(paths.len(), 1); + assert!(paths[0].failed); + + let mut match_function = function(&program, "choice.select"); + let ResolvedExprKind::Block { tail, .. } = &mut match_function.body.kind else { + panic!("match fixture retains its body block") + }; + let ResolvedExprKind::Match { arms, .. } = &mut tail.kind else { + panic!("match fixture retains its match tail") + }; + poison(&mut arms[0].value); + let expression = (**tail).clone(); + let paths = unreachable_prefix(&program, &match_function, &expression).unwrap(); + assert_eq!(paths.len(), 1); + assert!(paths[0].failed); + + let parsed = parse( + "module replay.lazy_unreachable; @id(\"lazy\") fn lazy(left: bool, right: bool) -> bool { left && right } @id(\"app.main\") fn main() -> i64 { 0 }", + Path::new("cleanup-replay-lazy-unreachable.spx"), + ) + .unwrap(); + let lazy_program = hir::resolve(&parsed).unwrap(); + let mut lazy_function = function(&lazy_program, "lazy"); + let ResolvedExprKind::Block { tail, .. } = &mut lazy_function.body.kind else { + panic!("lazy fixture retains its body block") + }; + let ResolvedExprKind::Binary { right, .. } = &mut tail.kind else { + panic!("lazy fixture retains its binary tail") + }; + poison(right); + let expression = (**tail).clone(); + let paths = unreachable_prefix(&lazy_program, &lazy_function, &expression).unwrap(); + assert_eq!(paths.len(), 1); + assert!(paths[0].failed); + } + + #[test] + fn wide_match_path_clones_and_pushes_are_charged_before_materialization() { + fn replay_with_limit( + program: &ResolvedProgram, + function: &ResolvedFunction, + expression: &ResolvedExpr, + limit: usize, + ) -> Result<(Vec, usize), Diagnostic> { + let mut budget = ReplayBudget::with_skeleton_limit(limit); + let paths = { + let mut work = SkeletonWork { + function, + budget: &mut budget, + }; + expression_skeleton(program, function, expression, &mut work)? + }; + Ok((paths, limit - budget.skeleton_remaining)) + } + + let program = program(); + let function = function(&program, "choice.select"); + let ResolvedExprKind::Block { tail, .. } = &function.body.kind else { + panic!("wide match fixture retains its body block") + }; + let (paths, charged) = + replay_with_limit(&program, &function, tail, MAX_REPLAY_WORK_UNITS).unwrap(); + let retained_units = paths.iter().fold(0usize, |total, path| { + total.saturating_add(path.observations.len().saturating_add(1)) + }); + assert!( + paths.len() >= 4, + "wide match produced {} paths", + paths.len() + ); + assert!( + charged > retained_units, + "clone/push work must exceed retained paths" + ); + replay_with_limit(&program, &function, tail, charged).unwrap(); + let diagnostic = match replay_with_limit(&program, &function, tail, charged - 1) { + Err(diagnostic) => diagnostic, + Ok(_) => panic!("one-less path budget unexpectedly succeeded"), + }; + assert_eq!(diagnostic.code, "SPX-H006"); + assert!(diagnostic.message.contains("work budget exhausted during")); + } + #[test] fn skeleton_replay_rejects_a_checked_status_lane_swap() { let program = program(); diff --git a/src/codegen.rs b/src/codegen.rs index bbc6eb8..9fb9e4b 100644 --- a/src/codegen.rs +++ b/src/codegen.rs @@ -97,6 +97,16 @@ impl COutput for crate::bounded_output::CappedString { /// Resolve a parsed program fail-closed, then emit its checked native bootstrap IR. pub fn emit_c(program: &Program) -> Result { + if program + .interfaces + .iter() + .flat_map(|interface| &interface.imports) + .any(|import| import.native_rust) + { + return Err(backend_error( + "native Rust imports are unavailable for the ordinary native target", + )); + } let resolved = hir::resolve(program).map_err(first_backend_diagnostic)?; emit_resolved_c_with_source(program, &resolved) } @@ -110,6 +120,7 @@ pub(crate) fn emit_resolved_c_with_source( source: &Program, resolved: &ResolvedProgram, ) -> Result { + reject_native_rust_for_native(resolved)?; let labels = contract_labels(source, resolved); emit_hir_c_with_labels(resolved, &labels) } @@ -120,9 +131,24 @@ pub(crate) fn emit_resolved_c_with_source( /// that code generation consumes semantic identities and centralized type facts, /// rather than reconstructing either from source names. pub fn emit_hir_c(program: &ResolvedProgram) -> Result { + reject_native_rust_for_native(program)?; emit_hir_c_with_labels(program, &HashMap::new()) } +fn reject_native_rust_for_native(program: &ResolvedProgram) -> Result<(), Diagnostic> { + if program + .interfaces + .iter() + .flat_map(|interface| &interface.imports) + .any(|import| import.native_rust) + { + return Err(backend_error( + "native Rust imports are unavailable for the ordinary native target", + )); + } + Ok(()) +} + /// Doc-hidden public descriptor/provider artifact for one already validated /// resource function. /// @@ -286,6 +312,14 @@ fn emit_native_callable_admission_core( program: &ResolvedProgram, function_id: &DeclarationId, ) -> Result { + if program + .interfaces + .iter() + .flat_map(|interface| &interface.imports) + .any(|import| import.native_rust) + { + return Err(resource_lowering_gate()); + } hir::validate(program)?; if !program.function_templates.is_empty() || !program.function_instances.is_empty() { return Err(backend_error( @@ -1838,6 +1872,7 @@ fn expression_has_try(expression: &ResolvedExpr) -> bool { match &expression.kind { ResolvedExprKind::Try { .. } | ResolvedExprKind::TryOption { .. } => true, ResolvedExprKind::Call { args, .. } => args.iter().any(expression_has_try), + ResolvedExprKind::NativeRustImportCall(call) => call.args.iter().any(expression_has_try), ResolvedExprKind::Unary { value, .. } | ResolvedExprKind::Project { base: value, .. } => { expression_has_try(value) } @@ -2176,6 +2211,12 @@ impl<'a, O: COutput> CEmitter<'a, O> { ty: target.return_type, } } + ResolvedExprKind::NativeRustImportCall(call) => { + return Err(backend_error(format!( + "native Rust import `{}` is unavailable in the ordinary native backend", + call.import + ))); + } ResolvedExprKind::Unary { op, value } => { let value = self.emit_expr(value)?; let (ty, operand_type) = match op { diff --git a/src/codegen/native_cleanup.rs b/src/codegen/native_cleanup.rs index 8528313..26df7b0 100644 --- a/src/codegen/native_cleanup.rs +++ b/src/codegen/native_cleanup.rs @@ -543,6 +543,10 @@ fn validate_supported_type( context: &str, ) -> Result<(), Diagnostic> { match ty { + ResolvedType::Unit => Err(unsupported( + function, + format!("does not support a unit {context} value"), + )), ResolvedType::I64 | ResolvedType::Bool => Ok(()), ResolvedType::TypeParameter { .. } => Err(unsupported( function, @@ -694,6 +698,15 @@ fn validate_expression( ), )); } + ResolvedExprKind::NativeRustImportCall(call) => { + return Err(unsupported( + function, + format!( + "does not support native Rust import execution `{}` to `{}` in the ordinary native cleanup backend", + expression.id, call.import + ), + )); + } ResolvedExprKind::Unary { value, .. } => { validate_expression(program, function, value)?; } diff --git a/src/codegen/native_conformance_materialize.rs b/src/codegen/native_conformance_materialize.rs index 9851dbe..7cdf5ed 100644 --- a/src/codegen/native_conformance_materialize.rs +++ b/src/codegen/native_conformance_materialize.rs @@ -303,6 +303,7 @@ fn materialize_result( }) } (_, WireResult::Unit) + | (ResolvedType::Unit, _) | (ResolvedType::I64, _) | (ResolvedType::Bool, _) | (ResolvedType::TypeParameter { .. }, _) diff --git a/src/codegen/native_host_contract.rs b/src/codegen/native_host_contract.rs index 4a12a37..4b5997e 100644 --- a/src/codegen/native_host_contract.rs +++ b/src/codegen/native_host_contract.rs @@ -315,6 +315,9 @@ pub(super) fn derive_from_admitted( let mut owner_ordinal = 0; for (parameter_index, parameter) in function.params.iter().enumerate() { match ¶meter.ty { + ResolvedType::Unit => { + return Err(host_error("unit is not an ordinary native host parameter")); + } ResolvedType::I64 => { if parameter.ownership != OwnershipMode::Value { return Err(host_error(format!( diff --git a/src/codegen/native_resource.rs b/src/codegen/native_resource.rs index f10eb97..bd3f19d 100644 --- a/src/codegen/native_resource.rs +++ b/src/codegen/native_resource.rs @@ -43,6 +43,9 @@ impl NativeResourceAbi { ty: &ResolvedType, ) -> Result<&'a str, Diagnostic> { match ty { + ResolvedType::Unit => Err(resource_error( + "unit has no ordinary native value representation", + )), ResolvedType::I64 => Ok("int64_t"), ResolvedType::Bool => Ok("bool"), ResolvedType::TypeParameter { .. } => Err(resource_error(format!( diff --git a/src/codegen/native_value.rs b/src/codegen/native_value.rs index bf57648..9d9061e 100644 --- a/src/codegen/native_value.rs +++ b/src/codegen/native_value.rs @@ -170,6 +170,11 @@ pub(crate) fn plan( let body_value = planner.lower_body_tail(tail)?; let result = match &function.return_type { + ResolvedType::Unit => { + return Err(value_error( + "unit result is outside the ordinary native value corpus", + )); + } ResolvedType::I64 => { let result = planner.new_scalar(&function.body.id, "int64_t")?; planner.push(NativeValueStep::Copy { @@ -951,6 +956,9 @@ fn validate_signature( } for parameter in &function.params { match ¶meter.ty { + ResolvedType::Unit => { + return Err(value_error("unit is not an ordinary native parameter")); + } ResolvedType::I64 | ResolvedType::Bool => { if parameter.ownership != OwnershipMode::Value { return Err(value_error("scalar parameter is not passed by value")); @@ -985,7 +993,10 @@ fn validate_signature( let _ = abi.c_type(program, &function.return_type)?; Ok(()) } - ResolvedType::Bool | ResolvedType::TypeParameter { .. } | ResolvedType::Nominal { .. } => { + ResolvedType::Unit + | ResolvedType::Bool + | ResolvedType::TypeParameter { .. } + | ResolvedType::Nominal { .. } => { Err(value_error("result type is outside the staged corpus")) } } diff --git a/src/economic_agent.rs b/src/economic_agent.rs new file mode 100644 index 0000000..7241eca --- /dev/null +++ b/src/economic_agent.rs @@ -0,0 +1,10083 @@ +//! Bounded Economic Agent v1 injected-host API. +//! +//! This safe-Rust state machine has no built-in transport, DNS, filesystem, +//! process, environment, key, custody, journal, or wallet implementation. +//! Caller implementations of the host traits are trusted authorities; all +//! adapter documents and bytes remain untrusted input. + +#![forbid(unsafe_code)] +#![allow( + dead_code, + clippy::field_reassign_with_default, + clippy::format_collect, + clippy::too_many_arguments, + reason = "private typed and replay internals support the opaque public C surface" +)] + +use std::collections::BTreeMap; +use std::fmt; +use std::net::IpAddr; + +use serde_json::{Map, Value}; +use sha2::{Digest, Sha256}; + +use crate::agent_runtime::{AgentCancellation, AgentRun, AgentRunStatus}; +use crate::bounded_output::{ + active_limit, active_remaining, clear_active_floor, reserve_active, reserve_active_preserving, + set_active_floor, with_limit_usage, +}; +use crate::diagnostic::{quote_json, Diagnostic}; + +const POLICY_SCHEMA: &str = "semaprax.economic-agent-policy.v1"; +const INTENT_SCHEMA: &str = "semaprax.economic-agent-payment-intent.v1"; +const INVOICE_SCHEMA: &str = "semaprax.economic-agent-x402-invoice.v1"; +const SNAPSHOT_SCHEMA: &str = "semaprax.economic-agent-chain-snapshot.v1"; +const PLAN_SCHEMA: &str = "semaprax.economic-agent-payment-plan.v1"; +const SIMULATION_SCHEMA: &str = "semaprax.economic-agent-simulation.v1"; +const APPROVAL_REQUEST_SCHEMA: &str = "semaprax.economic-agent-approval-request.v1"; +const APPROVAL_SCHEMA: &str = "semaprax.economic-agent-approval.v1"; +const JOURNAL_SCHEMA: &str = "semaprax.economic-agent-journal.v1"; +const BROADCAST_SCHEMA: &str = "semaprax.economic-agent-broadcast-receipt.v1"; +const RECONCILIATION_SCHEMA: &str = "semaprax.economic-agent-reconciliation.v1"; +const TRACE_SCHEMA: &str = "semaprax.economic-agent-trace.v1"; +const EVIDENCE_SCHEMA: &str = "semaprax.economic-agent-evidence.v1"; + +const POLICY_DOMAIN: &[u8] = b"semaprax.economic-agent.policy-digest.v1\0"; +const INTENT_DOMAIN: &[u8] = b"semaprax.economic-agent.payment-intent-digest.v1\0"; +const INVOICE_DOMAIN: &[u8] = b"semaprax.economic-agent.x402-invoice-digest.v1\0"; +const SNAPSHOT_DOMAIN: &[u8] = b"semaprax.economic-agent.chain-snapshot-digest.v1\0"; +const PLAN_DOMAIN: &[u8] = b"semaprax.economic-agent.payment-plan-digest.v1\0"; +const SIMULATION_DOMAIN: &[u8] = b"semaprax.economic-agent.simulation-digest.v1\0"; +const APPROVAL_REQUEST_DOMAIN: &[u8] = b"semaprax.economic-agent.approval-request-digest.v1\0"; +const APPROVAL_DOMAIN: &[u8] = b"semaprax.economic-agent.approval-digest.v1\0"; +const JOURNAL_DOMAIN: &[u8] = b"semaprax.economic-agent.journal-digest.v1\0"; +const BROADCAST_DOMAIN: &[u8] = b"semaprax.economic-agent.broadcast-receipt-digest.v1\0"; +const RECONCILIATION_DOMAIN: &[u8] = b"semaprax.economic-agent.reconciliation-digest.v1\0"; +const TRACE_DOMAIN: &[u8] = b"semaprax.economic-agent.trace-digest.v1\0"; +const EVIDENCE_DOMAIN: &[u8] = b"semaprax.economic-agent.evidence-digest.v1\0"; +const UNSIGNED_DOMAIN: &[u8] = b"semaprax.economic-agent.unsigned-transaction-digest.v1\0"; +const SIGNED_DOMAIN: &[u8] = b"semaprax.economic-agent.signed-transaction-digest.v1\0"; +const RUN_ID_DOMAIN: &[u8] = b"semaprax.economic-agent.run-id.v1\0"; + +const MAX_POLICY_BYTES: usize = 1_048_576; +const MAX_INTENT_BYTES: usize = 1_048_576; +const MAX_INVOICE_BYTES: usize = 1_048_576; +const MAX_SNAPSHOT_BYTES: usize = 1_048_576; +const MAX_PLAN_BYTES: usize = 1_048_576; +const MAX_SIMULATION_BYTES: usize = 1_048_576; +const MAX_APPROVAL_REQUEST_BYTES: usize = 1_048_576; +const MAX_APPROVAL_BYTES: usize = 65_536; +const MAX_JOURNAL_BYTES: usize = 8_388_608; +const MAX_UNSIGNED_BYTES: usize = 1_048_576; +const MAX_SIGNED_BYTES: usize = 2_097_152; +const MAX_BROADCAST_BYTES: usize = 1_048_576; +const MAX_RECONCILIATION_BYTES: usize = 1_048_576; +const MAX_TRACE_EVENTS: usize = 1_024; +const MAX_TRACE_BYTES: usize = 8_388_608; +const MAX_EVIDENCE_BYTES: usize = 16_777_216; +const MAX_BUILDER_BYTES: usize = 67_108_864; +const MAX_JSON_DEPTH: usize = 16; +const MAX_IDENTIFIER_BYTES: usize = 128; +const MAX_MEMO_BYTES: usize = 1_024; +const MAX_RECIPIENTS: usize = 128; +const MAX_NETWORK_POLICIES: usize = 16; +const MAX_X402_ORIGINS: usize = 32; +const MAX_UTXOS: usize = 100; + +const NONCLAIMS: [&str; 28] = [ + "no_model_output_payment_authority", + "no_model_self_approval_or_policy_expansion", + "no_seed_private_key_credential_or_signing_material_input", + "no_secret_prompt_trace_evidence_log_or_diagnostic_exposure", + "no_builtin_network_http_dns_custody_or_chain_authority", + "no_mainnet_authority", + "no_wildcard_network_asset_recipient_origin_or_resource", + "no_token_contract_program_script_swap_bridge_or_unlimited_approval", + "no_raw_signing_or_signed_transaction_export", + "no_exactly_once_signing_broadcast_or_payment", + "no_automatic_uncertain_broadcast_retry", + "no_guaranteed_confirmation_finality_or_reorg_freedom", + "no_compromised_wallet_approver_adapter_provider_or_chain_recovery", + "no_power_loss_durability_without_host_journal_contract", + "no_cross_process_or_distributed_concurrency_guarantee", + "no_live_price_exchange_rate_fee_or_cost_accuracy", + "no_balance_allowance_or_simulation_truth_beyond_adapter", + "no_human_identity_intent_approval_provenance_or_nonrepudiation", + "no_signature_attestation_or_custody_provenance", + "no_tax_accounting_legal_regulatory_sanctions_or_compliance_correctness", + "no_privacy_data_residency_or_unlinkability_guarantee", + "no_x402_redirect_ssrf_private_network_or_server_honesty_guarantee_beyond_admitted_adapter_contract", + "no_automatic_refund_chargeback_replacement_or_fee_bumping", + "no_wallet_recovery_rotation_backup_or_inheritance", + "no_general_payment_sdk_or_production_readiness", + "no_language_graph_cleanup_backend_or_workspace_atomicity_semantics", + "no_current_agent_runtime_schema_api_or_kat_modification", + "no_completion_matrix_status_promotion", +]; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +/// Outcome reported by an injected Economic Agent authority boundary. +pub enum EconomicAdapterDisposition { + Succeeded, + DefinitelyNotStarted, + FailedUncertain, + PolicyRejected, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +/// Outcome of the single injected journal load for an operation. +pub enum EconomicJournalLoad { + Missing, + Present, + DefinitelyNotStarted, + FailedUncertain, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +/// Frozen native-asset settlement rail. +pub enum EconomicRail { + Evm, + Solana, + Bitcoin, +} + +/// Opaque rolling-window reservation supplied only to the journal CAS. +pub struct EconomicRollingReservation { + wallet_id: String, + rail: EconomicRail, + network: String, + asset: String, + requested_at_ms: u64, + amount_atomic: u64, + max_rolling_24h_atomic: u64, +} +impl EconomicRollingReservation { + /// Bound wallet identifier. + pub fn wallet_id(&self) -> &str { + &self.wallet_id + } + /// Bound settlement rail. + pub const fn rail(&self) -> EconomicRail { + self.rail + } + /// Bound test network. + pub fn network(&self) -> &str { + &self.network + } + /// Bound native asset. + pub fn asset(&self) -> &str { + &self.asset + } + /// Admitted intent timestamp; the journal owns trusted clock time. + pub const fn requested_at_ms(&self) -> u64 { + self.requested_at_ms + } + /// Amount reserved in atomic native-asset units. + pub const fn amount_atomic(&self) -> u64 { + self.amount_atomic + } + /// Policy maximum for the matching rolling 24-hour tuple. + pub const fn max_rolling_24h_atomic(&self) -> u64 { + self.max_rolling_24h_atomic + } +} +/// Atomic rolling-reservation update accompanying a journal CAS. +pub enum EconomicRollingReservationUpdate<'a> { + Reserve(&'a EconomicRollingReservation), + Retain, + Release, +} + +impl EconomicRail { + fn text(self) -> &'static str { + match self { + Self::Evm => "evm", + Self::Solana => "solana", + Self::Bitcoin => "bitcoin", + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +/// Closed terminal status returned by an Economic Agent operation. +pub enum EconomicRunStatus { + Confirmed, + Pending, + Reorged, + Dropped, + Rejected, + Cancelled, + DeadlineExceeded, + BudgetExhausted, + JournalFailed, + AdapterFailed, + ApprovalFailed, + CustodyFailed, + BroadcastUnknown, + ReconciliationFailed, +} + +impl EconomicRunStatus { + fn text(self) -> &'static str { + match self { + Self::Confirmed => "confirmed", + Self::Pending => "pending", + Self::Reorged => "reorged", + Self::Dropped => "dropped", + Self::Rejected => "rejected", + Self::Cancelled => "cancelled", + Self::DeadlineExceeded => "deadline_exceeded", + Self::BudgetExhausted => "budget_exhausted", + Self::JournalFailed => "journal_failed", + Self::AdapterFailed => "adapter_failed", + Self::ApprovalFailed => "approval_failed", + Self::CustodyFailed => "custody_failed", + Self::BroadcastUnknown => "broadcast_unknown", + Self::ReconciliationFailed => "reconciliation_failed", + } + } +} + +/// Push-only bounded sink for canonical adapter documents. +pub struct EconomicDocumentSink { + bytes: Vec, + limit: usize, + closed: Option, + cancellation: AgentCancellation, + probe: Box, + started_ms: u64, + deadline_ms: u64, + builder_limit: u64, + terminal_floor: usize, +} + +#[derive(Clone, Copy)] +enum SinkClose { + Cancelled, + Deadline, + DeclaredLimit, + Builder, +} + +impl EconomicDocumentSink { + fn new( + limit: usize, + cancellation: AgentCancellation, + probe: Box, + started_ms: u64, + deadline_ms: u64, + builder_limit: u64, + terminal_floor: usize, + ) -> Self { + Self { + bytes: Vec::new(), + limit, + closed: None, + cancellation, + probe, + started_ms, + deadline_ms, + builder_limit, + terminal_floor, + } + } + + /// Appends one chunk, returning `false` permanently after closure. + pub fn push(&mut self, chunk: &[u8]) -> bool { + if self.closed.is_some() { + return false; + } + if self.cancellation.is_cancelled() { + self.closed = Some(SinkClose::Cancelled); + return false; + } + if self + .probe + .elapsed_ms() + .checked_sub(self.started_ms) + .is_none_or(|elapsed| elapsed > self.deadline_ms) + { + self.closed = Some(SinkClose::Deadline); + return false; + } + let Some(length) = self.bytes.len().checked_add(chunk.len()) else { + self.closed = Some(SinkClose::DeclaredLimit); + return false; + }; + if length > self.limit { + self.closed = Some(SinkClose::DeclaredLimit); + return false; + } + if !reserve_active_preserving(chunk.len(), self.terminal_floor) { + self.closed = Some(SinkClose::Builder); + return false; + } + self.bytes.extend_from_slice(chunk); + true + } + + fn finish(self, field: &str) -> Result { + match self.closed { + Some(SinkClose::Cancelled) => { + return Err(info("SPX-I228", "Economic Agent run was cancelled")) + } + Some(SinkClose::Deadline) => { + return Err(info("SPX-I229", "Economic Agent deadline was exceeded")) + } + Some(SinkClose::DeclaredLimit) => return Err(g216(field, self.limit as u64)), + Some(SinkClose::Builder) => return Err(g216("builder_bytes", self.builder_limit)), + None => {} + } + String::from_utf8(self.bytes).map_err(|_| g210(field, "UTF-8")) + } +} + +/// Push-only bounded sink for opaque custody-produced signed bytes. +pub struct EconomicBytesSink { + bytes: Vec, + limit: usize, + closed: Option, + cancellation: AgentCancellation, + probe: Box, + started_ms: u64, + deadline_ms: u64, + builder_limit: u64, + terminal_floor: usize, +} + +impl EconomicBytesSink { + fn new( + limit: usize, + cancellation: AgentCancellation, + probe: Box, + started_ms: u64, + deadline_ms: u64, + builder_limit: u64, + terminal_floor: usize, + ) -> Self { + Self { + bytes: Vec::new(), + limit, + closed: None, + cancellation, + probe, + started_ms, + deadline_ms, + builder_limit, + terminal_floor, + } + } + + /// Appends one chunk, returning `false` permanently after closure. + pub fn push(&mut self, chunk: &[u8]) -> bool { + if self.closed.is_some() { + return false; + } + if self.cancellation.is_cancelled() { + self.closed = Some(SinkClose::Cancelled); + return false; + } + if self + .probe + .elapsed_ms() + .checked_sub(self.started_ms) + .is_none_or(|elapsed| elapsed > self.deadline_ms) + { + self.closed = Some(SinkClose::Deadline); + return false; + } + let Some(length) = self.bytes.len().checked_add(chunk.len()) else { + self.closed = Some(SinkClose::DeclaredLimit); + return false; + }; + if length > self.limit { + self.closed = Some(SinkClose::DeclaredLimit); + return false; + } + if !reserve_active_preserving(chunk.len(), self.terminal_floor) { + self.closed = Some(SinkClose::Builder); + return false; + } + self.bytes.extend_from_slice(chunk); + true + } + + fn finish(self) -> Result, Diagnostic> { + match self.closed { + Some(SinkClose::Cancelled) => Err(info("SPX-I228", "Economic Agent run was cancelled")), + Some(SinkClose::Deadline) => { + Err(info("SPX-I229", "Economic Agent deadline was exceeded")) + } + Some(SinkClose::DeclaredLimit) => { + Err(g216("signed_transaction_bytes", self.limit as u64)) + } + Some(SinkClose::Builder) => Err(g216("builder_bytes", self.builder_limit)), + None => Ok(self.bytes), + } + } +} + +/// Caller-injected durable journal and rolling-window authority. +pub trait PaymentJournal { + /// Loads the exact journal bound to `idempotency_key` into `sink`. + fn load( + &mut self, + idempotency_key: &str, + sink: &mut EconomicDocumentSink, + ) -> EconomicJournalLoad; + /// Atomically compares the version, writes canonical Journal bytes, and applies rolling policy. + fn compare_and_swap( + &mut self, + idempotency_key: &str, + expected_version: u64, + journal: &str, + rolling: EconomicRollingReservationUpdate<'_>, + ) -> EconomicAdapterDisposition; +} + +/// Caller-injected x402 invoice data adapter; it performs no redirects through this API. +pub trait X402InvoiceAdapter { + /// Fetches the invoice bound to the admitted origin, method, and resource. + fn fetch_invoice( + &mut self, + origin: &str, + method: &str, + resource: &str, + sink: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition; +} + +macro_rules! rail_adapter { + ($name:ident,$snapshot:ident,$simulate:ident,$broadcast:ident,$reconcile:ident) => { + /// Caller-injected test-network chain observation and broadcast adapter. + pub trait $name { + /// Returns one canonical snapshot for the admitted Intent. + fn $snapshot( + &mut self, + intent: &str, + sink: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition; + /// Simulates the core-built unsigned transaction against its canonical Plan. + fn $simulate( + &mut self, + plan: &str, + unsigned_transaction: &[u8], + sink: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition; + /// Broadcasts the exact independently validated signed transaction once. + fn $broadcast( + &mut self, + signed_transaction: &[u8], + sink: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition; + /// Returns one reconciliation observation for the bound transaction ID. + fn $reconcile( + &mut self, + transaction_id: &str, + sink: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition; + } + }; +} +rail_adapter!( + EvmPaymentAdapter, + evm_snapshot, + evm_simulate, + evm_broadcast, + evm_reconcile +); +rail_adapter!( + SolanaPaymentAdapter, + solana_snapshot, + solana_simulate, + solana_broadcast, + solana_reconcile +); +rail_adapter!( + BitcoinPaymentAdapter, + bitcoin_snapshot, + bitcoin_simulate, + bitcoin_broadcast, + bitcoin_reconcile +); + +/// Caller-injected approval authority for an exact canonical Approval Request. +pub trait PaymentApprover { + /// Returns one canonical Approval document. + fn approve( + &mut self, + approval_request: &str, + sink: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition; +} + +/// Caller-injected opaque signing authority; key material never crosses this API. +pub trait WalletCustody { + /// Signs exact digest-bound unsigned bytes into the push-only sink. + fn sign( + &mut self, + wallet_id: &str, + rail: EconomicRail, + unsigned_transaction_digest: &str, + unsigned_transaction: &[u8], + approval_digest: &str, + sink: &mut EconomicBytesSink, + ) -> EconomicAdapterDisposition; +} + +/// Pure caller-injected monotonic observation for one Economic Agent run. +pub trait EconomicBoundaryProbe { + /// Returns a nondecreasing local elapsed-millisecond observation. + fn elapsed_ms(&self) -> u64; +} + +/// Complete caller-injected authority set required by [`EconomicAgent`]. +pub trait EconomicAgentHost: + PaymentJournal + + X402InvoiceAdapter + + EvmPaymentAdapter + + SolanaPaymentAdapter + + BitcoinPaymentAdapter + + PaymentApprover + + WalletCustody +{ + /// Creates a pure local elapsed-time probe; this method must not cross an external-effect boundary. + fn boundary_probe(&self) -> Box; +} + +#[derive(Clone)] +struct Limits { + max_policy_bytes: u64, + max_intent_bytes: u64, + max_invoice_bytes: u64, + max_snapshot_bytes: u64, + max_plan_bytes: u64, + max_simulation_bytes: u64, + max_approval_request_bytes: u64, + max_approval_bytes: u64, + max_journal_bytes: u64, + max_unsigned_transaction_bytes: u64, + max_signed_transaction_bytes: u64, + max_broadcast_receipt_bytes: u64, + max_reconciliation_bytes: u64, + max_trace_events: u64, + max_trace_bytes: u64, + max_evidence_bytes: u64, + max_builder_bytes: u64, + max_json_depth: u64, + max_identifier_bytes: u64, + max_memo_bytes: u64, + max_recipients: u64, + max_network_policies: u64, + max_x402_origins: u64, + max_utxos: u64, + max_reconciliations: u64, + max_elapsed_ms: u64, + max_amount_atomic: u64, + max_fee_atomic: u64, + max_compute_units: u64, + max_confirmation_target: u64, + max_concurrency: u64, + max_unexpected_authority_calls: u64, +} + +#[derive(Clone)] +struct NetworkPolicy { + rail: EconomicRail, + network: String, + asset: String, + recipients: Vec, + max_amount: u64, + max_fee: u64, + max_rolling: u64, +} + +#[derive(Clone)] +struct OriginPolicy { + origin: String, + methods: Vec, + resources: Vec, + rails: Vec, + max_amount: u64, +} + +#[derive(Clone)] +struct Policy { + economic_agent_id: String, + wallet_id: String, + networks: Vec, + origins: Vec, + limits: Limits, + source: String, + digest: String, +} + +#[derive(Clone)] +enum Payment { + Evm { + recipient: String, + amount: u64, + max_fee: u64, + }, + Solana { + recipient: String, + amount: u64, + max_fee: u64, + compute: u64, + priority: u64, + }, + Bitcoin { + recipient: String, + amount: u64, + max_fee: u64, + confirmations: u64, + }, + X402 { + origin: String, + method: String, + resource: String, + invoice_digest: String, + payee: String, + rail: EconomicRail, + network: String, + asset: String, + amount: u64, + max_fee: u64, + invoice_expires: u64, + nonce: String, + }, +} + +#[derive(Clone)] +struct Intent { + intent_id: String, + wallet_id: String, + rail_text: String, + idempotency_key: String, + created_at: u64, + expires_at: u64, + memo: Option, + payment: Payment, + source: String, + digest: String, +} + +impl Intent { + fn settlement_rail(&self) -> EconomicRail { + match &self.payment { + Payment::Evm { .. } => EconomicRail::Evm, + Payment::Solana { .. } => EconomicRail::Solana, + Payment::Bitcoin { .. } => EconomicRail::Bitcoin, + Payment::X402 { rail, .. } => *rail, + } + } + fn recipient(&self) -> &str { + match &self.payment { + Payment::Evm { recipient, .. } + | Payment::Solana { recipient, .. } + | Payment::Bitcoin { recipient, .. } => recipient, + Payment::X402 { payee, .. } => payee, + } + } + fn amount(&self) -> u64 { + match &self.payment { + Payment::Evm { amount, .. } + | Payment::Solana { amount, .. } + | Payment::Bitcoin { amount, .. } + | Payment::X402 { amount, .. } => *amount, + } + } + fn max_fee(&self) -> u64 { + match &self.payment { + Payment::Evm { max_fee, .. } + | Payment::Solana { max_fee, .. } + | Payment::Bitcoin { max_fee, .. } + | Payment::X402 { max_fee, .. } => *max_fee, + } + } + fn network_asset(&self) -> (&str, &str) { + match &self.payment { + Payment::Evm { .. } => ("sepolia", "native:eth"), + Payment::Solana { .. } => ("devnet", "native:sol"), + Payment::Bitcoin { .. } => ("regtest", "native:btc"), + Payment::X402 { network, asset, .. } => (network, asset), + } + } +} + +fn admit_intent(policy: &Policy, intent: &Intent) -> Result<(), Diagnostic> { + if intent.source.len() > policy.limits.max_intent_bytes as usize { + return Err(g216("intent_bytes", policy.limits.max_intent_bytes)); + } + configured_depth(&intent.source, &policy.limits)?; + if intent.wallet_id != policy.wallet_id { + return Err(g212("wallet mismatch")); + } + let identifier_limit = policy.limits.max_identifier_bytes as usize; + if [ + intent.intent_id.as_str(), + intent.wallet_id.as_str(), + intent.rail_text.as_str(), + intent.idempotency_key.as_str(), + ] + .into_iter() + .any(|value| value.len() > identifier_limit) + { + return Err(g216("identifier_bytes", policy.limits.max_identifier_bytes)); + } + if intent + .memo + .as_ref() + .is_some_and(|memo| memo.len() > policy.limits.max_memo_bytes as usize) + { + return Err(g216("memo_bytes", policy.limits.max_memo_bytes)); + } + let rail = intent.settlement_rail(); + let (network, asset) = intent.network_asset(); + let Some(network_policy) = policy + .networks + .iter() + .find(|row| row.rail == rail && row.network == network && row.asset == asset) + else { + return Err(g212("rail/network/asset not allowed")); + }; + if !network_policy + .recipients + .iter() + .any(|recipient| recipient == intent.recipient()) + { + return Err(g212("recipient not allowed")); + } + if intent.amount() == 0 + || intent.amount() > network_policy.max_amount + || intent.amount() > policy.limits.max_amount_atomic + || intent.max_fee() > network_policy.max_fee + || intent.max_fee() > policy.limits.max_fee_atomic + { + return Err(g212("amount or fee not allowed")); + } + match &intent.payment { + Payment::Solana { + compute, priority, .. + } if *compute == 0 + || *compute > policy.limits.max_compute_units + || *priority > intent.max_fee() => + { + return Err(g212("amount or fee not allowed")) + } + Payment::Bitcoin { confirmations, .. } + if *confirmations == 0 || *confirmations > policy.limits.max_confirmation_target => + { + return Err(g212("amount or fee not allowed")) + } + Payment::X402 { + origin, + method, + resource, + rail, + nonce, + .. + } => { + if *rail == EconomicRail::Solana && policy.limits.max_compute_units < 200_000 { + return Err(g212("amount or fee not allowed")); + } + if nonce.len() > identifier_limit { + return Err(g216("identifier_bytes", policy.limits.max_identifier_bytes)); + } + let Some(row) = policy.origins.iter().find(|row| row.origin == *origin) else { + return Err(g212("origin/method/resource not allowed")); + }; + if !row.methods.iter().any(|v| v == method) + || !row.resources.iter().any(|v| v == resource) + || !row.rails.contains(rail) + || intent.amount() > row.max_amount + { + return Err(g212("origin/method/resource not allowed")); + } + } + _ => {} + } + Ok(()) +} + +#[derive(Clone)] +struct Doc { + source: String, + digest: String, +} +#[derive(Clone)] +struct DocRef { + digest: String, + bytes: u64, +} +impl From<&Doc> for DocRef { + fn from(value: &Doc) -> Self { + Self { + digest: value.digest.clone(), + bytes: value.source.len() as u64, + } + } +} + +#[derive(Clone, Default)] +struct Usage { + journal_reads: u64, + journal_writes: u64, + invoice_reads: u64, + snapshot_reads: u64, + simulations: u64, + approvals: u64, + signatures: u64, + broadcasts: u64, + reconciliations: u64, + input_bytes: u64, + output_bytes: u64, + elapsed_ms: u64, +} + +#[derive(Clone)] +struct Event { + kind: &'static str, + rail: Option, + input: Option, + output: Option, + status: &'static str, + usage: Usage, + authority_uncertain: bool, +} + +#[derive(Clone)] +struct Terminal { + status: EconomicRunStatus, + transaction_id: Option, + confirmation: Option, + code: Option, + message: Option, +} + +#[derive(Clone, Default)] +struct Budget { + policy_bytes: u64, + intent_bytes: u64, + invoice_bytes: u64, + snapshot_bytes: u64, + plan_bytes: u64, + simulation_bytes: u64, + approval_request_bytes: u64, + approval_bytes: u64, + journal_bytes: u64, + unsigned_bytes: u64, + signed_bytes: u64, + broadcast_bytes: u64, + reconciliation_bytes: u64, + trace_events: u64, + trace_bytes: u64, + evidence_bytes: u64, + builder_bytes: u64, + recipients: u64, + network_policies: u64, + x402_origins: u64, + utxos: u64, + reconciliations: u64, + elapsed_ms: u64, + concurrency: u64, + unexpected_authority_calls: u64, +} + +/// Opaque replay-validated Economic Agent result. +pub struct EconomicRun { + status: EconomicRunStatus, + transaction_id: Option, + confirmation_status: Option, + trace: String, + trace_digest: String, + evidence: String, + evidence_digest: String, +} + +impl EconomicRun { + /// Returns the closed terminal status. + pub const fn status(&self) -> EconomicRunStatus { + self.status + } + /// Returns the bound transaction ID when present. + pub fn transaction_id(&self) -> Option<&str> { + self.transaction_id.as_deref() + } + /// Returns the latest confirmation status when present. + pub fn confirmation_status(&self) -> Option<&str> { + self.confirmation_status.as_deref() + } + /// Returns canonical Trace v1 JSON. + pub fn trace(&self) -> &str { + &self.trace + } + /// Returns the domain-separated Trace digest. + pub fn trace_digest(&self) -> &str { + &self.trace_digest + } + /// Returns canonical Evidence v1 JSON. + pub fn evidence(&self) -> &str { + &self.evidence + } + /// Returns the domain-separated Evidence digest. + pub fn evidence_digest(&self) -> &str { + &self.evidence_digest + } +} + +/// Opaque single-concurrency Economic Agent owning its injected host. +pub struct EconomicAgent { + policy: Policy, + retained_policy_bytes: usize, + host: H, + cancellation: AgentCancellation, +} + +impl EconomicAgent { + fn terminal_floor(&self) -> Result { + let lane = terminal_floor(&self.policy.limits)?; + if active_remaining().is_some_and(|remaining| remaining < lane) { + return Err(g216("builder_bytes", self.policy.limits.max_builder_bytes)); + } + if !set_active_floor(lane) { + return Err(g216("builder_bytes", self.policy.limits.max_builder_bytes)); + } + Ok(lane) + } + + /// Parses and retains one canonical Policy before consulting the host. + pub fn new( + policy: &str, + host: H, + cancellation: AgentCancellation, + ) -> Result> { + let (result, overflowed, consumed) = with_limit_usage(MAX_BUILDER_BYTES, || { + if !reserve_active(policy.len().saturating_mul(MAX_JSON_DEPTH + 2)) { + return Err(g216("builder_bytes", MAX_BUILDER_BYTES as u64)); + } + parse_policy(policy) + }); + if overflowed { + return Err(vec![g216("builder_bytes", MAX_BUILDER_BYTES as u64)]); + } + result + .map(|policy| Self { + policy, + retained_policy_bytes: consumed, + host, + cancellation, + }) + .map_err(|diagnostic| vec![diagnostic]) + } + + /// Executes one canonical Payment Intent proposed by a completed sealed Agent run. + pub fn execute(&mut self, source: &AgentRun) -> Result> { + let binding = source.economic_binding(); + if binding.status != AgentRunStatus::Completed { + return Err(vec![g212("agent run not completed")]); + } + let Some(message) = binding.final_message else { + return Err(vec![g212("agent run not completed")]); + }; + if self.cancellation.is_cancelled() { + return Err(vec![info("SPX-I228", "Economic Agent run was cancelled")]); + } + let policy_limit = self.policy.limits.max_builder_bytes as usize; + let started = self.host.boundary_probe().elapsed_ms(); + let result = with_limit_usage(policy_limit, || { + if !reserve_active(self.retained_policy_bytes) { + return Err(g216("builder_bytes", self.policy.limits.max_builder_bytes)); + } + if !reserve_active(message.len().saturating_mul(MAX_JSON_DEPTH + 2)) { + return Err(g216("builder_bytes", self.policy.limits.max_builder_bytes)); + } + let intent = parse_intent(message).and_then(|intent| { + admit_intent(&self.policy, &intent)?; + Ok(intent) + })?; + self.terminal_floor()?; + self.execute_bounded(&binding, intent, started) + }); + match result { + (Ok(run), false, _) => Ok(run), + (Err(diagnostic), _, _) => Err(vec![diagnostic]), + (Ok(_), true, _) => Err(vec![g216( + "builder_bytes", + self.policy.limits.max_builder_bytes, + )]), + } + } + + fn execute_bounded( + &mut self, + binding: &crate::agent_runtime::EconomicAgentBinding<'_>, + intent: Intent, + started: u64, + ) -> Result { + let profile_cost = binding.evidence.len(); + if !reserve_active(profile_cost) { + return Err(g216("builder_bytes", self.policy.limits.max_builder_bytes)); + } + let economic_run_id = run_id( + binding.evidence_digest, + &self.policy.digest, + &intent.digest, + &intent.idempotency_key, + ); + let policy_doc = Doc { + source: self.policy.source.clone(), + digest: self.policy.digest.clone(), + }; + let intent_doc = Doc { + source: intent.source.clone(), + digest: intent.digest.clone(), + }; + let mut journal = Journal { + idempotency_key: intent.idempotency_key.clone(), + version: 0, + policy: policy_doc, + intent: intent_doc, + run_id: economic_run_id.clone(), + state: JournalState::Reserved, + reserved_amount: intent.amount(), + reserved_fee: intent.max_fee(), + plan: None, + simulation: None, + approval: None, + unsigned: None, + signed: None, + broadcast: None, + reconciliation: None, + updated_at: intent.created_at, + }; + let mut budget = Budget { + policy_bytes: self.policy.source.len() as u64, + intent_bytes: intent.source.len() as u64, + recipients: self + .policy + .networks + .iter() + .map(|row| row.recipients.len() as u64) + .sum(), + network_policies: self.policy.networks.len() as u64, + x402_origins: self.policy.origins.len() as u64, + concurrency: 1, + ..Budget::default() + }; + let mut events = Vec::new(); + push_event( + &mut events, + event( + "run_started", + None, + Some(binding.evidence_digest.to_owned()), + Some(self.policy.digest.clone()), + "started", + Usage::default(), + )?, + )?; + self.pre_call(started, self.policy.limits.max_journal_bytes as usize)?; + let mut load_sink = EconomicDocumentSink::new( + self.policy.limits.max_journal_bytes as usize, + self.cancellation.clone(), + self.host.boundary_probe(), + started, + self.policy.limits.max_elapsed_ms, + self.policy.limits.max_builder_bytes, + self.terminal_floor()?, + ); + let load = self.host.load(&intent.idempotency_key, &mut load_sink); + let mut load_usage = Usage::default(); + load_usage.journal_reads = 1; + let loaded = match load { + EconomicJournalLoad::Missing => { + push_event( + &mut events, + event("journal_loaded", None, None, None, "missing", load_usage)?, + )?; + None + } + EconomicJournalLoad::Present => { + let source = match load_sink.finish("journal_bytes") { + Ok(source) => source, + Err(diagnostic) => { + push_event( + &mut events, + event("journal_loaded", None, None, None, "failed", load_usage)?, + )?; + return finish_run( + &economic_run_id, + binding, + &self.policy, + &intent, + None, + None, + None, + None, + &journal, + None, + None, + &mut events, + diagnostic_terminal(&diagnostic), + &mut budget, + started, + ); + } + }; + load_usage.output_bytes = source.len() as u64; + push_event( + &mut events, + event( + "journal_loaded", + None, + None, + Some(digest(JOURNAL_DOMAIN, source.as_bytes())), + "present", + load_usage, + )?, + )?; + let parsed = match parse_journal(&source, &self.policy, &intent, &economic_run_id) { + Ok(parsed) => parsed, + Err(diagnostic) => { + return finish_run( + &economic_run_id, + binding, + &self.policy, + &intent, + None, + None, + None, + None, + &journal, + None, + None, + &mut events, + diagnostic_terminal(&diagnostic), + &mut budget, + started, + ); + } + }; + budget.journal_bytes = source.len() as u64; + Some(parsed) + } + EconomicJournalLoad::DefinitelyNotStarted | EconomicJournalLoad::FailedUncertain => { + push_event( + &mut events, + event("journal_loaded", None, None, None, "failed", load_usage)?, + )?; + let terminal = + diagnostic_terminal(&info("SPX-I222", "Economic Agent journal adapter failed")); + return finish_run( + &economic_run_id, + binding, + &self.policy, + &intent, + None, + None, + None, + None, + &journal, + None, + None, + &mut events, + terminal, + &mut budget, + started, + ); + } + }; + if let Some(existing) = loaded { + if existing.version == 1 && existing.state == JournalState::Reserved { + return self.execute_reserved( + binding, + intent, + economic_run_id, + existing, + events, + budget, + started, + ); + } + if existing.version < 6 || existing.broadcast.is_none() { + budget.signed_bytes = existing.signed.as_ref().map_or(0, |value| value.1 as u64); + return finish_run( + &economic_run_id, + binding, + &self.policy, + &intent, + None, + None, + None, + None, + &existing, + None, + None, + &mut events, + diagnostic_terminal(&info("SPX-I222", "Economic Agent journal adapter failed")), + &mut budget, + self.elapsed_ms(started)?, + ); + } + return self.resume_loaded( + binding, + intent, + economic_run_id, + existing, + events, + budget, + started, + ); + } + let (network, asset) = intent.network_asset(); + let max_rolling = self + .policy + .networks + .iter() + .find(|row| { + row.rail == intent.settlement_rail() && row.network == network && row.asset == asset + }) + .ok_or_else(|| g212("rail/network/asset not allowed"))? + .max_rolling; + let rolling = EconomicRollingReservation { + wallet_id: intent.wallet_id.clone(), + rail: intent.settlement_rail(), + network: network.to_owned(), + asset: asset.to_owned(), + requested_at_ms: intent.created_at, + amount_atomic: intent.amount(), + max_rolling_24h_atomic: max_rolling, + }; + if let Err(diagnostic) = cas_journal( + &mut self.host, + &mut journal, + &mut events, + &mut budget, + self.policy.limits.max_journal_bytes, + EconomicRollingReservationUpdate::Reserve(&rolling), + ) { + return finish_run( + &economic_run_id, + binding, + &self.policy, + &intent, + None, + None, + None, + None, + &journal, + None, + None, + &mut events, + diagnostic_terminal(&diagnostic), + &mut budget, + started, + ); + } + push_event( + &mut events, + event( + "intent_reserved", + Some(intent.settlement_rail()), + Some(intent.digest.clone()), + Some(journal_digest(&journal)), + "reserved", + Usage::default(), + )?, + )?; + self.execute_reserved( + binding, + intent, + economic_run_id, + journal, + events, + budget, + started, + ) + } + + fn execute_reserved( + &mut self, + binding: &crate::agent_runtime::EconomicAgentBinding<'_>, + intent: Intent, + economic_run_id: String, + mut journal: Journal, + mut events: Vec, + mut budget: Budget, + started: u64, + ) -> Result { + let rail = intent.settlement_rail(); + let (network, _) = intent.network_asset(); + macro_rules! terminal_try { + ($expression:expr, $invoice:expr, $plan:expr, $simulation:expr, $approval:expr, $broadcast:expr, $reconciliation:expr) => { + match $expression { + Ok(value) => value, + Err(diagnostic) => { + return self.finish_failure( + binding, + &intent, + &economic_run_id, + &journal, + $invoice, + $plan, + $simulation, + $approval, + $broadcast, + $reconciliation, + &mut events, + &mut budget, + diagnostic, + started, + ) + } + } + }; + } + let invoice = if let Payment::X402 { + origin, + method, + resource, + .. + } = &intent.payment + { + terminal_try!( + self.pre_call(started, self.policy.limits.max_invoice_bytes as usize), + None, + None, + None, + None, + None, + None + ); + let mut sink = EconomicDocumentSink::new( + self.policy.limits.max_invoice_bytes as usize, + self.cancellation.clone(), + self.host.boundary_probe(), + started, + self.policy.limits.max_elapsed_ms, + self.policy.limits.max_builder_bytes, + self.terminal_floor()?, + ); + let disposition = self.host.fetch_invoice(origin, method, resource, &mut sink); + let mut usage = Usage::default(); + usage.invoice_reads = 1; + if disposition != EconomicAdapterDisposition::Succeeded { + push_event( + &mut events, + event( + "invoice_loaded", + Some(rail), + Some(intent.digest.clone()), + None, + "failed", + usage, + )?, + )?; + return self.finish_failure( + binding, + &intent, + &economic_run_id, + &journal, + None, + None, + None, + None, + None, + None, + &mut events, + &mut budget, + info("SPX-I223", "Economic Agent chain adapter failed"), + started, + ); + } + let source = terminal_try!( + sink.finish("invoice_bytes"), + None, + None, + None, + None, + None, + None + ); + usage.output_bytes = source.len() as u64; + let parsed = terminal_try!( + parse_invoice_limited(&source, &intent, &self.policy.limits), + None, + None, + None, + None, + None, + None + ); + budget.invoice_bytes = source.len() as u64; + push_event( + &mut events, + event( + "invoice_loaded", + Some(rail), + Some(intent.digest.clone()), + Some(parsed.doc.digest.clone()), + "loaded", + usage, + )?, + )?; + Some(parsed) + } else { + None + }; + terminal_try!( + self.pre_call(started, self.policy.limits.max_snapshot_bytes as usize), + invoice.as_ref(), + None, + None, + None, + None, + None + ); + let mut snapshot_sink = EconomicDocumentSink::new( + self.policy.limits.max_snapshot_bytes as usize, + self.cancellation.clone(), + self.host.boundary_probe(), + started, + self.policy.limits.max_elapsed_ms, + self.policy.limits.max_builder_bytes, + self.terminal_floor()?, + ); + let disposition = match rail { + EconomicRail::Evm => self.host.evm_snapshot(&intent.source, &mut snapshot_sink), + EconomicRail::Solana => self + .host + .solana_snapshot(&intent.source, &mut snapshot_sink), + EconomicRail::Bitcoin => self + .host + .bitcoin_snapshot(&intent.source, &mut snapshot_sink), + }; + let mut snapshot_usage = Usage::default(); + snapshot_usage.snapshot_reads = 1; + if disposition != EconomicAdapterDisposition::Succeeded { + push_event( + &mut events, + event( + "snapshot_loaded", + Some(rail), + Some(intent.digest.clone()), + None, + "failed", + snapshot_usage, + )?, + )?; + return self.finish_failure( + binding, + &intent, + &economic_run_id, + &journal, + invoice.as_ref(), + None, + None, + None, + None, + None, + &mut events, + &mut budget, + info("SPX-I223", "Economic Agent chain adapter failed"), + started, + ); + } + let snapshot_source = terminal_try!( + snapshot_sink.finish("snapshot_bytes"), + invoice.as_ref(), + None, + None, + None, + None, + None + ); + snapshot_usage.output_bytes = snapshot_source.len() as u64; + let snapshot = terminal_try!( + parse_snapshot_limited(&snapshot_source, rail, &self.policy.limits), + invoice.as_ref(), + None, + None, + None, + None, + None + ); + budget.snapshot_bytes = snapshot_source.len() as u64; + if let SnapshotState::Bitcoin { utxos, .. } = &snapshot.state { + budget.utxos = utxos.len() as u64; + } + push_event( + &mut events, + event( + "snapshot_loaded", + Some(rail), + Some(intent.digest.clone()), + Some(snapshot.doc.digest.clone()), + "loaded", + snapshot_usage, + )?, + )?; + let (unsigned, format) = terminal_try!( + build_unsigned_limited( + &intent, + &snapshot, + self.policy.limits.max_unsigned_transaction_bytes, + self.policy.limits.max_builder_bytes, + self.terminal_floor()?, + ), + invoice.as_ref(), + None, + None, + None, + None, + None + ); + if unsigned.len() > self.policy.limits.max_unsigned_transaction_bytes as usize { + return Err(g216( + "unsigned_transaction_bytes", + self.policy.limits.max_unsigned_transaction_bytes, + )); + } + let plan = terminal_try!( + make_plan( + &economic_run_id, + binding.run_id, + binding.evidence, + binding.evidence_digest, + &self.policy, + &intent, + invoice.as_ref(), + &snapshot, + unsigned, + format, + ), + invoice.as_ref(), + None, + None, + None, + None, + None + ); + budget.plan_bytes = plan.doc.source.len() as u64; + budget.unsigned_bytes = plan.unsigned.len() as u64; + push_event( + &mut events, + event( + "plan_built", + Some(rail), + Some(snapshot.doc.digest.clone()), + Some(plan.doc.digest.clone()), + "built", + Usage::default(), + )?, + )?; + terminal_try!( + self.pre_call(started, self.policy.limits.max_simulation_bytes as usize), + invoice.as_ref(), + Some(&plan), + None, + None, + None, + None + ); + let mut simulation_sink = EconomicDocumentSink::new( + self.policy.limits.max_simulation_bytes as usize, + self.cancellation.clone(), + self.host.boundary_probe(), + started, + self.policy.limits.max_elapsed_ms, + self.policy.limits.max_builder_bytes, + self.terminal_floor()?, + ); + let disposition = match rail { + EconomicRail::Evm => { + self.host + .evm_simulate(&plan.doc.source, &plan.unsigned, &mut simulation_sink) + } + EconomicRail::Solana => { + self.host + .solana_simulate(&plan.doc.source, &plan.unsigned, &mut simulation_sink) + } + EconomicRail::Bitcoin => { + self.host + .bitcoin_simulate(&plan.doc.source, &plan.unsigned, &mut simulation_sink) + } + }; + let mut sim_usage = Usage::default(); + sim_usage.simulations = 1; + if disposition != EconomicAdapterDisposition::Succeeded { + push_event( + &mut events, + event( + "simulation_finished", + Some(rail), + Some(plan.doc.digest.clone()), + None, + "failed", + sim_usage, + )?, + )?; + return self.finish_failure( + binding, + &intent, + &economic_run_id, + &journal, + invoice.as_ref(), + Some(&plan), + None, + None, + None, + None, + &mut events, + &mut budget, + info("SPX-I223", "Economic Agent chain adapter failed"), + started, + ); + } + let simulation_source = terminal_try!( + simulation_sink.finish("simulation_bytes"), + invoice.as_ref(), + Some(&plan), + None, + None, + None, + None + ); + sim_usage.output_bytes = simulation_source.len() as u64; + let simulation = terminal_try!( + parse_simulation_limited(&simulation_source, &plan, &intent, &self.policy.limits), + invoice.as_ref(), + Some(&plan), + None, + None, + None, + None + ); + budget.simulation_bytes = simulation_source.len() as u64; + push_event( + &mut events, + event( + "simulation_finished", + Some(rail), + Some(plan.doc.digest.clone()), + Some(simulation.doc.digest.clone()), + "succeeded", + sim_usage, + )?, + )?; + let mut prepared_journal = terminal_try!( + clone_journal_bounded(&journal, self.policy.limits.max_builder_bytes), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + None, + None, + None + ); + prepared_journal.state = JournalState::Prepared; + prepared_journal.plan = Some(DocRef::from(&plan.doc)); + prepared_journal.simulation = Some(DocRef::from(&simulation.doc)); + prepared_journal.unsigned = Some(( + plan.unsigned_digest.clone(), + plan.unsigned.len(), + plan.format, + )); + prepared_journal.updated_at = snapshot.observed; + terminal_try!( + cas_journal( + &mut self.host, + &mut prepared_journal, + &mut events, + &mut budget, + self.policy.limits.max_journal_bytes, + EconomicRollingReservationUpdate::Retain, + ), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + None, + None, + None + ); + journal = prepared_journal; + let request = terminal_try!( + make_approval_request(&economic_run_id, &self.policy, &intent, &plan, &simulation), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + None, + None, + None + ); + budget.approval_request_bytes = request.source.len() as u64; + terminal_try!( + self.pre_call(started, self.policy.limits.max_approval_bytes as usize), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + None, + None, + None + ); + let mut approval_sink = EconomicDocumentSink::new( + self.policy.limits.max_approval_bytes as usize, + self.cancellation.clone(), + self.host.boundary_probe(), + started, + self.policy.limits.max_elapsed_ms, + self.policy.limits.max_builder_bytes, + self.terminal_floor()?, + ); + let disposition = self.host.approve(&request.source, &mut approval_sink); + let mut approval_usage = Usage::default(); + approval_usage.approvals = 1; + if disposition != EconomicAdapterDisposition::Succeeded { + push_event( + &mut events, + event( + "approval_finished", + Some(rail), + Some(request.digest.clone()), + None, + "failed", + approval_usage, + )?, + )?; + return self.finish_failure( + binding, + &intent, + &economic_run_id, + &journal, + invoice.as_ref(), + Some(&plan), + Some(&simulation), + None, + None, + None, + &mut events, + &mut budget, + info("SPX-I224", "Economic Agent approval adapter failed"), + started, + ); + } + let approval_source = terminal_try!( + approval_sink.finish("approval_bytes"), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + None, + None, + None + ); + approval_usage.output_bytes = approval_source.len() as u64; + let approval = terminal_try!( + parse_approval_limited( + &approval_source, + &self.policy, + &intent, + &plan, + &simulation, + &request, + ), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + None, + None, + None + ); + budget.approval_bytes = approval_source.len() as u64; + push_event( + &mut events, + event( + "approval_finished", + Some(rail), + Some(request.digest.clone()), + Some(approval.doc.digest.clone()), + "approved", + approval_usage, + )?, + )?; + let mut approved_journal = terminal_try!( + clone_journal_bounded(&journal, self.policy.limits.max_builder_bytes), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + None, + None + ); + approved_journal.state = JournalState::Approved; + approved_journal.approval = Some(DocRef::from(&approval.doc)); + approved_journal.updated_at = snapshot.observed; + terminal_try!( + cas_journal( + &mut self.host, + &mut approved_journal, + &mut events, + &mut budget, + self.policy.limits.max_journal_bytes, + EconomicRollingReservationUpdate::Retain, + ), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + None, + None + ); + journal = approved_journal; + terminal_try!( + self.pre_call( + started, + self.policy.limits.max_signed_transaction_bytes as usize, + ), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + None, + None + ); + let current_ms = terminal_try!( + admitted_now_from(&intent, snapshot.observed, self.elapsed_ms(started)?), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + None, + None + ); + if current_ms >= plan.expires || current_ms >= simulation.expires { + return self.finish_failure( + binding, + &intent, + &economic_run_id, + &journal, + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + None, + None, + &mut events, + &mut budget, + g212("expired"), + started, + ); + } + let mut sign_marker = terminal_try!( + clone_journal_bounded(&journal, self.policy.limits.max_builder_bytes), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + None, + None + ); + terminal_try!( + cas_journal( + &mut self.host, + &mut sign_marker, + &mut events, + &mut budget, + self.policy.limits.max_journal_bytes, + EconomicRollingReservationUpdate::Retain, + ), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + None, + None + ); + journal = sign_marker; + terminal_try!( + self.pre_call( + started, + self.policy.limits.max_signed_transaction_bytes as usize, + ), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + None, + None + ); + let mut signed_sink = EconomicBytesSink::new( + self.policy.limits.max_signed_transaction_bytes as usize, + self.cancellation.clone(), + self.host.boundary_probe(), + started, + self.policy.limits.max_elapsed_ms, + self.policy.limits.max_builder_bytes, + self.terminal_floor()?, + ); + let disposition = self.host.sign( + &self.policy.wallet_id, + rail, + &plan.unsigned_digest, + &plan.unsigned, + &approval.doc.digest, + &mut signed_sink, + ); + let mut sign_usage = Usage::default(); + sign_usage.signatures = 1; + if disposition != EconomicAdapterDisposition::Succeeded { + push_event( + &mut events, + event( + "transaction_signed", + Some(rail), + Some(plan.unsigned_digest.clone()), + None, + "failed", + sign_usage, + )?, + )?; + return self.finish_failure( + binding, + &intent, + &economic_run_id, + &journal, + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + None, + None, + &mut events, + &mut budget, + info("SPX-I225", "Economic Agent custody adapter failed"), + started, + ); + } + let signed = match signed_sink.finish() { + Ok(value) => value, + Err(diagnostic) => { + push_event( + &mut events, + event( + "transaction_signed", + Some(rail), + Some(plan.unsigned_digest.clone()), + None, + "failed", + sign_usage, + )?, + )?; + return self.finish_failure( + binding, + &intent, + &economic_run_id, + &journal, + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + None, + None, + &mut events, + &mut budget, + diagnostic, + started, + ); + } + }; + if let Err(diagnostic) = verify_signed(rail, &plan.unsigned, &signed) { + push_event( + &mut events, + event( + "transaction_signed", + Some(rail), + Some(plan.unsigned_digest.clone()), + None, + "failed", + sign_usage, + )?, + )?; + return self.finish_failure( + binding, + &intent, + &economic_run_id, + &journal, + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + None, + None, + &mut events, + &mut budget, + diagnostic, + started, + ); + } + let signed_digest = digest(SIGNED_DOMAIN, &signed); + budget.signed_bytes = signed.len() as u64; + sign_usage.output_bytes = signed.len() as u64; + push_event( + &mut events, + event( + "transaction_signed", + Some(rail), + Some(plan.unsigned_digest.clone()), + Some(signed_digest.clone()), + "signed", + sign_usage, + )?, + )?; + let mut signed_journal = terminal_try!( + clone_journal_bounded(&journal, self.policy.limits.max_builder_bytes), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + None, + None + ); + signed_journal.state = JournalState::Signed; + signed_journal.signed = Some((signed_digest.clone(), signed.len())); + signed_journal.updated_at = snapshot.observed; + terminal_try!( + cas_journal( + &mut self.host, + &mut signed_journal, + &mut events, + &mut budget, + self.policy.limits.max_journal_bytes, + EconomicRollingReservationUpdate::Retain, + ), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + None, + None + ); + journal = signed_journal; + let current_ms = terminal_try!( + admitted_now_from(&intent, snapshot.observed, self.elapsed_ms(started)?), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + None, + None + ); + if current_ms >= plan.expires || current_ms >= approval_expires(&approval) { + return self.finish_failure( + binding, + &intent, + &economic_run_id, + &journal, + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + None, + None, + &mut events, + &mut budget, + g212("expired"), + started, + ); + } + let expected_transaction_id = terminal_try!( + transaction_id(rail, &signed).ok_or_else(g213), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + None, + None + ); + let provisional_source = format!("{{\"schema\":\"{BROADCAST_SCHEMA}\",\"rail\":{},\"network\":{},\"signed_transaction_digest\":{},\"transaction_id\":{},\"disposition\":\"unknown\",\"observed_at_ms\":0}}\n",quote_json(rail.text()),quote_json(network),quote_json(&signed_digest),quote_json(&expected_transaction_id)); + let provisional = terminal_try!( + parse_provisional_broadcast( + &provisional_source, + rail, + network, + &signed_digest, + &expected_transaction_id + ), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + None, + None + ); + terminal_try!( + self.pre_call( + started, + self.policy.limits.max_broadcast_receipt_bytes as usize, + ), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + None, + None + ); + let mut broadcast_marker = terminal_try!( + clone_journal_bounded(&journal, self.policy.limits.max_builder_bytes), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + Some(&provisional), + None + ); + broadcast_marker.state = JournalState::BroadcastUnknown; + broadcast_marker.broadcast = Some(provisional.doc.clone()); + broadcast_marker.updated_at = 0; + terminal_try!( + cas_journal( + &mut self.host, + &mut broadcast_marker, + &mut events, + &mut budget, + self.policy.limits.max_journal_bytes, + EconomicRollingReservationUpdate::Retain, + ), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + Some(&provisional), + None + ); + journal = broadcast_marker; + terminal_try!( + self.pre_call( + started, + self.policy.limits.max_broadcast_receipt_bytes as usize, + ), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + Some(&provisional), + None + ); + let mut broadcast_sink = EconomicDocumentSink::new( + self.policy.limits.max_broadcast_receipt_bytes as usize, + self.cancellation.clone(), + self.host.boundary_probe(), + started, + self.policy.limits.max_elapsed_ms, + self.policy.limits.max_builder_bytes, + self.terminal_floor()?, + ); + let disposition = match rail { + EconomicRail::Evm => self.host.evm_broadcast(&signed, &mut broadcast_sink), + EconomicRail::Solana => self.host.solana_broadcast(&signed, &mut broadcast_sink), + EconomicRail::Bitcoin => self.host.bitcoin_broadcast(&signed, &mut broadcast_sink), + }; + let mut broadcast_usage = Usage::default(); + broadcast_usage.broadcasts = 1; + if disposition != EconomicAdapterDisposition::Succeeded { + push_event( + &mut events, + event( + "broadcast_finished", + Some(rail), + Some(signed_digest.clone()), + Some(provisional.doc.digest.clone()), + "unknown", + broadcast_usage, + )?, + )?; + return self.finish_failure( + binding, + &intent, + &economic_run_id, + &journal, + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + Some(&provisional), + None, + &mut events, + &mut budget, + info("SPX-I226", "Economic Agent broadcast outcome is uncertain"), + started, + ); + } + let broadcast_source = terminal_try!( + broadcast_sink.finish("broadcast_receipt_bytes"), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + None, + None + ); + broadcast_usage.output_bytes = broadcast_source.len() as u64; + let broadcast = terminal_try!( + parse_broadcast_limited( + &broadcast_source, + rail, + network, + &signed_digest, + Some(&expected_transaction_id), + &self.policy.limits, + ), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + None, + None + ); + budget.signed_bytes = journal.signed.as_ref().map_or(0, |value| value.1 as u64); + budget.broadcast_bytes = broadcast.doc.source.len() as u64; + budget.broadcast_bytes = broadcast_source.len() as u64; + push_event( + &mut events, + event( + "broadcast_finished", + Some(rail), + Some(signed_digest), + Some(broadcast.doc.digest.clone()), + broadcast.disposition, + broadcast_usage, + )?, + )?; + if broadcast.disposition == "unknown" { + let mut outcome_journal = terminal_try!( + clone_journal_bounded(&journal, self.policy.limits.max_builder_bytes), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + Some(&broadcast), + None + ); + outcome_journal.state = JournalState::BroadcastUnknown; + outcome_journal.broadcast = Some(broadcast.doc.clone()); + outcome_journal.updated_at = broadcast.observed; + terminal_try!( + cas_journal( + &mut self.host, + &mut outcome_journal, + &mut events, + &mut budget, + self.policy.limits.max_journal_bytes, + EconomicRollingReservationUpdate::Retain, + ), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + Some(&broadcast), + None + ); + journal = outcome_journal; + return self.finish_failure( + binding, + &intent, + &economic_run_id, + &journal, + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + Some(&broadcast), + None, + &mut events, + &mut budget, + info("SPX-I226", "Economic Agent broadcast outcome is uncertain"), + started, + ); + } + if broadcast.disposition == "rejected" { + let mut outcome_journal = terminal_try!( + clone_journal_bounded(&journal, self.policy.limits.max_builder_bytes), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + Some(&broadcast), + None + ); + outcome_journal.state = JournalState::Rejected; + outcome_journal.broadcast = Some(broadcast.doc.clone()); + outcome_journal.updated_at = broadcast.observed; + terminal_try!( + cas_journal( + &mut self.host, + &mut outcome_journal, + &mut events, + &mut budget, + self.policy.limits.max_journal_bytes, + EconomicRollingReservationUpdate::Retain, + ), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + Some(&broadcast), + None + ); + journal = outcome_journal; + return self.finish_failure( + binding, + &intent, + &economic_run_id, + &journal, + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + Some(&broadcast), + None, + &mut events, + &mut budget, + info("SPX-I223", "Economic Agent chain adapter failed"), + started, + ); + } + let mut outcome_journal = terminal_try!( + clone_journal_bounded(&journal, self.policy.limits.max_builder_bytes), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + Some(&broadcast), + None + ); + outcome_journal.state = if broadcast.disposition == "pending" { + JournalState::Pending + } else { + JournalState::Broadcasted + }; + outcome_journal.broadcast = Some(broadcast.doc.clone()); + outcome_journal.updated_at = broadcast.observed; + terminal_try!( + cas_journal( + &mut self.host, + &mut outcome_journal, + &mut events, + &mut budget, + self.policy.limits.max_journal_bytes, + EconomicRollingReservationUpdate::Retain, + ), + invoice.as_ref(), + Some(&plan), + Some(&simulation), + Some(&approval), + Some(&broadcast), + None + ); + journal = outcome_journal; + self.reconcile_after_broadcast( + binding, + &intent, + &economic_run_id, + journal, + invoice.as_ref(), + &plan, + &simulation, + &approval, + &broadcast, + events, + budget, + started, + ) + } + + fn pre_call(&self, started: u64, maximum_output: usize) -> Result<(), Diagnostic> { + if self.cancellation.is_cancelled() { + return Err(info("SPX-I228", "Economic Agent run was cancelled")); + } + if self.elapsed_ms(started)? > self.policy.limits.max_elapsed_ms { + return Err(info("SPX-I229", "Economic Agent deadline was exceeded")); + } + let multiplier = if maximum_output == self.policy.limits.max_journal_bytes as usize { + 2 + } else if maximum_output == self.policy.limits.max_signed_transaction_bytes as usize { + 3 + } else { + usize::try_from(self.policy.limits.max_json_depth) + .map_err(|_| g217())? + .checked_add(3) + .ok_or_else(g217)? + }; + let output_lane = maximum_output + .checked_mul(multiplier) + .ok_or_else(|| g216("builder_bytes", self.policy.limits.max_builder_bytes))?; + let required = output_lane + .checked_add(self.terminal_floor()?) + .ok_or_else(|| g216("builder_bytes", self.policy.limits.max_builder_bytes))?; + if active_remaining().is_some_and(|remaining| remaining < required) { + return Err(g216("builder_bytes", self.policy.limits.max_builder_bytes)); + } + Ok(()) + } + + fn elapsed_ms(&self, started: u64) -> Result { + self.host + .boundary_probe() + .elapsed_ms() + .checked_sub(started) + .ok_or_else(|| info("SPX-I229", "Economic Agent deadline was exceeded")) + } + + #[allow(clippy::too_many_arguments)] + fn finish_failure( + &mut self, + binding: &crate::agent_runtime::EconomicAgentBinding<'_>, + intent: &Intent, + economic_run_id: &str, + journal: &Journal, + invoice: Option<&Invoice>, + plan: Option<&Plan>, + simulation: Option<&Simulation>, + approval: Option<&Approval>, + broadcast: Option<&BroadcastReceipt>, + reconciliation: Option<&Reconciliation>, + events: &mut Vec, + budget: &mut Budget, + diagnostic: Diagnostic, + started: u64, + ) -> Result { + let mut terminal_diagnostic = diagnostic; + let mut terminal_journal = + clone_journal_bounded(journal, self.policy.limits.max_builder_bytes)?; + let usage = cumulative_usage(events)?; + let uncertain_journal_cas = events + .last() + .is_some_and(|event| event.kind == "journal_committed" && event.authority_uncertain); + let already_sealed_effect_boundary = (journal.version == 4 + && journal.state == JournalState::Approved) + || (journal.version == 6 && journal.state == JournalState::BroadcastUnknown) + || journal.broadcast.is_some(); + let no_signature_or_broadcast_attempt = journal.signed.is_none() + && journal.broadcast.is_none() + && usage.signatures == 0 + && usage.broadcasts == 0 + && journal.version <= 3 + && (journal.version == 1 || usage.journal_writes > 0); + if !uncertain_journal_cas { + terminal_journal.state = if already_sealed_effect_boundary + || journal.signed.is_some() + || journal.broadcast.is_some() + { + journal.state + } else { + match terminal_diagnostic.code { + "SPX-I228" => JournalState::Cancelled, + "SPX-G212" | "SPX-G214" => JournalState::Rejected, + _ => JournalState::Failed, + } + }; + } + if no_signature_or_broadcast_attempt && !uncertain_journal_cas { + terminal_journal.updated_at = admitted_now_from( + intent, + journal.updated_at, + self.elapsed_ms(started) + .unwrap_or(self.policy.limits.max_elapsed_ms), + ) + .unwrap_or(intent.expires_at); + } + let rolling = if no_signature_or_broadcast_attempt { + EconomicRollingReservationUpdate::Release + } else { + EconomicRollingReservationUpdate::Retain + }; + if journal.version > 0 + && !uncertain_journal_cas + && no_signature_or_broadcast_attempt + && !already_sealed_effect_boundary + && cas_journal( + &mut self.host, + &mut terminal_journal, + events, + budget, + self.policy.limits.max_journal_bytes, + rolling, + ) + .is_err() + { + terminal_journal = + clone_journal_bounded(journal, self.policy.limits.max_builder_bytes)?; + terminal_diagnostic = info("SPX-I222", "Economic Agent journal adapter failed"); + } + let terminal = diagnostic_terminal(&terminal_diagnostic); + finish_run( + economic_run_id, + binding, + &self.policy, + intent, + invoice, + plan, + simulation, + approval, + &terminal_journal, + broadcast, + reconciliation, + events, + terminal, + budget, + self.elapsed_ms(started)?, + ) + } + + #[allow(clippy::too_many_arguments)] + fn reconcile_after_broadcast( + &mut self, + binding: &crate::agent_runtime::EconomicAgentBinding<'_>, + intent: &Intent, + economic_run_id: &str, + mut journal: Journal, + invoice: Option<&Invoice>, + plan: &Plan, + simulation: &Simulation, + approval: &Approval, + broadcast: &BroadcastReceipt, + mut events: Vec, + mut budget: Budget, + started: u64, + ) -> Result { + macro_rules! terminal_try { + ($expression:expr, $reconciliation:expr) => { + match $expression { + Ok(value) => value, + Err(diagnostic) => { + return self.finish_failure( + binding, + intent, + economic_run_id, + &journal, + invoice, + Some(plan), + Some(simulation), + Some(approval), + Some(broadcast), + $reconciliation, + &mut events, + &mut budget, + diagnostic, + started, + ) + } + } + }; + } + terminal_try!( + self.pre_call( + started, + self.policy.limits.max_reconciliation_bytes as usize, + ), + None + ); + let (attempts, odd) = terminal_try!(reconciliation_topology(&journal), None); + if odd || attempts >= self.policy.limits.max_reconciliations { + return self.finish_failure( + binding, + intent, + economic_run_id, + &journal, + invoice, + Some(plan), + Some(simulation), + Some(approval), + Some(broadcast), + None, + &mut events, + &mut budget, + g216("reconciliations", self.policy.limits.max_reconciliations), + started, + ); + } + terminal_try!( + cas_journal( + &mut self.host, + &mut journal, + &mut events, + &mut budget, + self.policy.limits.max_journal_bytes, + EconomicRollingReservationUpdate::Retain, + ), + None + ); + terminal_try!( + self.pre_call( + started, + self.policy.limits.max_reconciliation_bytes as usize, + ), + None + ); + let rail = intent.settlement_rail(); + let (network, _) = intent.network_asset(); + let mut sink = EconomicDocumentSink::new( + self.policy.limits.max_reconciliation_bytes as usize, + self.cancellation.clone(), + self.host.boundary_probe(), + started, + self.policy.limits.max_elapsed_ms, + self.policy.limits.max_builder_bytes, + self.terminal_floor()?, + ); + let disposition = match rail { + EconomicRail::Evm => self + .host + .evm_reconcile(&broadcast.transaction_id, &mut sink), + EconomicRail::Solana => self + .host + .solana_reconcile(&broadcast.transaction_id, &mut sink), + EconomicRail::Bitcoin => self + .host + .bitcoin_reconcile(&broadcast.transaction_id, &mut sink), + }; + let mut usage = Usage::default(); + usage.reconciliations = 1; + if disposition != EconomicAdapterDisposition::Succeeded { + push_event( + &mut events, + event( + "reconciliation_finished", + Some(rail), + Some(broadcast.doc.digest.clone()), + None, + "failed", + usage, + )?, + )?; + return self.finish_failure( + binding, + intent, + economic_run_id, + &journal, + invoice, + Some(plan), + Some(simulation), + Some(approval), + Some(broadcast), + None, + &mut events, + &mut budget, + info("SPX-I227", "Economic Agent reconciliation adapter failed"), + started, + ); + } + let source = terminal_try!(sink.finish("reconciliation_bytes"), None); + usage.output_bytes = source.len() as u64; + let reconciliation = terminal_try!( + parse_reconciliation_limited( + &source, + rail, + network, + &broadcast.transaction_id, + &self.policy.limits, + ), + None + ); + terminal_try!(validate_confirmation(intent, &reconciliation), None); + budget.reconciliation_bytes = source.len() as u64; + budget.reconciliations = attempts.checked_add(1).ok_or_else(g217)?; + push_event( + &mut events, + event( + "reconciliation_finished", + Some(rail), + Some(broadcast.doc.digest.clone()), + Some(reconciliation.doc.digest.clone()), + reconciliation.status, + usage, + )?, + )?; + journal.state = match reconciliation.status { + "confirmed" => JournalState::Confirmed, + "reorged" => JournalState::Reorged, + "dropped" => JournalState::Dropped, + _ => JournalState::Pending, + }; + journal.reconciliation = Some(reconciliation.doc.clone()); + journal.updated_at = reconciliation.observed; + terminal_try!( + cas_journal( + &mut self.host, + &mut journal, + &mut events, + &mut budget, + self.policy.limits.max_journal_bytes, + EconomicRollingReservationUpdate::Retain, + ), + Some(&reconciliation) + ); + let status = match reconciliation.status { + "confirmed" => EconomicRunStatus::Confirmed, + "reorged" => EconomicRunStatus::Reorged, + "dropped" => EconomicRunStatus::Dropped, + _ => EconomicRunStatus::Pending, + }; + let terminal = Terminal { + status, + transaction_id: Some(broadcast.transaction_id.clone()), + confirmation: Some(reconciliation.status.to_owned()), + code: None, + message: None, + }; + finish_run( + economic_run_id, + binding, + &self.policy, + intent, + invoice, + Some(plan), + Some(simulation), + Some(approval), + &journal, + Some(broadcast), + Some(&reconciliation), + &mut events, + terminal, + &mut budget, + self.elapsed_ms(started)?, + ) + } + + fn resume_loaded( + &mut self, + binding: &crate::agent_runtime::EconomicAgentBinding<'_>, + intent: Intent, + economic_run_id: String, + mut journal: Journal, + mut events: Vec, + mut budget: Budget, + started: u64, + ) -> Result { + macro_rules! terminal_try { + ($expression:expr, $broadcast:expr, $reconciliation:expr) => { + match $expression { + Ok(value) => value, + Err(diagnostic) => { + return self.finish_failure( + binding, + &intent, + &economic_run_id, + &journal, + None, + None, + None, + None, + $broadcast, + $reconciliation, + &mut events, + &mut budget, + diagnostic, + started, + ) + } + } + }; + } + let Some(broadcast_doc) = journal.broadcast.as_ref() else { + return self.finish_failure( + binding, + &intent, + &economic_run_id, + &journal, + None, + None, + None, + None, + None, + None, + &mut events, + &mut budget, + g215(), + started, + ); + }; + let (_, value) = terminal_try!( + canonical( + &broadcast_doc.source, + "broadcast receipt", + BROADCAST_SCHEMA, + self.policy.limits.max_broadcast_receipt_bytes as usize, + ), + None, + None + ); + let row = terminal_try!( + object(&value, "broadcast receipt", BROADCAST_SCHEMA), + None, + None + ); + let signed_digest = terminal_try!( + text( + row, + "signed_transaction_digest", + "broadcast receipt", + BROADCAST_SCHEMA, + ), + None, + None + ); + let (network, _) = intent.network_asset(); + let broadcast = terminal_try!( + if broadcast_is_provisional(broadcast_doc) { + let transaction_id = value["transaction_id"].as_str().ok_or_else(g215); + transaction_id.and_then(|transaction_id| { + parse_provisional_broadcast( + &broadcast_doc.source, + intent.settlement_rail(), + network, + signed_digest, + transaction_id, + ) + }) + } else { + parse_broadcast( + &broadcast_doc.source, + intent.settlement_rail(), + network, + signed_digest, + None, + ) + }, + None, + None + ); + budget.signed_bytes = journal.signed.as_ref().map_or(0, |value| value.1 as u64); + budget.broadcast_bytes = broadcast.doc.source.len() as u64; + if matches!( + journal.state, + JournalState::Confirmed | JournalState::Reorged | JournalState::Dropped + ) { + let status = match journal.state { + JournalState::Confirmed => EconomicRunStatus::Confirmed, + JournalState::Reorged => EconomicRunStatus::Reorged, + _ => EconomicRunStatus::Dropped, + }; + let terminal = Terminal { + status, + transaction_id: Some(broadcast.transaction_id.clone()), + confirmation: Some(journal.state.text().to_owned()), + code: None, + message: None, + }; + return finish_run( + &economic_run_id, + binding, + &self.policy, + &intent, + None, + None, + None, + None, + &journal, + Some(&broadcast), + None, + &mut events, + terminal, + &mut budget, + started, + ); + } + let (mut attempts, odd) = + terminal_try!(reconciliation_topology(&journal), Some(&broadcast), None); + if odd { + let mut closed = terminal_try!( + clone_journal_bounded(&journal, self.policy.limits.max_builder_bytes), + Some(&broadcast), + None + ); + terminal_try!( + cas_journal( + &mut self.host, + &mut closed, + &mut events, + &mut budget, + self.policy.limits.max_journal_bytes, + EconomicRollingReservationUpdate::Retain, + ), + Some(&broadcast), + None + ); + journal = closed; + } + if attempts >= self.policy.limits.max_reconciliations { + return self.finish_failure( + binding, + &intent, + &economic_run_id, + &journal, + None, + None, + None, + None, + Some(&broadcast), + None, + &mut events, + &mut budget, + g216("reconciliations", self.policy.limits.max_reconciliations), + started, + ); + } + terminal_try!( + self.pre_call( + started, + self.policy.limits.max_reconciliation_bytes as usize, + ), + Some(&broadcast), + None + ); + terminal_try!( + cas_journal( + &mut self.host, + &mut journal, + &mut events, + &mut budget, + self.policy.limits.max_journal_bytes, + EconomicRollingReservationUpdate::Retain, + ), + Some(&broadcast), + None + ); + attempts = attempts.checked_add(1).ok_or_else(g217)?; + terminal_try!( + self.pre_call( + started, + self.policy.limits.max_reconciliation_bytes as usize, + ), + Some(&broadcast), + None + ); + let mut sink = EconomicDocumentSink::new( + self.policy.limits.max_reconciliation_bytes as usize, + self.cancellation.clone(), + self.host.boundary_probe(), + started, + self.policy.limits.max_elapsed_ms, + self.policy.limits.max_builder_bytes, + self.terminal_floor()?, + ); + let disposition = match intent.settlement_rail() { + EconomicRail::Evm => self + .host + .evm_reconcile(&broadcast.transaction_id, &mut sink), + EconomicRail::Solana => self + .host + .solana_reconcile(&broadcast.transaction_id, &mut sink), + EconomicRail::Bitcoin => self + .host + .bitcoin_reconcile(&broadcast.transaction_id, &mut sink), + }; + if disposition != EconomicAdapterDisposition::Succeeded { + return self.finish_failure( + binding, + &intent, + &economic_run_id, + &journal, + None, + None, + None, + None, + Some(&broadcast), + None, + &mut events, + &mut budget, + info("SPX-I227", "Economic Agent reconciliation adapter failed"), + started, + ); + } + let source = terminal_try!(sink.finish("reconciliation_bytes"), Some(&broadcast), None); + let reconciliation = terminal_try!( + parse_reconciliation_limited( + &source, + intent.settlement_rail(), + network, + &broadcast.transaction_id, + &self.policy.limits, + ), + Some(&broadcast), + None + ); + terminal_try!( + validate_confirmation(&intent, &reconciliation), + Some(&broadcast), + None + ); + budget.reconciliation_bytes = source.len() as u64; + budget.reconciliations = attempts; + push_event( + &mut events, + event( + "reconciliation_finished", + Some(intent.settlement_rail()), + Some(broadcast.doc.digest.clone()), + Some(reconciliation.doc.digest.clone()), + reconciliation.status, + Usage { + reconciliations: 1, + output_bytes: source.len() as u64, + ..Usage::default() + }, + )?, + )?; + let mut next = terminal_try!( + clone_journal_bounded(&journal, self.policy.limits.max_builder_bytes), + Some(&broadcast), + Some(&reconciliation) + ); + next.state = match reconciliation.status { + "confirmed" => JournalState::Confirmed, + "reorged" => JournalState::Reorged, + "dropped" => JournalState::Dropped, + _ => JournalState::Pending, + }; + next.reconciliation = Some(reconciliation.doc.clone()); + next.updated_at = reconciliation.observed; + terminal_try!( + cas_journal( + &mut self.host, + &mut next, + &mut events, + &mut budget, + self.policy.limits.max_journal_bytes, + EconomicRollingReservationUpdate::Retain, + ), + Some(&broadcast), + Some(&reconciliation) + ); + let terminal = Terminal { + status: match reconciliation.status { + "confirmed" => EconomicRunStatus::Confirmed, + "reorged" => EconomicRunStatus::Reorged, + "dropped" => EconomicRunStatus::Dropped, + _ => EconomicRunStatus::Pending, + }, + transaction_id: Some(broadcast.transaction_id.clone()), + confirmation: Some(reconciliation.status.to_owned()), + code: None, + message: None, + }; + finish_run( + &economic_run_id, + binding, + &self.policy, + &intent, + None, + None, + None, + None, + &next, + Some(&broadcast), + Some(&reconciliation), + &mut events, + terminal, + &mut budget, + started, + ) + } + + /// Reconciles an idempotency binding using the same sealed Agent source. + pub fn reconcile( + &mut self, + idempotency_key: &str, + source: &AgentRun, + ) -> Result> { + if !identifier(idempotency_key) { + return Err(vec![g215()]); + } + let binding = source.economic_binding(); + if binding.status != AgentRunStatus::Completed { + return Err(vec![g212("agent run not completed")]); + } + let Some(message) = binding.final_message else { + return Err(vec![g212("agent run not completed")]); + }; + let started = self.host.boundary_probe().elapsed_ms(); + let limit = self.policy.limits.max_builder_bytes as usize; + let (result, overflowed, _) = with_limit_usage(limit, || { + if !reserve_active(self.retained_policy_bytes) { + return Err(g216("builder_bytes", self.policy.limits.max_builder_bytes)); + } + if !reserve_active(message.len().saturating_mul(MAX_JSON_DEPTH + 2)) { + return Err(g216("builder_bytes", self.policy.limits.max_builder_bytes)); + } + let intent = parse_intent(message).and_then(|intent| { + admit_intent(&self.policy, &intent)?; + Ok(intent) + })?; + if intent.idempotency_key != idempotency_key { + return Err(g215()); + } + self.terminal_floor()?; + self.reconcile_bounded(&binding, intent, started) + }); + if overflowed { + return Err(vec![g216( + "builder_bytes", + self.policy.limits.max_builder_bytes, + )]); + } + result.map_err(|diagnostic| vec![diagnostic]) + } + + fn reconcile_bounded( + &mut self, + binding: &crate::agent_runtime::EconomicAgentBinding<'_>, + intent: Intent, + started: u64, + ) -> Result { + let economic_run_id = run_id( + binding.evidence_digest, + &self.policy.digest, + &intent.digest, + &intent.idempotency_key, + ); + let mut journal = Journal { + idempotency_key: intent.idempotency_key.clone(), + version: 0, + policy: Doc { + source: self.policy.source.clone(), + digest: self.policy.digest.clone(), + }, + intent: Doc { + source: intent.source.clone(), + digest: intent.digest.clone(), + }, + run_id: economic_run_id.clone(), + state: JournalState::Failed, + reserved_amount: intent.amount(), + reserved_fee: intent.max_fee(), + plan: None, + simulation: None, + approval: None, + unsigned: None, + signed: None, + broadcast: None, + reconciliation: None, + updated_at: intent.created_at, + }; + let mut events = Vec::new(); + push_event( + &mut events, + event( + "run_started", + None, + Some(binding.evidence_digest.to_owned()), + Some(self.policy.digest.clone()), + "started", + Usage::default(), + )?, + )?; + let mut budget = Budget { + policy_bytes: self.policy.source.len() as u64, + intent_bytes: intent.source.len() as u64, + recipients: self + .policy + .networks + .iter() + .map(|row| row.recipients.len() as u64) + .sum(), + network_policies: self.policy.networks.len() as u64, + x402_origins: self.policy.origins.len() as u64, + concurrency: 1, + ..Budget::default() + }; + self.pre_call(started, self.policy.limits.max_journal_bytes as usize)?; + let mut sink = EconomicDocumentSink::new( + self.policy.limits.max_journal_bytes as usize, + self.cancellation.clone(), + self.host.boundary_probe(), + started, + self.policy.limits.max_elapsed_ms, + self.policy.limits.max_builder_bytes, + self.terminal_floor()?, + ); + let load = self.host.load(&intent.idempotency_key, &mut sink); + if load != EconomicJournalLoad::Present { + push_event( + &mut events, + event( + "journal_loaded", + None, + None, + None, + "failed", + Usage { + journal_reads: 1, + ..Usage::default() + }, + )?, + )?; + return self.finish_failure( + binding, + &intent, + &economic_run_id, + &journal, + None, + None, + None, + None, + None, + None, + &mut events, + &mut budget, + info("SPX-I222", "Economic Agent journal adapter failed"), + started, + ); + } + let source = match sink.finish("journal_bytes") { + Ok(source) => source, + Err(diagnostic) => { + push_event( + &mut events, + event( + "journal_loaded", + None, + None, + None, + "failed", + Usage { + journal_reads: 1, + ..Usage::default() + }, + )?, + )?; + return self.finish_failure( + binding, + &intent, + &economic_run_id, + &journal, + None, + None, + None, + None, + None, + None, + &mut events, + &mut budget, + diagnostic, + started, + ); + } + }; + push_event( + &mut events, + event( + "journal_loaded", + None, + None, + Some(digest(JOURNAL_DOMAIN, source.as_bytes())), + "present", + Usage { + journal_reads: 1, + output_bytes: source.len() as u64, + ..Usage::default() + }, + )?, + )?; + journal = match parse_journal_classified(&source, &self.policy, &intent, &economic_run_id) { + Ok(journal) => journal, + Err(JournalParseFailure::BindingMismatch) => return Err(g215()), + Err(JournalParseFailure::Diagnostic(diagnostic)) => { + return self.finish_failure( + binding, + &intent, + &economic_run_id, + &journal, + None, + None, + None, + None, + None, + None, + &mut events, + &mut budget, + diagnostic, + started, + ); + } + }; + budget.journal_bytes = source.len() as u64; + self.resume_loaded( + binding, + intent, + economic_run_id, + journal, + events, + budget, + started, + ) + } +} + +fn terminal_floor(limits: &Limits) -> Result { + usize::try_from(limits.max_trace_bytes) + .ok() + .and_then(|trace| { + usize::try_from(limits.max_evidence_bytes) + .ok() + .and_then(|evidence| evidence.checked_mul(2)) + .and_then(|evidence| trace.checked_add(evidence)) + }) + .and_then(|value| value.checked_add(4096)) + .ok_or_else(|| g216("builder_bytes", limits.max_builder_bytes)) +} + +fn digest(domain: &[u8], bytes: &[u8]) -> String { + let mut hash = Sha256::new(); + hash.update(domain); + hash.update(bytes); + format!("sha256:{:x}", hash.finalize()) +} + +fn admitted_now_from(intent: &Intent, observed: u64, elapsed: u64) -> Result { + intent + .created_at + .max(observed) + .checked_add(elapsed) + .ok_or_else(|| g212("expired")) +} + +fn confirmation_target(intent: &Intent) -> u64 { + match &intent.payment { + Payment::Bitcoin { confirmations, .. } => *confirmations, + Payment::X402 { + rail: EconomicRail::Bitcoin, + .. + } => 1, + _ => 0, + } +} + +fn validate_confirmation( + intent: &Intent, + reconciliation: &Reconciliation, +) -> Result<(), Diagnostic> { + let target = confirmation_target(intent); + if reconciliation.status == "confirmed" && reconciliation.confirmations.unwrap_or(0) < target { + return Err(g213()); + } + Ok(()) +} + +fn g210(document: &str, schema: &str) -> Diagnostic { + Diagnostic::io( + "SPX-G210", + format!("Economic Agent {document} is not canonical {schema} JSON"), + ) +} +fn g211(field: &str) -> Diagnostic { + Diagnostic::io( + "SPX-G211", + format!("Economic Agent policy invariant failed: {field}"), + ) +} +fn g212(reason: &'static str) -> Diagnostic { + Diagnostic::io( + "SPX-G212", + format!("Economic Agent payment intent was rejected: {reason}"), + ) +} +fn g213() -> Diagnostic { + Diagnostic::io( + "SPX-G213", + "Economic Agent prepared transaction or simulation disagrees with the admitted intent", + ) +} +fn g214() -> Diagnostic { + Diagnostic::io( + "SPX-G214", + "Economic Agent approval is absent, expired, rejected, or digest-mismatched", + ) +} +fn g215() -> Diagnostic { + Diagnostic::io( + "SPX-G215", + "Economic Agent journal state or idempotency replay disagrees with the admitted operation", + ) +} +fn g216(field: &str, maximum: u64) -> Diagnostic { + Diagnostic::io("SPX-G216", format!("{field} exceeds {maximum}")) +} +fn g217() -> Diagnostic { + Diagnostic::io( + "SPX-G217", + "Economic Agent Trace or Evidence disagrees with the replayed state machine", + ) +} +fn info(code: &'static str, message: &'static str) -> Diagnostic { + Diagnostic::io(code, message) +} + +fn canonical<'a>( + source: &'a str, + document: &str, + schema: &str, + maximum: usize, +) -> Result<(&'a str, Value), Diagnostic> { + if source.len() > maximum { + return Err(g216(document_bytes_field(document), maximum as u64)); + } + let Some(body) = source.strip_suffix('\n') else { + return Err(g210(document, schema)); + }; + if body.is_empty() || body.contains('\n') || body.contains('\r') || body.starts_with('\u{feff}') + { + return Err(g210(document, schema)); + } + let value: Value = serde_json::from_str(body).map_err(|_| g210(document, schema))?; + if depth(&value) > MAX_JSON_DEPTH { + return Err(g216("json_depth", MAX_JSON_DEPTH as u64)); + } + if value + .as_object() + .and_then(|row| row.get("schema")) + .and_then(Value::as_str) + != Some(schema) + { + return Err(g210(document, schema)); + } + Ok((body, value)) +} +fn canonical_policy_limited<'a>( + source: &'a str, + document: &str, + schema: &str, + maximum: u64, + max_depth: u64, +) -> Result<(&'a str, Value), Diagnostic> { + let (body, value) = canonical(source, document, schema, maximum as usize)?; + if depth(&value) as u64 > max_depth { + return Err(g216("json_depth", max_depth)); + } + Ok((body, value)) +} +fn configured_depth(source: &str, limits: &Limits) -> Result<(), Diagnostic> { + if structural_json_depth(source).ok_or_else(g217)? > limits.max_json_depth { + return Err(g216("json_depth", limits.max_json_depth)); + } + Ok(()) +} + +fn structural_json_depth(source: &str) -> Option { + let mut depth = 0_u64; + let mut maximum = 0_u64; + let mut quoted = false; + let mut escaped = false; + for byte in source.bytes() { + if quoted { + if escaped { + escaped = false; + } else if byte == b'\\' { + escaped = true; + } else if byte == b'"' { + quoted = false; + } + continue; + } + match byte { + b'"' => quoted = true, + b'{' | b'[' => { + depth = depth.checked_add(1)?; + maximum = maximum.max(depth); + } + b'}' | b']' => depth = depth.checked_sub(1)?, + _ => {} + } + } + (!quoted && !escaped && depth == 0).then(|| { + if source + .bytes() + .any(|byte| !byte.is_ascii_whitespace() && !matches!(byte, b'{' | b'}' | b'[' | b']')) + { + maximum.saturating_add(1) + } else { + maximum + } + }) +} + +fn configured_document_limits( + source: &str, + document: &str, + maximum: u64, + limits: &Limits, +) -> Result<(), Diagnostic> { + if source.len() > maximum as usize { + return Err(g216(document_bytes_field(document), maximum)); + } + configured_depth(source, limits) +} + +fn document_bytes_field(document: &str) -> &'static str { + match document { + "policy" => "policy_bytes", + "payment intent" => "intent_bytes", + "x402 invoice" => "invoice_bytes", + "chain snapshot" => "snapshot_bytes", + "payment plan" => "plan_bytes", + "simulation" => "simulation_bytes", + "approval request" => "approval_request_bytes", + "approval" => "approval_bytes", + "journal" => "journal_bytes", + "broadcast receipt" => "broadcast_receipt_bytes", + "reconciliation" => "reconciliation_bytes", + "trace" => "trace_bytes", + "evidence" => "evidence_bytes", + _ => "builder_bytes", + } +} + +fn depth(value: &Value) -> usize { + match value { + Value::Array(v) => 1 + v.iter().map(depth).max().unwrap_or(0), + Value::Object(v) => 1 + v.values().map(depth).max().unwrap_or(0), + _ => 1, + } +} +fn object<'a>( + value: &'a Value, + doc: &str, + schema: &str, +) -> Result<&'a Map, Diagnostic> { + value.as_object().ok_or_else(|| g210(doc, schema)) +} +fn keys(row: &Map, expected: &[&str]) -> bool { + row.len() == expected.len() && expected.iter().all(|key| row.contains_key(*key)) +} +fn text<'a>( + row: &'a Map, + key: &str, + doc: &str, + schema: &str, +) -> Result<&'a str, Diagnostic> { + row.get(key) + .and_then(Value::as_str) + .ok_or_else(|| g210(doc, schema)) +} +fn number(row: &Map, key: &str, doc: &str, schema: &str) -> Result { + row.get(key) + .and_then(Value::as_u64) + .ok_or_else(|| g210(doc, schema)) +} +fn policy_limit( + row: &Map, + key: &str, + maximum: u64, + nonzero: bool, +) -> Result { + let value = number(row, key, "policy", POLICY_SCHEMA)?; + if value > maximum || (nonzero && value == 0) { + return Err(g211(&format!("limits.{key}"))); + } + Ok(value) +} +fn identifier(value: &str) -> bool { + !value.is_empty() + && value.len() <= MAX_IDENTIFIER_BYTES + && value.bytes().all(|b| { + b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'.' | b'_' | b':' | b'-') + }) +} +fn sorted_unique(values: &[String]) -> bool { + values.windows(2).all(|w| w[0] < w[1]) +} +fn string_array(value: &Value) -> Option> { + value + .as_array()? + .iter() + .map(|v| v.as_str().map(str::to_owned)) + .collect() +} +fn string_list(values: &[String]) -> String { + format!( + "[{}]", + values + .iter() + .map(|v| quote_json(v)) + .collect::>() + .join(",") + ) +} +fn nonclaims_json() -> String { + format!( + "[{}]", + NONCLAIMS + .iter() + .map(|v| quote_json(v)) + .collect::>() + .join(",") + ) +} + +fn rail(value: &str) -> Option { + match value { + "evm" => Some(EconomicRail::Evm), + "solana" => Some(EconomicRail::Solana), + "bitcoin" => Some(EconomicRail::Bitcoin), + _ => None, + } +} + +fn limits_json(limits: &Limits) -> String { + let mut output = String::new(); + write_limits(&mut output, limits).expect("String writes cannot fail"); + output +} +fn write_limits(output: &mut W, limits: &Limits) -> fmt::Result { + write!(output,"{{\"max_policy_bytes\":{},\"max_intent_bytes\":{},\"max_invoice_bytes\":{},\"max_snapshot_bytes\":{},\"max_plan_bytes\":{},\"max_simulation_bytes\":{},\"max_approval_request_bytes\":{},\"max_approval_bytes\":{},\"max_journal_bytes\":{},\"max_unsigned_transaction_bytes\":{},\"max_signed_transaction_bytes\":{},\"max_broadcast_receipt_bytes\":{},\"max_reconciliation_bytes\":{},\"max_trace_events\":{},\"max_trace_bytes\":{},\"max_evidence_bytes\":{},\"max_builder_bytes\":{},\"max_json_depth\":{},\"max_identifier_bytes\":{},\"max_memo_bytes\":{},\"max_recipients\":{},\"max_network_policies\":{},\"max_x402_origins\":{},\"max_utxos\":{},\"max_reconciliations\":{},\"max_elapsed_ms\":{},\"max_amount_atomic\":{},\"max_fee_atomic\":{},\"max_compute_units\":{},\"max_confirmation_target\":{},\"max_concurrency\":{},\"max_unexpected_authority_calls\":{}}}",limits.max_policy_bytes,limits.max_intent_bytes,limits.max_invoice_bytes,limits.max_snapshot_bytes,limits.max_plan_bytes,limits.max_simulation_bytes,limits.max_approval_request_bytes,limits.max_approval_bytes,limits.max_journal_bytes,limits.max_unsigned_transaction_bytes,limits.max_signed_transaction_bytes,limits.max_broadcast_receipt_bytes,limits.max_reconciliation_bytes,limits.max_trace_events,limits.max_trace_bytes,limits.max_evidence_bytes,limits.max_builder_bytes,limits.max_json_depth,limits.max_identifier_bytes,limits.max_memo_bytes,limits.max_recipients,limits.max_network_policies,limits.max_x402_origins,limits.max_utxos,limits.max_reconciliations,limits.max_elapsed_ms,limits.max_amount_atomic,limits.max_fee_atomic,limits.max_compute_units,limits.max_confirmation_target,limits.max_concurrency,limits.max_unexpected_authority_calls) +} + +fn render_policy(policy: &Policy) -> String { + let mut networks = String::from("["); + for (index, row) in policy.networks.iter().enumerate() { + if index > 0 { + networks.push(','); + } + networks.push_str(&format!("{{\"rail\":{},\"network\":{},\"asset\":{},\"recipients\":{},\"max_amount_atomic\":{},\"max_fee_atomic\":{},\"max_rolling_24h_atomic\":{}}}",quote_json(row.rail.text()),quote_json(&row.network),quote_json(&row.asset),string_list(&row.recipients),row.max_amount,row.max_fee,row.max_rolling)); + } + networks.push(']'); + let mut origins = String::from("["); + for (index, row) in policy.origins.iter().enumerate() { + if index > 0 { + origins.push(','); + } + origins.push_str(&format!("{{\"origin\":{},\"methods\":{},\"resources\":{},\"settlement_rails\":{},\"max_amount_atomic\":{}}}",quote_json(&row.origin),string_list(&row.methods),string_list(&row.resources),string_list(&row.rails.iter().map(|r|r.text().to_owned()).collect::>()),row.max_amount)); + } + origins.push(']'); + format!("{{\"schema\":\"{POLICY_SCHEMA}\",\"economic_agent_id\":{},\"wallet_id\":{},\"network_policies\":{networks},\"x402_origins\":{origins},\"limits\":{},\"nonclaims\":{}}}\n",quote_json(&policy.economic_agent_id),quote_json(&policy.wallet_id),limits_json(&policy.limits),nonclaims_json()) +} + +fn parse_policy(source: &str) -> Result { + let (_, value) = canonical(source, "policy", POLICY_SCHEMA, MAX_POLICY_BYTES)?; + let top = object(&value, "policy", POLICY_SCHEMA)?; + if !keys( + top, + &[ + "schema", + "economic_agent_id", + "wallet_id", + "network_policies", + "x402_origins", + "limits", + "nonclaims", + ], + ) { + return Err(g210("policy", POLICY_SCHEMA)); + } + let economic_agent_id = text(top, "economic_agent_id", "policy", POLICY_SCHEMA)?.to_owned(); + let wallet_id = text(top, "wallet_id", "policy", POLICY_SCHEMA)?.to_owned(); + if !identifier(&economic_agent_id) || !identifier(&wallet_id) { + return Err(g211("identifiers")); + } + let rows = top["network_policies"] + .as_array() + .ok_or_else(|| g210("policy", POLICY_SCHEMA))?; + if rows.is_empty() || rows.len() > MAX_NETWORK_POLICIES { + return Err(g211("network_policies")); + } + let mut networks = Vec::new(); + for value in rows { + let row = object(value, "policy", POLICY_SCHEMA)?; + if !keys( + row, + &[ + "rail", + "network", + "asset", + "recipients", + "max_amount_atomic", + "max_fee_atomic", + "max_rolling_24h_atomic", + ], + ) { + return Err(g210("policy", POLICY_SCHEMA)); + } + let rail = rail(text(row, "rail", "policy", POLICY_SCHEMA)?) + .ok_or_else(|| g211("network_policies.rail"))?; + let network = text(row, "network", "policy", POLICY_SCHEMA)?.to_owned(); + let asset = text(row, "asset", "policy", POLICY_SCHEMA)?.to_owned(); + if (rail, network.as_str(), asset.as_str()) != (EconomicRail::Evm, "sepolia", "native:eth") + && (rail, network.as_str(), asset.as_str()) + != (EconomicRail::Solana, "devnet", "native:sol") + && (rail, network.as_str(), asset.as_str()) + != (EconomicRail::Bitcoin, "regtest", "native:btc") + { + return Err(g211("network_policies.network")); + } + let recipients = + string_array(&row["recipients"]).ok_or_else(|| g210("policy", POLICY_SCHEMA))?; + if recipients.is_empty() + || recipients.len() > MAX_RECIPIENTS + || !sorted_unique(&recipients) + || recipients.iter().any(|v| !valid_recipient(rail, v)) + { + return Err(g211("network_policies.recipients")); + } + let max_amount = number(row, "max_amount_atomic", "policy", POLICY_SCHEMA)?; + let max_fee = number(row, "max_fee_atomic", "policy", POLICY_SCHEMA)?; + let max_rolling = number(row, "max_rolling_24h_atomic", "policy", POLICY_SCHEMA)?; + if max_amount == 0 + || max_amount > 1_000_000_000_000_000_000 + || max_fee > 1_000_000_000_000_000 + || max_rolling < max_amount + { + return Err(g211("network_policies.limits")); + } + networks.push(NetworkPolicy { + rail, + network, + asset, + recipients, + max_amount, + max_fee, + max_rolling, + }); + } + if !networks.windows(2).all(|w| { + (w[0].rail.text(), w[0].network.as_str(), w[0].asset.as_str()) + < (w[1].rail.text(), w[1].network.as_str(), w[1].asset.as_str()) + }) { + return Err(g211("network_policies.order")); + } + let origin_rows = top["x402_origins"] + .as_array() + .ok_or_else(|| g210("policy", POLICY_SCHEMA))?; + if origin_rows.len() > MAX_X402_ORIGINS { + return Err(g211("x402_origins")); + } + let mut origins = Vec::new(); + for value in origin_rows { + let row = object(value, "policy", POLICY_SCHEMA)?; + if !keys( + row, + &[ + "origin", + "methods", + "resources", + "settlement_rails", + "max_amount_atomic", + ], + ) { + return Err(g210("policy", POLICY_SCHEMA)); + } + let origin = text(row, "origin", "policy", POLICY_SCHEMA)?.to_owned(); + if !valid_origin(&origin) { + return Err(g211("x402_origins.origin")); + } + let methods = string_array(&row["methods"]).ok_or_else(|| g210("policy", POLICY_SCHEMA))?; + let resources = + string_array(&row["resources"]).ok_or_else(|| g210("policy", POLICY_SCHEMA))?; + let rail_text = + string_array(&row["settlement_rails"]).ok_or_else(|| g210("policy", POLICY_SCHEMA))?; + let rails: Vec<_> = rail_text + .iter() + .map(|v| rail(v).ok_or_else(|| g211("x402_origins.settlement_rails"))) + .collect::>()?; + let max_amount = number(row, "max_amount_atomic", "policy", POLICY_SCHEMA)?; + if methods.is_empty() + || !sorted_unique(&methods) + || methods.iter().any(|v| v != "GET" && v != "POST") + || resources.is_empty() + || !sorted_unique(&resources) + || resources.iter().any(|v| !valid_resource(v)) + || rails.is_empty() + || !rails.windows(2).all(|w| w[0].text() < w[1].text()) + || max_amount == 0 + || max_amount > 1_000_000_000_000_000_000 + { + return Err(g211("x402_origins")); + } + origins.push(OriginPolicy { + origin, + methods, + resources, + rails, + max_amount, + }); + } + if !origins.windows(2).all(|w| w[0].origin < w[1].origin) { + return Err(g211("x402_origins.order")); + } + let limits = top["limits"] + .as_object() + .ok_or_else(|| g210("policy", POLICY_SCHEMA))?; + let expected_limit_keys = [ + "max_policy_bytes", + "max_intent_bytes", + "max_invoice_bytes", + "max_snapshot_bytes", + "max_plan_bytes", + "max_simulation_bytes", + "max_approval_request_bytes", + "max_approval_bytes", + "max_journal_bytes", + "max_unsigned_transaction_bytes", + "max_signed_transaction_bytes", + "max_broadcast_receipt_bytes", + "max_reconciliation_bytes", + "max_trace_events", + "max_trace_bytes", + "max_evidence_bytes", + "max_builder_bytes", + "max_json_depth", + "max_identifier_bytes", + "max_memo_bytes", + "max_recipients", + "max_network_policies", + "max_x402_origins", + "max_utxos", + "max_reconciliations", + "max_elapsed_ms", + "max_amount_atomic", + "max_fee_atomic", + "max_compute_units", + "max_confirmation_target", + "max_concurrency", + "max_unexpected_authority_calls", + ]; + if !keys(limits, &expected_limit_keys) { + return Err(g211("limits")); + } + let limits = Limits { + max_policy_bytes: policy_limit(limits, "max_policy_bytes", MAX_POLICY_BYTES as u64, true)?, + max_intent_bytes: policy_limit(limits, "max_intent_bytes", MAX_INTENT_BYTES as u64, true)?, + max_invoice_bytes: policy_limit( + limits, + "max_invoice_bytes", + MAX_INVOICE_BYTES as u64, + true, + )?, + max_snapshot_bytes: policy_limit( + limits, + "max_snapshot_bytes", + MAX_SNAPSHOT_BYTES as u64, + true, + )?, + max_plan_bytes: policy_limit(limits, "max_plan_bytes", MAX_PLAN_BYTES as u64, true)?, + max_simulation_bytes: policy_limit( + limits, + "max_simulation_bytes", + MAX_SIMULATION_BYTES as u64, + true, + )?, + max_approval_request_bytes: policy_limit( + limits, + "max_approval_request_bytes", + MAX_APPROVAL_REQUEST_BYTES as u64, + true, + )?, + max_approval_bytes: policy_limit( + limits, + "max_approval_bytes", + MAX_APPROVAL_BYTES as u64, + true, + )?, + max_journal_bytes: policy_limit( + limits, + "max_journal_bytes", + MAX_JOURNAL_BYTES as u64, + true, + )?, + max_unsigned_transaction_bytes: policy_limit( + limits, + "max_unsigned_transaction_bytes", + MAX_UNSIGNED_BYTES as u64, + true, + )?, + max_signed_transaction_bytes: policy_limit( + limits, + "max_signed_transaction_bytes", + MAX_SIGNED_BYTES as u64, + true, + )?, + max_broadcast_receipt_bytes: policy_limit( + limits, + "max_broadcast_receipt_bytes", + MAX_BROADCAST_BYTES as u64, + true, + )?, + max_reconciliation_bytes: policy_limit( + limits, + "max_reconciliation_bytes", + MAX_RECONCILIATION_BYTES as u64, + true, + )?, + max_trace_events: policy_limit(limits, "max_trace_events", MAX_TRACE_EVENTS as u64, true)?, + max_trace_bytes: policy_limit(limits, "max_trace_bytes", MAX_TRACE_BYTES as u64, true)?, + max_evidence_bytes: policy_limit( + limits, + "max_evidence_bytes", + MAX_EVIDENCE_BYTES as u64, + true, + )?, + max_builder_bytes: policy_limit( + limits, + "max_builder_bytes", + MAX_BUILDER_BYTES as u64, + true, + )?, + max_json_depth: policy_limit(limits, "max_json_depth", MAX_JSON_DEPTH as u64, true)?, + max_identifier_bytes: policy_limit( + limits, + "max_identifier_bytes", + MAX_IDENTIFIER_BYTES as u64, + true, + )?, + max_memo_bytes: policy_limit(limits, "max_memo_bytes", MAX_MEMO_BYTES as u64, true)?, + max_recipients: policy_limit(limits, "max_recipients", MAX_RECIPIENTS as u64, true)?, + max_network_policies: policy_limit( + limits, + "max_network_policies", + MAX_NETWORK_POLICIES as u64, + true, + )?, + max_x402_origins: policy_limit(limits, "max_x402_origins", MAX_X402_ORIGINS as u64, false)?, + max_utxos: policy_limit(limits, "max_utxos", MAX_UTXOS as u64, true)?, + max_reconciliations: policy_limit(limits, "max_reconciliations", 64, true)?, + max_elapsed_ms: policy_limit(limits, "max_elapsed_ms", 600_000, true)?, + max_amount_atomic: policy_limit( + limits, + "max_amount_atomic", + 1_000_000_000_000_000_000, + true, + )?, + max_fee_atomic: policy_limit(limits, "max_fee_atomic", 1_000_000_000_000_000, true)?, + max_compute_units: policy_limit(limits, "max_compute_units", 200_000, true)?, + max_confirmation_target: policy_limit(limits, "max_confirmation_target", 144, true)?, + max_concurrency: policy_limit(limits, "max_concurrency", 1, true)?, + max_unexpected_authority_calls: policy_limit( + limits, + "max_unexpected_authority_calls", + 0, + false, + )?, + }; + if limits.max_concurrency != 1 || limits.max_unexpected_authority_calls != 0 { + return Err(g211("limits")); + } + if economic_agent_id.len() > limits.max_identifier_bytes as usize + || wallet_id.len() > limits.max_identifier_bytes as usize + { + return Err(g211("limits.max_identifier_bytes")); + } + if networks.len() > limits.max_network_policies as usize + || origins.len() > limits.max_x402_origins as usize + || networks.iter().any(|network| { + network.recipients.len() > limits.max_recipients as usize + || network.max_amount > limits.max_amount_atomic + || network.max_fee > limits.max_fee_atomic + || network.max_rolling > limits.max_amount_atomic + }) + || origins + .iter() + .any(|origin| origin.max_amount > limits.max_amount_atomic) + { + return Err(g211("limits")); + } + let claims = string_array(&top["nonclaims"]).ok_or_else(|| g210("policy", POLICY_SCHEMA))?; + if claims + != NONCLAIMS + .iter() + .map(|v| (*v).to_owned()) + .collect::>() + { + return Err(g211("nonclaims")); + } + let mut policy = Policy { + economic_agent_id, + wallet_id, + networks, + origins, + limits, + source: source.to_owned(), + digest: digest(POLICY_DOMAIN, source.as_bytes()), + }; + if render_policy(&policy) != source { + return Err(g210("policy", POLICY_SCHEMA)); + } + if source.len() > policy.limits.max_policy_bytes as usize { + return Err(g216("policy_bytes", policy.limits.max_policy_bytes)); + } + let policy_value: Value = + serde_json::from_str(source.trim_end()).map_err(|_| g210("policy", POLICY_SCHEMA))?; + if depth(&policy_value) as u64 > policy.limits.max_json_depth { + return Err(g216("json_depth", policy.limits.max_json_depth)); + } + policy.source = source.to_owned(); + Ok(policy) +} + +fn valid_recipient(rail: EconomicRail, value: &str) -> bool { + match rail { + EconomicRail::Evm => { + value.len() == 42 + && value.starts_with("0x") + && value[2..] + .bytes() + .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) + } + EconomicRail::Solana => decode_base58_32(value).is_some(), + EconomicRail::Bitcoin => decode_regtest_p2wpkh(value).is_some(), + } +} +fn valid_origin(value: &str) -> bool { + let Some(host) = value.strip_prefix("https://") else { + return false; + }; + !host.is_empty() + && !host.contains(['/', ':', '@', '#', '?', '[', ']']) + && host.parse::().is_err() + && host != "localhost" + && !host.ends_with(".localhost") + && !host.ends_with(".local") + && host.bytes().all(|byte| { + byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'.' | b'-') + }) + && host + .split('.') + .all(|part| !part.is_empty() && !part.starts_with('-') && !part.ends_with('-')) +} + +fn valid_resource(value: &str) -> bool { + if !value.starts_with('/') || value.starts_with("//") || value.contains(['?', '#', '\\']) { + return false; + } + if value + .split('/') + .any(|segment| matches!(segment, "." | "..")) + { + return false; + } + let bytes = value.as_bytes(); + let mut index = 0; + while index < bytes.len() { + if bytes[index] != b'%' { + index += 1; + continue; + } + let Some(pair) = bytes.get(index + 1..index + 3) else { + return false; + }; + let Some(high) = (pair[0] as char).to_digit(16) else { + return false; + }; + let Some(low) = (pair[1] as char).to_digit(16) else { + return false; + }; + if matches!(((high << 4) | low) as u8, b'.' | b'/' | b'\\') { + return false; + } + index += 3; + } + true +} + +const BASE58_ALPHABET: &[u8] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + +fn encode_base58(bytes: &[u8]) -> String { + if bytes.is_empty() { + return String::new(); + } + let zeros = bytes.iter().take_while(|byte| **byte == 0).count(); + let mut digits = Vec::new(); + for byte in bytes.iter().skip(zeros) { + let mut carry = u32::from(*byte); + for digit in &mut digits { + let value = u32::from(*digit) * 256 + carry; + *digit = (value % 58) as u8; + carry = value / 58; + } + while carry != 0 { + digits.push((carry % 58) as u8); + carry /= 58; + } + } + let mut encoded = String::with_capacity(zeros + digits.len()); + encoded.extend(std::iter::repeat_n('1', zeros)); + for digit in digits.iter().rev() { + encoded.push(BASE58_ALPHABET[usize::from(*digit)] as char); + } + encoded +} + +fn decode_base58_32(value: &str) -> Option<[u8; 32]> { + if value.is_empty() { + return None; + } + let mut output = [0u8; 32]; + for byte in value.bytes() { + let digit = BASE58_ALPHABET + .iter() + .position(|candidate| *candidate == byte)? as u32; + let mut carry = digit; + for slot in output.iter_mut().rev() { + let expanded = u32::from(*slot) * 58 + carry; + *slot = expanded as u8; + carry = expanded >> 8; + } + if carry != 0 { + return None; + } + } + (encode_base58(&output) == value).then_some(output) +} +fn decode_regtest_p2wpkh(value: &str) -> Option> { + if value.to_ascii_lowercase() != value || !value.starts_with("bcrt1q") { + return None; + } + let position = value.rfind('1')?; + let hrp = &value[..position]; + if hrp != "bcrt" { + return None; + } + let charset = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + let data: Vec = value[position + 1..] + .bytes() + .map(|b| charset.iter().position(|v| *v == b).map(|n| n as u8)) + .collect::>()?; + if data.len() < 7 || !bech32_verify(hrp, &data) { + return None; + } + let payload = &data[..data.len() - 6]; + if payload.first() != Some(&0) { + return None; + } + let program = convert_bits(&payload[1..], 5, 8, false)?; + if program.len() != 20 { + return None; + } + let mut script = vec![0x00, 0x14]; + script.extend(program); + Some(script) +} +fn bech32_verify(hrp: &str, data: &[u8]) -> bool { + let mut values = Vec::new(); + for b in hrp.bytes() { + values.push(b >> 5); + } + values.push(0); + for b in hrp.bytes() { + values.push(b & 31); + } + values.extend_from_slice(data); + let mut chk = 1u32; + for v in values { + let top = chk >> 25; + chk = ((chk & 0x1ffffff) << 5) ^ u32::from(v); + for (index, g) in [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] + .iter() + .enumerate() + { + if ((top >> index) & 1) != 0 { + chk ^= *g; + } + } + } + chk == 1 +} +fn convert_bits(data: &[u8], from: u32, to: u32, pad: bool) -> Option> { + let mut acc = 0u32; + let mut bits = 0u32; + let maxv = (1u32 << to) - 1; + let mut out = Vec::new(); + for value in data { + if (u32::from(*value) >> from) != 0 { + return None; + } + acc = (acc << from) | u32::from(*value); + bits += from; + while bits >= to { + bits -= to; + out.push(((acc >> bits) & maxv) as u8); + } + } + if pad { + if bits > 0 { + out.push(((acc << (to - bits)) & maxv) as u8); + } + } else if bits >= from || ((acc << (to - bits)) & maxv) != 0 { + return None; + } + Some(out) +} + +fn render_intent(intent: &Intent) -> String { + let memo = intent + .memo + .as_ref() + .map_or_else(|| "null".to_owned(), |v| quote_json(v)); + let payment=match &intent.payment{ + Payment::Evm{recipient,amount,max_fee}=>format!("{{\"kind\":\"evm\",\"network\":\"sepolia\",\"asset\":\"native:eth\",\"recipient\":{},\"amount_atomic\":{amount},\"max_fee_atomic\":{max_fee}}}",quote_json(recipient)), + Payment::Solana{recipient,amount,max_fee,compute,priority}=>format!("{{\"kind\":\"solana\",\"network\":\"devnet\",\"asset\":\"native:sol\",\"recipient\":{},\"amount_atomic\":{amount},\"max_fee_atomic\":{max_fee},\"max_compute_units\":{compute},\"max_priority_fee_atomic\":{priority}}}",quote_json(recipient)), + Payment::Bitcoin{recipient,amount,max_fee,confirmations}=>format!("{{\"kind\":\"bitcoin\",\"network\":\"regtest\",\"asset\":\"native:btc\",\"recipient\":{},\"amount_atomic\":{amount},\"max_fee_atomic\":{max_fee},\"confirmation_target\":{confirmations}}}",quote_json(recipient)), + Payment::X402{origin,method,resource,invoice_digest,payee,rail,network,asset,amount,max_fee,invoice_expires,nonce}=>format!("{{\"kind\":\"x402\",\"origin\":{},\"method\":{},\"resource\":{},\"invoice_digest\":{},\"payee\":{},\"settlement_rail\":{},\"network\":{},\"asset\":{},\"amount_atomic\":{amount},\"max_fee_atomic\":{max_fee},\"invoice_expires_at_ms\":{invoice_expires},\"invoice_nonce\":{}}}",quote_json(origin),quote_json(method),quote_json(resource),quote_json(invoice_digest),quote_json(payee),quote_json(rail.text()),quote_json(network),quote_json(asset),quote_json(nonce)), + }; + format!("{{\"schema\":\"{INTENT_SCHEMA}\",\"intent_id\":{},\"wallet_id\":{},\"rail\":{},\"idempotency_key\":{},\"created_at_ms\":{},\"expires_at_ms\":{},\"memo\":{memo},\"payment\":{payment}}}\n",quote_json(&intent.intent_id),quote_json(&intent.wallet_id),quote_json(&intent.rail_text),quote_json(&intent.idempotency_key),intent.created_at,intent.expires_at) +} + +fn parse_intent(source: &str) -> Result { + let (_, value) = canonical(source, "payment intent", INTENT_SCHEMA, MAX_INTENT_BYTES)?; + let top = object(&value, "payment intent", INTENT_SCHEMA)?; + if !keys( + top, + &[ + "schema", + "intent_id", + "wallet_id", + "rail", + "idempotency_key", + "created_at_ms", + "expires_at_ms", + "memo", + "payment", + ], + ) { + return Err(g210("payment intent", INTENT_SCHEMA)); + } + let intent_id = text(top, "intent_id", "payment intent", INTENT_SCHEMA)?.to_owned(); + let wallet_id = text(top, "wallet_id", "payment intent", INTENT_SCHEMA)?.to_owned(); + let rail_text = text(top, "rail", "payment intent", INTENT_SCHEMA)?.to_owned(); + let idempotency_key = text(top, "idempotency_key", "payment intent", INTENT_SCHEMA)?.to_owned(); + if !identifier(&intent_id) || !identifier(&wallet_id) || !identifier(&idempotency_key) { + return Err(g210("payment intent", INTENT_SCHEMA)); + } + let created_at = number(top, "created_at_ms", "payment intent", INTENT_SCHEMA)?; + let expires_at = number(top, "expires_at_ms", "payment intent", INTENT_SCHEMA)?; + if expires_at <= created_at || expires_at - created_at > 600_000 { + return Err(g212("expired")); + } + let memo = if top["memo"].is_null() { + None + } else { + Some( + top["memo"] + .as_str() + .ok_or_else(|| g210("payment intent", INTENT_SCHEMA))? + .to_owned(), + ) + }; + if memo.as_ref().is_some_and(|v| v.len() > MAX_MEMO_BYTES) { + return Err(g216("memo_bytes", MAX_MEMO_BYTES as u64)); + } + let row = object(&top["payment"], "payment intent", INTENT_SCHEMA)?; + let kind = text(row, "kind", "payment intent", INTENT_SCHEMA)?; + let payment = match kind { + "evm" => { + if !keys( + row, + &[ + "kind", + "network", + "asset", + "recipient", + "amount_atomic", + "max_fee_atomic", + ], + ) || text(row, "network", "payment intent", INTENT_SCHEMA)? != "sepolia" + || text(row, "asset", "payment intent", INTENT_SCHEMA)? != "native:eth" + || rail_text != "evm" + { + return Err(g210("payment intent", INTENT_SCHEMA)); + } + Payment::Evm { + recipient: text(row, "recipient", "payment intent", INTENT_SCHEMA)?.to_owned(), + amount: number(row, "amount_atomic", "payment intent", INTENT_SCHEMA)?, + max_fee: number(row, "max_fee_atomic", "payment intent", INTENT_SCHEMA)?, + } + } + "solana" => { + if !keys( + row, + &[ + "kind", + "network", + "asset", + "recipient", + "amount_atomic", + "max_fee_atomic", + "max_compute_units", + "max_priority_fee_atomic", + ], + ) || text(row, "network", "payment intent", INTENT_SCHEMA)? != "devnet" + || text(row, "asset", "payment intent", INTENT_SCHEMA)? != "native:sol" + || rail_text != "solana" + { + return Err(g210("payment intent", INTENT_SCHEMA)); + } + Payment::Solana { + recipient: text(row, "recipient", "payment intent", INTENT_SCHEMA)?.to_owned(), + amount: number(row, "amount_atomic", "payment intent", INTENT_SCHEMA)?, + max_fee: number(row, "max_fee_atomic", "payment intent", INTENT_SCHEMA)?, + compute: number(row, "max_compute_units", "payment intent", INTENT_SCHEMA)?, + priority: number( + row, + "max_priority_fee_atomic", + "payment intent", + INTENT_SCHEMA, + )?, + } + } + "bitcoin" => { + if !keys( + row, + &[ + "kind", + "network", + "asset", + "recipient", + "amount_atomic", + "max_fee_atomic", + "confirmation_target", + ], + ) || text(row, "network", "payment intent", INTENT_SCHEMA)? != "regtest" + || text(row, "asset", "payment intent", INTENT_SCHEMA)? != "native:btc" + || rail_text != "bitcoin" + { + return Err(g210("payment intent", INTENT_SCHEMA)); + } + Payment::Bitcoin { + recipient: text(row, "recipient", "payment intent", INTENT_SCHEMA)?.to_owned(), + amount: number(row, "amount_atomic", "payment intent", INTENT_SCHEMA)?, + max_fee: number(row, "max_fee_atomic", "payment intent", INTENT_SCHEMA)?, + confirmations: number(row, "confirmation_target", "payment intent", INTENT_SCHEMA)?, + } + } + "x402" => { + if !keys( + row, + &[ + "kind", + "origin", + "method", + "resource", + "invoice_digest", + "payee", + "settlement_rail", + "network", + "asset", + "amount_atomic", + "max_fee_atomic", + "invoice_expires_at_ms", + "invoice_nonce", + ], + ) || rail_text != "x402" + { + return Err(g210("payment intent", INTENT_SCHEMA)); + } + Payment::X402 { + origin: text(row, "origin", "payment intent", INTENT_SCHEMA)?.to_owned(), + method: text(row, "method", "payment intent", INTENT_SCHEMA)?.to_owned(), + resource: text(row, "resource", "payment intent", INTENT_SCHEMA)?.to_owned(), + invoice_digest: text(row, "invoice_digest", "payment intent", INTENT_SCHEMA)? + .to_owned(), + payee: text(row, "payee", "payment intent", INTENT_SCHEMA)?.to_owned(), + rail: rail(text( + row, + "settlement_rail", + "payment intent", + INTENT_SCHEMA, + )?) + .ok_or_else(|| g210("payment intent", INTENT_SCHEMA))?, + network: text(row, "network", "payment intent", INTENT_SCHEMA)?.to_owned(), + asset: text(row, "asset", "payment intent", INTENT_SCHEMA)?.to_owned(), + amount: number(row, "amount_atomic", "payment intent", INTENT_SCHEMA)?, + max_fee: number(row, "max_fee_atomic", "payment intent", INTENT_SCHEMA)?, + invoice_expires: number( + row, + "invoice_expires_at_ms", + "payment intent", + INTENT_SCHEMA, + )?, + nonce: text(row, "invoice_nonce", "payment intent", INTENT_SCHEMA)?.to_owned(), + } + } + _ => return Err(g210("payment intent", INTENT_SCHEMA)), + }; + let intent = Intent { + intent_id, + wallet_id, + rail_text, + idempotency_key, + created_at, + expires_at, + memo, + payment, + source: source.to_owned(), + digest: digest(INTENT_DOMAIN, source.as_bytes()), + }; + if render_intent(&intent) != source { + return Err(g210("payment intent", INTENT_SCHEMA)); + } + Ok(intent) +} + +#[derive(Clone)] +struct Utxo { + txid: String, + vout: u64, + value: u64, + script: String, + confirmations: u64, +} +#[derive(Clone)] +enum SnapshotState { + Evm { + from: String, + nonce: u64, + base_fee: u64, + priority: u64, + gas: u64, + }, + Solana { + payer: String, + blockhash: String, + last_height: u64, + fee: u64, + }, + Bitcoin { + wallet_script: String, + height: u64, + fee_rate: u64, + utxos: Vec, + }, +} +#[derive(Clone)] +struct Snapshot { + rail: EconomicRail, + observed: u64, + expires: u64, + state: SnapshotState, + doc: Doc, +} + +fn render_snapshot(snapshot: &Snapshot) -> String { + let(network,state)=match &snapshot.state{ + SnapshotState::Evm{from,nonce,base_fee,priority,gas}=>("sepolia",format!("{{\"chain_id\":11155111,\"from\":{},\"nonce\":{nonce},\"base_fee_per_gas\":{base_fee},\"max_priority_fee_per_gas\":{priority},\"gas_limit\":{gas}}}",quote_json(from))), + SnapshotState::Solana{payer,blockhash,last_height,fee}=>("devnet",format!("{{\"fee_payer\":{},\"recent_blockhash\":{},\"last_valid_block_height\":{last_height},\"lamports_per_signature\":{fee}}}",quote_json(payer),quote_json(blockhash))), + SnapshotState::Bitcoin{wallet_script,height,fee_rate,utxos}=>{let mut rows=String::from("[");for(index,u)in utxos.iter().enumerate(){if index>0{rows.push(',');}rows.push_str(&format!("{{\"txid\":{},\"vout\":{},\"value_atomic\":{},\"script_pubkey\":{},\"confirmations\":{}}}",quote_json(&u.txid),u.vout,u.value,quote_json(&u.script),u.confirmations));}rows.push(']');("regtest",format!("{{\"wallet_script_pubkey\":{},\"height\":{height},\"fee_rate_sat_vbyte\":{fee_rate},\"utxos\":{rows}}}",quote_json(wallet_script)))} }; + format!("{{\"schema\":\"{SNAPSHOT_SCHEMA}\",\"rail\":{},\"network\":{},\"observed_at_ms\":{},\"expires_at_ms\":{},\"state\":{state}}}\n",quote_json(snapshot.rail.text()),quote_json(network),snapshot.observed,snapshot.expires) +} + +fn parse_snapshot(source: &str, expected: EconomicRail) -> Result { + let (_, value) = canonical( + source, + "chain snapshot", + SNAPSHOT_SCHEMA, + MAX_SNAPSHOT_BYTES, + )?; + let top = object(&value, "chain snapshot", SNAPSHOT_SCHEMA)?; + if !keys( + top, + &[ + "schema", + "rail", + "network", + "observed_at_ms", + "expires_at_ms", + "state", + ], + ) { + return Err(g210("chain snapshot", SNAPSHOT_SCHEMA)); + } + let parsed = rail(text(top, "rail", "chain snapshot", SNAPSHOT_SCHEMA)?).ok_or_else(g213)?; + if parsed != expected { + return Err(g213()); + } + let observed = number(top, "observed_at_ms", "chain snapshot", SNAPSHOT_SCHEMA)?; + let expires = number(top, "expires_at_ms", "chain snapshot", SNAPSHOT_SCHEMA)?; + if expires <= observed || expires - observed > 600_000 { + return Err(g213()); + } + let row = object(&top["state"], "chain snapshot", SNAPSHOT_SCHEMA)?; + let state = match parsed { + EconomicRail::Evm => { + if text(top, "network", "chain snapshot", SNAPSHOT_SCHEMA)? != "sepolia" + || !keys( + row, + &[ + "chain_id", + "from", + "nonce", + "base_fee_per_gas", + "max_priority_fee_per_gas", + "gas_limit", + ], + ) + || number(row, "chain_id", "chain snapshot", SNAPSHOT_SCHEMA)? != 11155111 + { + return Err(g213()); + } + let from = text(row, "from", "chain snapshot", SNAPSHOT_SCHEMA)?.to_owned(); + if !valid_recipient(parsed, &from) { + return Err(g213()); + } + let gas = number(row, "gas_limit", "chain snapshot", SNAPSHOT_SCHEMA)?; + if gas != 21000 { + return Err(g213()); + } + SnapshotState::Evm { + from, + nonce: number(row, "nonce", "chain snapshot", SNAPSHOT_SCHEMA)?, + base_fee: number(row, "base_fee_per_gas", "chain snapshot", SNAPSHOT_SCHEMA)?, + priority: number( + row, + "max_priority_fee_per_gas", + "chain snapshot", + SNAPSHOT_SCHEMA, + )?, + gas, + } + } + EconomicRail::Solana => { + if text(top, "network", "chain snapshot", SNAPSHOT_SCHEMA)? != "devnet" + || !keys( + row, + &[ + "fee_payer", + "recent_blockhash", + "last_valid_block_height", + "lamports_per_signature", + ], + ) + { + return Err(g213()); + } + let payer = text(row, "fee_payer", "chain snapshot", SNAPSHOT_SCHEMA)?.to_owned(); + let blockhash = + text(row, "recent_blockhash", "chain snapshot", SNAPSHOT_SCHEMA)?.to_owned(); + if decode_base58_32(&payer).is_none() || decode_base58_32(&blockhash).is_none() { + return Err(g213()); + } + SnapshotState::Solana { + payer, + blockhash, + last_height: number( + row, + "last_valid_block_height", + "chain snapshot", + SNAPSHOT_SCHEMA, + )?, + fee: number( + row, + "lamports_per_signature", + "chain snapshot", + SNAPSHOT_SCHEMA, + )?, + } + } + EconomicRail::Bitcoin => { + if text(top, "network", "chain snapshot", SNAPSHOT_SCHEMA)? != "regtest" + || !keys( + row, + &[ + "wallet_script_pubkey", + "height", + "fee_rate_sat_vbyte", + "utxos", + ], + ) + { + return Err(g213()); + } + let wallet_script = text( + row, + "wallet_script_pubkey", + "chain snapshot", + SNAPSHOT_SCHEMA, + )? + .to_owned(); + if !valid_script(&wallet_script) { + return Err(g213()); + } + let values = row["utxos"].as_array().ok_or_else(g213)?; + if values.is_empty() || values.len() > MAX_UTXOS { + return Err(g216("utxos", MAX_UTXOS as u64)); + } + let mut utxos = Vec::new(); + for value in values { + let u = object(value, "chain snapshot", SNAPSHOT_SCHEMA)?; + if !keys( + u, + &[ + "txid", + "vout", + "value_atomic", + "script_pubkey", + "confirmations", + ], + ) { + return Err(g210("chain snapshot", SNAPSHOT_SCHEMA)); + } + let txid = text(u, "txid", "chain snapshot", SNAPSHOT_SCHEMA)?.to_owned(); + let script = + text(u, "script_pubkey", "chain snapshot", SNAPSHOT_SCHEMA)?.to_owned(); + if !lower_hex(&txid, 64) || !valid_script(&script) { + return Err(g213()); + } + utxos.push(Utxo { + txid, + vout: number(u, "vout", "chain snapshot", SNAPSHOT_SCHEMA)?, + value: number(u, "value_atomic", "chain snapshot", SNAPSHOT_SCHEMA)?, + script, + confirmations: number(u, "confirmations", "chain snapshot", SNAPSHOT_SCHEMA)?, + }); + } + if !utxos + .windows(2) + .all(|w| (w[0].txid.as_str(), w[0].vout) < (w[1].txid.as_str(), w[1].vout)) + || utxos + .iter() + .any(|u| u.confirmations == 0 || u.script != wallet_script) + { + return Err(g213()); + } + SnapshotState::Bitcoin { + wallet_script, + height: number(row, "height", "chain snapshot", SNAPSHOT_SCHEMA)?, + fee_rate: number(row, "fee_rate_sat_vbyte", "chain snapshot", SNAPSHOT_SCHEMA)?, + utxos, + } + } + }; + let mut snapshot = Snapshot { + rail: parsed, + observed, + expires, + state, + doc: Doc { + source: source.to_owned(), + digest: digest(SNAPSHOT_DOMAIN, source.as_bytes()), + }, + }; + if render_snapshot(&snapshot) != source { + return Err(g210("chain snapshot", SNAPSHOT_SCHEMA)); + } + snapshot.doc.source = source.to_owned(); + Ok(snapshot) +} +fn parse_snapshot_limited( + source: &str, + expected: EconomicRail, + limits: &Limits, +) -> Result { + configured_document_limits(source, "chain snapshot", limits.max_snapshot_bytes, limits)?; + reserve_parse_sidecar(source, limits)?; + let snapshot = parse_snapshot(source, expected)?; + if let SnapshotState::Bitcoin { utxos, .. } = &snapshot.state { + if utxos.len() > limits.max_utxos as usize { + return Err(g216("utxos", limits.max_utxos)); + } + } + Ok(snapshot) +} +fn reserve_parse_sidecar(source: &str, limits: &Limits) -> Result<(), Diagnostic> { + let multiplier = usize::try_from(limits.max_json_depth) + .map_err(|_| g217())? + .checked_add(2) + .ok_or_else(g217)?; + let sidecar = source.len().checked_mul(multiplier).ok_or_else(g217)?; + if !reserve_active_preserving(sidecar, terminal_floor(limits)?) { + return Err(g216("builder_bytes", limits.max_builder_bytes)); + } + Ok(()) +} +fn lower_hex(value: &str, n: usize) -> bool { + value.len() == n + && value + .bytes() + .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) +} +fn valid_script(value: &str) -> bool { + value.len() == 44 && value.starts_with("0014") && lower_hex(value, 44) +} +fn hex_bytes(value: &str) -> Option> { + if value.len() % 2 != 0 { + return None; + } + value + .as_bytes() + .chunks(2) + .map(|c| u8::from_str_radix(std::str::from_utf8(c).ok()?, 16).ok()) + .collect() +} + +fn rlp_bytes(value: &[u8]) -> Vec { + if value.len() == 1 && value[0] < 0x80 { + return value.to_vec(); + } + if value.len() < 56 { + let mut out = vec![0x80 + value.len() as u8]; + out.extend_from_slice(value); + out + } else { + let len = (value.len() as u64).to_be_bytes(); + let first = len.iter().position(|b| *b != 0).unwrap_or(7); + let mut out = vec![0xb7 + (8 - first) as u8]; + out.extend_from_slice(&len[first..]); + out.extend_from_slice(value); + out + } +} +fn rlp_u64(value: u64) -> Vec { + if value == 0 { + return vec![0x80]; + } + let bytes = value.to_be_bytes(); + rlp_bytes(&bytes[bytes.iter().position(|b| *b != 0).unwrap_or(7)..]) +} +fn rlp_list(items: &[Vec]) -> Vec { + let payload = items.concat(); + if payload.len() < 56 { + let mut out = vec![0xc0 + payload.len() as u8]; + out.extend(payload); + out + } else { + let len = (payload.len() as u64).to_be_bytes(); + let first = len.iter().position(|b| *b != 0).unwrap_or(7); + let mut out = vec![0xf7 + (8 - first) as u8]; + out.extend_from_slice(&len[first..]); + out.extend(payload); + out + } +} +fn shortvec(mut value: usize) -> Vec { + let mut out = Vec::new(); + loop { + let mut byte = (value & 0x7f) as u8; + value >>= 7; + if value > 0 { + byte |= 0x80; + } + out.push(byte); + if value == 0 { + return out; + } + } +} + +fn build_unsigned( + intent: &Intent, + snapshot: &Snapshot, +) -> Result<(Vec, &'static str), Diagnostic> { + build_unsigned_limited( + intent, + snapshot, + MAX_UNSIGNED_BYTES as u64, + MAX_BUILDER_BYTES as u64, + 0, + ) +} +fn build_unsigned_limited( + intent: &Intent, + snapshot: &Snapshot, + unsigned_max: u64, + builder_max: u64, + terminal_floor: usize, +) -> Result<(Vec, &'static str), Diagnostic> { + let bytes = match (&intent.payment, &snapshot.state) { + ( + Payment::Evm { + recipient, + amount, + max_fee, + }, + SnapshotState::Evm { + nonce, + base_fee, + priority, + gas, + .. + }, + ) => { + let per_gas = base_fee + .checked_mul(2) + .and_then(|v| v.checked_add(*priority)) + .ok_or_else(g213)?; + let total = per_gas.checked_mul(21000).ok_or_else(g213)?; + if total > *max_fee || *gas != 21000 { + return Err(g213()); + } + let to = hex_bytes(&recipient[2..]).ok_or_else(g213)?; + let mut out = vec![0x02]; + out.extend(rlp_list(&[ + rlp_u64(11155111), + rlp_u64(*nonce), + rlp_u64(*priority), + rlp_u64(per_gas), + rlp_u64(21000), + rlp_bytes(&to), + rlp_u64(*amount), + rlp_bytes(&[]), + rlp_list(&[]), + ])); + out + } + ( + Payment::Solana { + recipient, + amount, + max_fee, + compute, + priority, + }, + SnapshotState::Solana { + payer, + blockhash, + fee, + .. + }, + ) => { + if *compute == 0 || *compute > 200000 { + return Err(g216("compute_units", 200000)); + } + let price = priority + .checked_mul(1_000_000) + .map(|v| v / compute) + .ok_or_else(g213)?; + let priority_fee = compute + .checked_mul(price) + .and_then(|v| v.checked_add(999999)) + .map(|v| v / 1000000) + .ok_or_else(g213)?; + if priority_fee > *priority + || fee.checked_add(priority_fee).ok_or_else(g213)? > *max_fee + { + return Err(g213()); + } + let payer = decode_base58_32(payer).ok_or_else(g213)?; + let recipient = decode_base58_32(recipient).ok_or_else(g213)?; + let system = decode_base58_32("11111111111111111111111111111111").ok_or_else(g213)?; + let compute_program = + decode_base58_32("ComputeBudget111111111111111111111111111111").ok_or_else(g213)?; + let blockhash = decode_base58_32(blockhash).ok_or_else(g213)?; + let mut out = vec![0x80, 1, 0, 2]; + out.extend(shortvec(4)); + out.extend(payer); + out.extend(recipient); + out.extend(compute_program); + out.extend(system); + out.extend(blockhash); + out.extend(shortvec(3)); + out.push(2); + out.extend(shortvec(0)); + out.extend(shortvec(5)); + out.push(2); + out.extend_from_slice(&(*compute as u32).to_le_bytes()); + out.push(2); + out.extend(shortvec(0)); + out.extend(shortvec(9)); + out.push(3); + out.extend_from_slice(&price.to_le_bytes()); + out.push(3); + out.extend(shortvec(2)); + out.extend([0, 1]); + out.extend(shortvec(12)); + out.extend_from_slice(&2u32.to_le_bytes()); + out.extend_from_slice(&amount.to_le_bytes()); + out.extend(shortvec(0)); + out + } + ( + Payment::Bitcoin { + recipient, + amount, + max_fee, + .. + }, + SnapshotState::Bitcoin { + height, + fee_rate, + utxos, + wallet_script, + }, + ) => build_psbt( + utxos, + wallet_script, + recipient, + *amount, + *max_fee, + *fee_rate, + *height, + )?, + (Payment::X402 { rail, .. }, _) => { + let mut clone = intent.clone(); + clone.payment = match rail { + EconomicRail::Evm => { + if let Payment::X402 { + payee, + amount, + max_fee, + .. + } = &intent.payment + { + Payment::Evm { + recipient: payee.clone(), + amount: *amount, + max_fee: *max_fee, + } + } else { + unreachable!() + } + } + EconomicRail::Solana => { + if let Payment::X402 { + payee, + amount, + max_fee, + .. + } = &intent.payment + { + Payment::Solana { + recipient: payee.clone(), + amount: *amount, + max_fee: *max_fee, + compute: 200_000, + priority: 0, + } + } else { + unreachable!() + } + } + EconomicRail::Bitcoin => { + if let Payment::X402 { + payee, + amount, + max_fee, + .. + } = &intent.payment + { + Payment::Bitcoin { + recipient: payee.clone(), + amount: *amount, + max_fee: *max_fee, + confirmations: 1, + } + } else { + unreachable!() + } + } + }; + return build_unsigned_limited( + &clone, + snapshot, + unsigned_max, + builder_max, + terminal_floor, + ); + } + _ => return Err(g213()), + }; + if bytes.len() as u64 > unsigned_max { + return Err(g216("unsigned_transaction_bytes", unsigned_max)); + } + if !reserve_active_preserving(bytes.len(), terminal_floor) { + return Err(g216("builder_bytes", builder_max)); + } + let format = match snapshot.rail { + EconomicRail::Evm => "eip1559-unsigned-v1", + EconomicRail::Solana => "solana-message-v0", + EconomicRail::Bitcoin => "psbt-v2", + }; + Ok((bytes, format)) +} + +fn build_psbt( + utxos: &[Utxo], + wallet_script: &str, + recipient: &str, + amount: u64, + max_fee: u64, + fee_rate: u64, + height: u64, +) -> Result, Diagnostic> { + let mut selected = Vec::new(); + let mut total = 0u64; + for u in utxos { + selected.push(u); + total = total.checked_add(u.value).ok_or_else(g213)?; + let estimate = 10 + selected.len() as u64 * 68 + 2 * 31; + let fee = estimate.checked_mul(fee_rate).ok_or_else(g213)?; + if total >= amount.saturating_add(fee) { + break; + } + } + let estimate = 10 + selected.len() as u64 * 68 + 2 * 31; + let mut fee = estimate.checked_mul(fee_rate).ok_or_else(g213)?; + if fee > max_fee || total < amount.saturating_add(fee) { + return Err(g213()); + } + let mut change = total - amount - fee; + if change < 546 { + fee = fee.checked_add(change).ok_or_else(g213)?; + change = 0; + } + if fee > max_fee { + return Err(g213()); + } + let recipient_script = decode_regtest_p2wpkh(recipient).ok_or_else(g213)?; + let change_script = hex_bytes(wallet_script).ok_or_else(g213)?; + let mut outputs = vec![(recipient_script, amount)]; + if change > 0 { + outputs.push((change_script, change)); + } + outputs.sort_by(|a, b| (a.1, a.0.as_slice()).cmp(&(b.1, b.0.as_slice()))); + let mut out = b"psbt\xff".to_vec(); + psbt_pair(&mut out, &[0x02], &2u32.to_le_bytes()); + psbt_pair(&mut out, &[0x03], &(height as u32).to_le_bytes()); + psbt_pair(&mut out, &[0x04], &compact_size(selected.len())); + psbt_pair(&mut out, &[0x05], &compact_size(outputs.len())); + psbt_pair(&mut out, &[0x06], &[0]); + psbt_pair(&mut out, &[0xfb], &2u32.to_le_bytes()); + out.push(0); + for u in selected { + let script = hex_bytes(&u.script).ok_or_else(g213)?; + let mut witness = u.value.to_le_bytes().to_vec(); + witness.extend(compact_size(script.len())); + witness.extend(script); + psbt_pair(&mut out, &[0x01], &witness); + psbt_pair(&mut out, &[0x03], &1u32.to_le_bytes()); + let mut txid = hex_bytes(&u.txid).ok_or_else(g213)?; + txid.reverse(); + psbt_pair(&mut out, &[0x0e], &txid); + psbt_pair(&mut out, &[0x0f], &(u.vout as u32).to_le_bytes()); + psbt_pair(&mut out, &[0x10], &0xffff_ffffu32.to_le_bytes()); + out.push(0); + } + for (script, value) in outputs { + psbt_pair(&mut out, &[0x03], &value.to_le_bytes()); + psbt_pair(&mut out, &[0x04], &script); + out.push(0); + } + Ok(out) +} +fn psbt_pair(out: &mut Vec, key: &[u8], value: &[u8]) { + out.extend(shortvec(key.len())); + out.extend(key); + out.extend(shortvec(value.len())); + out.extend(value); +} +fn rlp_header(bytes: &[u8]) -> Option<(bool, usize, usize)> { + let first = *bytes.first()?; + match first { + 0x00..=0x7f => Some((false, 0, 1)), + 0x80..=0xb7 => { + let len = (first - 0x80) as usize; + (bytes.len() > len && !(len == 1 && bytes[1] < 0x80)).then_some((false, 1, len)) + } + 0xb8..=0xbf => { + let n = (first - 0xb7) as usize; + if bytes.len() < 1 + n || bytes[1] == 0 { + return None; + } + let len = bytes[1..1 + n].iter().try_fold(0usize, |value, byte| { + value.checked_mul(256)?.checked_add(*byte as usize) + })?; + (len >= 56 && bytes.len() >= 1 + n + len).then_some((false, 1 + n, len)) + } + 0xc0..=0xf7 => { + let len = (first - 0xc0) as usize; + (bytes.len() > len).then_some((true, 1, len)) + } + 0xf8..=0xff => { + let n = (first - 0xf7) as usize; + if bytes.len() < 1 + n || bytes[1] == 0 { + return None; + } + let len = bytes[1..1 + n].iter().try_fold(0usize, |value, byte| { + value.checked_mul(256)?.checked_add(*byte as usize) + })?; + (len >= 56 && bytes.len() >= 1 + n + len).then_some((true, 1 + n, len)) + } + } +} +fn rlp_list_items(bytes: &[u8]) -> Option> { + let (list, header, len) = rlp_header(bytes)?; + if !list || header + len != bytes.len() { + return None; + } + let mut body = &bytes[header..]; + let mut out = Vec::new(); + while !body.is_empty() { + let (_, item_header, item_len) = rlp_header(body)?; + let total = item_header.checked_add(item_len)?; + out.push(&body[..total]); + body = &body[total..]; + } + Some(out) +} +fn rlp_scalar(item: &[u8]) -> Option<&[u8]> { + let (list, header, len) = rlp_header(item)?; + (!list && header + len == item.len()).then_some(&item[header..]) +} +fn valid_secp_scalar(value: &[u8], low_s: bool) -> bool { + const ORDER: [u8; 32] = [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xba, 0xae, 0xdc, 0xe6, 0xaf, 0x48, 0xa0, + 0x3b, 0xbf, 0xd2, 0x5e, 0x8c, 0xd0, 0x36, 0x41, 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + ]; + const HALF: [u8; 32] = [ + 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x5d, 0x57, 0x6e, 0x73, 0x57, 0xa4, 0x50, + 0x1d, 0xdf, 0xe9, 0x2f, 0x46, 0x68, 0x1b, 0x20, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + ]; + if value.is_empty() || value.len() > 32 || value.first() == Some(&0) { + return false; + } + let mut padded = [0u8; 32]; + padded[32 - value.len()..].copy_from_slice(value); + padded < ORDER && (!low_s || padded <= HALF) +} +fn verify_evm_signed(unsigned: &[u8], signed: &[u8]) -> bool { + if unsigned.first() != Some(&2) || signed.first() != Some(&2) { + return false; + } + let Some(unsigned_items) = rlp_list_items(&unsigned[1..]) else { + return false; + }; + let Some(signed_items) = rlp_list_items(&signed[1..]) else { + return false; + }; + if unsigned_items.len() != 9 + || signed_items.len() != 12 + || unsigned_items + .iter() + .zip(&signed_items[..9]) + .any(|(a, b)| a != b) + { + return false; + } + let Some(parity) = rlp_scalar(signed_items[9]) else { + return false; + }; + if !matches!(parity, [] | [1]) { + return false; + } + let Some(r) = rlp_scalar(signed_items[10]) else { + return false; + }; + let Some(s) = rlp_scalar(signed_items[11]) else { + return false; + }; + valid_secp_scalar(r, false) && valid_secp_scalar(s, true) +} +fn take<'a>(bytes: &mut &'a [u8], length: usize) -> Option<&'a [u8]> { + if bytes.len() < length { + return None; + } + let (value, rest) = bytes.split_at(length); + *bytes = rest; + Some(value) +} +fn read_compact(bytes: &mut &[u8]) -> Option { + let first = *take(bytes, 1)?.first()?; + match first { + 0..=0xfc => Some(first as u64), + 0xfd => { + let value = u16::from_le_bytes(take(bytes, 2)?.try_into().ok()?) as u64; + (value >= 0xfd).then_some(value) + } + 0xfe => { + let value = u32::from_le_bytes(take(bytes, 4)?.try_into().ok()?) as u64; + (value > u16::MAX as u64).then_some(value) + } + 0xff => { + let value = u64::from_le_bytes(take(bytes, 8)?.try_into().ok()?); + (value > u32::MAX as u64).then_some(value) + } + } +} +#[derive(Eq, PartialEq)] +struct BtcInput { + txid: [u8; 32], + vout: u32, + sequence: u32, +} +#[derive(Eq, PartialEq)] +struct BtcOutput { + value: u64, + script: Vec, +} +struct BtcTemplate { + locktime: u32, + inputs: Vec, + outputs: Vec, +} +fn psbt_map(bytes: &mut &[u8]) -> Option, Vec)>> { + let mut entries = Vec::new(); + let mut previous: Option> = None; + loop { + let key_len = read_compact(bytes)? as usize; + if key_len == 0 { + return Some(entries); + } + let key = take(bytes, key_len)?.to_vec(); + if previous.as_ref().is_some_and(|value| value >= &key) { + return None; + } + previous = Some(key.clone()); + let value_len = read_compact(bytes)? as usize; + let value = take(bytes, value_len)?.to_vec(); + entries.push((key, value)); + } +} +fn parse_psbt_template(unsigned: &[u8]) -> Option { + let mut bytes = unsigned; + if take(&mut bytes, 5)? != b"psbt\xff" { + return None; + } + let globals = psbt_map(&mut bytes)?; + let get = |key: u8| { + globals + .iter() + .find(|(candidate, _)| candidate.as_slice() == [key]) + .map(|(_, value)| value.as_slice()) + }; + if get(0xfb)? != 2u32.to_le_bytes() || get(0x02)? != 2i32.to_le_bytes() || get(0x06)? != [0] { + return None; + } + let locktime = u32::from_le_bytes(get(0x03)?.try_into().ok()?); + let mut count_bytes = get(0x04)?; + let input_count = read_compact(&mut count_bytes)? as usize; + if !count_bytes.is_empty() || input_count > 100 { + return None; + } + let mut count_bytes = get(0x05)?; + let output_count = read_compact(&mut count_bytes)? as usize; + if !count_bytes.is_empty() { + return None; + } + let mut inputs = Vec::new(); + for _ in 0..input_count { + let map = psbt_map(&mut bytes)?; + let get = |key: u8| { + map.iter() + .find(|(candidate, _)| candidate.as_slice() == [key]) + .map(|(_, value)| value.as_slice()) + }; + let txid = get(0x0e)?.try_into().ok()?; + let vout = u32::from_le_bytes(get(0x0f)?.try_into().ok()?); + let sequence = u32::from_le_bytes(get(0x10)?.try_into().ok()?); + if sequence != 0xffff_ffff || get(0x03)? != 1u32.to_le_bytes() || get(0x01).is_none() { + return None; + } + inputs.push(BtcInput { + txid, + vout, + sequence, + }); + } + let mut outputs = Vec::new(); + for _ in 0..output_count { + let map = psbt_map(&mut bytes)?; + let get = |key: u8| { + map.iter() + .find(|(candidate, _)| candidate.as_slice() == [key]) + .map(|(_, value)| value.as_slice()) + }; + outputs.push(BtcOutput { + value: u64::from_le_bytes(get(0x03)?.try_into().ok()?), + script: get(0x04)?.to_vec(), + }); + } + if !bytes.is_empty() { + return None; + } + Some(BtcTemplate { + locktime, + inputs, + outputs, + }) +} +fn valid_der_signature(value: &[u8]) -> bool { + if value.len() < 9 + || value.last() != Some(&1) + || value[0] != 0x30 + || value[1] as usize + 3 != value.len() + { + return false; + } + let body = &value[2..value.len() - 1]; + if body.first() != Some(&2) || body.len() < 2 { + return false; + } + let rlen = body[1] as usize; + if body.len() < 2 + rlen + 2 || rlen == 0 { + return false; + } + let r = &body[2..2 + rlen]; + let rest = &body[2 + rlen..]; + if rest.first() != Some(&2) || rest.len() < 2 || rest.len() != 2 + rest[1] as usize { + return false; + } + let s = &rest[2..]; + fn integer(bytes: &[u8]) -> Option<&[u8]> { + if bytes.is_empty() || bytes[0] & 0x80 != 0 { + return None; + } + if bytes.len() > 1 && bytes[0] == 0 && bytes[1] & 0x80 == 0 { + return None; + } + Some(if bytes[0] == 0 { &bytes[1..] } else { bytes }) + } + let Some(r) = integer(r) else { return false }; + let Some(s) = integer(s) else { return false }; + valid_secp_scalar(r, false) && valid_secp_scalar(s, true) +} +fn verify_bitcoin_signed(unsigned: &[u8], signed: &[u8]) -> bool { + let Some(template) = parse_psbt_template(unsigned) else { + return false; + }; + let mut bytes = signed; + if take(&mut bytes, 4) != Some(&2i32.to_le_bytes()) || take(&mut bytes, 2) != Some(&[0, 1]) { + return false; + } + let Some(input_count) = read_compact(&mut bytes).and_then(|value| usize::try_from(value).ok()) + else { + return false; + }; + if input_count != template.inputs.len() { + return false; + } + for expected in &template.inputs { + let Some(txid) = take(&mut bytes, 32) else { + return false; + }; + let Some(vout) = take(&mut bytes, 4) + .and_then(|value| value.try_into().ok()) + .map(u32::from_le_bytes) + else { + return false; + }; + let Some(script_len) = + read_compact(&mut bytes).and_then(|value| usize::try_from(value).ok()) + else { + return false; + }; + if script_len != 0 || take(&mut bytes, script_len).is_none() { + return false; + } + let Some(sequence) = take(&mut bytes, 4) + .and_then(|value| value.try_into().ok()) + .map(u32::from_le_bytes) + else { + return false; + }; + if txid != expected.txid || vout != expected.vout || sequence != expected.sequence { + return false; + } + } + let Some(output_count) = read_compact(&mut bytes).and_then(|value| usize::try_from(value).ok()) + else { + return false; + }; + if output_count != template.outputs.len() { + return false; + } + for expected in &template.outputs { + let Some(value) = take(&mut bytes, 8) + .and_then(|value| value.try_into().ok()) + .map(u64::from_le_bytes) + else { + return false; + }; + let Some(script_len) = + read_compact(&mut bytes).and_then(|value| usize::try_from(value).ok()) + else { + return false; + }; + let Some(script) = take(&mut bytes, script_len) else { + return false; + }; + if value != expected.value || script != expected.script { + return false; + } + } + for _ in &template.inputs { + if read_compact(&mut bytes) != Some(2) { + return false; + } + let Some(sig_len) = read_compact(&mut bytes).and_then(|value| usize::try_from(value).ok()) + else { + return false; + }; + let Some(signature) = take(&mut bytes, sig_len) else { + return false; + }; + if !valid_der_signature(signature) { + return false; + } + if read_compact(&mut bytes) != Some(33) { + return false; + } + let Some(pubkey) = take(&mut bytes, 33) else { + return false; + }; + if !matches!(pubkey.first(), Some(2 | 3)) { + return false; + } + } + take(&mut bytes, 4) == Some(&template.locktime.to_le_bytes()) && bytes.is_empty() +} +fn verify_signed(rail: EconomicRail, unsigned: &[u8], signed: &[u8]) -> Result<(), Diagnostic> { + let valid = match rail { + EconomicRail::Solana => { + signed.len() == 1 + 64 + unsigned.len() + && signed.first() == Some(&1) + && signed[1..65].iter().any(|byte| *byte != 0) + && &signed[65..] == unsigned + } + EconomicRail::Evm => verify_evm_signed(unsigned, signed), + EconomicRail::Bitcoin => verify_bitcoin_signed(unsigned, signed), + }; + if valid { + Ok(()) + } else { + Err(g213()) + } +} + +fn keccak_f(state: &mut [u64; 25]) { + const R: [u32; 25] = [ + 0, 1, 62, 28, 27, 36, 44, 6, 55, 20, 3, 10, 43, 25, 39, 41, 45, 15, 21, 8, 18, 2, 61, 56, + 14, + ]; + const RC: [u64; 24] = [ + 0x0000000000000001, + 0x0000000000008082, + 0x800000000000808a, + 0x8000000080008000, + 0x000000000000808b, + 0x0000000080000001, + 0x8000000080008081, + 0x8000000000008009, + 0x000000000000008a, + 0x0000000000000088, + 0x0000000080008009, + 0x000000008000000a, + 0x000000008000808b, + 0x800000000000008b, + 0x8000000000008089, + 0x8000000000008003, + 0x8000000000008002, + 0x8000000000000080, + 0x000000000000800a, + 0x800000008000000a, + 0x8000000080008081, + 0x8000000000008080, + 0x0000000080000001, + 0x8000000080008008, + ]; + for rc in RC { + let mut c = [0u64; 5]; + for x in 0..5 { + c[x] = state[x] ^ state[x + 5] ^ state[x + 10] ^ state[x + 15] ^ state[x + 20]; + } + let mut d = [0u64; 5]; + for x in 0..5 { + d[x] = c[(x + 4) % 5] ^ c[(x + 1) % 5].rotate_left(1); + } + for y in 0..5 { + for x in 0..5 { + state[x + 5 * y] ^= d[x]; + } + } + let mut b = [0u64; 25]; + for y in 0..5 { + for x in 0..5 { + b[y % 5 + 5 * ((2 * x + 3 * y) % 5)] = state[x + 5 * y].rotate_left(R[x + 5 * y]); + } + } + for y in 0..5 { + for x in 0..5 { + state[x + 5 * y] = + b[x + 5 * y] ^ ((!b[(x + 1) % 5 + 5 * y]) & b[(x + 2) % 5 + 5 * y]); + } + } + state[0] ^= rc; + } +} +fn keccak256(bytes: &[u8]) -> [u8; 32] { + let mut state = [0u64; 25]; + let mut chunks = bytes.chunks_exact(136); + for chunk in &mut chunks { + for (index, word) in chunk.chunks_exact(8).enumerate() { + state[index] ^= u64::from_le_bytes(word.try_into().unwrap_or([0; 8])); + } + keccak_f(&mut state); + } + let remainder = chunks.remainder(); + let mut block = [0u8; 136]; + block[..remainder.len()].copy_from_slice(remainder); + block[remainder.len()] = 0x01; + block[135] |= 0x80; + for (index, word) in block.chunks_exact(8).enumerate() { + state[index] ^= u64::from_le_bytes(word.try_into().unwrap_or([0; 8])); + } + keccak_f(&mut state); + let mut output = [0u8; 32]; + for (index, word) in state[..4].iter().enumerate() { + output[index * 8..index * 8 + 8].copy_from_slice(&word.to_le_bytes()); + } + output +} +fn bitcoin_stripped(signed: &[u8]) -> Option> { + let mut bytes = signed; + let version = take(&mut bytes, 4)?; + if take(&mut bytes, 2)? != [0, 1] { + return None; + } + let input_count = read_compact(&mut bytes)?; + let mut stripped = version.to_vec(); + stripped.extend(compact_size(input_count.try_into().ok()?)); + for _ in 0..input_count { + let txid = take(&mut bytes, 32)?; + let vout = take(&mut bytes, 4)?; + let script_len = read_compact(&mut bytes)?; + let script = take(&mut bytes, script_len.try_into().ok()?)?; + let sequence = take(&mut bytes, 4)?; + stripped.extend(txid); + stripped.extend(vout); + stripped.extend(compact_size(script.len())); + stripped.extend(script); + stripped.extend(sequence); + } + let output_count = read_compact(&mut bytes)?; + stripped.extend(compact_size(output_count.try_into().ok()?)); + for _ in 0..output_count { + let value = take(&mut bytes, 8)?; + let script_len = read_compact(&mut bytes)?; + let script = take(&mut bytes, script_len.try_into().ok()?)?; + stripped.extend(value); + stripped.extend(compact_size(script.len())); + stripped.extend(script); + } + for _ in 0..input_count { + let items = read_compact(&mut bytes)?; + for _ in 0..items { + let len = read_compact(&mut bytes)?; + take(&mut bytes, len.try_into().ok()?)?; + } + } + let locktime = take(&mut bytes, 4)?; + if !bytes.is_empty() { + return None; + } + stripped.extend(locktime); + Some(stripped) +} +fn transaction_id(rail: EconomicRail, signed: &[u8]) -> Option { + match rail { + EconomicRail::Evm => Some(format!( + "0x{}", + keccak256(signed) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::() + )), + EconomicRail::Solana => { + (signed.len() >= 65 && signed[0] == 1).then(|| encode_base58(&signed[1..65])) + } + EconomicRail::Bitcoin => { + let stripped = bitcoin_stripped(signed)?; + let first = Sha256::digest(&stripped); + let second = Sha256::digest(first); + Some( + second + .iter() + .rev() + .map(|byte| format!("{byte:02x}")) + .collect(), + ) + } + } +} + +fn compact_size(value: usize) -> Vec { + if value < 0xfd { + vec![value as u8] + } else if value <= 0xffff { + let mut out = vec![0xfd]; + out.extend_from_slice(&(value as u16).to_le_bytes()); + out + } else { + let mut out = vec![0xfe]; + out.extend_from_slice(&(value as u32).to_le_bytes()); + out + } +} + +fn verify_unsigned(intent: &Intent, snapshot: &Snapshot, bytes: &[u8]) -> Result<(), Diagnostic> { + let (expected, _) = build_unsigned(intent, snapshot)?; + if expected == bytes { + Ok(()) + } else { + Err(g213()) + } +} + +fn doc_ref(schema: &str, doc: &Doc) -> String { + format!( + "{{\"schema\":{},\"digest\":{},\"bytes\":{}}}", + quote_json(schema), + quote_json(&doc.digest), + doc.source.len() + ) +} +fn agent_ref(run_id: &str, evidence: &str, digest_value: &str) -> String { + format!("{{\"schema\":\"semaprax.agent-runtime-evidence.v1\",\"digest\":{},\"bytes\":{},\"run_id\":{}}}",quote_json(digest_value),evidence.len(),quote_json(run_id)) +} +fn ref_matches(value: &Value, schema: &str, doc: &Doc) -> bool { + let Some(row) = value.as_object() else { + return false; + }; + keys(row, &["schema", "digest", "bytes"]) + && row.get("schema").and_then(Value::as_str) == Some(schema) + && row.get("digest").and_then(Value::as_str) == Some(doc.digest.as_str()) + && row.get("bytes").and_then(Value::as_u64) == u64::try_from(doc.source.len()).ok() +} +fn ref_identity_matches(value: &Value, schema: &str, digest_value: &str, bytes: usize) -> bool { + let Some(row) = value.as_object() else { + return false; + }; + keys(row, &["schema", "digest", "bytes"]) + && row.get("schema").and_then(Value::as_str) == Some(schema) + && row.get("digest").and_then(Value::as_str) == Some(digest_value) + && row.get("bytes").and_then(Value::as_u64) == u64::try_from(bytes).ok() +} +fn unsigned_ref(bytes: &[u8], format: &str) -> String { + format!( + "{{\"digest\":{},\"bytes\":{},\"format\":{}}}", + quote_json(&digest(UNSIGNED_DOMAIN, bytes)), + bytes.len(), + quote_json(format) + ) +} + +#[derive(Clone)] +struct Invoice { + origin: String, + method: String, + resource: String, + invoice_id: String, + payee: String, + rail: EconomicRail, + network: String, + asset: String, + amount: u64, + max_fee: u64, + expires: u64, + nonce: String, + idempotency: String, + doc: Doc, +} +fn render_invoice(i: &Invoice) -> String { + format!("{{\"schema\":\"{INVOICE_SCHEMA}\",\"origin\":{},\"method\":{},\"resource\":{},\"invoice_id\":{},\"payee\":{},\"settlement_rail\":{},\"network\":{},\"asset\":{},\"amount_atomic\":{},\"max_fee_atomic\":{},\"expires_at_ms\":{},\"nonce\":{},\"idempotency_key\":{}}}\n",quote_json(&i.origin),quote_json(&i.method),quote_json(&i.resource),quote_json(&i.invoice_id),quote_json(&i.payee),quote_json(i.rail.text()),quote_json(&i.network),quote_json(&i.asset),i.amount,i.max_fee,i.expires,quote_json(&i.nonce),quote_json(&i.idempotency)) +} +fn parse_invoice(source: &str, intent: &Intent) -> Result { + let (_, value) = canonical(source, "x402 invoice", INVOICE_SCHEMA, MAX_INVOICE_BYTES)?; + let row = object(&value, "x402 invoice", INVOICE_SCHEMA)?; + if !keys( + row, + &[ + "schema", + "origin", + "method", + "resource", + "invoice_id", + "payee", + "settlement_rail", + "network", + "asset", + "amount_atomic", + "max_fee_atomic", + "expires_at_ms", + "nonce", + "idempotency_key", + ], + ) { + return Err(g210("x402 invoice", INVOICE_SCHEMA)); + } + let i = Invoice { + origin: text(row, "origin", "x402 invoice", INVOICE_SCHEMA)?.to_owned(), + method: text(row, "method", "x402 invoice", INVOICE_SCHEMA)?.to_owned(), + resource: text(row, "resource", "x402 invoice", INVOICE_SCHEMA)?.to_owned(), + invoice_id: text(row, "invoice_id", "x402 invoice", INVOICE_SCHEMA)?.to_owned(), + payee: text(row, "payee", "x402 invoice", INVOICE_SCHEMA)?.to_owned(), + rail: rail(text( + row, + "settlement_rail", + "x402 invoice", + INVOICE_SCHEMA, + )?) + .ok_or_else(g213)?, + network: text(row, "network", "x402 invoice", INVOICE_SCHEMA)?.to_owned(), + asset: text(row, "asset", "x402 invoice", INVOICE_SCHEMA)?.to_owned(), + amount: number(row, "amount_atomic", "x402 invoice", INVOICE_SCHEMA)?, + max_fee: number(row, "max_fee_atomic", "x402 invoice", INVOICE_SCHEMA)?, + expires: number(row, "expires_at_ms", "x402 invoice", INVOICE_SCHEMA)?, + nonce: text(row, "nonce", "x402 invoice", INVOICE_SCHEMA)?.to_owned(), + idempotency: text(row, "idempotency_key", "x402 invoice", INVOICE_SCHEMA)?.to_owned(), + doc: Doc { + source: source.to_owned(), + digest: digest(INVOICE_DOMAIN, source.as_bytes()), + }, + }; + if render_invoice(&i) != source { + return Err(g210("x402 invoice", INVOICE_SCHEMA)); + } + if let Payment::X402 { + origin, + method, + resource, + invoice_digest, + payee, + rail, + network, + asset, + amount, + max_fee, + invoice_expires, + nonce, + } = &intent.payment + { + if i.origin != *origin + || i.method != *method + || i.resource != *resource + || i.doc.digest != *invoice_digest + || i.payee != *payee + || i.rail != *rail + || i.network != *network + || i.asset != *asset + || i.amount != *amount + || i.max_fee != *max_fee + || i.expires != *invoice_expires + || i.nonce != *nonce + || i.idempotency != intent.idempotency_key + { + return Err(g213()); + } + } else { + return Err(g213()); + } + Ok(i) +} +fn parse_invoice_limited( + source: &str, + intent: &Intent, + limits: &Limits, +) -> Result { + configured_document_limits(source, "x402 invoice", limits.max_invoice_bytes, limits)?; + reserve_parse_sidecar(source, limits)?; + let invoice = parse_invoice(source, intent)?; + if [ + invoice.invoice_id.as_str(), + invoice.nonce.as_str(), + invoice.idempotency.as_str(), + ] + .into_iter() + .any(|value| value.len() > limits.max_identifier_bytes as usize) + { + return Err(g216("identifier_bytes", limits.max_identifier_bytes)); + } + Ok(invoice) +} + +#[derive(Clone)] +struct Plan { + doc: Doc, + unsigned: Vec, + unsigned_digest: String, + format: &'static str, + observed: u64, + expires: u64, + utxos: u64, +} +fn make_plan( + run_id: &str, + agent_run_id: &str, + agent_evidence: &str, + agent_digest: &str, + policy: &Policy, + intent: &Intent, + invoice: Option<&Invoice>, + snapshot: &Snapshot, + unsigned: Vec, + format: &'static str, +) -> Result { + if snapshot.observed < intent.created_at + || snapshot.observed >= intent.expires_at + || invoice.is_some_and(|value| snapshot.observed >= value.expires) + { + return Err(g212("expired")); + } + let unsigned_digest = digest(UNSIGNED_DOMAIN, &unsigned); + let expires = snapshot + .expires + .min(intent.expires_at) + .min(invoice.map_or(u64::MAX, |value| value.expires)); + if expires <= snapshot.observed { + return Err(g212("expired")); + } + let mut count = CountSink::default(); + write_plan( + &mut count, + run_id, + agent_run_id, + agent_evidence, + agent_digest, + policy, + intent, + invoice, + snapshot, + &unsigned, + format, + &unsigned_digest, + expires, + ) + .map_err(|_| g217())?; + if count.0 > policy.limits.max_plan_bytes as usize { + return Err(g216("plan_bytes", policy.limits.max_plan_bytes)); + } + let mut source = String::with_capacity(count.0); + write_plan( + &mut source, + run_id, + agent_run_id, + agent_evidence, + agent_digest, + policy, + intent, + invoice, + snapshot, + &unsigned, + format, + &unsigned_digest, + expires, + ) + .map_err(|_| g217())?; + let doc = Doc { + digest: digest(PLAN_DOMAIN, source.as_bytes()), + source, + }; + let utxos = match &snapshot.state { + SnapshotState::Bitcoin { utxos, .. } => utxos.len() as u64, + _ => 0, + }; + Ok(Plan { + doc, + unsigned, + unsigned_digest, + format, + observed: snapshot.observed, + expires, + utxos, + }) +} + +#[allow(clippy::too_many_arguments)] +fn write_plan( + output: &mut W, + run_id: &str, + agent_run_id: &str, + agent_evidence: &str, + agent_digest: &str, + policy: &Policy, + intent: &Intent, + invoice: Option<&Invoice>, + snapshot: &Snapshot, + unsigned: &[u8], + format: &str, + unsigned_digest: &str, + expires: u64, +) -> fmt::Result { + output.write_str("{\"schema\":")?; + write_json(output, PLAN_SCHEMA)?; + output.write_str(",\"run_id\":")?; + write_json(output, run_id)?; + output.write_str( + ",\"source_agent_evidence\":{\"schema\":\"semaprax.agent-runtime-evidence.v1\",\"digest\":", + )?; + write_json(output, agent_digest)?; + write!(output, ",\"bytes\":{},\"run_id\":", agent_evidence.len())?; + write_json(output, agent_run_id)?; + output.write_char('}')?; + output.write_str(",\"policy\":")?; + write_doc_reference(output, POLICY_SCHEMA, &policy.digest, policy.source.len())?; + output.write_str(",\"intent\":")?; + write_doc_reference(output, INTENT_SCHEMA, &intent.digest, intent.source.len())?; + output.write_str(",\"x402_invoice\":")?; + write_optional_reference(output, INVOICE_SCHEMA, invoice.map(|v| &v.doc))?; + output.write_str(",\"chain_snapshot\":")?; + write_doc_reference( + output, + SNAPSHOT_SCHEMA, + &snapshot.doc.digest, + snapshot.doc.source.len(), + )?; + output.write_str(",\"rail\":")?; + write_json(output, intent.settlement_rail().text())?; + let (network, asset) = intent.network_asset(); + output.write_str(",\"network\":")?; + write_json(output, network)?; + output.write_str(",\"asset\":")?; + write_json(output, asset)?; + output.write_str(",\"wallet_id\":")?; + write_json(output, &intent.wallet_id)?; + output.write_str(",\"recipient\":")?; + write_json(output, intent.recipient())?; + write!( + output, + ",\"amount_atomic\":{},\"max_fee_atomic\":{}", + intent.amount(), + intent.max_fee() + )?; + output.write_str(",\"unsigned_transaction\":{\"digest\":")?; + write_json(output, unsigned_digest)?; + write!(output, ",\"bytes\":{},\"format\":", unsigned.len())?; + write_json(output, format)?; + writeln!(output, "}},\"expires_at_ms\":{expires}}}") +} + +#[derive(Clone)] +struct Simulation { + doc: Doc, + fee: u64, + expires: u64, +} +fn parse_simulation(source: &str, plan: &Plan, intent: &Intent) -> Result { + let (_, value) = canonical( + source, + "simulation", + SIMULATION_SCHEMA, + MAX_SIMULATION_BYTES, + )?; + let row = object(&value, "simulation", SIMULATION_SCHEMA)?; + if !keys( + row, + &[ + "schema", + "plan", + "success", + "fee_atomic", + "balance_before_atomic", + "balance_after_atomic", + "allowance_atomic", + "units", + "expires_at_ms", + ], + ) { + return Err(g210("simulation", SIMULATION_SCHEMA)); + } + if !ref_matches(&row["plan"], PLAN_SCHEMA, &plan.doc) || row["success"].as_bool() != Some(true) + { + return Err(g213()); + } + let fee = number(row, "fee_atomic", "simulation", SIMULATION_SCHEMA)?; + if fee > intent.max_fee() { + return Err(g213()); + } + let before = number( + row, + "balance_before_atomic", + "simulation", + SIMULATION_SCHEMA, + )?; + let after = number(row, "balance_after_atomic", "simulation", SIMULATION_SCHEMA)?; + if after + .checked_add(intent.amount()) + .and_then(|value| value.checked_add(fee)) + != Some(before) + { + return Err(g213()); + } + if intent.settlement_rail() == EconomicRail::Evm && row["allowance_atomic"].as_u64() != Some(0) + { + return Err(g213()); + } + if intent.settlement_rail() != EconomicRail::Evm && !row["allowance_atomic"].is_null() { + return Err(g213()); + } + let units = number(row, "units", "simulation", SIMULATION_SCHEMA)?; + if intent.settlement_rail() == EconomicRail::Evm && units != 21_000 { + return Err(g213()); + } + if let Payment::Solana { compute, .. } = &intent.payment { + if units != *compute { + return Err(g213()); + } + } + let expires = number(row, "expires_at_ms", "simulation", SIMULATION_SCHEMA)?; + if expires <= plan.observed || expires > plan.expires { + return Err(g213()); + } + let plan_ref = doc_ref(PLAN_SCHEMA, &plan.doc); + let canonical_source=format!("{{\"schema\":\"{SIMULATION_SCHEMA}\",\"plan\":{},\"success\":true,\"fee_atomic\":{fee},\"balance_before_atomic\":{before},\"balance_after_atomic\":{after},\"allowance_atomic\":{},\"units\":{units},\"expires_at_ms\":{expires}}}\n",plan_ref,if intent.settlement_rail()==EconomicRail::Evm{"0"}else{"null"}); + if canonical_source != source { + return Err(g210("simulation", SIMULATION_SCHEMA)); + } + Ok(Simulation { + doc: Doc { + source: source.to_owned(), + digest: digest(SIMULATION_DOMAIN, source.as_bytes()), + }, + fee, + expires, + }) +} +fn parse_simulation_limited( + source: &str, + plan: &Plan, + intent: &Intent, + limits: &Limits, +) -> Result { + configured_document_limits(source, "simulation", limits.max_simulation_bytes, limits)?; + reserve_parse_sidecar(source, limits)?; + parse_simulation(source, plan, intent) +} + +fn make_approval_request( + run_id: &str, + policy: &Policy, + intent: &Intent, + plan: &Plan, + simulation: &Simulation, +) -> Result { + let mut count = CountSink::default(); + write_approval_request(&mut count, run_id, policy, intent, plan, simulation) + .map_err(|_| g217())?; + if count.0 > policy.limits.max_approval_request_bytes as usize { + return Err(g216( + "approval_request_bytes", + policy.limits.max_approval_request_bytes, + )); + } + let mut source = String::with_capacity(count.0); + write_approval_request(&mut source, run_id, policy, intent, plan, simulation) + .map_err(|_| g217())?; + canonical_policy_limited( + &source, + "approval request", + APPROVAL_REQUEST_SCHEMA, + policy.limits.max_approval_request_bytes, + policy.limits.max_json_depth, + )?; + Ok(Doc { + digest: digest(APPROVAL_REQUEST_DOMAIN, source.as_bytes()), + source, + }) +} +fn write_approval_request( + output: &mut W, + run_id: &str, + policy: &Policy, + intent: &Intent, + plan: &Plan, + simulation: &Simulation, +) -> fmt::Result { + output.write_str("{\"schema\":")?; + write_json(output, APPROVAL_REQUEST_SCHEMA)?; + output.write_str(",\"run_id\":")?; + write_json(output, run_id)?; + output.write_str(",\"wallet_id\":")?; + write_json(output, &intent.wallet_id)?; + output.write_str(",\"rail\":")?; + write_json(output, intent.settlement_rail().text())?; + let (network, asset) = intent.network_asset(); + output.write_str(",\"network\":")?; + write_json(output, network)?; + output.write_str(",\"asset\":")?; + write_json(output, asset)?; + output.write_str(",\"recipient\":")?; + write_json(output, intent.recipient())?; + write!( + output, + ",\"amount_atomic\":{},\"max_fee_atomic\":{}", + intent.amount(), + intent.max_fee() + )?; + let x402 = match &intent.payment { + Payment::X402 { + origin, + method, + resource, + .. + } => Some((origin.as_str(), method.as_str(), resource.as_str())), + _ => None, + }; + output.write_str(",\"origin\":")?; + write_optional_json(output, x402.map(|v| v.0))?; + output.write_str(",\"method\":")?; + write_optional_json(output, x402.map(|v| v.1))?; + output.write_str(",\"resource\":")?; + write_optional_json(output, x402.map(|v| v.2))?; + output.write_str(",\"policy\":")?; + write_doc_reference(output, POLICY_SCHEMA, &policy.digest, policy.source.len())?; + output.write_str(",\"intent\":")?; + write_doc_reference(output, INTENT_SCHEMA, &intent.digest, intent.source.len())?; + output.write_str(",\"plan\":")?; + write_doc_reference(output, PLAN_SCHEMA, &plan.doc.digest, plan.doc.source.len())?; + output.write_str(",\"simulation\":")?; + write_doc_reference( + output, + SIMULATION_SCHEMA, + &simulation.doc.digest, + simulation.doc.source.len(), + )?; + writeln!(output, ",\"expires_at_ms\":{}}}", simulation.expires) +} + +#[derive(Clone)] +struct Approval { + doc: Doc, +} +fn parse_approval( + source: &str, + policy: &Policy, + intent: &Intent, + plan: &Plan, + simulation: &Simulation, + request: &Doc, +) -> Result { + let (_, value) = canonical(source, "approval", APPROVAL_SCHEMA, MAX_APPROVAL_BYTES)?; + let row = object(&value, "approval", APPROVAL_SCHEMA)?; + if !keys( + row, + &[ + "schema", + "approval_id", + "approver_id", + "policy", + "intent", + "plan", + "simulation", + "approval_request", + "decision", + "approved_amount_atomic", + "approved_fee_atomic", + "expires_at_ms", + ], + ) { + return Err(g210("approval", APPROVAL_SCHEMA)); + } + let approval_expires = number(row, "expires_at_ms", "approval", APPROVAL_SCHEMA)?; + let approval_id = text(row, "approval_id", "approval", APPROVAL_SCHEMA)?; + let approver_id = text(row, "approver_id", "approval", APPROVAL_SCHEMA)?; + if approval_id.len() > policy.limits.max_identifier_bytes as usize + || approver_id.len() > policy.limits.max_identifier_bytes as usize + { + return Err(g216("identifier_bytes", policy.limits.max_identifier_bytes)); + } + if !identifier(approval_id) + || !identifier(approver_id) + || text(row, "decision", "approval", APPROVAL_SCHEMA)? != "approved" + || number(row, "approved_amount_atomic", "approval", APPROVAL_SCHEMA)? != intent.amount() + || number(row, "approved_fee_atomic", "approval", APPROVAL_SCHEMA)? != intent.max_fee() + || approval_expires <= plan.observed + || approval_expires > simulation.expires + { + return Err(g214()); + } + let refs = [ + ( + "policy", + POLICY_SCHEMA, + Doc { + source: policy.source.clone(), + digest: policy.digest.clone(), + }, + ), + ( + "intent", + INTENT_SCHEMA, + Doc { + source: intent.source.clone(), + digest: intent.digest.clone(), + }, + ), + ("plan", PLAN_SCHEMA, plan.doc.clone()), + ("simulation", SIMULATION_SCHEMA, simulation.doc.clone()), + ("approval_request", APPROVAL_REQUEST_SCHEMA, request.clone()), + ]; + if refs + .iter() + .any(|(key, schema, doc)| !ref_matches(&row[*key], schema, doc)) + { + return Err(g214()); + } + let canonical_source=format!("{{\"schema\":\"{APPROVAL_SCHEMA}\",\"approval_id\":{},\"approver_id\":{},\"policy\":{},\"intent\":{},\"plan\":{},\"simulation\":{},\"approval_request\":{},\"decision\":\"approved\",\"approved_amount_atomic\":{},\"approved_fee_atomic\":{},\"expires_at_ms\":{}}}\n",quote_json(text(row,"approval_id","approval",APPROVAL_SCHEMA)?),quote_json(text(row,"approver_id","approval",APPROVAL_SCHEMA)?),doc_ref(POLICY_SCHEMA,&Doc{source:policy.source.clone(),digest:policy.digest.clone()}),doc_ref(INTENT_SCHEMA,&Doc{source:intent.source.clone(),digest:intent.digest.clone()}),doc_ref(PLAN_SCHEMA,&plan.doc),doc_ref(SIMULATION_SCHEMA,&simulation.doc),doc_ref(APPROVAL_REQUEST_SCHEMA,request),intent.amount(),intent.max_fee(),number(row,"expires_at_ms","approval",APPROVAL_SCHEMA)?); + if canonical_source != source { + return Err(g210("approval", APPROVAL_SCHEMA)); + } + Ok(Approval { + doc: Doc { + source: source.to_owned(), + digest: digest(APPROVAL_DOMAIN, source.as_bytes()), + }, + }) +} +fn parse_approval_limited( + source: &str, + policy: &Policy, + intent: &Intent, + plan: &Plan, + simulation: &Simulation, + request: &Doc, +) -> Result { + configured_document_limits( + source, + "approval", + policy.limits.max_approval_bytes, + &policy.limits, + )?; + reserve_parse_sidecar(source, &policy.limits)?; + parse_approval(source, policy, intent, plan, simulation, request) +} +fn approval_expires(approval: &Approval) -> u64 { + serde_json::from_str::(approval.doc.source.trim_end()) + .ok() + .and_then(|value| value.get("expires_at_ms").and_then(Value::as_u64)) + .unwrap_or(0) +} + +#[derive(Clone, Copy, Eq, PartialEq)] +enum JournalState { + Reserved, + Prepared, + Approved, + Signed, + BroadcastUnknown, + Broadcasted, + Pending, + Confirmed, + Reorged, + Dropped, + Rejected, + Cancelled, + Failed, +} + +impl JournalState { + fn text(self) -> &'static str { + match self { + Self::Reserved => "reserved", + Self::Prepared => "prepared", + Self::Approved => "approved", + Self::Signed => "signed", + Self::BroadcastUnknown => "broadcast_unknown", + Self::Broadcasted => "broadcasted", + Self::Pending => "pending", + Self::Confirmed => "confirmed", + Self::Reorged => "reorged", + Self::Dropped => "dropped", + Self::Rejected => "rejected", + Self::Cancelled => "cancelled", + Self::Failed => "failed", + } + } + fn parse(value: &str) -> Option { + Some(match value { + "reserved" => Self::Reserved, + "prepared" => Self::Prepared, + "approved" => Self::Approved, + "signed" => Self::Signed, + "broadcast_unknown" => Self::BroadcastUnknown, + "broadcasted" => Self::Broadcasted, + "pending" => Self::Pending, + "confirmed" => Self::Confirmed, + "reorged" => Self::Reorged, + "dropped" => Self::Dropped, + "rejected" => Self::Rejected, + "cancelled" => Self::Cancelled, + "failed" => Self::Failed, + _ => return None, + }) + } +} + +#[derive(Clone)] +struct Journal { + idempotency_key: String, + version: u64, + policy: Doc, + intent: Doc, + run_id: String, + state: JournalState, + reserved_amount: u64, + reserved_fee: u64, + plan: Option, + simulation: Option, + approval: Option, + unsigned: Option<(String, usize, &'static str)>, + signed: Option<(String, usize)>, + broadcast: Option, + reconciliation: Option, + updated_at: u64, +} +fn journal_owned_bytes(journal: &Journal) -> Result { + let mut total = 0usize; + let mut add = |value: usize| -> Result<(), Diagnostic> { + total = total.checked_add(value).ok_or_else(g217)?; + Ok(()) + }; + for value in [ + journal.idempotency_key.len(), + journal.policy.source.len(), + journal.policy.digest.len(), + journal.intent.source.len(), + journal.intent.digest.len(), + journal.run_id.len(), + ] { + add(value)?; + } + for value in [&journal.plan, &journal.simulation, &journal.approval] + .into_iter() + .flatten() + { + add(value.digest.len())?; + } + if let Some((digest, _, _)) = &journal.unsigned { + add(digest.len())?; + } + if let Some((digest, _)) = &journal.signed { + add(digest.len())?; + } + for value in [&journal.broadcast, &journal.reconciliation] + .into_iter() + .flatten() + { + add(value.source.len())?; + add(value.digest.len())?; + } + Ok(total) +} +fn clone_journal_bounded(journal: &Journal, builder_max: u64) -> Result { + let bytes = journal_owned_bytes(journal)?; + if active_remaining().is_some_and(|remaining| bytes > remaining) || !reserve_active(bytes) { + return Err(g216("builder_bytes", builder_max)); + } + Ok(journal.clone()) +} + +fn optional_ref(schema: &str, doc: Option<&Doc>) -> String { + doc.map_or_else(|| "null".to_owned(), |value| doc_ref(schema, value)) +} +fn optional_typed_ref(schema: &str, value: Option<&DocRef>) -> String { + value.map_or_else( + || "null".to_owned(), + |value| { + format!( + "{{\"schema\":{},\"digest\":{},\"bytes\":{}}}", + quote_json(schema), + quote_json(&value.digest), + value.bytes + ) + }, + ) +} +fn optional_capsule(schema: &str, doc: Option<&Doc>) -> String { + doc.map_or_else( + || "null".to_owned(), + |value| { + format!( + "{{\"schema\":{},\"digest\":{},\"bytes\":{},\"document\":{}}}", + quote_json(schema), + quote_json(&value.digest), + value.source.len(), + quote_json(&value.source) + ) + }, + ) +} +fn optional_unsigned(value: Option<&(String, usize, &'static str)>) -> String { + value.map_or_else( + || "null".to_owned(), + |(digest_value, bytes, format)| { + format!( + "{{\"digest\":{},\"bytes\":{bytes},\"format\":{}}}", + quote_json(digest_value), + quote_json(format) + ) + }, + ) +} +fn optional_signed(value: Option<&(String, usize)>) -> String { + value.map_or_else( + || "null".to_owned(), + |(digest_value, bytes)| { + format!( + "{{\"digest\":{},\"bytes\":{bytes}}}", + quote_json(digest_value) + ) + }, + ) +} +fn render_journal(journal: &Journal) -> String { + let mut count = CountSink::default(); + write_journal(&mut count, journal).expect("journal count cannot fail"); + let mut output = String::with_capacity(count.0); + write_journal(&mut output, journal).expect("String writes cannot fail"); + output +} +fn write_optional_journal_ref( + output: &mut W, + schema: &str, + value: Option<&DocRef>, +) -> fmt::Result { + match value { + Some(value) => write_doc_reference(output, schema, &value.digest, value.bytes as usize), + None => output.write_str("null"), + } +} +fn write_capsule(output: &mut W, schema: &str, value: Option<&Doc>) -> fmt::Result { + match value { + Some(value) => { + output.write_str("{\"schema\":")?; + write_json(output, schema)?; + output.write_str(",\"digest\":")?; + write_json(output, &value.digest)?; + write!(output, ",\"bytes\":{},\"document\":", value.source.len())?; + write_json(output, &value.source)?; + output.write_char('}') + } + None => output.write_str("null"), + } +} +fn write_journal(output: &mut W, journal: &Journal) -> fmt::Result { + output.write_str("{\"schema\":\"")?; + output.write_str(JOURNAL_SCHEMA)?; + output.write_str("\",\"idempotency_key\":")?; + write_json(output, &journal.idempotency_key)?; + write!(output, ",\"version\":{},\"policy\":", journal.version)?; + write_doc_reference( + output, + POLICY_SCHEMA, + &journal.policy.digest, + journal.policy.source.len(), + )?; + output.write_str(",\"intent\":")?; + write_doc_reference( + output, + INTENT_SCHEMA, + &journal.intent.digest, + journal.intent.source.len(), + )?; + output.write_str(",\"run_id\":")?; + write_json(output, &journal.run_id)?; + output.write_str(",\"state\":")?; + write_json(output, journal.state.text())?; + write!( + output, + ",\"reserved_amount_atomic\":{},\"reserved_fee_atomic\":{},\"plan\":", + journal.reserved_amount, journal.reserved_fee + )?; + write_optional_journal_ref(output, PLAN_SCHEMA, journal.plan.as_ref())?; + output.write_str(",\"simulation\":")?; + write_optional_journal_ref(output, SIMULATION_SCHEMA, journal.simulation.as_ref())?; + output.write_str(",\"approval\":")?; + write_optional_journal_ref(output, APPROVAL_SCHEMA, journal.approval.as_ref())?; + output.write_str(",\"unsigned_transaction\":")?; + match journal.unsigned.as_ref() { + Some((digest_value, bytes, format)) => { + output.write_str("{\"digest\":")?; + write_json(output, digest_value)?; + write!(output, ",\"bytes\":{bytes},\"format\":")?; + write_json(output, format)?; + output.write_char('}')?; + } + None => output.write_str("null")?, + } + output.write_str(",\"signed_transaction\":")?; + match journal.signed.as_ref() { + Some((digest_value, bytes)) => { + output.write_str("{\"digest\":")?; + write_json(output, digest_value)?; + write!(output, ",\"bytes\":{bytes}}}")?; + } + None => output.write_str("null")?, + } + output.write_str(",\"broadcast\":")?; + write_capsule(output, BROADCAST_SCHEMA, journal.broadcast.as_ref())?; + output.write_str(",\"reconciliation\":")?; + write_capsule( + output, + RECONCILIATION_SCHEMA, + journal.reconciliation.as_ref(), + )?; + writeln!(output, ",\"updated_at_ms\":{}}}", journal.updated_at) +} + +#[derive(Clone)] +struct BroadcastReceipt { + doc: Doc, + transaction_id: String, + disposition: &'static str, + observed: u64, +} +fn parse_broadcast( + source: &str, + rail: EconomicRail, + network: &str, + signed_digest: &str, + expected_transaction_id: Option<&str>, +) -> Result { + parse_broadcast_mode( + source, + rail, + network, + signed_digest, + expected_transaction_id, + false, + ) +} +fn parse_broadcast_limited( + source: &str, + rail: EconomicRail, + network: &str, + signed_digest: &str, + expected_transaction_id: Option<&str>, + limits: &Limits, +) -> Result { + configured_document_limits( + source, + "broadcast receipt", + limits.max_broadcast_receipt_bytes, + limits, + )?; + reserve_parse_sidecar(source, limits)?; + parse_broadcast( + source, + rail, + network, + signed_digest, + expected_transaction_id, + ) +} +fn parse_provisional_broadcast( + source: &str, + rail: EconomicRail, + network: &str, + signed_digest: &str, + expected_transaction_id: &str, +) -> Result { + parse_broadcast_mode( + source, + rail, + network, + signed_digest, + Some(expected_transaction_id), + true, + ) +} +fn parse_broadcast_mode( + source: &str, + rail: EconomicRail, + network: &str, + signed_digest: &str, + expected_transaction_id: Option<&str>, + provisional: bool, +) -> Result { + let (_, value) = canonical( + source, + "broadcast receipt", + BROADCAST_SCHEMA, + MAX_BROADCAST_BYTES, + )?; + let row = object(&value, "broadcast receipt", BROADCAST_SCHEMA)?; + if !keys( + row, + &[ + "schema", + "rail", + "network", + "signed_transaction_digest", + "transaction_id", + "disposition", + "observed_at_ms", + ], + ) || text(row, "rail", "broadcast receipt", BROADCAST_SCHEMA)? != rail.text() + || text(row, "network", "broadcast receipt", BROADCAST_SCHEMA)? != network + || text( + row, + "signed_transaction_digest", + "broadcast receipt", + BROADCAST_SCHEMA, + )? != signed_digest + { + return Err(g213()); + } + let transaction_id = + text(row, "transaction_id", "broadcast receipt", BROADCAST_SCHEMA)?.to_owned(); + if expected_transaction_id.is_some_and(|expected| expected != transaction_id) { + return Err(g213()); + } + let disposition = match text(row, "disposition", "broadcast receipt", BROADCAST_SCHEMA)? { + "accepted" => "accepted", + "pending" => "pending", + "unknown" => "unknown", + "rejected" => "rejected", + _ => return Err(g210("broadcast receipt", BROADCAST_SCHEMA)), + }; + let observed = number(row, "observed_at_ms", "broadcast receipt", BROADCAST_SCHEMA)?; + if provisional { + if disposition != "unknown" || observed != 0 { + return Err(g213()); + } + } else if observed == 0 { + return Err(g213()); + } + let canonical_source=format!("{{\"schema\":\"{BROADCAST_SCHEMA}\",\"rail\":{},\"network\":{},\"signed_transaction_digest\":{},\"transaction_id\":{},\"disposition\":{},\"observed_at_ms\":{observed}}}\n",quote_json(rail.text()),quote_json(network),quote_json(signed_digest),quote_json(&transaction_id),quote_json(disposition)); + if canonical_source != source { + return Err(g210("broadcast receipt", BROADCAST_SCHEMA)); + } + Ok(BroadcastReceipt { + doc: Doc { + source: source.to_owned(), + digest: digest(BROADCAST_DOMAIN, source.as_bytes()), + }, + transaction_id, + disposition, + observed, + }) +} + +#[derive(Clone)] +struct Reconciliation { + doc: Doc, + status: &'static str, + transaction_id: String, + observed: u64, + confirmations: Option, +} +fn nullable_u64(value: &Value) -> Option> { + if value.is_null() { + Some(None) + } else { + value.as_u64().map(Some) + } +} +fn nullable_text(value: &Value) -> Option> { + if value.is_null() { + Some(None) + } else { + value.as_str().map(|text| Some(text.to_owned())) + } +} +fn parse_reconciliation( + source: &str, + rail: EconomicRail, + network: &str, + transaction_id: &str, +) -> Result { + parse_reconciliation_with_identifier_limit( + source, + rail, + network, + transaction_id, + MAX_IDENTIFIER_BYTES as u64, + ) +} + +fn parse_reconciliation_with_identifier_limit( + source: &str, + rail: EconomicRail, + network: &str, + transaction_id: &str, + max_identifier_bytes: u64, +) -> Result { + let (_, value) = canonical( + source, + "reconciliation", + RECONCILIATION_SCHEMA, + MAX_RECONCILIATION_BYTES, + )?; + let row = object(&value, "reconciliation", RECONCILIATION_SCHEMA)?; + if !keys( + row, + &[ + "schema", + "rail", + "network", + "transaction_id", + "status", + "observed_at_ms", + "observed_height", + "confirmations", + "canonical_block_id", + ], + ) || text(row, "rail", "reconciliation", RECONCILIATION_SCHEMA)? != rail.text() + || text(row, "network", "reconciliation", RECONCILIATION_SCHEMA)? != network + || text( + row, + "transaction_id", + "reconciliation", + RECONCILIATION_SCHEMA, + )? != transaction_id + { + return Err(g215()); + } + let status = match text(row, "status", "reconciliation", RECONCILIATION_SCHEMA)? { + "pending" => "pending", + "confirmed" => "confirmed", + "reorged" => "reorged", + "dropped" => "dropped", + _ => return Err(g210("reconciliation", RECONCILIATION_SCHEMA)), + }; + let observed = number( + row, + "observed_at_ms", + "reconciliation", + RECONCILIATION_SCHEMA, + )?; + let height = nullable_u64(&row["observed_height"]) + .ok_or_else(|| g210("reconciliation", RECONCILIATION_SCHEMA))?; + let confirmations = nullable_u64(&row["confirmations"]) + .ok_or_else(|| g210("reconciliation", RECONCILIATION_SCHEMA))?; + let block = nullable_text(&row["canonical_block_id"]) + .ok_or_else(|| g210("reconciliation", RECONCILIATION_SCHEMA))?; + if transaction_id.len() > max_identifier_bytes as usize + || block + .as_deref() + .is_some_and(|value| value.len() > max_identifier_bytes as usize) + { + return Err(g216("identifier_bytes", max_identifier_bytes)); + } + if status == "confirmed" && (height.is_none() || confirmations.is_none() || block.is_none()) { + return Err(g215()); + } + let canonical_source=format!("{{\"schema\":\"{RECONCILIATION_SCHEMA}\",\"rail\":{},\"network\":{},\"transaction_id\":{},\"status\":{},\"observed_at_ms\":{observed},\"observed_height\":{},\"confirmations\":{},\"canonical_block_id\":{}}}\n",quote_json(rail.text()),quote_json(network),quote_json(transaction_id),quote_json(status),height.map_or_else(||"null".to_owned(),|v|v.to_string()),confirmations.map_or_else(||"null".to_owned(),|v|v.to_string()),block.as_deref().map_or_else(||"null".to_owned(),quote_json)); + if canonical_source != source { + return Err(g210("reconciliation", RECONCILIATION_SCHEMA)); + } + Ok(Reconciliation { + doc: Doc { + source: source.to_owned(), + digest: digest(RECONCILIATION_DOMAIN, source.as_bytes()), + }, + status, + transaction_id: transaction_id.to_owned(), + observed, + confirmations, + }) +} +fn parse_reconciliation_limited( + source: &str, + rail: EconomicRail, + network: &str, + transaction_id: &str, + limits: &Limits, +) -> Result { + configured_document_limits( + source, + "reconciliation", + limits.max_reconciliation_bytes, + limits, + )?; + reserve_parse_sidecar(source, limits)?; + parse_reconciliation_with_identifier_limit( + source, + rail, + network, + transaction_id, + limits.max_identifier_bytes, + ) +} + +fn capsule_doc( + value: &Value, + schema: &str, + domain: &[u8], + maximum: usize, + max_depth: u64, + document: &str, +) -> Result, Diagnostic> { + if value.is_null() { + return Ok(None); + } + let row = object(value, "journal", JOURNAL_SCHEMA)?; + if !keys(row, &["schema", "digest", "bytes", "document"]) { + return Err(g215()); + } + let source = text(row, "document", "journal", JOURNAL_SCHEMA)?.to_owned(); + let sidecar = source + .len() + .checked_mul( + usize::try_from(max_depth) + .map_err(|_| g217())? + .checked_add(2) + .ok_or_else(g217)?, + ) + .ok_or_else(g217)?; + if active_remaining().is_some_and(|remaining| sidecar > remaining) || !reserve_active(sidecar) { + return Err(g216( + "builder_bytes", + active_limit().unwrap_or(MAX_BUILDER_BYTES) as u64, + )); + } + if source.len() > maximum + || row.get("schema").and_then(Value::as_str) != Some(schema) + || row.get("bytes").and_then(Value::as_u64) != u64::try_from(source.len()).ok() + { + return Err(g215()); + } + canonical_policy_limited(&source, document, schema, maximum as u64, max_depth)?; + let digest_value = digest(domain, source.as_bytes()); + if row.get("digest").and_then(Value::as_str) != Some(digest_value.as_str()) { + return Err(g215()); + } + Ok(Some(Doc { + source, + digest: digest_value, + })) +} +fn generic_ref_doc( + value: &Value, + schema: &str, + maximum: u64, +) -> Result, Diagnostic> { + if value.is_null() { + return Ok(None); + } + let row = object(value, "journal", JOURNAL_SCHEMA)?; + if !keys(row, &["schema", "digest", "bytes"]) + || row.get("schema").and_then(Value::as_str) != Some(schema) + { + return Err(g215()); + } + let digest_value = text(row, "digest", "journal", JOURNAL_SCHEMA)?.to_owned(); + let bytes = number(row, "bytes", "journal", JOURNAL_SCHEMA)?; + if bytes > maximum + || !digest_value.starts_with("sha256:") + || digest_value.len() != 71 + || !digest_value[7..] + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + { + return Err(g215()); + } + Ok(Some(DocRef { + bytes, + digest: digest_value, + })) +} +fn unsigned_journal_ref( + value: &Value, + maximum: u64, +) -> Result, Diagnostic> { + if value.is_null() { + return Ok(None); + } + let row = object(value, "journal", JOURNAL_SCHEMA)?; + if !keys(row, &["digest", "bytes", "format"]) { + return Err(g215()); + } + let digest_value = text(row, "digest", "journal", JOURNAL_SCHEMA)?.to_owned(); + let bytes = number(row, "bytes", "journal", JOURNAL_SCHEMA)?; + let format = match text(row, "format", "journal", JOURNAL_SCHEMA)? { + "eip1559-unsigned-v1" => "eip1559-unsigned-v1", + "solana-message-v0" => "solana-message-v0", + "psbt-v2" => "psbt-v2", + _ => return Err(g215()), + }; + if bytes > maximum || digest_value.len() != 71 || !digest_value.starts_with("sha256:") { + return Err(g215()); + } + Ok(Some((digest_value, bytes as usize, format))) +} +fn signed_journal_ref(value: &Value, maximum: u64) -> Result, Diagnostic> { + if value.is_null() { + return Ok(None); + } + let row = object(value, "journal", JOURNAL_SCHEMA)?; + if !keys(row, &["digest", "bytes"]) { + return Err(g215()); + } + let digest_value = text(row, "digest", "journal", JOURNAL_SCHEMA)?.to_owned(); + let bytes = number(row, "bytes", "journal", JOURNAL_SCHEMA)?; + if bytes > maximum || digest_value.len() != 71 || !digest_value.starts_with("sha256:") { + return Err(g215()); + } + Ok(Some((digest_value, bytes as usize))) +} +enum JournalParseFailure { + BindingMismatch, + Diagnostic(Diagnostic), +} + +impl From for JournalParseFailure { + fn from(diagnostic: Diagnostic) -> Self { + Self::Diagnostic(diagnostic) + } +} + +fn parse_journal( + source: &str, + policy: &Policy, + intent: &Intent, + run_id: &str, +) -> Result { + parse_journal_classified(source, policy, intent, run_id).map_err(|failure| match failure { + JournalParseFailure::BindingMismatch => g215(), + JournalParseFailure::Diagnostic(diagnostic) => diagnostic, + }) +} + +fn parse_journal_classified( + source: &str, + policy: &Policy, + intent: &Intent, + run_id: &str, +) -> Result { + configured_document_limits( + source, + "journal", + policy.limits.max_journal_bytes, + &policy.limits, + )?; + let sidecar = source.len().checked_mul(2).ok_or_else(g217)?; + if active_remaining().is_some_and(|remaining| sidecar > remaining) || !reserve_active(sidecar) { + return Err(g216("builder_bytes", policy.limits.max_builder_bytes).into()); + } + let (_, value) = canonical( + source, + "journal", + JOURNAL_SCHEMA, + policy.limits.max_journal_bytes as usize, + )?; + let row = object(&value, "journal", JOURNAL_SCHEMA)?; + if !keys( + row, + &[ + "schema", + "idempotency_key", + "version", + "policy", + "intent", + "run_id", + "state", + "reserved_amount_atomic", + "reserved_fee_atomic", + "plan", + "simulation", + "approval", + "unsigned_transaction", + "signed_transaction", + "broadcast", + "reconciliation", + "updated_at_ms", + ], + ) { + return Err(g215().into()); + } + let policy_doc = Doc { + source: policy.source.clone(), + digest: policy.digest.clone(), + }; + let intent_doc = Doc { + source: intent.source.clone(), + digest: intent.digest.clone(), + }; + if text(row, "idempotency_key", "journal", JOURNAL_SCHEMA)? != intent.idempotency_key + || text(row, "run_id", "journal", JOURNAL_SCHEMA)? != run_id + || !ref_matches(&row["policy"], POLICY_SCHEMA, &policy_doc) + || !ref_matches(&row["intent"], INTENT_SCHEMA, &intent_doc) + { + return Err(JournalParseFailure::BindingMismatch); + } + let broadcast = capsule_doc( + &row["broadcast"], + BROADCAST_SCHEMA, + BROADCAST_DOMAIN, + policy.limits.max_broadcast_receipt_bytes as usize, + policy.limits.max_json_depth, + "broadcast receipt", + )?; + let reconciliation = capsule_doc( + &row["reconciliation"], + RECONCILIATION_SCHEMA, + RECONCILIATION_DOMAIN, + policy.limits.max_reconciliation_bytes as usize, + policy.limits.max_json_depth, + "reconciliation", + )?; + let journal = Journal { + idempotency_key: intent.idempotency_key.clone(), + version: number(row, "version", "journal", JOURNAL_SCHEMA)?, + policy: policy_doc, + intent: intent_doc, + run_id: run_id.to_owned(), + state: JournalState::parse(text(row, "state", "journal", JOURNAL_SCHEMA)?) + .ok_or_else(g215)?, + reserved_amount: number(row, "reserved_amount_atomic", "journal", JOURNAL_SCHEMA)?, + reserved_fee: number(row, "reserved_fee_atomic", "journal", JOURNAL_SCHEMA)?, + plan: generic_ref_doc(&row["plan"], PLAN_SCHEMA, policy.limits.max_plan_bytes)?, + simulation: generic_ref_doc( + &row["simulation"], + SIMULATION_SCHEMA, + policy.limits.max_simulation_bytes, + )?, + approval: generic_ref_doc( + &row["approval"], + APPROVAL_SCHEMA, + policy.limits.max_approval_bytes, + )?, + unsigned: unsigned_journal_ref( + &row["unsigned_transaction"], + policy.limits.max_unsigned_transaction_bytes, + )?, + signed: signed_journal_ref( + &row["signed_transaction"], + policy.limits.max_signed_transaction_bytes, + )?, + broadcast, + reconciliation, + updated_at: number(row, "updated_at_ms", "journal", JOURNAL_SCHEMA)?, + }; + if journal.reserved_amount != intent.amount() || journal.reserved_fee != intent.max_fee() { + return Err(g215().into()); + } + let prepared = + journal.plan.is_some() && journal.simulation.is_some() && journal.unsigned.is_some(); + let approved = prepared && journal.approval.is_some(); + let signed = approved && journal.signed.is_some(); + let broadcasted = signed && journal.broadcast.is_some(); + let reserved_prefix = journal.plan.is_none() + && journal.simulation.is_none() + && journal.approval.is_none() + && journal.unsigned.is_none() + && journal.signed.is_none() + && journal.broadcast.is_none() + && journal.reconciliation.is_none(); + let prepared_prefix = prepared + && journal.approval.is_none() + && journal.signed.is_none() + && journal.broadcast.is_none() + && journal.reconciliation.is_none(); + let approved_prefix = approved + && journal.signed.is_none() + && journal.broadcast.is_none() + && journal.reconciliation.is_none(); + let valid_shape = match journal.state { + JournalState::Reserved => reserved_prefix, + JournalState::Prepared => prepared_prefix, + JournalState::Approved => approved_prefix, + JournalState::Signed => { + signed && journal.broadcast.is_none() && journal.reconciliation.is_none() + } + JournalState::BroadcastUnknown | JournalState::Broadcasted => { + broadcasted && journal.reconciliation.is_none() + } + JournalState::Pending => broadcasted, + JournalState::Confirmed | JournalState::Reorged | JournalState::Dropped => { + broadcasted && journal.reconciliation.is_some() + } + JournalState::Rejected => { + (reserved_prefix || prepared_prefix || approved_prefix) + || (broadcasted && journal.reconciliation.is_none()) + } + JournalState::Cancelled | JournalState::Failed => { + reserved_prefix || prepared_prefix || approved_prefix + } + }; + let version_shape = match journal.state { + JournalState::Reserved => journal.version == 1, + JournalState::Prepared => journal.version == 2, + JournalState::Approved => matches!(journal.version, 3 | 4), + JournalState::Signed => journal.version == 5, + JournalState::BroadcastUnknown => journal.version >= 6, + JournalState::Broadcasted + | JournalState::Pending + | JournalState::Confirmed + | JournalState::Reorged + | JournalState::Dropped + | JournalState::Rejected => journal.version >= 7, + JournalState::Cancelled | JournalState::Failed => journal.version >= 2, + }; + if !valid_shape || !version_shape { + return Err(g215().into()); + } + if let Some(broadcast_doc) = journal.broadcast.as_ref() { + let signed_digest = journal.signed.as_ref().ok_or_else(g215)?.0.as_str(); + let (network, _) = intent.network_asset(); + let provisional = broadcast_is_provisional(broadcast_doc); + let base = if provisional { 6 } else { 7 }; + let offset = journal.version.checked_sub(base).ok_or_else(g215)?; + let attempts = offset.checked_add(1).ok_or_else(g215)? / 2; + let odd = offset % 2 == 1; + if attempts > policy.limits.max_reconciliations + || (journal.reconciliation.is_some() && odd) + || (matches!( + journal.state, + JournalState::Confirmed | JournalState::Reorged | JournalState::Dropped + ) && (journal.reconciliation.is_none() || attempts == 0 || odd)) + { + return Err(g215().into()); + } + let broadcast = if provisional { + let value: Value = + serde_json::from_str(broadcast_doc.source.trim_end()).map_err(|_| g215())?; + let transaction_id = value["transaction_id"].as_str().ok_or_else(g215)?; + parse_provisional_broadcast( + &broadcast_doc.source, + intent.settlement_rail(), + network, + signed_digest, + transaction_id, + )? + } else { + parse_broadcast( + &broadcast_doc.source, + intent.settlement_rail(), + network, + signed_digest, + None, + )? + }; + let allowed_disposition = match journal.state { + JournalState::BroadcastUnknown => broadcast.disposition == "unknown", + JournalState::Broadcasted => broadcast.disposition == "accepted", + JournalState::Pending if journal.reconciliation.is_none() => { + broadcast.disposition == "pending" + } + JournalState::Pending => matches!(broadcast.disposition, "accepted" | "pending"), + JournalState::Confirmed | JournalState::Reorged | JournalState::Dropped => { + matches!(broadcast.disposition, "accepted" | "pending") + } + JournalState::Rejected => broadcast.disposition == "rejected", + _ => false, + }; + if !allowed_disposition { + return Err(g215().into()); + } + if let Some(reconciliation_doc) = journal.reconciliation.as_ref() { + let reconciliation = parse_reconciliation( + &reconciliation_doc.source, + intent.settlement_rail(), + network, + &broadcast.transaction_id, + )?; + validate_confirmation(intent, &reconciliation)?; + let status_matches = match journal.state { + JournalState::Pending => reconciliation.status == "pending", + JournalState::Confirmed => reconciliation.status == "confirmed", + JournalState::Reorged => reconciliation.status == "reorged", + JournalState::Dropped => reconciliation.status == "dropped", + _ => false, + }; + if reconciliation.observed < broadcast.observed + || reconciliation.observed != journal.updated_at + || !status_matches + { + return Err(g215().into()); + } + } else if journal.updated_at != broadcast.observed + && !(journal.state == JournalState::BroadcastUnknown + && provisional + && broadcast.disposition == "unknown" + && broadcast.observed == 0) + { + return Err(g215().into()); + } + } + Ok(journal) +} + +fn broadcast_is_provisional(document: &Doc) -> bool { + serde_json::from_str::(document.source.trim_end()) + .ok() + .is_some_and(|value| { + value["disposition"].as_str() == Some("unknown") + && value["observed_at_ms"].as_u64() == Some(0) + }) +} + +fn reconciliation_topology(journal: &Journal) -> Result<(u64, bool), Diagnostic> { + let broadcast = journal.broadcast.as_ref().ok_or_else(g215)?; + let base = if broadcast_is_provisional(broadcast) { + 6 + } else { + 7 + }; + let offset = journal.version.checked_sub(base).ok_or_else(g215)?; + let attempts = offset.checked_add(1).ok_or_else(g215)? / 2; + Ok((attempts, offset % 2 == 1)) +} + +#[derive(Default)] +struct CountSink(usize); +impl fmt::Write for CountSink { + fn write_str(&mut self, value: &str) -> fmt::Result { + self.0 = self.0.checked_add(value.len()).ok_or(fmt::Error)?; + Ok(()) + } +} + +struct MatchSink<'a> { + expected: &'a [u8], + offset: usize, +} + +struct DigestSink { + hash: Sha256, + bytes: usize, +} +impl DigestSink { + fn new(domain: &[u8]) -> Self { + let mut hash = Sha256::new(); + hash.update(domain); + Self { hash, bytes: 0 } + } + fn finish(self) -> (String, usize) { + (format!("sha256:{:x}", self.hash.finalize()), self.bytes) + } +} +impl fmt::Write for DigestSink { + fn write_str(&mut self, value: &str) -> fmt::Result { + self.bytes = self.bytes.checked_add(value.len()).ok_or(fmt::Error)?; + self.hash.update(value.as_bytes()); + Ok(()) + } +} +impl fmt::Write for MatchSink<'_> { + fn write_str(&mut self, value: &str) -> fmt::Result { + let end = self.offset.checked_add(value.len()).ok_or(fmt::Error)?; + if self.expected.get(self.offset..end) != Some(value.as_bytes()) { + return Err(fmt::Error); + } + self.offset = end; + Ok(()) + } +} + +fn write_json(output: &mut W, value: &str) -> fmt::Result { + output.write_char('"')?; + for character in value.chars() { + match character { + '"' => output.write_str("\\\"")?, + '\\' => output.write_str("\\\\")?, + '\n' => output.write_str("\\n")?, + '\r' => output.write_str("\\r")?, + '\t' => output.write_str("\\t")?, + value if value.is_control() => write!(output, "\\u{:04x}", value as u32)?, + value => output.write_char(value)?, + } + } + output.write_char('"') +} +fn write_optional_json(output: &mut W, value: Option<&str>) -> fmt::Result { + match value { + Some(value) => write_json(output, value), + None => output.write_str("null"), + } +} +fn write_usage(output: &mut W, usage: &Usage) -> fmt::Result { + write!(output,"{{\"journal_reads\":{},\"journal_writes\":{},\"invoice_reads\":{},\"snapshot_reads\":{},\"simulations\":{},\"approvals\":{},\"signatures\":{},\"broadcasts\":{},\"reconciliations\":{},\"input_bytes\":{},\"output_bytes\":{},\"elapsed_ms\":{}}}",usage.journal_reads,usage.journal_writes,usage.invoice_reads,usage.snapshot_reads,usage.simulations,usage.approvals,usage.signatures,usage.broadcasts,usage.reconciliations,usage.input_bytes,usage.output_bytes,usage.elapsed_ms) +} +fn write_event(output: &mut W, index: usize, event: &Event) -> fmt::Result { + write!(output, "{{\"index\":{index},\"kind\":")?; + write_json(output, event.kind)?; + output.write_str(",\"rail\":")?; + write_optional_json(output, event.rail.map(EconomicRail::text))?; + output.write_str(",\"input_digest\":")?; + write_optional_json(output, event.input.as_deref())?; + output.write_str(",\"output_digest\":")?; + write_optional_json(output, event.output.as_deref())?; + output.write_str(",\"status\":")?; + write_json(output, event.status)?; + output.write_str(",\"usage\":")?; + write_usage(output, &event.usage)?; + output.write_char('}') +} +fn write_result(output: &mut W, terminal: &Terminal) -> fmt::Result { + output.write_str("{\"status\":")?; + write_json(output, terminal.status.text())?; + output.write_str(",\"transaction_id\":")?; + write_optional_json(output, terminal.transaction_id.as_deref())?; + output.write_str(",\"confirmation_status\":")?; + write_optional_json(output, terminal.confirmation.as_deref())?; + output.write_str(",\"code\":")?; + write_optional_json(output, terminal.code.as_deref())?; + output.write_str(",\"message\":")?; + write_optional_json(output, terminal.message.as_deref())?; + output.write_char('}') +} +fn write_nonclaims(output: &mut W) -> fmt::Result { + output.write_char('[')?; + for (index, value) in NONCLAIMS.iter().enumerate() { + if index > 0 { + output.write_char(',')?; + } + write_json(output, value)?; + } + output.write_char(']') +} +fn write_trace( + output: &mut W, + run_id: &str, + source_agent_digest: &str, + policy: &Policy, + intent: &Intent, + events: &[Event], + terminal: &Terminal, +) -> fmt::Result { + output.write_str("{\"schema\":\"")?; + output.write_str(TRACE_SCHEMA)?; + output.write_str("\",\"run_id\":")?; + write_json(output, run_id)?; + output.write_str(",\"source_agent_evidence_digest\":")?; + write_json(output, source_agent_digest)?; + output.write_str(",\"policy_digest\":")?; + write_json(output, &policy.digest)?; + output.write_str(",\"intent_digest\":")?; + write_json(output, &intent.digest)?; + output.write_str(",\"events\":[")?; + for (index, event) in events.iter().enumerate() { + if index > 0 { + output.write_char(',')?; + } + write_event(output, index, event)?; + } + output.write_str("],\"result\":")?; + write_result(output, terminal)?; + output.write_str(",\"nonclaims\":")?; + write_nonclaims(output)?; + output.write_str("}\n") +} + +fn usage_json(usage: &Usage) -> String { + let mut output = String::new(); + write_usage(&mut output, usage).expect("String writes cannot fail"); + output +} +fn event_json(index: usize, event: &Event) -> String { + let mut output = String::new(); + write_event(&mut output, index, event).expect("String writes cannot fail"); + output +} +fn result_json(terminal: &Terminal) -> String { + let mut output = String::new(); + write_result(&mut output, terminal).expect("String writes cannot fail"); + output +} +fn render_trace( + run_id: &str, + source_agent_digest: &str, + policy: &Policy, + intent: &Intent, + events: &[Event], + terminal: &Terminal, +) -> Result { + if events.len() > policy.limits.max_trace_events as usize { + return Err(g216("trace_events", policy.limits.max_trace_events)); + } + let mut count = CountSink::default(); + write_trace( + &mut count, + run_id, + source_agent_digest, + policy, + intent, + events, + terminal, + ) + .map_err(|_| g217())?; + if count.0 > policy.limits.max_trace_bytes as usize { + return Err(g216("trace_bytes", policy.limits.max_trace_bytes)); + } + if !reserve_active(count.0) { + return Err(g216("builder_bytes", policy.limits.max_builder_bytes)); + } + let mut source = String::with_capacity(count.0); + write_trace( + &mut source, + run_id, + source_agent_digest, + policy, + intent, + events, + terminal, + ) + .map_err(|_| g217())?; + if source.len() != count.0 { + return Err(g217()); + } + Ok(Doc { + digest: digest(TRACE_DOMAIN, source.as_bytes()), + source, + }) +} +fn limits_evidence_json(l: &Limits) -> String { + limits_json(l) +} +fn budget_json(b: &Budget) -> String { + let mut output = String::new(); + write_budget(&mut output, b).expect("String writes cannot fail"); + output +} +fn write_budget(output: &mut W, b: &Budget) -> fmt::Result { + write!(output,"{{\"used_policy_bytes\":{},\"used_intent_bytes\":{},\"used_invoice_bytes\":{},\"used_snapshot_bytes\":{},\"used_plan_bytes\":{},\"used_simulation_bytes\":{},\"used_approval_request_bytes\":{},\"used_approval_bytes\":{},\"used_journal_bytes\":{},\"used_unsigned_transaction_bytes\":{},\"used_signed_transaction_bytes\":{},\"used_broadcast_receipt_bytes\":{},\"used_reconciliation_bytes\":{},\"used_trace_events\":{},\"used_trace_bytes\":{},\"used_evidence_bytes\":{},\"used_builder_bytes\":{},\"used_recipients\":{},\"used_network_policies\":{},\"used_x402_origins\":{},\"used_utxos\":{},\"used_reconciliations\":{},\"used_elapsed_ms\":{},\"used_concurrency\":{},\"used_unexpected_authority_calls\":{}}}",b.policy_bytes,b.intent_bytes,b.invoice_bytes,b.snapshot_bytes,b.plan_bytes,b.simulation_bytes,b.approval_request_bytes,b.approval_bytes,b.journal_bytes,b.unsigned_bytes,b.signed_bytes,b.broadcast_bytes,b.reconciliation_bytes,b.trace_events,b.trace_bytes,b.evidence_bytes,b.builder_bytes,b.recipients,b.network_policies,b.x402_origins,b.utxos,b.reconciliations,b.elapsed_ms,b.concurrency,b.unexpected_authority_calls) +} +struct EvidenceParts<'a> { + run_id: &'a str, + agent_run_id: &'a str, + agent_evidence: &'a str, + agent_digest: &'a str, + policy: &'a Policy, + intent: &'a Intent, + invoice: Option<&'a Invoice>, + plan: Option<&'a Plan>, + simulation: Option<&'a Simulation>, + approval: Option<&'a Approval>, + journal: &'a Journal, + broadcast: Option<&'a BroadcastReceipt>, + reconciliation: Option<&'a Reconciliation>, + trace: &'a Doc, + terminal: &'a Terminal, + budget: &'a mut Budget, +} + +fn write_doc_reference( + output: &mut W, + schema: &str, + digest_value: &str, + bytes: usize, +) -> fmt::Result { + output.write_str("{\"schema\":")?; + write_json(output, schema)?; + output.write_str(",\"digest\":")?; + write_json(output, digest_value)?; + write!(output, ",\"bytes\":{bytes}}}") +} + +fn write_optional_reference( + output: &mut W, + schema: &str, + document: Option<&Doc>, +) -> fmt::Result { + match document { + Some(document) => { + write_doc_reference(output, schema, &document.digest, document.source.len()) + } + None => output.write_str("null"), + } +} + +fn write_evidence( + output: &mut W, + parts: &EvidenceParts<'_>, + journal_digest: &str, + journal_bytes: usize, +) -> fmt::Result { + output.write_str("{\"schema\":\"")?; + output.write_str(EVIDENCE_SCHEMA)?; + output.write_str("\",\"run_id\":")?; + write_json(output, parts.run_id)?; + output.write_str( + ",\"source_agent\":{\"schema\":\"semaprax.agent-runtime-evidence.v1\",\"digest\":", + )?; + write_json(output, parts.agent_digest)?; + write!( + output, + ",\"bytes\":{},\"run_id\":", + parts.agent_evidence.len() + )?; + write_json(output, parts.agent_run_id)?; + output.write_str("},\"policy\":")?; + write_doc_reference( + output, + POLICY_SCHEMA, + &parts.policy.digest, + parts.policy.source.len(), + )?; + output.write_str(",\"intent\":")?; + write_doc_reference( + output, + INTENT_SCHEMA, + &parts.intent.digest, + parts.intent.source.len(), + )?; + output.write_str(",\"x402_invoice\":")?; + write_optional_reference(output, INVOICE_SCHEMA, parts.invoice.map(|v| &v.doc))?; + output.write_str(",\"plan\":")?; + write_optional_reference(output, PLAN_SCHEMA, parts.plan.map(|v| &v.doc))?; + output.write_str(",\"simulation\":")?; + write_optional_reference(output, SIMULATION_SCHEMA, parts.simulation.map(|v| &v.doc))?; + output.write_str(",\"approval\":")?; + write_optional_reference(output, APPROVAL_SCHEMA, parts.approval.map(|v| &v.doc))?; + output.write_str(",\"journal\":")?; + write_doc_reference(output, JOURNAL_SCHEMA, journal_digest, journal_bytes)?; + output.write_str(",\"broadcast\":")?; + write_optional_reference(output, BROADCAST_SCHEMA, parts.broadcast.map(|v| &v.doc))?; + output.write_str(",\"reconciliation\":")?; + write_optional_reference( + output, + RECONCILIATION_SCHEMA, + parts.reconciliation.map(|v| &v.doc), + )?; + output.write_str(",\"trace\":{\"schema\":")?; + write_json(output, TRACE_SCHEMA)?; + output.write_str(",\"digest\":")?; + write_json(output, &parts.trace.digest)?; + write!( + output, + ",\"bytes\":{},\"document\":", + parts.trace.source.len() + )?; + write_json(output, &parts.trace.source)?; + output.write_str("},\"result\":")?; + write_result(output, parts.terminal)?; + output.write_str(",\"limits\":")?; + write_limits(output, &parts.policy.limits)?; + output.write_str(",\"budget\":")?; + write_budget(output, parts.budget)?; + output.write_str(",\"nonclaims\":")?; + write_nonclaims(output)?; + output.write_str("}\n") +} +fn journal_identity(parts: &EvidenceParts<'_>) -> (String, usize) { + let mut sink = DigestSink::new(JOURNAL_DOMAIN); + write_journal(&mut sink, parts.journal).expect("journal identity cannot fail"); + sink.finish() +} + +fn render_evidence(parts: &mut EvidenceParts<'_>) -> Result { + let (journal_digest, journal_bytes) = journal_identity(parts); + let builder_before_evidence = parts + .policy + .limits + .max_builder_bytes + .checked_sub(active_remaining().ok_or_else(g217)? as u64) + .ok_or_else(g217)?; + let mut evidence_bytes = 0; + let mut builder_bytes = builder_before_evidence; + let mut converged = false; + for _ in 0..24 { + parts.budget.evidence_bytes = evidence_bytes; + parts.budget.builder_bytes = builder_bytes; + let mut count = CountSink::default(); + write_evidence(&mut count, parts, &journal_digest, journal_bytes).map_err(|_| g217())?; + let next_evidence = u64::try_from(count.0).map_err(|_| g217())?; + let next_builder = builder_before_evidence + .checked_add(next_evidence) + .ok_or_else(g217)?; + if next_evidence == evidence_bytes && next_builder == builder_bytes { + converged = true; + break; + } + evidence_bytes = next_evidence; + builder_bytes = next_builder; + } + if !converged { + return Err(g217()); + } + if evidence_bytes > parts.policy.limits.max_evidence_bytes { + return Err(g216( + "evidence_bytes", + parts.policy.limits.max_evidence_bytes, + )); + } + parts.budget.evidence_bytes = evidence_bytes; + parts.budget.builder_bytes = builder_bytes; + let evidence_len = usize::try_from(evidence_bytes).map_err(|_| g217())?; + if !reserve_active(evidence_len) { + return Err(g216("builder_bytes", parts.policy.limits.max_builder_bytes)); + } + let mut source = String::with_capacity(evidence_len); + write_evidence(&mut source, parts, &journal_digest, journal_bytes).map_err(|_| g217())?; + if source.len() != evidence_len { + return Err(g217()); + } + Ok(Doc { + digest: digest(EVIDENCE_DOMAIN, source.as_bytes()), + source, + }) +} + +fn run_id( + agent_digest: &str, + policy_digest: &str, + intent_digest: &str, + idempotency: &str, +) -> String { + let mut hash = Sha256::new(); + hash.update(RUN_ID_DOMAIN); + hash.update(agent_digest.as_bytes()); + hash.update(policy_digest.as_bytes()); + hash.update(intent_digest.as_bytes()); + hash.update(idempotency.as_bytes()); + format!("sha256:{:x}", hash.finalize()) +} +fn cumulative_usage(events: &[Event]) -> Result { + let mut total = Usage::default(); + for event in events { + macro_rules! add { + ($field:ident) => { + total.$field = total + .$field + .checked_add(event.usage.$field) + .ok_or_else(g217)? + }; + } + add!(journal_reads); + add!(journal_writes); + add!(invoice_reads); + add!(snapshot_reads); + add!(simulations); + add!(approvals); + add!(signatures); + add!(broadcasts); + add!(reconciliations); + add!(input_bytes); + add!(output_bytes); + add!(elapsed_ms); + } + Ok(total) +} +fn valid_event(kind: &str, status: &str) -> bool { + match kind { + "run_started" => status == "started", + "journal_loaded" => matches!(status, "missing" | "present" | "failed"), + "intent_reserved" => matches!(status, "reserved" | "failed"), + "invoice_loaded" | "snapshot_loaded" => matches!(status, "loaded" | "failed"), + "plan_built" => matches!(status, "built" | "failed"), + "simulation_finished" => matches!(status, "succeeded" | "rejected" | "failed"), + "approval_finished" => matches!(status, "approved" | "rejected" | "failed"), + "transaction_signed" => matches!(status, "signed" | "failed"), + "broadcast_finished" => matches!( + status, + "accepted" | "pending" | "unknown" | "rejected" | "failed" + ), + "reconciliation_finished" => matches!( + status, + "pending" | "confirmed" | "reorged" | "dropped" | "failed" + ), + "journal_committed" => matches!(status, "committed" | "failed"), + "run_finished" => matches!( + status, + "confirmed" + | "pending" + | "reorged" + | "dropped" + | "rejected" + | "cancelled" + | "deadline_exceeded" + | "budget_exhausted" + | "journal_failed" + | "adapter_failed" + | "approval_failed" + | "custody_failed" + | "broadcast_unknown" + | "reconciliation_failed" + ), + _ => false, + } +} +fn replay_events(events: &[Event], terminal: &Terminal) -> Result<(), Diagnostic> { + if events + .first() + .is_none_or(|event| event.kind != "run_started") + || events.last().is_none_or(|event| { + event.kind != "run_finished" || event.status != terminal.status.text() + }) + || events + .iter() + .any(|event| !valid_event(event.kind, event.status)) + { + return Err(g217()); + } + let order = [ + "run_started", + "journal_loaded", + "intent_reserved", + "invoice_loaded", + "snapshot_loaded", + "plan_built", + "simulation_finished", + "approval_finished", + "transaction_signed", + "broadcast_finished", + "reconciliation_finished", + "run_finished", + ]; + let mut previous = 0usize; + let mut counts = BTreeMap::<&str, u64>::new(); + for event in events { + let count = counts.entry(event.kind).or_default(); + *count = count.checked_add(1).ok_or_else(g217)?; + if event.kind == "journal_committed" { + continue; + } + let Some(position) = order.iter().position(|kind| *kind == event.kind) else { + return Err(g217()); + }; + if position < previous { + return Err(g217()); + } + previous = position; + } + if counts.get("run_started") != Some(&1) + || counts.get("journal_loaded") != Some(&1) + || counts.get("run_finished") != Some(&1) + || counts.get("transaction_signed").copied().unwrap_or(0) > 1 + || counts.get("broadcast_finished").copied().unwrap_or(0) > 1 + { + return Err(g217()); + } + for kind in [ + "intent_reserved", + "invoice_loaded", + "snapshot_loaded", + "plan_built", + "simulation_finished", + "approval_finished", + "reconciliation_finished", + ] { + if counts.get(kind).copied().unwrap_or(0) > 1 { + return Err(g217()); + } + } + let has = |kind: &str, status: &str| { + events + .iter() + .any(|event| event.kind == kind && event.status == status) + }; + let successful_fresh_prefix = has("intent_reserved", "reserved") + && has("snapshot_loaded", "loaded") + && has("plan_built", "built") + && has("simulation_finished", "succeeded") + && has("approval_finished", "approved") + && has("transaction_signed", "signed"); + let broadcast_terminal = matches!( + terminal.status, + EconomicRunStatus::Confirmed + | EconomicRunStatus::Pending + | EconomicRunStatus::Reorged + | EconomicRunStatus::Dropped + | EconomicRunStatus::BroadcastUnknown + ); + let fresh_invocation = counts.get("intent_reserved").copied().unwrap_or(0) != 0; + if broadcast_terminal + && fresh_invocation + && (!successful_fresh_prefix + || !(has("broadcast_finished", "accepted") + || has("broadcast_finished", "pending") + || has("broadcast_finished", "unknown"))) + { + return Err(g217()); + } + if matches!( + terminal.status, + EconomicRunStatus::Confirmed + | EconomicRunStatus::Pending + | EconomicRunStatus::Reorged + | EconomicRunStatus::Dropped + ) && !events.iter().any(|event| { + event.kind == "reconciliation_finished" + && matches!( + event.status, + "confirmed" | "pending" | "reorged" | "dropped" + ) + }) { + return Err(g217()); + } + if terminal.status == EconomicRunStatus::BroadcastUnknown + && ((!has("broadcast_finished", "unknown") && fresh_invocation) + || counts.get("reconciliation_finished").copied().unwrap_or(0) != 0) + { + return Err(g217()); + } + if let Some(failed) = events.iter().position(|event| { + event.status == "failed" || matches!(event.status, "rejected" | "unknown") + }) { + if events[failed + 1..] + .iter() + .any(|event| event.kind != "journal_committed" && event.kind != "run_finished") + { + return Err(g217()); + } + } + let usage = cumulative_usage(events)?; + if usage.journal_reads != 1 || usage.signatures > 1 || usage.broadcasts > 1 { + return Err(g217()); + } + Ok(()) +} +fn diagnostic_terminal(diagnostic: &Diagnostic) -> Terminal { + let (code, message) = (diagnostic.code, diagnostic.message.as_str()); + let status = match code { + "SPX-I222" => EconomicRunStatus::JournalFailed, + "SPX-I224" => EconomicRunStatus::ApprovalFailed, + "SPX-I225" => EconomicRunStatus::CustodyFailed, + "SPX-I226" => EconomicRunStatus::BroadcastUnknown, + "SPX-I227" => EconomicRunStatus::ReconciliationFailed, + "SPX-I228" => EconomicRunStatus::Cancelled, + "SPX-I229" => EconomicRunStatus::DeadlineExceeded, + "SPX-G216" => EconomicRunStatus::BudgetExhausted, + "SPX-G214" => EconomicRunStatus::ApprovalFailed, + "SPX-G212" => EconomicRunStatus::Rejected, + _ => EconomicRunStatus::AdapterFailed, + }; + Terminal { + status, + transaction_id: None, + confirmation: None, + code: Some(code.to_owned()), + message: Some(message.to_owned()), + } +} + +fn replay_bundle( + evidence: &Doc, + trace: &Doc, + parts: &EvidenceParts<'_>, + events: &[Event], +) -> Result<(), Diagnostic> { + replay_events(events, parts.terminal)?; + let mut trace_match = MatchSink { + expected: trace.source.as_bytes(), + offset: 0, + }; + if write_trace( + &mut trace_match, + parts.run_id, + parts.agent_digest, + parts.policy, + parts.intent, + events, + parts.terminal, + ) + .is_err() + || trace_match.offset != trace.source.len() + || digest(TRACE_DOMAIN, trace.source.as_bytes()) != trace.digest + { + return Err(g217()); + } + let (_journal_digest, journal_bytes) = journal_identity(parts); + if digest(EVIDENCE_DOMAIN, evidence.source.as_bytes()) != evidence.digest + || evidence.source.len() as u64 != parts.budget.evidence_bytes + || trace.source.len() as u64 != parts.budget.trace_bytes + || events.len() as u64 != parts.budget.trace_events + { + return Err(g217()); + } + let usage = cumulative_usage(events)?; + let expected_journal_bytes = journal_bytes as u64; + let expected_recipients = parts + .policy + .networks + .iter() + .try_fold(0u64, |sum, row| { + sum.checked_add(row.recipients.len() as u64) + }) + .ok_or_else(g217)?; + let expected_utxos = parts.plan.map_or(0, |plan| plan.utxos); + if usage.journal_reads != 1 + || usage.reconciliations > parts.budget.reconciliations + || usage.signatures > 1 + || usage.broadcasts > 1 + || parts.budget.policy_bytes != parts.policy.source.len() as u64 + || parts.budget.intent_bytes != parts.intent.source.len() as u64 + || parts.budget.invoice_bytes + != parts + .invoice + .map_or(0, |value| value.doc.source.len() as u64) + || parts.budget.plan_bytes != parts.plan.map_or(0, |value| value.doc.source.len() as u64) + || parts.budget.simulation_bytes + != parts + .simulation + .map_or(0, |value| value.doc.source.len() as u64) + || parts.budget.approval_bytes + != parts + .approval + .map_or(0, |value| value.doc.source.len() as u64) + || parts.budget.journal_bytes != expected_journal_bytes + || parts.budget.unsigned_bytes != parts.plan.map_or(0, |value| value.unsigned.len() as u64) + || parts.budget.signed_bytes + != parts + .journal + .signed + .as_ref() + .map_or(0, |value| value.1 as u64) + || (parts + .broadcast + .is_some_and(|broadcast| broadcast.observed != 0) + && parts.budget.broadcast_bytes + != parts + .broadcast + .map_or(0, |value| value.doc.source.len() as u64)) + || parts.budget.reconciliation_bytes + != parts + .reconciliation + .map_or(0, |value| value.doc.source.len() as u64) + || parts.budget.recipients != expected_recipients + || parts.budget.network_policies != parts.policy.networks.len() as u64 + || parts.budget.x402_origins != parts.policy.origins.len() as u64 + || parts.budget.utxos != expected_utxos + || parts.budget.concurrency != 1 + || parts.budget.unexpected_authority_calls != 0 + { + return Err(g217()); + } + let (journal_digest, journal_bytes) = journal_identity(parts); + let mut evidence_match = MatchSink { + expected: evidence.source.as_bytes(), + offset: 0, + }; + if write_evidence(&mut evidence_match, parts, &journal_digest, journal_bytes).is_err() + || evidence_match.offset != evidence.source.len() + || digest(EVIDENCE_DOMAIN, evidence.source.as_bytes()) != evidence.digest + { + return Err(g217()); + } + Ok(()) +} + +fn event( + kind: &'static str, + rail: Option, + input: Option, + output: Option, + status: &'static str, + usage: Usage, +) -> Result { + let owned_bytes = input + .as_ref() + .map_or(0, String::len) + .checked_add(output.as_ref().map_or(0, String::len)) + .and_then(|bytes| bytes.checked_add(std::mem::size_of::())) + .unwrap_or(usize::MAX); + if !reserve_active(owned_bytes) { + return Err(g216( + "builder_bytes", + active_limit().unwrap_or(MAX_BUILDER_BYTES) as u64, + )); + } + Ok(Event { + kind, + rail, + input, + output, + status, + usage, + authority_uncertain: false, + }) +} + +fn push_event(events: &mut Vec, event: Event) -> Result<(), Diagnostic> { + events.try_reserve(1).map_err(|_| { + g216( + "builder_bytes", + active_limit().unwrap_or(MAX_BUILDER_BYTES) as u64, + ) + })?; + events.push(event); + Ok(()) +} +fn journal_digest(journal: &Journal) -> String { + let mut sink = DigestSink::new(JOURNAL_DOMAIN); + write_journal(&mut sink, journal).expect("journal digest cannot fail"); + sink.finish().0 +} +fn cas_journal( + host: &mut H, + journal: &mut Journal, + events: &mut Vec, + budget: &mut Budget, + maximum: u64, + rolling: EconomicRollingReservationUpdate<'_>, +) -> Result<(), Diagnostic> { + let expected = journal.version; + let authenticated_journal_bytes = budget.journal_bytes; + let mut current_count = CountSink::default(); + write_journal(&mut current_count, journal).map_err(|_| g215())?; + budget.journal_bytes = u64::try_from(current_count.0).map_err(|_| g217())?; + let mut prospective = + clone_journal_bounded(journal, active_limit().unwrap_or(MAX_BUILDER_BYTES) as u64)?; + prospective.version = prospective.version.checked_add(1).ok_or_else(g215)?; + let mut count = CountSink::default(); + write_journal(&mut count, &prospective).map_err(|_| g215())?; + if count.0 > maximum as usize { + return Err(g216("journal_bytes", maximum)); + } + if active_remaining().is_some_and(|remaining| count.0 > remaining) || !reserve_active(count.0) { + return Err(g216( + "builder_bytes", + active_limit().unwrap_or(MAX_BUILDER_BYTES) as u64, + )); + } + let mut source = String::with_capacity(count.0); + write_journal(&mut source, &prospective).map_err(|_| g215())?; + let mut journal_match = MatchSink { + expected: source.as_bytes(), + offset: 0, + }; + if write_journal(&mut journal_match, &prospective).is_err() + || journal_match.offset != source.len() + { + return Err(g215()); + } + let prospective_digest = digest(JOURNAL_DOMAIN, source.as_bytes()); + if !reserve_active( + prospective_digest + .len() + .checked_add(std::mem::size_of::()) + .ok_or_else(g217)?, + ) { + return Err(g216( + "builder_bytes", + active_limit().unwrap_or(MAX_BUILDER_BYTES) as u64, + )); + } + events.try_reserve(1).map_err(|_| { + g216( + "builder_bytes", + active_limit().unwrap_or(MAX_BUILDER_BYTES) as u64, + ) + })?; + let disposition = host.compare_and_swap(&journal.idempotency_key, expected, &source, rolling); + let mut usage = Usage::default(); + usage.journal_writes = 1; + usage.input_bytes = source.len() as u64; + events.push(Event { + kind: "journal_committed", + rail: None, + input: Some(prospective_digest), + output: None, + status: if disposition == EconomicAdapterDisposition::Succeeded { + "committed" + } else { + "failed" + }, + usage, + authority_uncertain: disposition == EconomicAdapterDisposition::FailedUncertain, + }); + if disposition == EconomicAdapterDisposition::FailedUncertain { + debug_assert!(events.last().is_some_and(|event| event.authority_uncertain)); + } + if disposition == EconomicAdapterDisposition::PolicyRejected { + budget.journal_bytes = if expected == 0 { + current_count.0 as u64 + } else { + authenticated_journal_bytes + }; + return Err(g212("amount or fee not allowed")); + } + if disposition != EconomicAdapterDisposition::Succeeded { + budget.journal_bytes = if expected == 0 { + current_count.0 as u64 + } else { + authenticated_journal_bytes + }; + return Err(info("SPX-I222", "Economic Agent journal adapter failed")); + } + *journal = prospective; + budget.journal_bytes = source.len() as u64; + Ok(()) +} +fn finish_run( + run_id: &str, + binding: &crate::agent_runtime::EconomicAgentBinding<'_>, + policy: &Policy, + intent: &Intent, + invoice: Option<&Invoice>, + plan: Option<&Plan>, + simulation: Option<&Simulation>, + approval: Option<&Approval>, + journal: &Journal, + broadcast: Option<&BroadcastReceipt>, + reconciliation: Option<&Reconciliation>, + events: &mut Vec, + terminal: Terminal, + budget: &mut Budget, + elapsed_ms: u64, +) -> Result { + clear_active_floor(); + push_event( + events, + event( + "run_finished", + None, + None, + None, + terminal.status.text(), + Usage::default(), + )?, + )?; + replay_events(events, &terminal)?; + let usage = cumulative_usage(events)?; + if usage.journal_reads != 1 || usage.signatures > 1 || usage.broadcasts > 1 { + return Err(g217()); + } + budget.trace_events = events.len().try_into().map_err(|_| g217())?; + budget.elapsed_ms = elapsed_ms.min(policy.limits.max_elapsed_ms); + let trace = render_trace( + run_id, + binding.evidence_digest, + policy, + intent, + events, + &terminal, + )?; + budget.trace_bytes = trace.source.len() as u64; + let mut parts = EvidenceParts { + run_id, + agent_run_id: binding.run_id, + agent_evidence: binding.evidence, + agent_digest: binding.evidence_digest, + policy, + intent, + invoice, + plan, + simulation, + approval, + journal, + broadcast, + reconciliation, + trace: &trace, + terminal: &terminal, + budget, + }; + let evidence = render_evidence(&mut parts)?; + let exact_builder = policy + .limits + .max_builder_bytes + .checked_sub(active_remaining().ok_or_else(g217)? as u64) + .ok_or_else(g217)?; + if exact_builder != parts.budget.builder_bytes { + return Err(g217()); + } + replay_bundle(&evidence, &trace, &parts, events)?; + Ok(EconomicRun { + status: terminal.status, + transaction_id: terminal.transaction_id, + confirmation_status: terminal.confirmation, + trace: trace.source, + trace_digest: trace.digest, + evidence: evidence.source, + evidence_digest: evidence.digest, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::BTreeMap; + + fn limits() -> Limits { + Limits { + max_policy_bytes: MAX_POLICY_BYTES as u64, + max_intent_bytes: MAX_INTENT_BYTES as u64, + max_invoice_bytes: MAX_INVOICE_BYTES as u64, + max_snapshot_bytes: MAX_SNAPSHOT_BYTES as u64, + max_plan_bytes: MAX_PLAN_BYTES as u64, + max_simulation_bytes: MAX_SIMULATION_BYTES as u64, + max_approval_request_bytes: MAX_APPROVAL_REQUEST_BYTES as u64, + max_approval_bytes: MAX_APPROVAL_BYTES as u64, + max_journal_bytes: MAX_JOURNAL_BYTES as u64, + max_unsigned_transaction_bytes: MAX_UNSIGNED_BYTES as u64, + max_signed_transaction_bytes: MAX_SIGNED_BYTES as u64, + max_broadcast_receipt_bytes: MAX_BROADCAST_BYTES as u64, + max_reconciliation_bytes: MAX_RECONCILIATION_BYTES as u64, + max_trace_events: MAX_TRACE_EVENTS as u64, + max_trace_bytes: MAX_TRACE_BYTES as u64, + max_evidence_bytes: MAX_EVIDENCE_BYTES as u64, + max_builder_bytes: MAX_BUILDER_BYTES as u64, + max_json_depth: MAX_JSON_DEPTH as u64, + max_identifier_bytes: MAX_IDENTIFIER_BYTES as u64, + max_memo_bytes: MAX_MEMO_BYTES as u64, + max_recipients: MAX_RECIPIENTS as u64, + max_network_policies: MAX_NETWORK_POLICIES as u64, + max_x402_origins: MAX_X402_ORIGINS as u64, + max_utxos: MAX_UTXOS as u64, + max_reconciliations: 64, + max_elapsed_ms: 600_000, + max_amount_atomic: 1_000_000_000_000_000_000, + max_fee_atomic: 1_000_000_000_000_000, + max_compute_units: 200_000, + max_confirmation_target: 144, + max_concurrency: 1, + max_unexpected_authority_calls: 0, + } + } + fn evm_policy() -> Policy { + let mut policy = Policy { + economic_agent_id: "fixture.economic".to_owned(), + wallet_id: "fixture.wallet".to_owned(), + networks: vec![NetworkPolicy { + rail: EconomicRail::Evm, + network: "sepolia".to_owned(), + asset: "native:eth".to_owned(), + recipients: vec!["0x1111111111111111111111111111111111111111".to_owned()], + max_amount: 1_000_000, + max_fee: 1_000_000, + max_rolling: 1_000_000, + }], + origins: vec![], + limits: limits(), + source: String::new(), + digest: String::new(), + }; + policy.source = render_policy(&policy); + policy.digest = digest(POLICY_DOMAIN, policy.source.as_bytes()); + policy + } + fn evm_intent() -> Intent { + let mut intent = Intent { + intent_id: "fixture.intent".to_owned(), + wallet_id: "fixture.wallet".to_owned(), + rail_text: "evm".to_owned(), + idempotency_key: "fixture.payment.evm".to_owned(), + created_at: 1_700_000_000_000, + expires_at: 1_700_000_300_000, + memo: None, + payment: Payment::Evm { + recipient: "0x1111111111111111111111111111111111111111".to_owned(), + amount: 10, + max_fee: 100_000, + }, + source: String::new(), + digest: String::new(), + }; + intent.source = render_intent(&intent); + intent.digest = digest(INTENT_DOMAIN, intent.source.as_bytes()); + intent + } + + fn regtest_recipient(program: [u8; 20]) -> String { + let charset = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + let mut data = vec![0]; + data.extend(convert_bits(&program, 8, 5, true).unwrap()); + let mut values = vec![3, 3, 3, 3, 0, 2, 3, 18, 20]; + values.extend_from_slice(&data); + values.extend([0; 6]); + let mut polymod = 1u32; + for value in values { + let top = polymod >> 25; + polymod = ((polymod & 0x01ff_ffff) << 5) ^ u32::from(value); + for (index, generator) in [ + 0x3b6a_57b2, + 0x2650_8e6d, + 0x1ea1_19fa, + 0x3d42_33dd, + 0x2a14_62b3, + ] + .iter() + .enumerate() + { + if ((top >> index) & 1) != 0 { + polymod ^= generator; + } + } + } + polymod ^= 1; + let mut encoded = String::from("bcrt1"); + for value in data + .into_iter() + .chain((0..6).map(|index| ((polymod >> (5 * (5 - index))) & 31) as u8)) + { + encoded.push(charset[usize::from(value)] as char); + } + assert!(decode_regtest_p2wpkh(&encoded).is_some()); + encoded + } + + fn rail_fixture(rail: EconomicRail) -> (Policy, Intent) { + let (network, asset, recipient, payment, rail_text) = match rail { + EconomicRail::Evm => { + let recipient = "0x1111111111111111111111111111111111111111".to_owned(); + ( + "sepolia", + "native:eth", + recipient.clone(), + Payment::Evm { + recipient, + amount: 10, + max_fee: 100_000, + }, + "evm", + ) + } + EconomicRail::Solana => { + let recipient = encode_base58(&[3; 32]); + ( + "devnet", + "native:sol", + recipient.clone(), + Payment::Solana { + recipient, + amount: 10, + max_fee: 6_000, + compute: 200_000, + priority: 1_000, + }, + "solana", + ) + } + EconomicRail::Bitcoin => { + let recipient = regtest_recipient([9; 20]); + ( + "regtest", + "native:btc", + recipient.clone(), + Payment::Bitcoin { + recipient, + amount: 10_000, + max_fee: 10_000, + confirmations: 1, + }, + "bitcoin", + ) + } + }; + let mut policy = Policy { + economic_agent_id: "fixture.economic".to_owned(), + wallet_id: "fixture.wallet".to_owned(), + networks: vec![NetworkPolicy { + rail, + network: network.to_owned(), + asset: asset.to_owned(), + recipients: vec![recipient], + max_amount: 1_000_000, + max_fee: 1_000_000, + max_rolling: 1_000_000, + }], + origins: vec![], + limits: limits(), + source: String::new(), + digest: String::new(), + }; + policy.source = render_policy(&policy); + policy.digest = digest(POLICY_DOMAIN, policy.source.as_bytes()); + let mut intent = Intent { + intent_id: format!("fixture.intent.{rail_text}"), + wallet_id: "fixture.wallet".to_owned(), + rail_text: rail_text.to_owned(), + idempotency_key: format!("fixture.payment.{rail_text}"), + created_at: 1_700_000_000_000, + expires_at: 1_700_000_300_000, + memo: None, + payment, + source: String::new(), + digest: String::new(), + }; + intent.source = render_intent(&intent); + intent.digest = digest(INTENT_DOMAIN, intent.source.as_bytes()); + (policy, intent) + } + + fn x402_fixture() -> (Policy, Intent, Invoice) { + let (mut policy, mut intent) = rail_fixture(EconomicRail::Evm); + let invoice = Invoice { + origin: "https://pay.example.com".to_owned(), + method: "POST".to_owned(), + resource: "/v1/payments".to_owned(), + invoice_id: "fixture.invoice".to_owned(), + payee: "0x1111111111111111111111111111111111111111".to_owned(), + rail: EconomicRail::Evm, + network: "sepolia".to_owned(), + asset: "native:eth".to_owned(), + amount: 10, + max_fee: 100_000, + expires: intent.expires_at - 1, + nonce: "fixture.nonce".to_owned(), + idempotency: "fixture.payment.x402".to_owned(), + doc: Doc { + source: String::new(), + digest: String::new(), + }, + }; + let invoice_source = render_invoice(&invoice); + let mut invoice = invoice; + invoice.doc = Doc { + digest: digest(INVOICE_DOMAIN, invoice_source.as_bytes()), + source: invoice_source, + }; + policy.origins = vec![OriginPolicy { + origin: invoice.origin.clone(), + methods: vec![invoice.method.clone()], + resources: vec![invoice.resource.clone()], + rails: vec![EconomicRail::Evm], + max_amount: 1_000_000, + }]; + policy.source = render_policy(&policy); + policy.digest = digest(POLICY_DOMAIN, policy.source.as_bytes()); + intent.intent_id = "fixture.intent.x402".to_owned(); + intent.rail_text = "x402".to_owned(); + intent.idempotency_key = invoice.idempotency.clone(); + intent.payment = Payment::X402 { + origin: invoice.origin.clone(), + method: invoice.method.clone(), + resource: invoice.resource.clone(), + invoice_digest: invoice.doc.digest.clone(), + payee: invoice.payee.clone(), + rail: invoice.rail, + network: invoice.network.clone(), + asset: invoice.asset.clone(), + amount: invoice.amount, + max_fee: invoice.max_fee, + invoice_expires: invoice.expires, + nonce: invoice.nonce.clone(), + }; + intent.source = render_intent(&intent); + intent.digest = digest(INTENT_DOMAIN, intent.source.as_bytes()); + (policy, intent, invoice) + } + + #[test] + fn policy_and_intent_are_exact_canonical_documents() { + let policy = evm_policy(); + assert_eq!(parse_policy(&policy.source).unwrap().digest, policy.digest); + let intent = evm_intent(); + assert_eq!(parse_intent(&intent.source).unwrap().digest, intent.digest); + assert_eq!( + parse_policy(&policy.source.replace("\n", "\r\n")) + .err() + .unwrap() + .code, + "SPX-G210" + ); + let mut over = intent.source.clone(); + over.insert_str(1, "\"extra\":0,"); + assert_eq!(parse_intent(&over).err().unwrap().code, "SPX-G210"); + } + + #[test] + fn sealed_agent_fixture_binds_exact_canonical_payment_intent() { + let intent = evm_intent(); + let run = crate::agent_runtime::completed_run_for_economic_test(&intent.source); + let binding = run.economic_binding(); + assert_eq!(binding.status, AgentRunStatus::Completed); + assert_eq!(binding.final_message, Some(intent.source.as_str())); + assert_eq!( + parse_intent(binding.final_message.unwrap()).unwrap().digest, + intent.digest + ); + assert_eq!(binding.evidence_digest, run.evidence_digest()); + } + + #[test] + fn origin_resource_and_base58_identity_are_fail_closed() { + for origin in [ + "https://127.0.0.1", + "https://10.0.0.1", + "https://localhost", + "https://wallet.local", + "https://example.com:443", + "http://example.com", + ] { + assert!(!valid_origin(origin), "{origin}"); + } + assert!(valid_origin("https://pay.example.com")); + for path in [ + "//admin", + "/../admin", + "/%2e%2e/admin", + "/a%2Fb", + "/a%5cb", + "/a?b", + ] { + assert!(!valid_resource(path), "{path}"); + } + assert!(valid_resource("/v1/payments")); + let system = "11111111111111111111111111111111"; + assert_eq!(encode_base58(&decode_base58_32(system).unwrap()), system); + assert!(decode_base58_32(&format!("1{system}")).is_none()); + } + + #[test] + fn evm_unsigned_and_signed_replay_bind_every_field() { + let intent = evm_intent(); + let snapshot = Snapshot { + rail: EconomicRail::Evm, + observed: intent.created_at + 1, + expires: intent.expires_at - 1, + state: SnapshotState::Evm { + from: "0x2222222222222222222222222222222222222222".to_owned(), + nonce: 7, + base_fee: 1, + priority: 2, + gas: 21_000, + }, + doc: Doc { + source: "snapshot\n".to_owned(), + digest: "sha256:fixture".to_owned(), + }, + }; + let (unsigned, format) = build_unsigned(&intent, &snapshot).unwrap(); + assert_eq!(format, "eip1559-unsigned-v1"); + let mut fields = rlp_list_items(&unsigned[1..]) + .unwrap() + .into_iter() + .map(<[u8]>::to_vec) + .collect::>(); + fields.extend([rlp_u64(1), rlp_u64(1), rlp_u64(1)]); + let mut signed = vec![2]; + signed.extend(rlp_list(&fields)); + verify_signed(EconomicRail::Evm, &unsigned, &signed).unwrap(); + let last = signed.len() - 1; + signed[last] = 0; + assert_eq!( + verify_signed(EconomicRail::Evm, &unsigned, &signed) + .unwrap_err() + .code, + "SPX-G213" + ); + } + + #[test] + fn solana_fee_conversion_and_v0_shape_are_exact() { + let payer = encode_base58(&[2; 32]); + let recipient = encode_base58(&[3; 32]); + let blockhash = encode_base58(&[4; 32]); + let intent = Intent { + intent_id: "i".into(), + wallet_id: "w".into(), + rail_text: "solana".into(), + idempotency_key: "k".into(), + created_at: 1, + expires_at: 10, + memo: None, + payment: Payment::Solana { + recipient, + amount: 7, + max_fee: 6_000, + compute: 200_000, + priority: 1_000, + }, + source: String::new(), + digest: String::new(), + }; + let snapshot = Snapshot { + rail: EconomicRail::Solana, + observed: 2, + expires: 9, + state: SnapshotState::Solana { + payer, + blockhash, + last_height: 5, + fee: 5_000, + }, + doc: Doc { + source: String::new(), + digest: String::new(), + }, + }; + let (bytes, format) = build_unsigned(&intent, &snapshot).unwrap(); + assert_eq!(format, "solana-message-v0"); + assert_eq!(&bytes[..4], &[0x80, 1, 0, 2]); + assert_eq!(bytes.last(), Some(&0)); + } + + #[test] + fn keccak_and_rail_transaction_id_vectors_are_pinned() { + let empty = keccak256(b""); + assert_eq!( + empty + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(), + "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" + ); + let abc = keccak256(b"abc"); + assert_eq!( + abc.iter() + .map(|byte| format!("{byte:02x}")) + .collect::(), + "4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45" + ); + let mut solana = vec![1]; + solana.extend([7u8; 64]); + assert_eq!( + transaction_id(EconomicRail::Solana, &solana), + Some(encode_base58(&[7u8; 64])) + ); + } + + #[test] + fn simulation_requires_exact_native_value_conservation() { + let intent = evm_intent(); + let plan = Plan { + doc: Doc { + source: "plan\n".into(), + digest: "sha256:plan".into(), + }, + unsigned: vec![], + unsigned_digest: "sha256:u".into(), + format: "eip1559-unsigned-v1", + observed: intent.created_at + 1, + expires: intent.expires_at - 1, + utxos: 0, + }; + let good=format!("{{\"schema\":\"{SIMULATION_SCHEMA}\",\"plan\":{},\"success\":true,\"fee_atomic\":5,\"balance_before_atomic\":115,\"balance_after_atomic\":100,\"allowance_atomic\":0,\"units\":21000,\"expires_at_ms\":{}}}\n",doc_ref(PLAN_SCHEMA,&plan.doc),plan.expires); + assert!(parse_simulation(&good, &plan, &intent).is_ok()); + let hostile = good.replace( + "\"balance_before_atomic\":115", + "\"balance_before_atomic\":116", + ); + assert_eq!( + parse_simulation(&hostile, &plan, &intent) + .err() + .unwrap() + .code, + "SPX-G213" + ); + } + + type RollingKey = (String, String, String, String); + type RollingRows = Vec<(String, u64, u64)>; + + struct FixedEconomicProbe(u64); + impl EconomicBoundaryProbe for FixedEconomicProbe { + fn elapsed_ms(&self) -> u64 { + self.0 + } + } + + struct FullHost { + journals: BTreeMap, + calls: Vec<&'static str>, + intent: Intent, + invoice: Option, + broadcast_disposition: EconomicAdapterDisposition, + reconciliation_status: &'static str, + trusted_now_ms: u64, + elapsed_ms: u64, + rolling: BTreeMap, + malformed_simulation: bool, + documents: BTreeMap<&'static str, String>, + cas_fault: Option<(usize, EconomicAdapterDisposition, bool)>, + cancel_after_version: Option<(u64, AgentCancellation)>, + elapsed_after_version: Option<(u64, u64)>, + rolling_updates: Vec<&'static str>, + } + + impl FullHost { + fn new(intent: Intent) -> Self { + Self { + journals: BTreeMap::new(), + calls: vec![], + intent, + invoice: None, + broadcast_disposition: EconomicAdapterDisposition::Succeeded, + reconciliation_status: "confirmed", + trusted_now_ms: 1_700_000_000_000, + elapsed_ms: 0, + rolling: BTreeMap::new(), + malformed_simulation: false, + documents: BTreeMap::new(), + cas_fault: None, + cancel_after_version: None, + elapsed_after_version: None, + rolling_updates: vec![], + } + } + + fn with_invoice(intent: Intent, invoice: Invoice) -> Self { + Self { + invoice: Some(invoice), + ..Self::new(intent) + } + } + + fn record_call(&mut self, call: &'static str) { + self.calls.push(call); + if let Some(directory) = std::env::var_os("SEMAPRAX_ECONOMIC_DURABLE_DIR") { + use std::io::Write as _; + let path = std::path::PathBuf::from(directory).join("calls"); + let mut file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(path) + .unwrap(); + writeln!(file, "{call}").unwrap(); + } + } + + fn stop_after_effect_if_requested(&self, stage: &str) { + if std::env::var("SEMAPRAX_ECONOMIC_KILL_STAGE").as_deref() != Ok(stage) { + return; + } + let directory = std::path::PathBuf::from( + std::env::var_os("SEMAPRAX_ECONOMIC_DURABLE_DIR").unwrap(), + ); + std::fs::write(directory.join("ready"), stage.as_bytes()).unwrap(); + loop { + std::thread::sleep(std::time::Duration::from_millis(20)); + } + } + + fn simulation(&mut self, plan: &str, sink: &mut EconomicDocumentSink) { + self.record_call("simulate"); + if self.malformed_simulation { + assert!(sink.push(b"{\"secret\":\"economic-secret-sentinel\"}\n")); + return; + } + let value: Value = serde_json::from_str(plan.trim_end()).unwrap(); + let plan_doc = Doc { + source: plan.to_owned(), + digest: digest(PLAN_DOMAIN, plan.as_bytes()), + }; + let amount = value["amount_atomic"].as_u64().unwrap(); + let fee = match self.intent.settlement_rail() { + EconomicRail::Evm => 63_000, + EconomicRail::Solana => 6_000, + EconomicRail::Bitcoin => 10_000, + }; + let after = 1_000_000; + let expires = value["expires_at_ms"].as_u64().unwrap(); + let units = match self.intent.settlement_rail() { + EconomicRail::Evm => 21_000, + EconomicRail::Solana => 200_000, + EconomicRail::Bitcoin => 1, + }; + let allowance = if self.intent.settlement_rail() == EconomicRail::Evm { + "0" + } else { + "null" + }; + let simulation = format!( + "{{\"schema\":\"{SIMULATION_SCHEMA}\",\"plan\":{},\"success\":true,\"fee_atomic\":{fee},\"balance_before_atomic\":{},\"balance_after_atomic\":{after},\"allowance_atomic\":{allowance},\"units\":{units},\"expires_at_ms\":{expires}}}\n", + doc_ref(PLAN_SCHEMA, &plan_doc), + after + amount + fee, + ); + self.documents.insert("plan", plan.to_owned()); + self.documents.insert("simulation", simulation.clone()); + assert!(sink.push(simulation.as_bytes())); + } + + fn broadcast(&mut self, signed: &[u8], sink: &mut EconomicDocumentSink) { + self.record_call("broadcast"); + let rail = self.intent.settlement_rail(); + let (network, _) = self.intent.network_asset(); + let signed_digest = digest(SIGNED_DOMAIN, signed); + let txid = transaction_id(rail, signed).unwrap(); + let disposition = + if self.broadcast_disposition == EconomicAdapterDisposition::FailedUncertain { + "unknown" + } else { + "accepted" + }; + let source = format!( + "{{\"schema\":\"{BROADCAST_SCHEMA}\",\"rail\":{},\"network\":{},\"signed_transaction_digest\":{},\"transaction_id\":{},\"disposition\":{},\"observed_at_ms\":{}}}\n", + quote_json(rail.text()), + quote_json(network), + quote_json(&signed_digest), + quote_json(&txid), + quote_json(disposition), + self.intent.created_at + 2, + ); + self.documents.insert("broadcast", source.clone()); + assert!(sink.push(source.as_bytes())); + self.stop_after_effect_if_requested("broadcast_effect"); + } + + fn reconciliation(&mut self, transaction_id: &str, sink: &mut EconomicDocumentSink) { + self.record_call("reconcile"); + let rail = self.intent.settlement_rail(); + let (network, _) = self.intent.network_asset(); + let (height, confirmations, block) = if self.reconciliation_status == "confirmed" { + ("1", "1", quote_json("fixture.block")) + } else { + ("null", "null", "null".to_owned()) + }; + let source = format!( + "{{\"schema\":\"{RECONCILIATION_SCHEMA}\",\"rail\":{},\"network\":{},\"transaction_id\":{},\"status\":{},\"observed_at_ms\":{},\"observed_height\":{height},\"confirmations\":{confirmations},\"canonical_block_id\":{block}}}\n", + quote_json(rail.text()), + quote_json(network), + quote_json(transaction_id), + quote_json(self.reconciliation_status), + self.intent.created_at + 3, + ); + self.documents.insert("reconciliation", source.clone()); + assert!(sink.push(source.as_bytes())); + } + } + + impl EconomicAgentHost for FullHost { + fn boundary_probe(&self) -> Box { + Box::new(FixedEconomicProbe(self.elapsed_ms)) + } + } + + impl PaymentJournal for FullHost { + fn load( + &mut self, + idempotency_key: &str, + sink: &mut EconomicDocumentSink, + ) -> EconomicJournalLoad { + self.record_call("load"); + if !self.journals.contains_key(idempotency_key) { + if let Some(directory) = std::env::var_os("SEMAPRAX_ECONOMIC_DURABLE_DIR") { + let path = std::path::PathBuf::from(directory).join("journal"); + if let Ok(source) = std::fs::read_to_string(path) { + self.journals.insert(idempotency_key.to_owned(), source); + } + } + } + match self.journals.get(idempotency_key) { + Some(source) => { + assert!(sink.push(source.as_bytes())); + EconomicJournalLoad::Present + } + None => EconomicJournalLoad::Missing, + } + } + + fn compare_and_swap( + &mut self, + idempotency_key: &str, + expected_version: u64, + journal: &str, + rolling: EconomicRollingReservationUpdate<'_>, + ) -> EconomicAdapterDisposition { + self.record_call("cas"); + self.rolling_updates.push(match rolling { + EconomicRollingReservationUpdate::Reserve(_) => "reserve", + EconomicRollingReservationUpdate::Retain => "retain", + EconomicRollingReservationUpdate::Release => "release", + }); + let cas_ordinal = self.calls.iter().filter(|call| **call == "cas").count(); + let fault = self + .cas_fault + .filter(|(ordinal, _, _)| *ordinal == cas_ordinal); + if let Some((_, disposition, false)) = fault { + return disposition; + } + let actual = self + .journals + .get(idempotency_key) + .and_then(|source| serde_json::from_str::(source.trim_end()).ok()) + .and_then(|value| value["version"].as_u64()) + .unwrap_or(0); + if actual != expected_version { + return EconomicAdapterDisposition::FailedUncertain; + } + if expected_version == 0 { + let EconomicRollingReservationUpdate::Reserve(reservation) = rolling else { + return EconomicAdapterDisposition::FailedUncertain; + }; + assert_eq!(reservation.wallet_id(), "fixture.wallet"); + assert_eq!(reservation.rail(), self.intent.settlement_rail()); + let (network, asset) = self.intent.network_asset(); + assert_eq!(reservation.network(), network); + assert_eq!(reservation.asset(), asset); + assert_eq!(reservation.requested_at_ms(), self.intent.created_at); + assert_eq!(reservation.amount_atomic(), self.intent.amount()); + assert_eq!(reservation.max_rolling_24h_atomic(), 1_000_000); + let key = ( + reservation.wallet_id().to_owned(), + reservation.rail().text().to_owned(), + reservation.network().to_owned(), + reservation.asset().to_owned(), + ); + let rows = self.rolling.entry(key).or_default(); + rows.retain(|(_, admitted_at, _)| { + self.trusted_now_ms.saturating_sub(*admitted_at) < 86_400_000 + }); + let Some(total) = rows + .iter() + .try_fold(reservation.amount_atomic(), |sum, (_, _, amount)| { + sum.checked_add(*amount) + }) + else { + return EconomicAdapterDisposition::PolicyRejected; + }; + if total > reservation.max_rolling_24h_atomic() { + return EconomicAdapterDisposition::PolicyRejected; + } + rows.push(( + idempotency_key.to_owned(), + self.trusted_now_ms, + reservation.amount_atomic(), + )); + } + self.journals + .insert(idempotency_key.to_owned(), journal.to_owned()); + self.documents.insert("journal", journal.to_owned()); + let committed_version = serde_json::from_str::(journal.trim_end()).unwrap() + ["version"] + .as_u64() + .unwrap(); + if self + .cancel_after_version + .as_ref() + .is_some_and(|(version, _)| *version == committed_version) + { + self.cancel_after_version.as_ref().unwrap().1.cancel(); + } + if let Some((version, elapsed)) = self.elapsed_after_version { + if version == committed_version { + self.elapsed_ms = elapsed; + } + } + if let Some(directory) = std::env::var_os("SEMAPRAX_ECONOMIC_DURABLE_DIR") { + let directory = std::path::PathBuf::from(directory); + std::fs::write(directory.join("journal"), journal).unwrap(); + if let Some(stage) = std::env::var_os("SEMAPRAX_ECONOMIC_KILL_STAGE") { + let value: Value = serde_json::from_str(journal.trim_end()).unwrap(); + let version = value["version"].as_u64().unwrap(); + let state = value["state"].as_str().unwrap(); + let matches = match stage.to_str().unwrap() { + "v4" => version == 4, + "v5" => version == 5, + "v6" => version == 6, + "odd" => version >= 7 && version % 2 == 1 && state != "approved", + "even" => version >= 8 && version % 2 == 0, + _ => false, + }; + if matches { + std::fs::write(directory.join("ready"), stage.to_string_lossy().as_bytes()) + .unwrap(); + loop { + std::thread::sleep(std::time::Duration::from_millis(20)); + } + } + } + } + fault.map_or( + EconomicAdapterDisposition::Succeeded, + |(_, disposition, _)| disposition, + ) + } + } + + impl X402InvoiceAdapter for FullHost { + fn fetch_invoice( + &mut self, + origin: &str, + method: &str, + resource: &str, + sink: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition { + self.record_call("invoice"); + let Some(invoice) = &self.invoice else { + return EconomicAdapterDisposition::DefinitelyNotStarted; + }; + assert_eq!( + (origin, method, resource), + (&*invoice.origin, &*invoice.method, &*invoice.resource) + ); + assert!(sink.push(invoice.doc.source.as_bytes())); + EconomicAdapterDisposition::Succeeded + } + } + + impl EvmPaymentAdapter for FullHost { + fn evm_snapshot( + &mut self, + _: &str, + sink: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition { + self.record_call("snapshot"); + let snapshot = Snapshot { + rail: EconomicRail::Evm, + observed: self.intent.created_at + 1, + expires: self.intent.expires_at - 1, + state: SnapshotState::Evm { + from: "0x2222222222222222222222222222222222222222".to_owned(), + nonce: 7, + base_fee: 1, + priority: 2, + gas: 21_000, + }, + doc: Doc { + source: String::new(), + digest: String::new(), + }, + }; + let source = render_snapshot(&snapshot); + self.documents.insert("snapshot", source.clone()); + assert!(sink.push(source.as_bytes())); + EconomicAdapterDisposition::Succeeded + } + + fn evm_simulate( + &mut self, + plan: &str, + _: &[u8], + sink: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition { + self.simulation(plan, sink); + EconomicAdapterDisposition::Succeeded + } + + fn evm_broadcast( + &mut self, + signed: &[u8], + sink: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition { + self.broadcast(signed, sink); + self.broadcast_disposition + } + + fn evm_reconcile( + &mut self, + transaction_id: &str, + sink: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition { + self.reconciliation(transaction_id, sink); + EconomicAdapterDisposition::Succeeded + } + } + + impl SolanaPaymentAdapter for FullHost { + fn solana_snapshot( + &mut self, + _: &str, + sink: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition { + self.record_call("snapshot"); + let snapshot = Snapshot { + rail: EconomicRail::Solana, + observed: self.intent.created_at + 1, + expires: self.intent.expires_at - 1, + state: SnapshotState::Solana { + payer: encode_base58(&[2; 32]), + blockhash: encode_base58(&[4; 32]), + last_height: 5, + fee: 5_000, + }, + doc: Doc { + source: String::new(), + digest: String::new(), + }, + }; + let source = render_snapshot(&snapshot); + self.documents.insert("snapshot", source.clone()); + assert!(sink.push(source.as_bytes())); + EconomicAdapterDisposition::Succeeded + } + + fn solana_simulate( + &mut self, + plan: &str, + _: &[u8], + sink: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition { + self.simulation(plan, sink); + EconomicAdapterDisposition::Succeeded + } + + fn solana_broadcast( + &mut self, + signed: &[u8], + sink: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition { + self.broadcast(signed, sink); + self.broadcast_disposition + } + + fn solana_reconcile( + &mut self, + transaction_id: &str, + sink: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition { + self.reconciliation(transaction_id, sink); + EconomicAdapterDisposition::Succeeded + } + } + + impl BitcoinPaymentAdapter for FullHost { + fn bitcoin_snapshot( + &mut self, + _: &str, + sink: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition { + self.record_call("snapshot"); + let snapshot = Snapshot { + rail: EconomicRail::Bitcoin, + observed: self.intent.created_at + 1, + expires: self.intent.expires_at - 1, + state: SnapshotState::Bitcoin { + wallet_script: format!("0014{}", "11".repeat(20)), + height: 100, + fee_rate: 1, + utxos: vec![Utxo { + txid: format!("{}01", "00".repeat(31)), + vout: 0, + value: 100_000, + script: format!("0014{}", "11".repeat(20)), + confirmations: 1, + }], + }, + doc: Doc { + source: String::new(), + digest: String::new(), + }, + }; + let source = render_snapshot(&snapshot); + self.documents.insert("snapshot", source.clone()); + assert!(sink.push(source.as_bytes())); + EconomicAdapterDisposition::Succeeded + } + + fn bitcoin_simulate( + &mut self, + plan: &str, + _: &[u8], + sink: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition { + self.simulation(plan, sink); + EconomicAdapterDisposition::Succeeded + } + + fn bitcoin_broadcast( + &mut self, + signed: &[u8], + sink: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition { + self.broadcast(signed, sink); + self.broadcast_disposition + } + + fn bitcoin_reconcile( + &mut self, + transaction_id: &str, + sink: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition { + self.reconciliation(transaction_id, sink); + EconomicAdapterDisposition::Succeeded + } + } + + impl PaymentApprover for FullHost { + fn approve( + &mut self, + request: &str, + sink: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition { + self.record_call("approve"); + let value: Value = serde_json::from_str(request.trim_end()).unwrap(); + let ref_text = |name: &str| { + let row = value[name].as_object().unwrap(); + format!( + "{{\"schema\":{},\"digest\":{},\"bytes\":{}}}", + quote_json(row["schema"].as_str().unwrap()), + quote_json(row["digest"].as_str().unwrap()), + row["bytes"].as_u64().unwrap(), + ) + }; + let request_doc = Doc { + source: request.to_owned(), + digest: digest(APPROVAL_REQUEST_DOMAIN, request.as_bytes()), + }; + let source = format!( + "{{\"schema\":\"{APPROVAL_SCHEMA}\",\"approval_id\":\"fixture.approval\",\"approver_id\":\"fixture.approver\",\"policy\":{},\"intent\":{},\"plan\":{},\"simulation\":{},\"approval_request\":{},\"decision\":\"approved\",\"approved_amount_atomic\":{},\"approved_fee_atomic\":{},\"expires_at_ms\":{}}}\n", + ref_text("policy"), ref_text("intent"), ref_text("plan"), ref_text("simulation"), + doc_ref(APPROVAL_REQUEST_SCHEMA, &request_doc), + value["amount_atomic"].as_u64().unwrap(), value["max_fee_atomic"].as_u64().unwrap(), value["expires_at_ms"].as_u64().unwrap(), + ); + self.documents + .insert("approval_request", request.to_owned()); + self.documents.insert("approval", source.clone()); + assert!(sink.push(source.as_bytes())); + EconomicAdapterDisposition::Succeeded + } + } + + impl WalletCustody for FullHost { + fn sign( + &mut self, + _: &str, + _: EconomicRail, + _: &str, + unsigned: &[u8], + _: &str, + sink: &mut EconomicBytesSink, + ) -> EconomicAdapterDisposition { + self.record_call("sign"); + let signed = match self.intent.settlement_rail() { + EconomicRail::Evm => { + let mut fields = rlp_list_items(&unsigned[1..]) + .unwrap() + .into_iter() + .map(<[u8]>::to_vec) + .collect::>(); + fields.extend([rlp_u64(1), rlp_u64(1), rlp_u64(1)]); + let mut signed = vec![2]; + signed.extend(rlp_list(&fields)); + signed + } + EconomicRail::Solana => { + let mut signed = vec![1]; + signed.extend([7; 64]); + signed.extend(unsigned); + signed + } + EconomicRail::Bitcoin => { + let template = parse_psbt_template(unsigned).unwrap(); + let mut signed = 2i32.to_le_bytes().to_vec(); + signed.extend([0, 1]); + signed.extend(compact_size(template.inputs.len())); + for input in &template.inputs { + signed.extend(input.txid); + signed.extend(input.vout.to_le_bytes()); + signed.push(0); + signed.extend(input.sequence.to_le_bytes()); + } + signed.extend(compact_size(template.outputs.len())); + for output in &template.outputs { + signed.extend(output.value.to_le_bytes()); + signed.extend(compact_size(output.script.len())); + signed.extend(&output.script); + } + let signature = [0x30, 0x06, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01, 0x01]; + for _ in &template.inputs { + signed.push(2); + signed.extend(compact_size(signature.len())); + signed.extend(signature); + signed.push(33); + signed.extend([2]); + signed.extend([1; 32]); + } + signed.extend(template.locktime.to_le_bytes()); + signed + } + }; + assert!(sink.push(&signed)); + self.stop_after_effect_if_requested("sign_effect"); + EconomicAdapterDisposition::Succeeded + } + } + + #[test] + fn full_evm_authority_route_is_ordered_and_self_replayed() { + let policy = evm_policy(); + let intent = evm_intent(); + let source = crate::agent_runtime::completed_run_for_economic_test(&intent.source); + let mut agent = EconomicAgent::new( + &policy.source, + FullHost::new(intent), + AgentCancellation::new(), + ) + .unwrap(); + let run = agent.execute(&source).unwrap(); + assert_eq!(run.status(), EconomicRunStatus::Confirmed); + assert!(run.transaction_id().is_some()); + assert_eq!(run.confirmation_status(), Some("confirmed")); + assert!(run.trace().ends_with('\n')); + assert!(run.evidence().contains("\"used_builder_bytes\":")); + assert_eq!( + run.trace_digest(), + "sha256:ce7bec5f627a6d48990573353370dc0953203153f0db2ab60a6101cc9a5146d0" + ); + assert_eq!( + run.evidence_digest(), + digest(EVIDENCE_DOMAIN, run.evidence().as_bytes()) + ); + assert_eq!( + agent.host.calls, + [ + "load", + "cas", + "snapshot", + "simulate", + "cas", + "approve", + "cas", + "cas", + "sign", + "cas", + "cas", + "broadcast", + "cas", + "cas", + "reconcile", + "cas" + ] + ); + } + + #[test] + fn solana_bitcoin_and_x402_routes_are_chain_distinct_and_self_replayed() { + for rail in [EconomicRail::Solana, EconomicRail::Bitcoin] { + let (policy, intent) = rail_fixture(rail); + let source = crate::agent_runtime::completed_run_for_economic_test(&intent.source); + let mut agent = EconomicAgent::new( + &policy.source, + FullHost::new(intent), + AgentCancellation::new(), + ) + .unwrap(); + let run = agent.execute(&source).unwrap(); + assert_eq!(run.status(), EconomicRunStatus::Confirmed, "{rail:?}"); + assert_eq!(run.confirmation_status(), Some("confirmed")); + assert!(run.transaction_id().is_some()); + assert!(run.trace().ends_with('\n')); + assert!(run.evidence().ends_with('\n')); + assert_eq!( + agent.host.calls, + [ + "load", + "cas", + "snapshot", + "simulate", + "cas", + "approve", + "cas", + "cas", + "sign", + "cas", + "cas", + "broadcast", + "cas", + "cas", + "reconcile", + "cas" + ] + ); + } + + let (policy, intent, invoice) = x402_fixture(); + let source = crate::agent_runtime::completed_run_for_economic_test(&intent.source); + let mut agent = EconomicAgent::new( + &policy.source, + FullHost::with_invoice(intent, invoice), + AgentCancellation::new(), + ) + .unwrap(); + let run = agent.execute(&source).unwrap(); + assert_eq!(run.status(), EconomicRunStatus::Confirmed); + assert_eq!(run.confirmation_status(), Some("confirmed")); + assert_eq!(agent.host.calls[2], "invoice"); + assert_eq!( + agent + .host + .calls + .iter() + .filter(|call| **call == "invoice") + .count(), + 1 + ); + } + + #[test] + fn uncertain_broadcast_is_never_retried_and_restart_reconciles_retained_capsule() { + let (policy, intent) = rail_fixture(EconomicRail::Evm); + let source = crate::agent_runtime::completed_run_for_economic_test(&intent.source); + let mut host = FullHost::new(intent.clone()); + host.broadcast_disposition = EconomicAdapterDisposition::FailedUncertain; + let mut first = EconomicAgent::new(&policy.source, host, AgentCancellation::new()).unwrap(); + let first_run = first.execute(&source).unwrap(); + assert_eq!(first_run.status(), EconomicRunStatus::BroadcastUnknown); + assert_eq!( + first + .host + .calls + .iter() + .filter(|call| **call == "broadcast") + .count(), + 1 + ); + let journals = std::mem::take(&mut first.host.journals); + let mut restart_host = FullHost::new(intent.clone()); + restart_host.journals = journals; + let mut restart = + EconomicAgent::new(&policy.source, restart_host, AgentCancellation::new()).unwrap(); + let reconciled = restart.reconcile(&intent.idempotency_key, &source).unwrap(); + assert_eq!(reconciled.status(), EconomicRunStatus::Confirmed); + assert_eq!( + restart + .host + .calls + .iter() + .filter(|call| **call == "broadcast") + .count(), + 0 + ); + assert_eq!( + restart + .host + .calls + .iter() + .filter(|call| **call == "sign") + .count(), + 0 + ); + assert_eq!(restart.host.calls, ["load", "cas", "reconcile", "cas"]); + } + + #[test] + fn rolling_window_uses_trusted_admission_time_and_expires_at_exact_24h() { + let (_, mut intent) = rail_fixture(EconomicRail::Evm); + intent.payment = Payment::Evm { + recipient: "0x1111111111111111111111111111111111111111".to_owned(), + amount: 600_000, + max_fee: 100_000, + }; + intent.source = render_intent(&intent); + intent.digest = digest(INTENT_DOMAIN, intent.source.as_bytes()); + let mut host = FullHost::new(intent.clone()); + host.trusted_now_ms = intent.created_at + 300_000; + let reservation = EconomicRollingReservation { + wallet_id: "fixture.wallet".to_owned(), + rail: EconomicRail::Evm, + network: "sepolia".to_owned(), + asset: "native:eth".to_owned(), + requested_at_ms: intent.created_at, + amount_atomic: intent.amount(), + max_rolling_24h_atomic: 1_000_000, + }; + assert_eq!( + host.compare_and_swap( + "rolling.first", + 0, + "{\"version\":1}\n", + EconomicRollingReservationUpdate::Reserve(&reservation), + ), + EconomicAdapterDisposition::Succeeded + ); + let inventory = host.journals.clone(); + host.trusted_now_ms += 86_399_999; + assert_eq!( + host.compare_and_swap( + "rolling.second", + 0, + "{\"version\":1}\n", + EconomicRollingReservationUpdate::Reserve(&reservation), + ), + EconomicAdapterDisposition::PolicyRejected + ); + assert_eq!(host.journals, inventory); + host.trusted_now_ms += 1; + assert_eq!( + host.compare_and_swap( + "rolling.second", + 0, + "{\"version\":1}\n", + EconomicRollingReservationUpdate::Reserve(&reservation), + ), + EconomicAdapterDisposition::Succeeded + ); + assert_eq!(host.rolling.values().flatten().count(), 1); + assert_eq!( + host.rolling.values().next().unwrap()[0].1, + intent.created_at + 300_000 + 86_400_000 + ); + } + + #[test] + fn rolling_window_distinct_keys_race_to_one_atomic_winner() { + let (_, mut intent) = rail_fixture(EconomicRail::Evm); + intent.payment = Payment::Evm { + recipient: "0x1111111111111111111111111111111111111111".to_owned(), + amount: 600_000, + max_fee: 100_000, + }; + let host = std::sync::Arc::new(std::sync::Mutex::new(FullHost::new(intent.clone()))); + let barrier = std::sync::Arc::new(std::sync::Barrier::new(3)); + let mut workers = Vec::new(); + for key in ["rolling.race.a", "rolling.race.b"] { + let host = std::sync::Arc::clone(&host); + let barrier = std::sync::Arc::clone(&barrier); + let intent = intent.clone(); + workers.push(std::thread::spawn(move || { + let reservation = EconomicRollingReservation { + wallet_id: "fixture.wallet".to_owned(), + rail: EconomicRail::Evm, + network: "sepolia".to_owned(), + asset: "native:eth".to_owned(), + requested_at_ms: intent.created_at, + amount_atomic: intent.amount(), + max_rolling_24h_atomic: 1_000_000, + }; + barrier.wait(); + host.lock().unwrap().compare_and_swap( + key, + 0, + "{\"version\":1}\n", + EconomicRollingReservationUpdate::Reserve(&reservation), + ) + })); + } + barrier.wait(); + let dispositions = workers + .into_iter() + .map(|worker| worker.join().unwrap()) + .collect::>(); + assert_eq!( + dispositions + .iter() + .filter(|value| **value == EconomicAdapterDisposition::Succeeded) + .count(), + 1 + ); + assert_eq!( + dispositions + .iter() + .filter(|value| **value == EconomicAdapterDisposition::PolicyRejected) + .count(), + 1 + ); + } + + #[test] + fn malformed_post_effect_adapter_output_is_terminal_replayable_and_secret_free() { + let (policy, intent) = rail_fixture(EconomicRail::Evm); + let source = crate::agent_runtime::completed_run_for_economic_test(&intent.source); + let mut host = FullHost::new(intent); + host.malformed_simulation = true; + let mut agent = EconomicAgent::new(&policy.source, host, AgentCancellation::new()).unwrap(); + let run = agent.execute(&source).unwrap(); + assert_eq!(run.status(), EconomicRunStatus::AdapterFailed); + assert!(run.trace().contains("SPX-G210")); + assert!(run.evidence().contains("SPX-G210")); + assert!(!run.trace().contains("economic-secret-sentinel")); + assert!(!run.evidence().contains("economic-secret-sentinel")); + assert_eq!( + agent.host.calls, + ["load", "cas", "snapshot", "simulate", "cas"] + ); + } + + #[test] + fn pre_effect_cancellation_is_diagnostic_only_and_invokes_no_authority() { + let (policy, intent) = rail_fixture(EconomicRail::Evm); + let source = crate::agent_runtime::completed_run_for_economic_test(&intent.source); + let cancellation = AgentCancellation::new(); + cancellation.cancel(); + let mut agent = + EconomicAgent::new(&policy.source, FullHost::new(intent), cancellation).unwrap(); + let diagnostics = match agent.execute(&source) { + Ok(_) => panic!("pre-effect cancellation unexpectedly returned Evidence"), + Err(diagnostics) => diagnostics, + }; + assert_eq!(diagnostics.len(), 1); + assert_eq!(diagnostics[0].code, "SPX-I228"); + assert_eq!(diagnostics[0].message, "Economic Agent run was cancelled"); + assert!(agent.host.calls.is_empty()); + assert!(agent.host.journals.is_empty()); + } + + #[test] + fn cancellation_and_deadline_after_durable_markers_block_the_next_effect() { + for version in [4, 6] { + let (policy, intent) = rail_fixture(EconomicRail::Evm); + let source = crate::agent_runtime::completed_run_for_economic_test(&intent.source); + let cancellation = AgentCancellation::new(); + let mut host = FullHost::new(intent); + host.cancel_after_version = Some((version, cancellation.clone())); + let mut agent = EconomicAgent::new(&policy.source, host, cancellation).unwrap(); + let run = agent.execute(&source).unwrap(); + assert_eq!(run.status(), EconomicRunStatus::Cancelled); + if version == 4 { + assert!(!agent.host.calls.contains(&"sign")); + } else { + assert!(!agent.host.calls.contains(&"broadcast")); + } + } + let (policy, intent) = rail_fixture(EconomicRail::Evm); + let source = crate::agent_runtime::completed_run_for_economic_test(&intent.source); + let mut host = FullHost::new(intent); + host.elapsed_after_version = Some((4, policy.limits.max_elapsed_ms + 1)); + let mut agent = EconomicAgent::new(&policy.source, host, AgentCancellation::new()).unwrap(); + let run = agent.execute(&source).unwrap(); + assert_eq!(run.status(), EconomicRunStatus::DeadlineExceeded); + assert!(!agent.host.calls.contains(&"sign")); + } + + #[test] + fn chain_documents_reject_key_order_schema_reference_and_identity_mutations() { + let intent = evm_intent(); + let mut snapshot = Snapshot { + rail: EconomicRail::Evm, + observed: intent.created_at + 1, + expires: intent.expires_at - 1, + state: SnapshotState::Evm { + from: "0x2222222222222222222222222222222222222222".to_owned(), + nonce: 7, + base_fee: 1, + priority: 2, + gas: 21_000, + }, + doc: Doc { + source: String::new(), + digest: String::new(), + }, + }; + snapshot.doc.source = render_snapshot(&snapshot); + snapshot.doc.digest = digest(SNAPSHOT_DOMAIN, snapshot.doc.source.as_bytes()); + assert!(parse_snapshot(&snapshot.doc.source, EconomicRail::Evm).is_ok()); + for hostile in [ + snapshot.doc.source.replace( + "\"schema\":\"semaprax.economic-agent-chain-snapshot.v1\",\"rail\":\"evm\"", + "\"rail\":\"evm\",\"schema\":\"semaprax.economic-agent-chain-snapshot.v1\"", + ), + snapshot + .doc + .source + .replace("\"network\":\"sepolia\"", "\"network\":\"devnet\""), + ] { + assert!(parse_snapshot(&hostile, EconomicRail::Evm).is_err()); + } + let mutated = parse_snapshot( + &snapshot.doc.source.replace("\"nonce\":7", "\"nonce\":8"), + EconomicRail::Evm, + ) + .unwrap(); + assert!(matches!(mutated.state, SnapshotState::Evm { nonce: 8, .. })); + + let (unsigned, _) = build_unsigned(&intent, &snapshot).unwrap(); + let mut fields = rlp_list_items(&unsigned[1..]) + .unwrap() + .into_iter() + .map(<[u8]>::to_vec) + .collect::>(); + fields.extend([rlp_u64(1), rlp_u64(1), rlp_u64(1)]); + let mut signed = vec![2]; + signed.extend(rlp_list(&fields)); + let signed_digest = digest(SIGNED_DOMAIN, &signed); + let txid = transaction_id(EconomicRail::Evm, &signed).unwrap(); + let broadcast = format!( + "{{\"schema\":\"{BROADCAST_SCHEMA}\",\"rail\":\"evm\",\"network\":\"sepolia\",\"signed_transaction_digest\":{},\"transaction_id\":{},\"disposition\":\"accepted\",\"observed_at_ms\":{}}}\n", + quote_json(&signed_digest), + quote_json(&txid), + intent.created_at + 2, + ); + assert!(parse_broadcast( + &broadcast, + EconomicRail::Evm, + "sepolia", + &signed_digest, + Some(&txid) + ) + .is_ok()); + for hostile in [ + broadcast.replace( + &txid, + "0x0000000000000000000000000000000000000000000000000000000000000000", + ), + broadcast.replace( + &signed_digest, + "sha256:0000000000000000000000000000000000000000000000000000000000000000", + ), + ] { + assert!(parse_broadcast( + &hostile, + EconomicRail::Evm, + "sepolia", + &signed_digest, + Some(&txid) + ) + .is_err()); + } + assert_eq!( + parse_broadcast( + &broadcast.replace( + "\"disposition\":\"accepted\"", + "\"disposition\":\"rejected\"" + ), + EconomicRail::Evm, + "sepolia", + &signed_digest, + Some(&txid) + ) + .unwrap() + .disposition, + "rejected" + ); + + let reconciliation = format!( + "{{\"schema\":\"{RECONCILIATION_SCHEMA}\",\"rail\":\"evm\",\"network\":\"sepolia\",\"transaction_id\":{},\"status\":\"confirmed\",\"observed_at_ms\":{},\"observed_height\":1,\"confirmations\":1,\"canonical_block_id\":\"fixture.block\"}}\n", + quote_json(&txid), + intent.created_at + 3, + ); + assert!(parse_reconciliation(&reconciliation, EconomicRail::Evm, "sepolia", &txid).is_ok()); + for hostile in [ + reconciliation.replace("\"confirmations\":1", "\"confirmations\":null"), + reconciliation.replace("\"status\":\"confirmed\"", "\"status\":\"unknown\""), + reconciliation.replace("\"network\":\"sepolia\"", "\"network\":\"devnet\""), + ] { + assert!(parse_reconciliation(&hostile, EconomicRail::Evm, "sepolia", &txid).is_err()); + } + } + + #[test] + fn configured_child_limits_are_exact_and_lower_than_global_caps() { + let (mut policy, intent, invoice) = x402_fixture(); + policy.limits.max_intent_bytes = intent.source.len() as u64; + assert!(admit_intent(&policy, &intent).is_ok()); + policy.limits.max_intent_bytes -= 1; + let diagnostic = admit_intent(&policy, &intent).unwrap_err(); + assert_eq!(diagnostic.code, "SPX-G216"); + assert_eq!( + diagnostic.message, + format!("intent_bytes exceeds {}", intent.source.len() - 1) + ); + + policy.limits.max_intent_bytes = intent.source.len() as u64; + policy.limits.max_identifier_bytes = intent.idempotency_key.len() as u64; + assert!(admit_intent(&policy, &intent).is_ok()); + policy.limits.max_identifier_bytes -= 1; + assert_eq!(admit_intent(&policy, &intent).unwrap_err().code, "SPX-G216"); + + policy.limits.max_identifier_bytes = MAX_IDENTIFIER_BYTES as u64; + let intent_depth = depth(&serde_json::from_str::(intent.source.trim_end()).unwrap()); + policy.limits.max_json_depth = intent_depth as u64; + assert!(admit_intent(&policy, &intent).is_ok()); + policy.limits.max_json_depth -= 1; + assert_eq!(admit_intent(&policy, &intent).unwrap_err().code, "SPX-G216"); + + let mut invoice_limits = limits(); + invoice_limits.max_invoice_bytes = invoice.doc.source.len() as u64; + assert!(parse_invoice_limited(&invoice.doc.source, &intent, &invoice_limits).is_ok()); + invoice_limits.max_invoice_bytes -= 1; + let diagnostic = match parse_invoice_limited(&invoice.doc.source, &intent, &invoice_limits) + { + Ok(_) => panic!("over-limit invoice was admitted"), + Err(diagnostic) => diagnostic, + }; + assert_eq!(diagnostic.code, "SPX-G216"); + + let mut snapshot = Snapshot { + rail: EconomicRail::Bitcoin, + observed: intent.created_at + 1, + expires: intent.expires_at - 1, + state: SnapshotState::Bitcoin { + wallet_script: format!("0014{}", "11".repeat(20)), + height: 100, + fee_rate: 1, + utxos: vec![ + Utxo { + txid: format!("{}01", "00".repeat(31)), + vout: 0, + value: 100_000, + script: format!("0014{}", "11".repeat(20)), + confirmations: 1, + }, + Utxo { + txid: format!("{}02", "00".repeat(31)), + vout: 0, + value: 100_000, + script: format!("0014{}", "11".repeat(20)), + confirmations: 1, + }, + ], + }, + doc: Doc { + source: String::new(), + digest: String::new(), + }, + }; + snapshot.doc.source = render_snapshot(&snapshot); + snapshot.doc.digest = digest(SNAPSHOT_DOMAIN, snapshot.doc.source.as_bytes()); + let mut snapshot_limits = limits(); + snapshot_limits.max_snapshot_bytes = snapshot.doc.source.len() as u64; + snapshot_limits.max_utxos = 2; + assert!(parse_snapshot_limited( + &snapshot.doc.source, + EconomicRail::Bitcoin, + &snapshot_limits + ) + .is_ok()); + snapshot_limits.max_utxos = 1; + let diagnostic = match parse_snapshot_limited( + &snapshot.doc.source, + EconomicRail::Bitcoin, + &snapshot_limits, + ) { + Ok(_) => panic!("over-limit UTXO set was admitted"), + Err(diagnostic) => diagnostic, + }; + assert_eq!(diagnostic.code, "SPX-G216"); + assert_eq!(diagnostic.message, "utxos exceeds 1"); + snapshot_limits.max_utxos = 2; + snapshot_limits.max_snapshot_bytes -= 1; + let diagnostic = match parse_snapshot_limited( + &snapshot.doc.source, + EconomicRail::Bitcoin, + &snapshot_limits, + ) { + Ok(_) => panic!("over-limit snapshot was admitted"), + Err(diagnostic) => diagnostic, + }; + assert_eq!(diagnostic.code, "SPX-G216"); + } + + #[test] + fn thirteen_document_x402_raw_sha_and_domain_digest_ledger_is_pinned() { + let (policy, intent, invoice) = x402_fixture(); + let source = crate::agent_runtime::completed_run_for_economic_test(&intent.source); + let mut agent = EconomicAgent::new( + &policy.source, + FullHost::with_invoice(intent.clone(), invoice.clone()), + AgentCancellation::new(), + ) + .unwrap(); + let run = agent.execute(&source).unwrap(); + let mut documents = vec![ + ("policy", policy.source.as_str(), POLICY_DOMAIN), + ("intent", intent.source.as_str(), INTENT_DOMAIN), + ("invoice", invoice.doc.source.as_str(), INVOICE_DOMAIN), + ]; + for (name, domain) in [ + ("snapshot", SNAPSHOT_DOMAIN), + ("plan", PLAN_DOMAIN), + ("simulation", SIMULATION_DOMAIN), + ("approval_request", APPROVAL_REQUEST_DOMAIN), + ("approval", APPROVAL_DOMAIN), + ("journal", JOURNAL_DOMAIN), + ("broadcast", BROADCAST_DOMAIN), + ("reconciliation", RECONCILIATION_DOMAIN), + ] { + documents.push((name, agent.host.documents[name].as_str(), domain)); + } + documents.extend([ + ("trace", run.trace(), TRACE_DOMAIN), + ("evidence", run.evidence(), EVIDENCE_DOMAIN), + ]); + assert_eq!(documents.len(), 13); + let ledger = documents + .iter() + .map(|(name, source, domain)| { + let raw = Sha256::digest(source.as_bytes()); + format!( + "{name}|{}|{}|{}", + raw.iter() + .map(|byte| format!("{byte:02x}")) + .collect::(), + digest(domain, source.as_bytes()), + source.len() + ) + }) + .collect::>(); + assert_eq!(ledger, [ + "policy|57ce4d3844f49c9102eb1a2c17f1946305c623e587d4f52a31744ba96ff6114a|sha256:ee623062817928e0088f24b8215705f9aad8e19a52861db6d6051679889c0b53|2987", + "intent|bfe0695c7e2a5bdfd545b264fb79777cfdadaa449d9089c59753ae3739e36d86|sha256:2a13c2a14cfafba6b4087e647de9e5609c8bb65ddad25c305aa8f5bc28091e2c|670", + "invoice|38b5b00511f2e461f8df0fe1a830e89109376c893e3c52cea5d23a8d36d8733b|sha256:24cb1025c6beb2a081a05ab504f7d7f6cbb37b27e003da35cbbea003a52ac095|417", + "snapshot|d005d0f573f337d804d80b8489b63a9f6b03099837b230af69e18a4692b4b9eb|sha256:4123e22e7449e4bbcef812af71337f2e3e5390b4cce20e59f7080b74eeb727d0|309", + "plan|75418fad0967fa4791d9f146f6997af67b3e76f51dcb2320bfbe2211814bde45|sha256:81dad7aa8e82bdf8ef7b02e2b5a94b899715c86e7d82895c7cdb18c5e7ed28d8|1391", + "simulation|b3508d24fd29028a9fad89703ba72ade9f4e620eec30f0dd2017b711b96db483|sha256:3a1ac9d741be20bf0d5e35a78e475369a5ccad5c3662bbc5f5365f123df81f1d|369", + "approval_request|fc932dcef1eb518ba05f463df9e3dd7193ce96df408edb60f3aaa9214a3f19b9|sha256:0833d896f4be4e4feb08d0558e43e7512d589a6c96c368c6ce73ef3a8435adf1|1056", + "approval|48f716162ae5ec67b28303c5e5c09b641a16a1711b7b83bd4d16a1be6094a56c|sha256:63f3e81facdc9e0ece43b28cbd47310b1a7898662cd4afe3abaae24d06eab8db|1022", + "journal|f65a8f115c405b086d9a6edb1366a594c87b9f295be8739ea2a56724297f69c9|sha256:9f3d1f568a090c280cfe645536e912d4eb0c18c740bb7309d434ebfd1d1cb169|2394", + "broadcast|51479da80d60c4e4c363302963010a6675278e53958ce563dafb2892da3c537f|sha256:64882648d5bac5fb58a7408d38e5fa737ba314d27decba34e2106c2310c651a6|335", + "reconciliation|b1b449375018c27465332384d67205438bea8a2660d3144c417e5de5d5198ba1|sha256:e26e7655d758b53867228950de241c3265675f18687110550fc89ecdc46f2b4a|301", + "trace|a388543ab6c1a57a0b7798fbd0c5d721bb33c0ab7f7123f3bb8f24c4c965db58|sha256:f28c44894b93948068381bb9047fedade3b855cdd1831a992466a08fa97f6f11|11023", + "evidence|2d4d4164476bd4fdd037f138b264d0a72728b125d6819baae87da165242788b0|sha256:9dd80e5a13aaaa02b5b854cee0f68870ac22dfdabca14e79d32857ae35980cc6|17399", + ]); + for (name, source, domain) in documents { + let mut mutated = source.as_bytes().to_vec(); + let index = mutated.iter().position(|byte| *byte == b'v').unwrap(); + mutated[index] = b'w'; + assert_ne!( + Sha256::digest(source.as_bytes()), + Sha256::digest(&mutated), + "{name}" + ); + assert_ne!( + digest(domain, source.as_bytes()), + digest(domain, &mutated), + "{name}" + ); + } + } + + #[test] + fn journal_uncertainty_never_retries_in_process_and_reload_governs_persistence() { + for persisted in [false, true] { + let (policy, intent) = rail_fixture(EconomicRail::Evm); + let source = crate::agent_runtime::completed_run_for_economic_test(&intent.source); + let mut host = FullHost::new(intent.clone()); + host.cas_fault = Some((2, EconomicAdapterDisposition::FailedUncertain, persisted)); + let mut agent = + EconomicAgent::new(&policy.source, host, AgentCancellation::new()).unwrap(); + let run = agent.execute(&source).unwrap(); + assert_eq!(run.status(), EconomicRunStatus::JournalFailed); + assert_eq!( + agent + .host + .calls + .iter() + .filter(|call| **call == "cas") + .count(), + 2 + ); + assert_eq!( + agent + .host + .calls + .iter() + .filter(|call| **call == "approve") + .count(), + 0 + ); + let journals = std::mem::take(&mut agent.host.journals); + let retained_version = + serde_json::from_str::(journals[&intent.idempotency_key].trim_end()) + .unwrap()["version"] + .as_u64() + .unwrap(); + assert_eq!(retained_version, if persisted { 2 } else { 1 }); + + let mut restart_host = FullHost::new(intent.clone()); + restart_host.journals = journals; + let mut restart = + EconomicAgent::new(&policy.source, restart_host, AgentCancellation::new()).unwrap(); + let restarted = restart.execute(&source).unwrap(); + assert_eq!( + restarted.status(), + if persisted { + EconomicRunStatus::JournalFailed + } else { + EconomicRunStatus::Confirmed + } + ); + assert_eq!(restart.host.calls.contains(&"snapshot"), !persisted); + if persisted { + assert_eq!(restart.host.calls, ["load"]); + assert!(restart.host.rolling_updates.is_empty()); + } else { + assert!(restart + .host + .rolling_updates + .iter() + .all(|update| *update == "retain")); + } + } + + let (policy, intent) = rail_fixture(EconomicRail::Evm); + let source = crate::agent_runtime::completed_run_for_economic_test(&intent.source); + let mut host = FullHost::new(intent); + host.cas_fault = Some((1, EconomicAdapterDisposition::DefinitelyNotStarted, false)); + let mut agent = EconomicAgent::new(&policy.source, host, AgentCancellation::new()).unwrap(); + let run = agent.execute(&source).unwrap(); + assert_eq!(run.status(), EconomicRunStatus::JournalFailed); + assert_eq!(agent.host.calls, ["load", "cas"]); + assert!(agent.host.journals.is_empty()); + assert!(agent.host.rolling.values().all(Vec::is_empty)); + } + + #[test] + fn economic_process_kill_markers_never_repeat_sign_or_broadcast() { + const ROLE: &str = "SEMAPRAX_ECONOMIC_KILL_ROLE"; + const DIRECTORY: &str = "SEMAPRAX_ECONOMIC_DURABLE_DIR"; + const STAGE: &str = "SEMAPRAX_ECONOMIC_KILL_STAGE"; + if std::env::var_os(ROLE).is_some() { + let (policy, intent) = rail_fixture(EconomicRail::Evm); + let source = crate::agent_runtime::completed_run_for_economic_test(&intent.source); + let mut agent = EconomicAgent::new( + &policy.source, + FullHost::new(intent), + AgentCancellation::new(), + ) + .unwrap(); + let result = agent.execute(&source); + if std::env::var_os(STAGE).is_none() { + assert!(result.is_ok()); + } + return; + } + let executable = std::env::current_exe().unwrap(); + for stage in [ + "v4", + "sign_effect", + "v5", + "v6", + "broadcast_effect", + "odd", + "even", + ] { + let directory = std::env::temp_dir().join(format!( + "semaprax-economic-kill-{}-{stage}", + std::process::id() + )); + std::fs::create_dir(&directory).unwrap(); + let mut child = std::process::Command::new(&executable) + .args([ + "economic_agent::tests::economic_process_kill_markers_never_repeat_sign_or_broadcast", + "--exact", + "--nocapture", + ]) + .env(ROLE, "child") + .env(DIRECTORY, &directory) + .env(STAGE, stage) + .spawn() + .unwrap(); + let ready = directory.join("ready"); + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10); + while !ready.exists() && std::time::Instant::now() < deadline { + std::thread::sleep(std::time::Duration::from_millis(20)); + } + assert!(ready.exists(), "child did not reach {stage}"); + child.kill().unwrap(); + let _ = child.wait().unwrap(); + let status = std::process::Command::new(&executable) + .args([ + "economic_agent::tests::economic_process_kill_markers_never_repeat_sign_or_broadcast", + "--exact", + "--nocapture", + ]) + .env(ROLE, "resume") + .env(DIRECTORY, &directory) + .status() + .unwrap(); + assert!(status.success(), "resume failed at {stage}"); + let calls = std::fs::read_to_string(directory.join("calls")).unwrap(); + let sign_calls = calls.lines().filter(|call| *call == "sign").count(); + let broadcast_calls = calls.lines().filter(|call| *call == "broadcast").count(); + assert!(sign_calls <= 1); + assert!(broadcast_calls <= 1); + if stage == "sign_effect" { + assert_eq!(sign_calls, 1); + assert_eq!(broadcast_calls, 0); + let journal = std::fs::read_to_string(directory.join("journal")).unwrap(); + let value: Value = serde_json::from_str(journal.trim_end()).unwrap(); + assert_eq!(value["version"], 4); + assert_eq!(value["state"], "approved"); + } + if stage == "broadcast_effect" { + assert_eq!(sign_calls, 1); + assert_eq!(broadcast_calls, 1); + let journal = std::fs::read_to_string(directory.join("journal")).unwrap(); + let value: Value = serde_json::from_str(journal.trim_end()).unwrap(); + assert_eq!(value["state"], "confirmed"); + assert!(value["version"].as_u64().unwrap() >= 8); + } + for name in ["ready", "journal", "calls"] { + let path = directory.join(name); + if path.exists() { + std::fs::remove_file(path).unwrap(); + } + } + std::fs::remove_dir(directory).unwrap(); + } + } + + #[test] + fn reconciliation_authority_is_durably_bounded_at_exact_sixty_four() { + let (policy, intent) = rail_fixture(EconomicRail::Evm); + let idempotency = intent.idempotency_key.clone(); + let source = crate::agent_runtime::completed_run_for_economic_test(&intent.source); + let mut host = FullHost::new(intent); + host.reconciliation_status = "pending"; + let mut agent = EconomicAgent::new(&policy.source, host, AgentCancellation::new()).unwrap(); + let first = agent.execute(&source).unwrap(); + assert_eq!(first.status(), EconomicRunStatus::Pending); + for _ in 1..64 { + let observation = agent.reconcile(&idempotency, &source).unwrap(); + assert_eq!(observation.status(), EconomicRunStatus::Pending); + } + let calls = agent + .host + .calls + .iter() + .filter(|call| **call == "reconcile") + .count(); + assert_eq!(calls, 64); + let exhausted = agent.reconcile(&idempotency, &source).unwrap(); + assert_eq!(exhausted.status(), EconomicRunStatus::BudgetExhausted); + assert_eq!( + agent + .host + .calls + .iter() + .filter(|call| **call == "reconcile") + .count(), + 64 + ); + } +} diff --git a/src/format.rs b/src/format.rs index 526f0be..3edf03a 100644 --- a/src/format.rs +++ b/src/format.rs @@ -1,101 +1,286 @@ -use std::fmt::Write; - use crate::ast::{ - Expr, ExprKind, ImportFailure, MatchPattern, ModuleUseKind, Program, ResourceLifecycleKind, - Statement, TypeDeclarationKind, UnaryOp, + BinaryOp, Expr, ExprKind, ImportFailure, MatchPattern, ModuleUseKind, Program, + ResourceLifecycleKind, Statement, TypeDeclarationKind, UnaryOp, }; -macro_rules! format { - ($($argument:tt)*) => { - crate::bounded_output::budgeted_format(format_args!($($argument)*)) - }; +enum ExprFormatFrame<'a> { + Expr(&'a Expr, u8), + CallArgs(&'a [Expr], usize), + BinaryRight(&'a Expr, BinaryOp, bool), + Block(&'a [Statement], &'a Expr, usize), + BlockNext(&'a [Statement], &'a Expr, usize), + IfThen(&'a Expr, &'a Expr), + IfElse(&'a Expr), + Fields(&'a [crate::ast::FieldInitializer], usize, &'static str), + MatchArms(&'a [crate::ast::MatchArm], usize), + TryEnd(bool), + PostfixFields(&'a [crate::ast::FieldInitializer]), + ProjectField(&'a str), + Close(char), +} + +enum PatternFormatFrame<'a> { + Enter(&'a str, &'a [crate::ast::RecordMatchPatternField]), + Fields(&'a [crate::ast::RecordMatchPatternField], usize), +} + +enum ContainsRecordFrame<'a> { + Enter(&'a Expr), + Children(&'a Expr, usize), +} + +enum TypeFormatFrame<'a> { + Type(&'a crate::ast::Type), + Arguments(&'a [crate::ast::Type], usize), +} + +#[derive(Clone, Copy)] +#[allow(dead_code)] +pub(crate) struct PrivateScratchCapacity { + expression_slots: usize, + contains_record_slots: usize, + type_slots: usize, + pattern_slots: usize, + bytes: usize, +} + +#[allow(dead_code)] +impl PrivateScratchCapacity { + pub(crate) fn bytes(self) -> usize { + self.bytes + } + + #[cfg(test)] + pub(crate) fn slots(self) -> [usize; 4] { + [ + self.expression_slots, + self.contains_record_slots, + self.type_slots, + self.pattern_slots, + ] + } +} + +#[allow(dead_code)] +pub(crate) fn private_scratch_capacity( + expression_depth: usize, + type_depth: usize, + pattern_depth: usize, +) -> Option { + let expression_slots = expression_depth.checked_mul(2)?.checked_add(3)?; + let contains_record_slots = expression_depth.checked_add(1)?; + let type_slots = type_depth.checked_add(1)?; + let pattern_slots = pattern_depth.checked_add(1)?; + let bytes = expression_slots + .checked_mul(std::mem::size_of::>())? + .checked_add( + contains_record_slots.checked_mul(std::mem::size_of::>())?, + )? + .checked_add(type_slots.checked_mul(std::mem::size_of::>())?)? + .checked_add(pattern_slots.checked_mul(std::mem::size_of::>())?)?; + Some(PrivateScratchCapacity { + expression_slots, + contains_record_slots, + type_slots, + pattern_slots, + bytes, + }) +} + +#[derive(Clone, Copy)] +enum ScratchStackKind { + Expression, + ContainsRecord, + Type, + Pattern, +} + +impl ScratchStackKind { + #[cfg(test)] + fn index(self) -> usize { + match self { + Self::Expression => 0, + Self::ContainsRecord => 1, + Self::Type => 2, + Self::Pattern => 3, + } + } +} + +thread_local! { + static PRIVATE_SCRATCH_CAPACITY: std::cell::Cell> = const { std::cell::Cell::new(None) }; + #[cfg(test)] + static PRIVATE_SCRATCH_HIGH_WATER: std::cell::Cell<[(usize, usize); 4]> = const { std::cell::Cell::new([(0, 0); 4]) }; +} + +#[cfg(test)] +#[allow(dead_code)] +pub(crate) fn reset_private_scratch_high_water() { + PRIVATE_SCRATCH_HIGH_WATER.with(|water| water.set([(0, 0); 4])); +} + +#[cfg(test)] +#[allow(dead_code)] +pub(crate) fn private_scratch_high_water() -> [(usize, usize); 4] { + PRIVATE_SCRATCH_HIGH_WATER.with(std::cell::Cell::get) +} + +#[cfg(test)] +fn note_private_scratch_high_water(kind: ScratchStackKind, len: usize, capacity: usize) { + PRIVATE_SCRATCH_HIGH_WATER.with(|water| { + let mut values = water.get(); + let entry = &mut values[kind.index()]; + entry.0 = entry.0.max(len); + entry.1 = entry.1.max(capacity); + water.set(values); + }); +} + +#[cfg(not(test))] +fn note_private_scratch_high_water(_: ScratchStackKind, _: usize, _: usize) {} + +struct FormatFrameStack { + values: Vec, + limit: Option, + kind: ScratchStackKind, +} + +impl FormatFrameStack { + fn new(initial: T, kind: ScratchStackKind) -> Self { + let limit = PRIVATE_SCRATCH_CAPACITY.with(|capacity| { + capacity.get().map(|capacity| match kind { + ScratchStackKind::Expression => capacity.expression_slots, + ScratchStackKind::ContainsRecord => capacity.contains_record_slots, + ScratchStackKind::Type => capacity.type_slots, + ScratchStackKind::Pattern => capacity.pattern_slots, + }) + }); + let mut values = Vec::with_capacity(limit.unwrap_or(1)); + if let Some(limit) = limit { + assert_eq!( + values.capacity(), + limit, + "private formatter Vec capacity drift" + ); + } + values.push(initial); + note_private_scratch_high_water(kind, values.len(), values.capacity()); + Self { + values, + limit, + kind, + } + } + + fn push(&mut self, value: T) { + if let Some(limit) = self.limit { + assert!( + self.values.len() < limit, + "private formatter scratch census underflow" + ); + } + self.values.push(value); + note_private_scratch_high_water(self.kind, self.values.len(), self.values.capacity()); + } + + fn pop(&mut self) -> Option { + self.values.pop() + } } pub fn canonical(program: &Program) -> String { + let _ = crate::bounded_output::reserve_active(legacy_canonical_temporary_bytes(program)); let mut output = crate::bounded_output::CappedString::new(); + write_canonical(program, &mut output); + output.into_string() +} + +/// Write the canonical source projection into a caller-owned bounded sink. +/// Native/private builders use this to count and reserve the exact final +/// capacity before materializing a retained source String. +pub(crate) fn write_canonical(program: &Program, output: &mut impl std::fmt::Write) { writeln!(output, "module {};", program.module).unwrap(); for module_use in &program.module_uses { let kind = match module_use.kind { ModuleUseKind::Function => "function", ModuleUseKind::Type => "type", }; + write!(output, "use {kind} @id(\"").unwrap(); + write_escaped(output, &module_use.persistent_id); writeln!( output, - "use {kind} @id(\"{}\") from {} as {};", - escape_string(&module_use.persistent_id), - module_use.target_module, - module_use.alias + "\") from {} as {};", + module_use.target_module, module_use.alias ) .unwrap(); } if !program.permits.is_empty() { - writeln!( - output, - "\npermit {{ {} }}", - crate::bounded_output::budgeted_join( - program - .permits - .iter() - .map(|value| crate::bounded_output::budgeted_clone(value)) - .collect::>(), - ", ", - ) - ) - .unwrap(); + write!(output, "\npermit {{ ").unwrap(); + write_joined(output, &program.permits, ", "); + writeln!(output, " }}").unwrap(); } for declaration in &program.types { writeln!(output).unwrap(); if declaration.explicit_id { - writeln!(output, "@id(\"{}\")", escape_string(&declaration.stable_id)).unwrap(); + write!(output, "@id(\"").unwrap(); + write_escaped(output, &declaration.stable_id); + writeln!(output, "\")").unwrap(); } match &declaration.kind { TypeDeclarationKind::Resource { lifecycles } => { - let parameters = type_parameter_suffix(&declaration.type_parameters); if lifecycles.is_empty() { - writeln!(output, "resource {}{};", declaration.name, parameters).unwrap(); + write!(output, "resource {}", declaration.name).unwrap(); + write_type_parameters(output, &declaration.type_parameters); + writeln!(output, ";").unwrap(); continue; } - writeln!(output, "resource {}{} {{", declaration.name, parameters).unwrap(); + write!(output, "resource {}", declaration.name).unwrap(); + write_type_parameters(output, &declaration.type_parameters); + writeln!(output, " {{").unwrap(); for lifecycle in lifecycles { if let Some(stable_id) = &lifecycle.stable_id { - writeln!(output, " @id(\"{}\")", escape_string(stable_id)).unwrap(); + write!(output, " @id(\"").unwrap(); + write_escaped(output, stable_id); + writeln!(output, "\")").unwrap(); } match &lifecycle.kind { ResourceLifecycleKind::Trivial => { writeln!(output, " drop trivial;").unwrap(); } ResourceLifecycleKind::Imported { import_key } => { - writeln!(output, " drop import \"{}\";", escape_string(import_key)) - .unwrap(); + write!(output, " drop import \"").unwrap(); + write_escaped(output, import_key); + writeln!(output, "\";").unwrap(); } } } writeln!(output, "}}").unwrap(); } TypeDeclarationKind::Record { fields } => { - writeln!( - output, - "record {}{} {{", - declaration.name, - type_parameter_suffix(&declaration.type_parameters) - ) - .unwrap(); + write!(output, "record {}", declaration.name).unwrap(); + write_type_parameters(output, &declaration.type_parameters); + writeln!(output, " {{").unwrap(); for field in fields { if field.explicit_id { - writeln!(output, " @id(\"{}\")", escape_string(&field.stable_id)) - .unwrap(); + write!(output, " @id(\"").unwrap(); + write_escaped(output, &field.stable_id); + writeln!(output, "\")").unwrap(); } - writeln!(output, " {}: {},", field.name, field.ty).unwrap(); + write!(output, " {}: ", field.name).unwrap(); + write_type(output, &field.ty); + writeln!(output, ",").unwrap(); } writeln!(output, "}}").unwrap(); } TypeDeclarationKind::Variant { cases } => { - let parameters = type_parameter_suffix(&declaration.type_parameters); - writeln!(output, "variant {}{} {{", declaration.name, parameters).unwrap(); + write!(output, "variant {}", declaration.name).unwrap(); + write_type_parameters(output, &declaration.type_parameters); + writeln!(output, " {{").unwrap(); for case in cases { if case.explicit_id { - writeln!(output, " @id(\"{}\")", escape_string(&case.stable_id)) - .unwrap(); + write!(output, " @id(\"").unwrap(); + write_escaped(output, &case.stable_id); + writeln!(output, "\")").unwrap(); } if case.fields.is_empty() { writeln!(output, " {},", case.name).unwrap(); @@ -104,14 +289,13 @@ pub fn canonical(program: &Program) -> String { writeln!(output, " {} {{", case.name).unwrap(); for field in &case.fields { if field.explicit_id { - writeln!( - output, - " @id(\"{}\")", - escape_string(&field.stable_id) - ) - .unwrap(); + write!(output, " @id(\"").unwrap(); + write_escaped(output, &field.stable_id); + writeln!(output, "\")").unwrap(); } - writeln!(output, " {}: {},", field.name, field.ty).unwrap(); + write!(output, " {}: ", field.name).unwrap(); + write_type(output, &field.ty); + writeln!(output, ",").unwrap(); } writeln!(output, " }},").unwrap(); } @@ -122,470 +306,1048 @@ pub fn canonical(program: &Program) -> String { for interface in &program.interfaces { writeln!(output).unwrap(); if interface.explicit_id { - writeln!(output, "@id(\"{}\")", escape_string(&interface.stable_id)).unwrap(); + write!(output, "@id(\"").unwrap(); + write_escaped(output, &interface.stable_id); + writeln!(output, "\")").unwrap(); } writeln!(output, "interface {}", interface.name).unwrap(); - writeln!( - output, - " permits {{ {} }}", - crate::bounded_output::budgeted_join( - interface - .permits - .iter() - .map(|value| crate::bounded_output::budgeted_clone(value)) - .collect::>(), - ", ", - ) - ) - .unwrap(); + write!(output, " permits {{ ").unwrap(); + write_joined(output, &interface.permits, ", "); + writeln!(output, " }}").unwrap(); writeln!(output, "{{").unwrap(); for import in &interface.imports { if import.explicit_id { - writeln!(output, " @id(\"{}\")", escape_string(&import.stable_id)).unwrap(); + write!(output, " @id(\"").unwrap(); + write_escaped(output, &import.stable_id); + writeln!(output, "\")").unwrap(); } - write!(output, " import fn {}(", import.name).unwrap(); - for (index, param) in import.params.iter().enumerate() { - if index > 0 { - output.push_str(", "); - } - write!( - output, - "{}: {}{}", - param.name, - param.mode.source_prefix(), - param.ty - ) - .unwrap(); - } - writeln!(output, ") -> unit").unwrap(); - writeln!( + write!( output, - " effects {{ {} }}", - crate::bounded_output::budgeted_join( - import - .effects - .iter() - .map(|value| crate::bounded_output::budgeted_clone(value)) - .collect::>(), - ", ", - ) + " import {}fn {}(", + if import.native_rust { "rust " } else { "" }, + import.name ) .unwrap(); + for (index, param) in import.params.iter().enumerate() { + if index > 0 { + output.write_str(", ").unwrap(); + } + write!(output, "{}: {}", param.name, param.mode.source_prefix()).unwrap(); + write_type(output, ¶m.ty); + } + writeln!(output, ") -> {}", import.result).unwrap(); + write!(output, " effects {{ ").unwrap(); + write_joined(output, &import.effects, ", "); + writeln!(output, " }}").unwrap(); match &import.failure { ImportFailure::Infallible => { - writeln!(output, " failure infallible").unwrap(); - } - ImportFailure::Status { domain_id } => { writeln!( output, - " failure status \"{}\"", - escape_string(domain_id) + " failure infallible{}", + if import.native_rust { ";" } else { "" } ) .unwrap(); } + ImportFailure::Status { domain_id } => { + write!(output, " failure status \"").unwrap(); + write_escaped(output, domain_id); + writeln!(output, "\"{}", if import.native_rust { ";" } else { "" }).unwrap(); + } + } + if !import.native_rust { + writeln!(output, " consumes {} always;", import.consumes).unwrap(); } - writeln!(output, " consumes {} always;", import.consumes).unwrap(); } writeln!(output, "}}").unwrap(); } for function in &program.functions { writeln!(output).unwrap(); if function.explicit_id { - writeln!(output, "@id(\"{}\")", escape_string(&function.stable_id)).unwrap(); + write!(output, "@id(\"").unwrap(); + write_escaped(output, &function.stable_id); + writeln!(output, "\")").unwrap(); } - write!( - output, - "fn {}{}(", - function.name, - type_parameter_suffix(&function.type_parameters) - ) - .unwrap(); + write!(output, "fn {}", function.name).unwrap(); + write_type_parameters(output, &function.type_parameters); + output.write_char('(').unwrap(); for (index, param) in function.params.iter().enumerate() { if index > 0 { - output.push_str(", "); + output.write_str(", ").unwrap(); } - write!( - output, - "{}: {}{}", - param.name, - param.mode.source_prefix(), - param.ty - ) - .unwrap(); + write!(output, "{}: {}", param.name, param.mode.source_prefix()).unwrap(); + write_type(output, ¶m.ty); } - writeln!(output, ") -> {}", function.return_type).unwrap(); + output.write_str(") -> ").unwrap(); + write_type(output, &function.return_type); + writeln!(output).unwrap(); if !function.effects.is_empty() { - writeln!( - output, - " uses {{ {} }}", - crate::bounded_output::budgeted_join( - function - .effects - .iter() - .map(|value| crate::bounded_output::budgeted_clone(value)) - .collect::>(), - ", ", - ) - ) - .unwrap(); + write!(output, " uses {{ ").unwrap(); + write_joined(output, &function.effects, ", "); + writeln!(output, " }}").unwrap(); } for contract in &function.requires { - writeln!( - output, - " requires {}", - record_literal_delimited_expr(contract) - ) - .unwrap(); + write!(output, " requires ").unwrap(); + write_record_literal_delimited_expr(output, contract); + writeln!(output).unwrap(); } for contract in &function.ensures { - writeln!( - output, - " ensures {}", - record_literal_delimited_expr(contract) - ) - .unwrap(); + write!(output, " ensures ").unwrap(); + write_record_literal_delimited_expr(output, contract); + writeln!(output).unwrap(); } - write_function_body(&mut output, &function.body); + write_function_body(output, &function.body); } - output.into_string() } -fn type_parameter_suffix(parameters: &[crate::ast::TypeParameterDeclaration]) -> String { - if parameters.is_empty() { - String::new() - } else { - format!( - "<{}>", - crate::bounded_output::budgeted_join( - parameters - .iter() - .map(|parameter| crate::bounded_output::budgeted_clone(¶meter.name)) - .collect::>(), - ", " - ) - ) +#[allow(dead_code)] +pub(crate) fn write_canonical_with_scratch( + program: &Program, + output: &mut impl std::fmt::Write, + capacity: PrivateScratchCapacity, +) { + struct Restore(Option); + impl Drop for Restore { + fn drop(&mut self) { + PRIVATE_SCRATCH_CAPACITY.with(|slot| slot.set(self.0)); + } } + let previous = PRIVATE_SCRATCH_CAPACITY.with(|slot| slot.replace(Some(capacity))); + let _restore = Restore(previous); + write_canonical(program, output); } pub fn expr(value: &Expr, parent_precedence: u8) -> String { - match &value.kind { - ExprKind::Int(number) => number.to_string(), - ExprKind::Bool(value) => value.to_string(), - ExprKind::Var(name) => crate::bounded_output::budgeted_clone(name), - ExprKind::Call { - name, - type_arguments, - args, - } => format!( - "{}{}({})", - name, - if type_arguments.is_empty() { - String::new() - } else { - format!( - "<{}>", - crate::bounded_output::budgeted_join( - type_arguments - .iter() - .map(|argument| format!("{argument}")) - .collect::>(), - ", " - ) - ) - }, - crate::bounded_output::budgeted_join( - args.iter().map(|arg| expr(arg, 0)).collect::>(), - ", " - ) - ), - ExprKind::Unary { op, value } => { - let operator = match op { - UnaryOp::Neg => "-", - UnaryOp::Not => "!", - }; - format!("{operator}{}", expr(value, 7)) - } - ExprKind::Binary { op, left, right } => { - let precedence = op.precedence(); - let rendered = format!( - "{} {} {}", - expr(left, precedence), - op.text(), - expr(right, precedence + 1) - ); - if precedence < parent_precedence { - format!("({rendered})") - } else { - rendered - } + let _ = crate::bounded_output::reserve_active(legacy_expr_temporary_bytes( + value, + parent_precedence, + )); + let mut output = String::new(); + write_expr(&mut output, value, parent_precedence); + output +} + +fn rendered_expr_len(value: &Expr, parent_precedence: u8) -> usize { + struct Counter(usize); + impl std::fmt::Write for Counter { + fn write_str(&mut self, value: &str) -> std::fmt::Result { + self.0 = self.0.saturating_add(value.len()); + Ok(()) + } + } + let mut counter = Counter(0); + write_expr(&mut counter, value, parent_precedence); + counter.0 +} + +fn display_len(value: &impl std::fmt::Display) -> usize { + struct Counter(usize); + impl std::fmt::Write for Counter { + fn write_str(&mut self, value: &str) -> std::fmt::Result { + self.0 = self.0.saturating_add(value.len()); + Ok(()) + } + } + let mut counter = Counter(0); + std::fmt::write(&mut counter, format_args!("{value}")).unwrap(); + counter.0 +} + +fn joined_len(lengths: impl IntoIterator, count: usize, separator: usize) -> usize { + lengths + .into_iter() + .fold(0usize, usize::saturating_add) + .saturating_add(separator.saturating_mul(count.saturating_sub(1))) +} + +fn escaped_len(value: &str) -> usize { + value.bytes().fold(0usize, |length, byte| { + length.saturating_add(if matches!(byte, b'\\' | b'"') { 2 } else { 1 }) + }) +} + +fn legacy_type_parameter_bytes(parameters: &[crate::ast::TypeParameterDeclaration]) -> usize { + if parameters.is_empty() { + return 0; + } + let names = parameters.iter().map(|parameter| parameter.name.len()); + let cloned = names.clone().fold(0usize, usize::saturating_add); + let joined = joined_len(names, parameters.len(), 2); + cloned + .saturating_add(joined) + .saturating_add(joined.saturating_add(2)) +} + +fn legacy_string_join_bytes(values: &[String]) -> usize { + let cloned = values + .iter() + .map(String::len) + .fold(0usize, usize::saturating_add); + cloned.saturating_add(joined_len(values.iter().map(String::len), values.len(), 2)) +} + +fn legacy_canonical_temporary_bytes(program: &Program) -> usize { + let mut total = 0usize; + for module_use in &program.module_uses { + total = total.saturating_add(escaped_len(&module_use.persistent_id)); + } + if !program.permits.is_empty() { + total = total.saturating_add(legacy_string_join_bytes(&program.permits)); + } + for declaration in &program.types { + if declaration.explicit_id { + total = total.saturating_add(escaped_len(&declaration.stable_id)); } - ExprKind::Block { statements, tail } => { - let mut parts = statements - .iter() - .map(|statement| match statement { - Statement::Let { name, value, .. } => { - format!("let {name} = {};", expr(value, 0)) + total = total.saturating_add(legacy_type_parameter_bytes(&declaration.type_parameters)); + match &declaration.kind { + TypeDeclarationKind::Resource { lifecycles } => { + for lifecycle in lifecycles { + if let Some(stable_id) = &lifecycle.stable_id { + total = total.saturating_add(escaped_len(stable_id)); } - }) - .collect::>(); - parts.push(expr(tail, 0)); - format!("{{ {} }}", crate::bounded_output::budgeted_join(parts, " ")) - } - ExprKind::If { - condition, - then_branch, - else_branch, - } => format!( - "if {} {} else {}", - record_literal_delimited_expr(condition), - expr(then_branch, 0), - expr(else_branch, 0) - ), - ExprKind::ConstructRecord { - type_name, - type_arguments, - fields, - .. - } => { - let qualifier = if type_arguments.is_empty() { - crate::bounded_output::budgeted_clone(type_name) - } else { - format!( - "{}<{}>", - type_name, - crate::bounded_output::budgeted_join( - type_arguments - .iter() - .map(|argument| format!("{argument}")) - .collect::>(), - ", " - ) - ) - }; - if fields.is_empty() { - format!("{qualifier} {{}}") - } else { - format!( - "{qualifier} {{ {} }}", - crate::bounded_output::budgeted_join( - fields - .iter() - .map(|field| format!("{}: {}", field.name, expr(&field.value, 0))) - .collect::>(), - ", " - ) - ) + if let ResourceLifecycleKind::Imported { import_key } = &lifecycle.kind { + total = total.saturating_add(escaped_len(import_key)); + } + } + } + TypeDeclarationKind::Record { fields } => { + for field in fields { + if field.explicit_id { + total = total.saturating_add(escaped_len(&field.stable_id)); + } + } + } + TypeDeclarationKind::Variant { cases } => { + for case in cases { + if case.explicit_id { + total = total.saturating_add(escaped_len(&case.stable_id)); + } + for field in &case.fields { + if field.explicit_id { + total = total.saturating_add(escaped_len(&field.stable_id)); + } + } + } + } + } + } + for interface in &program.interfaces { + if interface.explicit_id { + total = total.saturating_add(escaped_len(&interface.stable_id)); + } + total = total.saturating_add(legacy_string_join_bytes(&interface.permits)); + for import in &interface.imports { + if import.explicit_id { + total = total.saturating_add(escaped_len(&import.stable_id)); + } + total = total.saturating_add(legacy_string_join_bytes(&import.effects)); + if let ImportFailure::Status { domain_id } = &import.failure { + total = total.saturating_add(escaped_len(domain_id)); } } - ExprKind::ConstructVariant { + } + for function in &program.functions { + if function.explicit_id { + total = total.saturating_add(escaped_len(&function.stable_id)); + } + total = total.saturating_add(legacy_type_parameter_bytes(&function.type_parameters)); + if !function.effects.is_empty() { + total = total.saturating_add(legacy_string_join_bytes(&function.effects)); + } + for contract in function.requires.iter().chain(&function.ensures) { + total = total.saturating_add(legacy_expr_temporary_bytes(contract, 0)); + if contains_record_construction(contract) { + total = total.saturating_add(rendered_expr_len(contract, 0).saturating_add(2)); + } + } + if let ExprKind::Block { statements, tail } = &function.body.kind { + for statement in statements { + let Statement::Let { value, .. } = statement; + total = total.saturating_add(legacy_expr_temporary_bytes(value, 0)); + } + total = total.saturating_add(legacy_expr_temporary_bytes(tail, 0)); + } else { + total = total.saturating_add(legacy_expr_temporary_bytes(&function.body, 0)); + } + } + total +} + +fn legacy_expr_temporary_bytes(root: &Expr, root_precedence: u8) -> usize { + let mut total = 0usize; + let mut stack = vec![(root, root_precedence)]; + while let Some((value, parent_precedence)) = stack.pop() { + let rendered = rendered_expr_len(value, parent_precedence); + match &value.kind { + ExprKind::Int(_) | ExprKind::Bool(_) => {} + ExprKind::Var(name) => total = total.saturating_add(name.len()), + ExprKind::Call { + type_arguments, + args, + .. + } => { + if !type_arguments.is_empty() { + let argument_lengths = type_arguments.iter().map(display_len); + let arguments = argument_lengths.clone().fold(0usize, usize::saturating_add); + let joined = joined_len(argument_lengths, type_arguments.len(), 2); + total = total + .saturating_add(arguments) + .saturating_add(joined) + .saturating_add(joined.saturating_add(2)); + } + let joined = joined_len( + args.iter().map(|argument| rendered_expr_len(argument, 0)), + args.len(), + 2, + ); + total = total.saturating_add(joined).saturating_add(rendered); + stack.extend(args.iter().rev().map(|argument| (argument, 0))); + } + ExprKind::Unary { value, .. } => { + total = total.saturating_add(rendered); + stack.push((value, 7)); + } + ExprKind::Binary { + op, left, right, .. + } => { + let inner = rendered_expr_len(left, op.precedence()) + .saturating_add(rendered_expr_len(right, op.precedence() + 1)) + .saturating_add(op.text().len()) + .saturating_add(2); + total = total.saturating_add(inner); + if op.precedence() < parent_precedence { + total = total.saturating_add(inner.saturating_add(2)); + } + stack.push((right, op.precedence() + 1)); + stack.push((left, op.precedence())); + } + ExprKind::Block { statements, tail } => { + let mut parts = Vec::with_capacity(statements.len() + 1); + for statement in statements { + let Statement::Let { name, value, .. } = statement; + let part = name + .len() + .saturating_add(rendered_expr_len(value, 0)) + .saturating_add(8); + total = total.saturating_add(part); + parts.push(part); + stack.push((value, 0)); + } + parts.push(rendered_expr_len(tail, 0)); + let joined = joined_len(parts, statements.len() + 1, 1); + total = total.saturating_add(joined).saturating_add(rendered); + stack.push((tail, 0)); + } + ExprKind::If { + condition, + then_branch, + else_branch, + } => { + if contains_record_construction(condition) { + total = total.saturating_add(rendered_expr_len(condition, 0).saturating_add(2)); + } + total = total.saturating_add(rendered); + stack.push((else_branch, 0)); + stack.push((then_branch, 0)); + stack.push((condition, 0)); + } + ExprKind::ConstructRecord { + type_name, + type_arguments, + fields, + .. + } + | ExprKind::ConstructVariant { + type_name, + type_arguments, + fields, + .. + } => { + if type_arguments.is_empty() { + total = total.saturating_add(type_name.len()); + } else { + let argument_lengths = type_arguments.iter().map(display_len); + let arguments = argument_lengths.clone().fold(0usize, usize::saturating_add); + let joined = joined_len(argument_lengths, type_arguments.len(), 2); + total = total + .saturating_add(arguments) + .saturating_add(joined) + .saturating_add(type_name.len().saturating_add(joined).saturating_add(2)); + } + let mut parts = Vec::with_capacity(fields.len()); + for field in fields { + let part = field + .name + .len() + .saturating_add(rendered_expr_len(&field.value, 0)) + .saturating_add(2); + total = total.saturating_add(part); + parts.push(part); + stack.push((&field.value, 0)); + } + if !fields.is_empty() { + total = total.saturating_add(joined_len(parts, fields.len(), 2)); + } + total = total.saturating_add(rendered); + } + ExprKind::Match { scrutinee, arms } => { + if contains_record_construction(scrutinee) { + total = total.saturating_add(rendered_expr_len(scrutinee, 0).saturating_add(2)); + } + let mut parts = Vec::with_capacity(arms.len()); + for arm in arms { + total = total.saturating_add(legacy_match_pattern_bytes(&arm.pattern)); + let part = rendered_match_pattern_len(&arm.pattern) + .saturating_add(rendered_expr_len(&arm.value, 0)) + .saturating_add(5); + total = total.saturating_add(part); + parts.push(part); + stack.push((&arm.value, 0)); + } + total = total + .saturating_add(joined_len(parts, arms.len(), 1)) + .saturating_add(rendered); + stack.push((scrutinee, 0)); + } + ExprKind::Try { operand } => { + let delimited = matches!( + operand.kind, + ExprKind::Binary { .. } | ExprKind::If { .. } | ExprKind::Block { .. } + ); + if delimited { + total = total.saturating_add(rendered_expr_len(operand, 0).saturating_add(2)); + } + total = total.saturating_add(rendered); + stack.push((operand, if delimited { 0 } else { 8 })); + } + ExprKind::UpdateRecord { base, fields } => { + let delimited = matches!( + base.kind, + ExprKind::Binary { .. } | ExprKind::If { .. } | ExprKind::Block { .. } + ); + if delimited { + total = total.saturating_add(rendered_expr_len(base, 0).saturating_add(2)); + } + let mut parts = Vec::with_capacity(fields.len()); + for field in fields { + let part = field + .name + .len() + .saturating_add(rendered_expr_len(&field.value, 0)) + .saturating_add(2); + total = total.saturating_add(part); + parts.push(part); + stack.push((&field.value, 0)); + } + if !fields.is_empty() { + total = total.saturating_add(joined_len(parts, fields.len(), 2)); + } + total = total.saturating_add(rendered); + stack.push((base, if delimited { 0 } else { 8 })); + } + ExprKind::Project { base, .. } => { + let delimited = matches!( + base.kind, + ExprKind::Binary { .. } | ExprKind::If { .. } | ExprKind::Block { .. } + ); + if delimited { + total = total.saturating_add(rendered_expr_len(base, 0).saturating_add(2)); + } + total = total.saturating_add(rendered); + stack.push((base, if delimited { 0 } else { 8 })); + } + } + } + total +} + +fn rendered_match_pattern_len(pattern: &MatchPattern) -> usize { + struct Counter(usize); + impl std::fmt::Write for Counter { + fn write_str(&mut self, value: &str) -> std::fmt::Result { + self.0 = self.0.saturating_add(value.len()); + Ok(()) + } + } + let mut counter = Counter(0); + write_match_pattern(&mut counter, pattern); + counter.0 +} + +fn legacy_match_pattern_bytes(pattern: &MatchPattern) -> usize { + match pattern { + MatchPattern::Wildcard { .. } => 0, + MatchPattern::Variant { type_name, - type_arguments, case_name, fields, .. } => { - let qualifier = if type_arguments.is_empty() { - crate::bounded_output::budgeted_clone(type_name) - } else { - format!( - "{}<{}>", - type_name, - crate::bounded_output::budgeted_join( - type_arguments - .iter() - .map(|argument| format!("{argument}")) - .collect::>(), - ", " - ) - ) - }; - if fields.is_empty() { - format!("{qualifier}::{case_name} {{}}") - } else { - format!( - "{qualifier}::{case_name} {{ {} }}", - crate::bounded_output::budgeted_join( - fields - .iter() - .map(|field| format!("{}: {}", field.name, expr(&field.value, 0))) - .collect::>(), - ", " - ) - ) - } - } - ExprKind::Match { scrutinee, arms } => format!( - "match {} {{ {} }}", - record_literal_delimited_expr(scrutinee), - crate::bounded_output::budgeted_join( - arms.iter() - .map(|arm| format!( - "{} => {},", - match_pattern(&arm.pattern), - expr(&arm.value, 0) - )) - .collect::>(), - " " - ) - ), - ExprKind::Try { operand } => { - let operand = match &operand.kind { - ExprKind::Binary { .. } | ExprKind::If { .. } | ExprKind::Block { .. } => { - format!("({})", expr(operand, 0)) + let parts = fields.iter().map(|field| { + if field.name == field.binding { + field.name.len() + } else { + field + .name + .len() + .saturating_add(field.binding.len()) + .saturating_add(2) } - _ => expr(operand, 8), - }; - format!("{operand}?") + }); + let part_total = parts.clone().fold(0usize, usize::saturating_add); + let joined = joined_len(parts, fields.len(), 2); + let outer = type_name + .len() + .saturating_add(case_name.len()) + .saturating_add(if fields.is_empty() { + 5 + } else { + joined.saturating_add(8) + }); + part_total.saturating_add(joined).saturating_add(outer) } - ExprKind::UpdateRecord { base, fields } => { - let base = match &base.kind { - ExprKind::Binary { .. } | ExprKind::If { .. } | ExprKind::Block { .. } => { - format!("({})", expr(base, 0)) + MatchPattern::Record { + type_name, fields, .. + } => legacy_record_pattern_bytes(type_name, fields), + } +} + +fn legacy_record_pattern_bytes( + root_name: &str, + root_fields: &[crate::ast::RecordMatchPatternField], +) -> usize { + use crate::ast::RecordMatchFieldPattern; + + let mut total = 0usize; + let mut stack = vec![(root_name, root_fields)]; + while let Some((type_name, fields)) = stack.pop() { + let mut part_lengths = Vec::with_capacity(fields.len()); + for field in fields { + let part = match &field.pattern { + RecordMatchFieldPattern::Binding { name, .. } if name == &field.name => { + total = total.saturating_add(field.name.len()); + field.name.len() + } + RecordMatchFieldPattern::Binding { name, .. } => field + .name + .len() + .saturating_add(name.len()) + .saturating_add(2), + RecordMatchFieldPattern::Wildcard { .. } => field.name.len().saturating_add(3), + RecordMatchFieldPattern::Record { + type_name, + fields: nested, + .. + } => { + let length = field + .name + .len() + .saturating_add(rendered_record_pattern_len(type_name, nested)) + .saturating_add(2); + stack.push((type_name, nested)); + length } - _ => expr(base, 8), }; - if fields.is_empty() { - format!("{base} with {{}}") + total = total.saturating_add(part); + part_lengths.push(part); + } + let joined = joined_len(part_lengths, fields.len(), 2); + total = total + .saturating_add(joined) + .saturating_add(type_name.len().saturating_add(if fields.is_empty() { + 3 } else { - format!( - "{base} with {{ {} }}", - crate::bounded_output::budgeted_join( - fields - .iter() - .map(|field| format!("{}: {}", field.name, expr(&field.value, 0))) - .collect::>(), - ", " - ) - ) - } + joined.saturating_add(4) + })); + } + total +} + +fn rendered_record_pattern_len( + type_name: &str, + fields: &[crate::ast::RecordMatchPatternField], +) -> usize { + struct Counter(usize); + impl std::fmt::Write for Counter { + fn write_str(&mut self, value: &str) -> std::fmt::Result { + self.0 = self.0.saturating_add(value.len()); + Ok(()) } - ExprKind::Project { base, field, .. } => { - let base = match &base.kind { - ExprKind::Binary { .. } | ExprKind::If { .. } | ExprKind::Block { .. } => { - format!("({})", expr(base, 0)) + } + let mut counter = Counter(0); + write_record_match_pattern(&mut counter, type_name, fields); + counter.0 +} +fn write_expr(output: &mut impl std::fmt::Write, value: &Expr, parent_precedence: u8) { + use ExprFormatFrame as Frame; + let mut frames = FormatFrameStack::new( + Frame::Expr(value, parent_precedence), + ScratchStackKind::Expression, + ); + while let Some(frame) = frames.pop() { + match frame { + Frame::Expr(value, parent_precedence) => match &value.kind { + ExprKind::Int(number) => write!(output, "{number}").unwrap(), + ExprKind::Bool(value) => write!(output, "{value}").unwrap(), + ExprKind::Var(name) => output.write_str(name).unwrap(), + ExprKind::Call { + name, + type_arguments, + args, + } => { + output.write_str(name).unwrap(); + if !type_arguments.is_empty() { + output.write_char('<').unwrap(); + for (index, argument) in type_arguments.iter().enumerate() { + if index != 0 { + output.write_str(", ").unwrap(); + } + write_type(output, argument); + } + output.write_char('>').unwrap(); + } + output.write_char('(').unwrap(); + frames.push(Frame::Close(')')); + frames.push(Frame::CallArgs(args, 0)); } - _ => expr(base, 8), - }; - format!("{base}.{field}") + ExprKind::Unary { op, value } => { + output + .write_str(match op { + UnaryOp::Neg => "-", + UnaryOp::Not => "!", + }) + .unwrap(); + frames.push(Frame::Expr(value, 7)); + } + ExprKind::Binary { op, left, right } => { + let precedence = op.precedence(); + let delimited = precedence < parent_precedence; + if delimited { + output.write_char('(').unwrap(); + } + frames.push(Frame::BinaryRight(right, *op, delimited)); + frames.push(Frame::Expr(left, precedence)); + } + ExprKind::Block { statements, tail } => { + output.write_str("{ ").unwrap(); + frames.push(Frame::Block(statements, tail, 0)); + } + ExprKind::If { + condition, + then_branch, + else_branch, + } => { + output.write_str("if ").unwrap(); + let delimited = contains_record_construction(condition); + if delimited { + output.write_char('(').unwrap(); + } + frames.push(Frame::IfThen(then_branch, else_branch)); + if delimited { + frames.push(Frame::Close(')')); + } + frames.push(Frame::Expr(condition, 0)); + } + ExprKind::ConstructRecord { + type_name, + type_arguments, + fields, + .. + } => { + output.write_str(type_name).unwrap(); + write_type_arguments(output, type_arguments); + if fields.is_empty() { + output.write_str(" {}").unwrap(); + } else { + output.write_str(" { ").unwrap(); + frames.push(Frame::Fields(fields, 0, " }")); + } + } + ExprKind::ConstructVariant { + type_name, + type_arguments, + case_name, + fields, + .. + } => { + output.write_str(type_name).unwrap(); + write_type_arguments(output, type_arguments); + write!(output, "::{case_name}").unwrap(); + if fields.is_empty() { + output.write_str(" {}").unwrap(); + } else { + output.write_str(" { ").unwrap(); + frames.push(Frame::Fields(fields, 0, " }")); + } + } + ExprKind::Match { scrutinee, arms } => { + output.write_str("match ").unwrap(); + let delimited = contains_record_construction(scrutinee); + if delimited { + output.write_char('(').unwrap(); + } + frames.push(Frame::MatchArms(arms, 0)); + if delimited { + frames.push(Frame::Close(')')); + } + frames.push(Frame::Expr(scrutinee, 0)); + } + ExprKind::Try { operand } => { + let delimited = matches!( + operand.kind, + ExprKind::Binary { .. } | ExprKind::If { .. } | ExprKind::Block { .. } + ); + if delimited { + output.write_char('(').unwrap(); + } + frames.push(Frame::TryEnd(delimited)); + frames.push(Frame::Expr(operand, if delimited { 0 } else { 8 })); + } + ExprKind::UpdateRecord { base, fields } => { + let delimited = matches!( + base.kind, + ExprKind::Binary { .. } | ExprKind::If { .. } | ExprKind::Block { .. } + ); + if delimited { + output.write_char('(').unwrap(); + } + frames.push(Frame::PostfixFields(fields)); + if delimited { + frames.push(Frame::Close(')')); + } + frames.push(Frame::Expr(base, if delimited { 0 } else { 8 })); + } + ExprKind::Project { base, field, .. } => { + let delimited = matches!( + base.kind, + ExprKind::Binary { .. } | ExprKind::If { .. } | ExprKind::Block { .. } + ); + if delimited { + output.write_char('(').unwrap(); + } + frames.push(Frame::ProjectField(field)); + if delimited { + frames.push(Frame::Close(')')); + } + frames.push(Frame::Expr(base, if delimited { 0 } else { 8 })); + } + }, + Frame::CallArgs(args, index) => { + if let Some(argument) = args.get(index) { + if index != 0 { + output.write_str(", ").unwrap(); + } + frames.push(Frame::CallArgs(args, index + 1)); + frames.push(Frame::Expr(argument, 0)); + } + } + Frame::BinaryRight(right, op, delimited) => { + write!(output, " {} ", op.text()).unwrap(); + if delimited { + frames.push(Frame::Close(')')); + } + frames.push(Frame::Expr(right, op.precedence() + 1)); + } + Frame::Block(statements, tail, index) => { + if let Some(statement) = statements.get(index) { + let Statement::Let { name, value, .. } = statement; + write!(output, "let {name} = ").unwrap(); + frames.push(Frame::BlockNext(statements, tail, index + 1)); + frames.push(Frame::Expr(value, 0)); + } else { + frames.push(Frame::Close('}')); + frames.push(Frame::Expr(tail, 0)); + } + } + Frame::BlockNext(statements, tail, index) => { + output.write_str("; ").unwrap(); + frames.push(Frame::Block(statements, tail, index)); + } + Frame::IfThen(then_branch, else_branch) => { + output.write_char(' ').unwrap(); + frames.push(Frame::IfElse(else_branch)); + frames.push(Frame::Expr(then_branch, 0)); + } + Frame::IfElse(else_branch) => { + output.write_str(" else ").unwrap(); + frames.push(Frame::Expr(else_branch, 0)); + } + Frame::Fields(fields, index, suffix) => { + if let Some(field) = fields.get(index) { + if index != 0 { + output.write_str(", ").unwrap(); + } + write!(output, "{}: ", field.name).unwrap(); + frames.push(Frame::Fields(fields, index + 1, suffix)); + frames.push(Frame::Expr(&field.value, 0)); + } else { + output.write_str(suffix).unwrap(); + } + } + Frame::MatchArms(arms, index) => { + if let Some(arm) = arms.get(index) { + if index == 0 { + output.write_str(" { ").unwrap(); + } else { + output.write_str(", ").unwrap(); + } + write_match_pattern(output, &arm.pattern); + output.write_str(" => ").unwrap(); + frames.push(Frame::MatchArms(arms, index + 1)); + frames.push(Frame::Expr(&arm.value, 0)); + } else if index == 0 { + output.write_str(" { }").unwrap(); + } else { + output.write_str(", }").unwrap(); + } + } + Frame::TryEnd(delimited) => { + if delimited { + output.write_char(')').unwrap(); + } + output.write_char('?').unwrap(); + } + Frame::PostfixFields(fields) => { + if fields.is_empty() { + output.write_str(" with {}").unwrap(); + } else { + output.write_str(" with { ").unwrap(); + frames.push(Frame::Fields(fields, 0, " }")); + } + } + Frame::ProjectField(field) => write!(output, ".{field}").unwrap(), + Frame::Close('}') => output.write_str(" }").unwrap(), + Frame::Close(character) => output.write_char(character).unwrap(), } } } -fn record_literal_delimited_expr(value: &Expr) -> String { - let rendered = expr(value, 0); - if contains_record_construction(value) { - format!("({rendered})") - } else { - rendered +fn write_record_literal_delimited_expr(output: &mut impl std::fmt::Write, value: &Expr) { + let delimited = contains_record_construction(value); + if delimited { + output.write_char('(').unwrap(); + } + write_expr(output, value, 0); + if delimited { + output.write_char(')').unwrap(); + } +} + +fn write_type_parameters( + output: &mut impl std::fmt::Write, + parameters: &[crate::ast::TypeParameterDeclaration], +) { + if parameters.is_empty() { + return; + } + output.write_char('<').unwrap(); + for (index, parameter) in parameters.iter().enumerate() { + if index != 0 { + output.write_str(", ").unwrap(); + } + output.write_str(¶meter.name).unwrap(); + } + output.write_char('>').unwrap(); +} + +fn write_type_arguments(output: &mut impl std::fmt::Write, arguments: &[crate::ast::Type]) { + if arguments.is_empty() { + return; + } + output.write_char('<').unwrap(); + for (index, argument) in arguments.iter().enumerate() { + if index != 0 { + output.write_str(", ").unwrap(); + } + write_type(output, argument); + } + output.write_char('>').unwrap(); +} + +fn write_type(output: &mut impl std::fmt::Write, ty: &crate::ast::Type) { + use TypeFormatFrame as Frame; + let mut frames = FormatFrameStack::new(Frame::Type(ty), ScratchStackKind::Type); + while let Some(frame) = frames.pop() { + match frame { + Frame::Type(crate::ast::Type::I64) => output.write_str("i64").unwrap(), + Frame::Type(crate::ast::Type::Bool) => output.write_str("bool").unwrap(), + Frame::Type(crate::ast::Type::Named { name, arguments }) => { + output.write_str(name).unwrap(); + if !arguments.is_empty() { + output.write_char('<').unwrap(); + frames.push(Frame::Arguments(arguments, 0)); + } + } + Frame::Arguments(arguments, index) => { + if let Some(argument) = arguments.get(index) { + if index != 0 { + output.write_str(", ").unwrap(); + } + frames.push(Frame::Arguments(arguments, index + 1)); + frames.push(Frame::Type(argument)); + } else { + output.write_char('>').unwrap(); + } + } + } } } -fn match_pattern(pattern: &MatchPattern) -> String { +fn write_match_pattern(output: &mut impl std::fmt::Write, pattern: &MatchPattern) { match pattern { - MatchPattern::Wildcard { .. } => "_".to_owned(), + MatchPattern::Wildcard { .. } => output.write_char('_').unwrap(), MatchPattern::Variant { type_name, case_name, fields, .. } => { - let fields = crate::bounded_output::budgeted_join( - fields - .iter() - .map(|field| { - if field.name == field.binding { - crate::bounded_output::budgeted_clone(&field.name) - } else { - format!("{}: {}", field.name, field.binding) - } - }) - .collect::>(), - ", ", - ); + write!(output, "{type_name}::{case_name}").unwrap(); if fields.is_empty() { - format!("{type_name}::{case_name} {{}}") + output.write_str(" {}").unwrap(); } else { - format!("{type_name}::{case_name} {{ {fields} }}") + output.write_str(" { ").unwrap(); + for (index, field) in fields.iter().enumerate() { + if index != 0 { + output.write_str(", ").unwrap(); + } + if field.name == field.binding { + output.write_str(&field.name).unwrap(); + } else { + write!(output, "{}: {}", field.name, field.binding).unwrap(); + } + } + output.write_str(" }").unwrap(); } } MatchPattern::Record { type_name, fields, .. - } => record_match_pattern(type_name, fields), + } => write_record_match_pattern(output, type_name, fields), } } -fn record_match_pattern(type_name: &str, fields: &[crate::ast::RecordMatchPatternField]) -> String { - let fields = crate::bounded_output::budgeted_join( - fields - .iter() - .map(|field| match &field.pattern { - crate::ast::RecordMatchFieldPattern::Binding { name, .. } - if name == &field.name => - { - crate::bounded_output::budgeted_clone(&field.name) +fn write_record_match_pattern( + output: &mut impl std::fmt::Write, + type_name: &str, + fields: &[crate::ast::RecordMatchPatternField], +) { + use PatternFormatFrame as Frame; + let mut frames = + FormatFrameStack::new(Frame::Enter(type_name, fields), ScratchStackKind::Pattern); + while let Some(frame) = frames.pop() { + match frame { + Frame::Enter(type_name, fields) => { + output.write_str(type_name).unwrap(); + if fields.is_empty() { + output.write_str(" {}").unwrap(); + } else { + output.write_str(" { ").unwrap(); + frames.push(Frame::Fields(fields, 0)); } - crate::ast::RecordMatchFieldPattern::Binding { name, .. } => { - format!("{}: {name}", field.name) + } + Frame::Fields(fields, index) => { + let Some(field) = fields.get(index) else { + output.write_str(" }").unwrap(); + continue; + }; + if index != 0 { + output.write_str(", ").unwrap(); } - crate::ast::RecordMatchFieldPattern::Wildcard { .. } => { - format!("{}: _", field.name) + frames.push(Frame::Fields(fields, index + 1)); + match &field.pattern { + crate::ast::RecordMatchFieldPattern::Binding { name, .. } + if name == &field.name => + { + output.write_str(&field.name).unwrap(); + } + crate::ast::RecordMatchFieldPattern::Binding { name, .. } => { + write!(output, "{}: {name}", field.name).unwrap(); + } + crate::ast::RecordMatchFieldPattern::Wildcard { .. } => { + write!(output, "{}: _", field.name).unwrap(); + } + crate::ast::RecordMatchFieldPattern::Record { + type_name, fields, .. + } => { + write!(output, "{}: ", field.name).unwrap(); + frames.push(Frame::Enter(type_name, fields)); + } } - crate::ast::RecordMatchFieldPattern::Record { - type_name, fields, .. - } => format!( - "{}: {}", - field.name, - record_match_pattern(type_name, fields) - ), - }) - .collect::>(), - ", ", - ); - if fields.is_empty() { - format!("{type_name} {{}}") - } else { - format!("{type_name} {{ {fields} }}") + } + } } } fn contains_record_construction(value: &Expr) -> bool { - match &value.kind { - ExprKind::ConstructRecord { .. } | ExprKind::ConstructVariant { .. } => true, - ExprKind::Call { args, .. } => args.iter().any(contains_record_construction), - ExprKind::Unary { value, .. } - | ExprKind::Try { operand: value } - | ExprKind::Project { base: value, .. } => contains_record_construction(value), - ExprKind::UpdateRecord { base, fields } => { - contains_record_construction(base) - || fields - .iter() - .any(|field| contains_record_construction(&field.value)) - } - ExprKind::Binary { left, right, .. } => { - contains_record_construction(left) || contains_record_construction(right) - } - ExprKind::Block { statements, tail } => { - statements.iter().any(|statement| match statement { - Statement::Let { value, .. } => contains_record_construction(value), - }) || contains_record_construction(tail) - } - ExprKind::If { - condition, - then_branch, - else_branch, - } => { - contains_record_construction(condition) - || contains_record_construction(then_branch) - || contains_record_construction(else_branch) + use ContainsRecordFrame as Frame; + fn child(value: &Expr, index: usize) -> Option<&Expr> { + match &value.kind { + ExprKind::Call { args, .. } => args.get(index), + ExprKind::Unary { value, .. } + | ExprKind::Try { operand: value } + | ExprKind::Project { base: value, .. } => (index == 0).then_some(value), + ExprKind::UpdateRecord { base, fields } => { + if index == 0 { + Some(base) + } else { + fields.get(index - 1).map(|field| &field.value) + } + } + ExprKind::Binary { left, right, .. } => { + [left.as_ref(), right.as_ref()].get(index).copied() + } + ExprKind::Block { statements, tail } => statements + .get(index) + .map(|statement| { + let Statement::Let { value, .. } = statement; + value + }) + .or_else(|| (index == statements.len()).then_some(tail)), + ExprKind::If { + condition, + then_branch, + else_branch, + } => [ + condition.as_ref(), + then_branch.as_ref(), + else_branch.as_ref(), + ] + .get(index) + .copied(), + ExprKind::Match { scrutinee, arms } => { + if index == 0 { + Some(scrutinee) + } else { + arms.get(index - 1).map(|arm| &arm.value) + } + } + ExprKind::ConstructRecord { .. } + | ExprKind::ConstructVariant { .. } + | ExprKind::Int(_) + | ExprKind::Bool(_) + | ExprKind::Var(_) => None, } - ExprKind::Match { scrutinee, arms } => { - contains_record_construction(scrutinee) - || arms - .iter() - .any(|arm| contains_record_construction(&arm.value)) + } + let mut frames = FormatFrameStack::new(Frame::Enter(value), ScratchStackKind::ContainsRecord); + while let Some(frame) = frames.pop() { + match frame { + Frame::Enter(value) => { + if matches!( + value.kind, + ExprKind::ConstructRecord { .. } | ExprKind::ConstructVariant { .. } + ) { + return true; + } + frames.push(Frame::Children(value, 0)); + } + Frame::Children(value, index) => { + if let Some(child) = child(value, index) { + frames.push(Frame::Children(value, index + 1)); + frames.push(Frame::Enter(child)); + } + } } - ExprKind::Int(_) | ExprKind::Bool(_) | ExprKind::Var(_) => false, } + false } fn write_function_body(output: &mut impl std::fmt::Write, body: &Expr) { @@ -594,25 +1356,67 @@ fn write_function_body(output: &mut impl std::fmt::Write, body: &Expr) { for statement in statements { match statement { Statement::Let { name, value, .. } => { - writeln!(output, " let {name} = {};", expr(value, 0)).unwrap(); + write!(output, " let {name} = ").unwrap(); + write_expr(output, value, 0); + writeln!(output, ";").unwrap(); } } } - writeln!(output, " {}", expr(tail, 0)).unwrap(); + write!(output, " ").unwrap(); + write_expr(output, tail, 0); + writeln!(output).unwrap(); } else { - writeln!(output, " {}", expr(body, 0)).unwrap(); + write!(output, " ").unwrap(); + write_expr(output, body, 0); + writeln!(output).unwrap(); } writeln!(output, "}}").unwrap(); } -fn escape_string(value: &str) -> String { - let mut escaped = crate::bounded_output::CappedString::new(); +fn write_escaped(output: &mut impl std::fmt::Write, value: &str) { for value in value.chars() { match value { - '\\' => escaped.push_str("\\\\"), - '"' => escaped.push_str("\\\""), - value => escaped.push(value), + '\\' => output.write_str("\\\\").unwrap(), + '"' => output.write_str("\\\"").unwrap(), + value => output.write_char(value).unwrap(), + } + } +} + +fn write_joined(output: &mut impl std::fmt::Write, values: &[String], separator: &str) { + for (index, value) in values.iter().enumerate() { + if index != 0 { + output.write_str(separator).unwrap(); } + output.write_str(value).unwrap(); + } +} + +#[cfg(test)] +mod iterative_formatter_tests { + use super::*; + use std::path::Path; + + #[test] + fn expression_blocks_and_empty_match_keep_exact_separators() { + let block = crate::parse( + "module t; fn main()->i64 { { let x = 1; let y = 2; x + y } }", + Path::new("format-block.spx"), + ) + .unwrap(); + let ExprKind::Block { tail, .. } = &block.functions[0].body.kind else { + unreachable!() + }; + assert_eq!(expr(tail, 0), "{ let x = 1; let y = 2; x + y }"); + + let empty = crate::parse( + "module t; fn main(value:i64)->i64 { match value { } }", + Path::new("format-empty-match.spx"), + ) + .unwrap(); + let ExprKind::Block { tail, .. } = &empty.functions[0].body.kind else { + unreachable!() + }; + assert_eq!(expr(tail, 0), "match value { }"); } - escaped.into_string() } diff --git a/src/graph.rs b/src/graph.rs index 63e5452..4804184 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -27,6 +27,38 @@ macro_rules! format { }; } +pub(crate) fn reject_native_rust_imports(program: &ResolvedProgram) -> Result<(), Diagnostic> { + if program + .interfaces + .iter() + .flat_map(|interface| &interface.imports) + .any(|import| import.native_rust) + { + Err(Diagnostic::io( + "SPX-G218", + "Native Rust import declarations are outside the current semantic Graph schemas", + )) + } else { + Ok(()) + } +} + +fn reject_source_native_rust_imports(program: &Program) -> Result<(), Vec> { + if program + .interfaces + .iter() + .flat_map(|interface| &interface.imports) + .any(|import| import.native_rust) + { + Err(vec![Diagnostic::io( + "SPX-G218", + "Native Rust import declarations are outside the current semantic Graph schemas", + )]) + } else { + Ok(()) + } +} + /// Hash the canonical human-readable source projection and implicit prelude. /// /// This revision intentionally does not depend on HIR spans, display metadata, @@ -60,6 +92,7 @@ pub(crate) fn revision_from_canonical_source(source: &str) -> String { /// Resolution is deliberately part of this public boundary. Invalid source /// cannot be mistaken for a checked semantic graph by library callers. pub fn to_json(program: &Program) -> Result> { + reject_source_native_rust_imports(program)?; let revision = revision(program); let resolved = hir::resolve(program)?; to_hir_json(&resolved, &revision).map_err(|diagnostic| vec![diagnostic]) @@ -75,8 +108,10 @@ pub fn context_json( symbol: &str, depth: usize, ) -> Result, Vec> { + reject_source_native_rust_imports(program)?; let revision = revision(program); let resolved = hir::resolve(program)?; + reject_native_rust_imports(&resolved).map_err(|diagnostic| vec![diagnostic])?; context_hir_json(&resolved, &revision, symbol, depth).map_err(|diagnostic| vec![diagnostic]) } @@ -320,8 +355,10 @@ pub fn agent_context_json( symbol: &str, options: &AgentContextOptions, ) -> Result, Vec> { + reject_source_native_rust_imports(program)?; let source_revision = revision(program); let resolved = hir::resolve(program)?; + reject_native_rust_imports(&resolved).map_err(|diagnostic| vec![diagnostic])?; agent_context_hir_json(&resolved, &source_revision, symbol, options) .map_err(|diagnostic| vec![diagnostic]) } @@ -333,8 +370,10 @@ pub fn agent_context_v2_json( symbol: &str, options: &AgentContextV2Options, ) -> Result, Vec> { + reject_source_native_rust_imports(program)?; let source_revision = revision(program); let resolved = hir::resolve(program)?; + reject_native_rust_imports(&resolved).map_err(|diagnostic| vec![diagnostic])?; agent_context_v2_hir_json(&resolved, &source_revision, symbol, options) .map_err(|diagnostic| vec![diagnostic]) } @@ -1095,6 +1134,11 @@ fn collect_result_propagations<'a>( collect_result_propagations(argument, propagations); } } + ResolvedExprKind::NativeRustImportCall(call) => { + for argument in &call.args { + collect_result_propagations(argument, propagations); + } + } ResolvedExprKind::Unary { value, .. } | ResolvedExprKind::Project { base: value, .. } => { collect_result_propagations(value, propagations); } @@ -1246,6 +1290,9 @@ pub(crate) fn graph_schema_from_parts( fn expression_has_record_pattern(expression: &ResolvedExpr) -> bool { match &expression.kind { ResolvedExprKind::Call { args, .. } => args.iter().any(expression_has_record_pattern), + ResolvedExprKind::NativeRustImportCall(call) => { + call.args.iter().any(expression_has_record_pattern) + } ResolvedExprKind::Unary { value, .. } | ResolvedExprKind::Try { operand: value, .. } | ResolvedExprKind::TryOption { operand: value, .. } @@ -1348,6 +1395,11 @@ fn collect_agent_contract_values(expression: &ResolvedExpr, values: &mut BTreeSe collect_agent_contract_values(argument, values); } } + ResolvedExprKind::NativeRustImportCall(call) => { + for argument in &call.args { + collect_agent_contract_values(argument, values); + } + } ResolvedExprKind::Unary { value, .. } => collect_agent_contract_values(value, values), ResolvedExprKind::Binary { left, right, .. } => { collect_agent_contract_values(left, values); @@ -1445,6 +1497,12 @@ fn agent_contract_expr_json(expression: &ResolvedExpr) -> Result { + return Err(Diagnostic::io( + "SPX-G218", + "Native Rust import declarations are outside the current semantic Graph schemas", + )); + } ResolvedExprKind::Unary { op, value } => format!( "{{\"kind\":\"unary\",\"op\":{},\"value\":{}}}", quote_json(unary_text(*op)), @@ -2275,6 +2333,7 @@ pub(crate) fn to_hir_json( program: &ResolvedProgram, source_revision: &str, ) -> Result { + reject_native_rust_imports(program)?; hir::validate(program)?; let selected_functions = program .functions @@ -3031,6 +3090,11 @@ fn visit_expr_call_instances( visit_expr_call_instances(argument, visit); } } + ResolvedExprKind::NativeRustImportCall(call) => { + for argument in &call.args { + visit_expr_call_instances(argument, visit); + } + } ResolvedExprKind::Unary { value, .. } | ResolvedExprKind::Project { base: value, .. } => { visit_expr_call_instances(value, visit); } @@ -3087,6 +3151,11 @@ fn visit_expr_calls(expression: &ResolvedExpr, visit: &mut impl FnMut(&Declarati visit_expr_calls(argument, visit); } } + ResolvedExprKind::NativeRustImportCall(call) => { + for argument in &call.args { + visit_expr_calls(argument, visit); + } + } ResolvedExprKind::Unary { value, .. } => visit_expr_calls(value, visit), ResolvedExprKind::Binary { left, right, .. } => { visit_expr_calls(left, visit); @@ -3166,6 +3235,11 @@ fn collect_expr_type_declarations( collect_expr_type_declarations(argument, declarations); } } + ResolvedExprKind::NativeRustImportCall(call) => { + for argument in &call.args { + collect_expr_type_declarations(argument, declarations); + } + } ResolvedExprKind::Unary { value, .. } => { collect_expr_type_declarations(value, declarations); } @@ -3388,6 +3462,12 @@ fn expr_json(program: &ResolvedProgram, expression: &ResolvedExpr) -> Result { + return Err(Diagnostic::io( + "SPX-G218", + "Native Rust import declarations are outside the current semantic Graph schemas", + )); + } ResolvedExprKind::Unary { op, value } => format!( "{{{header},\"kind\":\"unary\",\"op\":{},\"value\":{}}}", quote_json(unary_text(*op)), @@ -3666,6 +3746,11 @@ fn collect_expr_types(expression: &ResolvedExpr, types: &mut BTreeMap { + for argument in &call.args { + collect_expr_types(argument, types); + } + } ResolvedExprKind::Unary { value, .. } => collect_expr_types(value, types), ResolvedExprKind::Binary { left, right, .. } => { collect_expr_types(left, types); @@ -3768,6 +3853,7 @@ fn collect_type(ty: &ResolvedType, types: &mut BTreeMap) { fn type_json(ty: &ResolvedType) -> String { match ty { + ResolvedType::Unit => unreachable!("native Rust Unit is excluded before Graph projection"), ResolvedType::I64 => "{\"kind\":\"primitive\",\"name\":\"i64\"}".to_owned(), ResolvedType::Bool => "{\"kind\":\"primitive\",\"name\":\"bool\"}".to_owned(), ResolvedType::TypeParameter { owner, index } => format!( diff --git a/src/graph_cleanup.rs b/src/graph_cleanup.rs index a225e29..e0aeb06 100644 --- a/src/graph_cleanup.rs +++ b/src/graph_cleanup.rs @@ -396,6 +396,7 @@ fn result_source_json(source: &CleanupResultSource) -> String { fn type_json(ty: &ResolvedType) -> String { match ty { + ResolvedType::Unit => "{\"kind\":\"primitive\",\"name\":\"unit\"}".to_owned(), ResolvedType::I64 => "{\"kind\":\"primitive\",\"name\":\"i64\"}".to_owned(), ResolvedType::Bool => "{\"kind\":\"primitive\",\"name\":\"bool\"}".to_owned(), ResolvedType::TypeParameter { owner, index } => format!( diff --git a/src/hir.rs b/src/hir.rs index bb3c78a..7ce516a 100644 --- a/src/hir.rs +++ b/src/hir.rs @@ -7,6 +7,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::fmt; use std::fmt::Write as _; +use std::rc::Rc; use crate::ast::{ BinaryOp, Expr, ExprKind, ImportFailure, MatchPattern, ParamMode, Program, @@ -18,18 +19,571 @@ use crate::conformance::STATUS_DOMAIN_MAX_BYTES_V1; use crate::diagnostic::Diagnostic; use crate::source_verify; +#[cfg(test)] +thread_local! { + static ITERATIVE_PHASE_CAPACITY_HIGH_WATER: std::cell::Cell<[usize; 3]> = const { std::cell::Cell::new([0; 3]) }; + static TYPE_FACTS_OUTER_BASELINE: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +#[cfg(test)] +mod private_capacity_contract_tests { + use super::*; + use std::path::Path; + + #[test] + fn private_capacity_prelude_identity_contract_matches_root_prelude() { + assert_eq!( + crate::private_capacity_contract::PRELUDE_CAPACITY_IDENTITIES, + crate::prelude::all_ids() + ); + } + + #[test] + fn opaque_declaration_index_is_bounded_by_shared_private_contract() { + fn maximum_occurrences(program: &crate::ast::Program) -> usize { + fn type_occurrences( + ty: &crate::ast::Type, + program: &crate::ast::Program, + memo: &mut BTreeMap, + visiting: &mut BTreeSet, + ) -> usize { + let crate::ast::Type::Named { name, arguments } = ty else { + return 1; + }; + let argument_total = arguments + .iter() + .map(|argument| type_occurrences(argument, program, memo, visiting)) + .sum::(); + let Some(declaration) = program.types.iter().find(|item| item.name == *name) else { + return 1 + argument_total; + }; + if let Some(value) = memo.get(name) { + return value.saturating_add(argument_total); + } + assert!( + visiting.insert(name.clone()), + "cycle must fail before capacity proof" + ); + let fields: Vec<&crate::ast::Type> = match &declaration.kind { + crate::ast::TypeDeclarationKind::Resource { .. } => Vec::new(), + crate::ast::TypeDeclarationKind::Record { fields } => { + fields.iter().map(|field| &field.ty).collect() + } + crate::ast::TypeDeclarationKind::Variant { cases } => cases + .iter() + .flat_map(|case| &case.fields) + .map(|field| &field.ty) + .collect(), + }; + let value = 1usize.saturating_add( + fields + .into_iter() + .map(|field| type_occurrences(field, program, memo, visiting)) + .sum::(), + ); + visiting.remove(name); + memo.insert(name.clone(), value); + value.saturating_add(argument_total) + } + let mut memo = BTreeMap::new(); + let mut visiting = BTreeSet::new(); + let mut maximum = 1; + for declaration in &program.types { + let ty = crate::ast::Type::Named { + name: declaration.name.clone(), + arguments: Vec::new(), + }; + maximum = maximum.max(type_occurrences(&ty, program, &mut memo, &mut visiting)); + } + maximum + } + + let sources = [ + "module capacity.index;\n@id(\"capacity.main\") fn main() -> i64 { 0 }\n", + include_str!("../tests/fixtures/native_rust_hir_capacity.spx"), + "module capacity.generic;\n@id(\"box\") record Box { @id(\"box.value\") value: T, }\n@id(\"identity\") fn identity(value: T) -> T { value }\n@id(\"capacity.main\") fn main() -> i64 { identity(1) }\n", + "module capacity.import;\npermit { host.echo }\n@id(\"host\") interface Host permits { host.echo } { @id(\"host.echo\") import rust fn echo(value: i64) -> i64 effects { host.echo } failure status \"host.echo.v1\"; }\n@id(\"capacity.main\") fn main() -> i64 uses { host.echo } { echo(1) }\n", + ]; + for source in sources { + let program = crate::parse(source, Path::new("capacity-index.spx")).unwrap(); + let canonical = crate::format::canonical(&program); + let resolved = resolve(&program).unwrap(); + let layout_upper = crate::private_capacity_contract::type_facts_layout_upper( + canonical.len(), + program.types.len(), + maximum_occurrences(&program), + ) + .unwrap(); + assert!(resolved.declarations.type_facts_layout_capacity() <= layout_upper); + let upper = crate::private_capacity_contract::declaration_index_upper( + canonical.len(), + program.types.len(), + program.interfaces.len(), + program.functions.len(), + layout_upper, + ) + .unwrap(); + assert!( + resolved.declarations.owned_capacity_for_private_contract() <= upper, + "opaque DeclarationIndex exceeded shared source-derived upper" + ); + } + + let mut wide = String::from("module capacity.index.wide;\n"); + for index in 0..514 { + use std::fmt::Write as _; + writeln!( + wide, + "@id(\"wide.r{index}\") record R{index} {{ @id(\"wide.r{index}.v\") v: i64, }}" + ) + .unwrap(); + } + wide.push_str("@id(\"capacity.main\") fn main() -> i64 { 0 }\n"); + let program = crate::parse(&wide, Path::new("capacity-index-wide.spx")).unwrap(); + let canonical = crate::format::canonical(&program); + let resolved = resolve(&program).unwrap(); + let layout_upper = crate::private_capacity_contract::type_facts_layout_upper( + canonical.len(), + program.types.len(), + maximum_occurrences(&program), + ) + .unwrap(); + assert!(resolved.declarations.type_facts_layout_capacity() <= layout_upper); + let upper = crate::private_capacity_contract::declaration_index_upper( + canonical.len(), + program.types.len(), + program.interfaces.len(), + program.functions.len(), + layout_upper, + ) + .unwrap(); + assert!(resolved.declarations.owned_capacity_for_private_contract() <= upper); + + let mut chain = String::from( + "module capacity.index.chain;\n@id(\"chain.r0\") record R0 { @id(\"chain.r0.v\") v: i64, }\n", + ); + for index in 1..514 { + use std::fmt::Write as _; + writeln!( + chain, + "@id(\"chain.r{index}\") record R{index} {{ @id(\"chain.r{index}.v\") v: R{}, }}", + index - 1 + ) + .unwrap(); + } + chain.push_str("@id(\"capacity.main\") fn main() -> i64 { 0 }\n"); + let program = crate::parse(&chain, Path::new("capacity-index-chain.spx")).unwrap(); + let canonical = crate::format::canonical(&program); + let resolved = resolve(&program).unwrap(); + let layout_upper = crate::private_capacity_contract::type_facts_layout_upper( + canonical.len(), + program.types.len(), + maximum_occurrences(&program), + ) + .unwrap(); + assert!(resolved.declarations.type_facts_layout_capacity() <= layout_upper); + let upper = crate::private_capacity_contract::declaration_index_upper( + canonical.len(), + program.types.len(), + program.interfaces.len(), + program.functions.len(), + layout_upper, + ) + .unwrap(); + assert!(resolved.declarations.owned_capacity_for_private_contract() <= upper); + drop(resolved); + + let nested = "module capacity.index.nested;\n@id(\"nested.box\") record Box { @id(\"nested.box.v\") v: T, }\n@id(\"nested.deep\") record Deep { @id(\"nested.deep.v\") v: Box>, }\n@id(\"capacity.main\") fn main() -> i64 { 0 }\n"; + let program = crate::parse(nested, Path::new("capacity-index-nested.spx")).unwrap(); + let error = resolve(&program).unwrap_err(); + assert!(error.iter().any(|diagnostic| diagnostic.code == "SPX-T223")); + + let parameter_argument = "module capacity.index.parameter;\n@id(\"capacity.identity\") fn identity(value: T) -> i64 { 0 }\n@id(\"capacity.main\") fn main() -> i64 { 0 }\n"; + let program = crate::parse( + parameter_argument, + Path::new("capacity-index-parameter.spx"), + ) + .unwrap(); + let error = resolve(&program).unwrap_err(); + assert!(error.iter().any(|diagnostic| diagnostic.code == "SPX-T220")); + } +} + +#[cfg(test)] +pub(crate) fn reset_iterative_phase_capacity_high_water() { + ITERATIVE_PHASE_CAPACITY_HIGH_WATER.with(|water| water.set([0; 3])); +} + +#[cfg(test)] +pub(crate) fn iterative_phase_capacity_high_water() -> [usize; 3] { + ITERATIVE_PHASE_CAPACITY_HIGH_WATER.with(std::cell::Cell::get) +} + +#[cfg(test)] +fn note_iterative_phase_capacity(index: usize, bytes: usize) { + ITERATIVE_PHASE_CAPACITY_HIGH_WATER.with(|water| { + let mut values = water.get(); + values[index] = values[index].max(bytes); + water.set(values); + }); +} + +#[cfg(test)] +fn type_facts_outer_baseline() -> usize { + TYPE_FACTS_OUTER_BASELINE.with(std::cell::Cell::get) +} + +#[cfg(test)] +fn validation_scope_owned_capacity(scope: &BTreeMap) -> usize { + let node_bytes = scope.len().saturating_mul( + std::mem::size_of::<(ValueId, ValidationBinding)>() + + std::mem::size_of::>(), + ); + node_bytes + + scope.iter().fold(0usize, |bytes, (id, binding)| { + let moved = binding + .moved_places + .iter() + .fold(0usize, |bytes, (place, _)| { + bytes + + std::mem::size_of::<(Vec, Availability)>() + + place.capacity() * std::mem::size_of::() + + place + .iter() + .map(place_projection_owned_capacity) + .sum::() + }); + let partial = binding + .definitely_partial + .iter() + .fold(0usize, |bytes, place| { + bytes + + std::mem::size_of::>() + + place.capacity() * std::mem::size_of::() + + place + .iter() + .map(place_projection_owned_capacity) + .sum::() + }); + bytes + id.as_str().len() + resolved_type_owned_capacity(&binding.ty) + moved + partial + }) +} + +#[cfg(test)] +fn place_projection_owned_capacity(projection: &PlaceProjection) -> usize { + match projection { + PlaceProjection::Field(field) => field.as_str().len(), + PlaceProjection::VariantField { case, field } => { + case.as_str().len().saturating_add(field.as_str().len()) + } + } +} + +#[cfg(test)] +fn resolved_type_owned_capacity(ty: &ResolvedType) -> usize { + match ty { + ResolvedType::Unit | ResolvedType::I64 | ResolvedType::Bool => 0, + ResolvedType::TypeParameter { owner, .. } => owner.as_str().len(), + ResolvedType::Nominal { + declaration, + arguments, + } => declaration + .as_str() + .len() + .saturating_add(arguments.capacity() * std::mem::size_of::()) + .saturating_add( + arguments + .iter() + .map(resolved_type_owned_capacity) + .sum::(), + ), + } +} + +#[cfg(test)] +fn resolved_place_owned_capacity(place: &Place) -> usize { + place.root.as_str().len() + + place.projections.capacity() * std::mem::size_of::() + + place + .projections + .iter() + .map(place_projection_owned_capacity) + .sum::() +} + +#[cfg(test)] +fn resolved_expr_owned_capacity(expression: &ResolvedExpr) -> usize { + let mut bytes = expression + .id + .as_str() + .len() + .saturating_add(resolved_type_owned_capacity(&expression.ty)); + let child = |value: &ResolvedExpr| { + std::mem::size_of::().saturating_add(resolved_expr_owned_capacity(value)) + }; + match &expression.kind { + ResolvedExprKind::Place(place) => bytes += resolved_place_owned_capacity(place), + ResolvedExprKind::Unary { value: operand, .. } => bytes += child(operand), + ResolvedExprKind::Project { base, field } => { + bytes += child(base) + field.as_str().len(); + } + ResolvedExprKind::Binary { left, right, .. } => bytes += child(left) + child(right), + ResolvedExprKind::If { + condition, + then_branch, + else_branch, + } => bytes += child(condition) + child(then_branch) + child(else_branch), + ResolvedExprKind::Call { + callee, + args, + type_arguments, + instance, + } => { + bytes += callee.as_str().len(); + bytes += instance.as_ref().map_or(0, |id| id.as_str().len()); + bytes += args.capacity() * std::mem::size_of::(); + bytes += args.iter().map(resolved_expr_owned_capacity).sum::(); + bytes += type_arguments.capacity() * std::mem::size_of::(); + bytes += type_arguments + .iter() + .map(resolved_type_owned_capacity) + .sum::(); + } + ResolvedExprKind::NativeRustImportCall(call) => { + bytes += call.expression.as_str().len() + call.import.as_str().len(); + bytes += call.args.capacity() * std::mem::size_of::(); + bytes += call + .args + .iter() + .map(resolved_expr_owned_capacity) + .sum::(); + } + ResolvedExprKind::Try { + operand, + result, + ok_case, + ok_field, + err_case, + err_field, + residual_type, + } => { + bytes += child(operand) + + result.as_str().len() + + ok_case.as_str().len() + + ok_field.as_str().len() + + err_case.as_str().len() + + err_field.as_str().len() + + resolved_type_owned_capacity(residual_type); + } + ResolvedExprKind::TryOption { + operand, + option, + some_case, + some_field, + none_case, + residual_type, + } => { + bytes += child(operand) + + option.as_str().len() + + some_case.as_str().len() + + some_field.as_str().len() + + none_case.as_str().len() + + resolved_type_owned_capacity(residual_type); + } + ResolvedExprKind::Block { statements, tail } => { + bytes += statements.capacity() * std::mem::size_of::(); + for statement in statements { + let ResolvedStatement::Let { binding, value, .. } = statement; + bytes += binding.id.as_str().len() + + binding.name.capacity() + + resolved_type_owned_capacity(&binding.ty) + + resolved_expr_owned_capacity(value); + } + bytes += child(tail); + } + ResolvedExprKind::ConstructRecord { record, fields } => { + bytes += record.as_str().len(); + bytes += fields.capacity() * std::mem::size_of::(); + bytes += fields + .iter() + .map(|field| { + field.field.as_str().len() + resolved_expr_owned_capacity(&field.value) + }) + .sum::(); + } + ResolvedExprKind::ConstructVariant { + variant, + case, + fields, + } => { + bytes += variant.as_str().len() + case.as_str().len(); + bytes += fields.capacity() * std::mem::size_of::(); + bytes += fields + .iter() + .map(|field| { + field.field.as_str().len() + resolved_expr_owned_capacity(&field.value) + }) + .sum::(); + } + ResolvedExprKind::Match { scrutinee, arms } => { + bytes += child(scrutinee); + bytes += arms.capacity() * std::mem::size_of::(); + bytes += arms + .iter() + .map(|arm| { + resolved_match_pattern_owned_capacity(&arm.pattern) + + resolved_expr_owned_capacity(&arm.value) + }) + .sum::(); + } + ResolvedExprKind::UpdateRecord { + base, + record, + fields, + } => { + bytes += child(base) + record.as_str().len(); + bytes += fields.capacity() * std::mem::size_of::(); + bytes += fields + .iter() + .map(|field| { + field.field.as_str().len() + resolved_expr_owned_capacity(&field.value) + }) + .sum::(); + } + ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) => {} + } + bytes +} + +#[cfg(test)] +fn resolved_binding_owned_capacity(binding: &ResolvedBinding) -> usize { + binding.id.as_str().len() + binding.name.capacity() + resolved_type_owned_capacity(&binding.ty) +} + +#[cfg(test)] +fn resolved_record_pattern_field_owned_capacity(field: &ResolvedRecordMatchPatternField) -> usize { + field.field.as_str().len() + + match &field.pattern { + ResolvedRecordMatchFieldPattern::Binding(binding) => { + resolved_binding_owned_capacity(binding) + } + ResolvedRecordMatchFieldPattern::Wildcard => 0, + ResolvedRecordMatchFieldPattern::Record { + record, + instance, + fields, + } => { + record.as_str().len() + + resolved_type_owned_capacity(instance) + + fields.capacity() * std::mem::size_of::() + + fields + .iter() + .map(resolved_record_pattern_field_owned_capacity) + .sum::() + } + } +} + +#[cfg(test)] +fn resolved_match_pattern_owned_capacity(pattern: &ResolvedMatchPattern) -> usize { + match pattern { + ResolvedMatchPattern::Wildcard => 0, + ResolvedMatchPattern::Variant { + variant, + case, + fields, + } => { + variant.as_str().len() + + case.as_str().len() + + fields.capacity() * std::mem::size_of::() + + fields + .iter() + .map(|field| { + field.field.as_str().len() + resolved_binding_owned_capacity(&field.binding) + }) + .sum::() + } + ResolvedMatchPattern::Record { + record, + instance, + fields, + } => { + record.as_str().len() + + resolved_type_owned_capacity(instance) + + fields.capacity() * std::mem::size_of::() + + fields + .iter() + .map(resolved_record_pattern_field_owned_capacity) + .sum::() + } + } +} + +#[cfg(test)] +fn resolved_statement_owned_capacity(statement: &ResolvedStatement) -> usize { + let ResolvedStatement::Let { binding, value, .. } = statement; + resolved_binding_owned_capacity(binding) + resolved_expr_owned_capacity(value) +} + +#[cfg(test)] +fn resolved_field_initializer_owned_capacity(field: &ResolvedFieldInitializer) -> usize { + field.field.as_str().len() + resolved_expr_owned_capacity(&field.value) +} + +#[cfg(test)] +fn resolved_match_arm_owned_capacity(arm: &ResolvedMatchArm) -> usize { + resolved_match_pattern_owned_capacity(&arm.pattern) + resolved_expr_owned_capacity(&arm.value) +} + +#[cfg(test)] +fn resolved_field_declaration_owned_capacity(field: &ResolvedFieldDeclaration) -> usize { + field.id.as_str().len() + field.name.capacity() + resolved_type_owned_capacity(&field.ty) +} + +#[cfg(test)] +fn resolved_variant_case_owned_capacity(case: &ResolvedVariantCaseDeclaration) -> usize { + case.id.as_str().len() + + case.name.capacity() + + case.fields.capacity() * std::mem::size_of::() + + case + .fields + .iter() + .map(resolved_field_declaration_owned_capacity) + .sum::() +} + +#[cfg(test)] +fn resolver_scope_owned_capacity(scope: &BTreeMap) -> usize { + scope + .len() + .saturating_mul( + std::mem::size_of::<(String, Binding)>() + + std::mem::size_of::>(), + ) + .saturating_add( + scope + .iter() + .map(|(name, binding)| { + name.capacity() + + binding.id.as_str().len() + + resolved_type_owned_capacity(&binding.ty) + }) + .sum::(), + ) +} + macro_rules! format { ($($argument:tt)*) => { crate::bounded_output::budgeted_format(format_args!($($argument)*)) }; } -#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[derive(Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct DeclarationId(String); impl DeclarationId { pub fn new(value: impl Into) -> Self { - Self(value.into()) + Self(exact_string(value.into())) } pub fn as_str(&self) -> &str { @@ -37,13 +591,19 @@ impl DeclarationId { } } +impl Clone for DeclarationId { + fn clone(&self) -> Self { + Self(exact_string(self.0.clone())) + } +} + impl fmt::Display for DeclarationId { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(&self.0) } } -#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[derive(Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct FunctionInstanceId(String); impl FunctionInstanceId { @@ -52,6 +612,12 @@ impl FunctionInstanceId { } } +impl Clone for FunctionInstanceId { + fn clone(&self) -> Self { + Self(exact_string(self.0.clone())) + } +} + #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum FunctionExecutionId { Monomorphic(DeclarationId), @@ -108,20 +674,24 @@ impl fmt::Display for FunctionInstanceId { } } -#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[derive(Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct ValueId(String); impl ValueId { fn parameter(function: &FunctionExecutionId, index: usize) -> Self { - Self(scoped_identity(function, "value:param", &index.to_string())) + Self(exact_string(scoped_identity( + function, + "value:param", + &index.to_string(), + ))) } fn local(function: &FunctionExecutionId, path: &str) -> Self { - Self(scoped_identity(function, "value:local", path)) + Self(exact_string(scoped_identity(function, "value:local", path))) } fn result(function: &FunctionExecutionId) -> Self { - Self(scoped_identity(function, "value:result", "")) + Self(exact_string(scoped_identity(function, "value:result", ""))) } pub fn as_str(&self) -> &str { @@ -129,18 +699,24 @@ impl ValueId { } } +impl Clone for ValueId { + fn clone(&self) -> Self { + Self(exact_string(self.0.clone())) + } +} + impl fmt::Display for ValueId { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(&self.0) } } -#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[derive(Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct ExpressionId(String); impl ExpressionId { fn new(function: &FunctionExecutionId, path: &str) -> Self { - Self(scoped_identity(function, "expression", path)) + Self(exact_string(scoped_identity(function, "expression", path))) } pub fn as_str(&self) -> &str { @@ -148,6 +724,16 @@ impl ExpressionId { } } +impl Clone for ExpressionId { + fn clone(&self) -> Self { + Self(exact_string(self.0.clone())) + } +} + +fn exact_string(value: String) -> String { + value.into_boxed_str().into_string() +} + fn scoped_identity(owner: &FunctionExecutionId, kind: &str, path: &str) -> String { match owner { FunctionExecutionId::Monomorphic(owner) => format!( @@ -234,10 +820,132 @@ pub struct DeclarationIndex { case_fields: BTreeMap>, type_parameters: BTreeMap>, imports_by_key: BTreeMap, + native_rust_imports_by_name: BTreeMap, type_facts_by_id: BTreeMap, } impl DeclarationIndex { + #[cfg(test)] + fn type_facts_layout_capacity(&self) -> usize { + self.type_facts_by_id + .values() + .map(|facts| facts.layout_key.capacity()) + .sum() + } + + #[cfg(test)] + fn owned_capacity_for_private_contract(&self) -> usize { + fn string_map_capacity(map: &BTreeMap) -> usize { + map.len() * std::mem::size_of::<(String, V)>() + + map.keys().map(String::capacity).sum::() + } + fn named_id_map_capacity(map: &BTreeMap) -> usize { + string_map_capacity(map) + map.values().map(|id| id.as_str().len()).sum::() + } + fn owner_name_map_capacity( + map: &BTreeMap<(DeclarationId, String), DeclarationId>, + ) -> usize { + map.len() * std::mem::size_of::<((DeclarationId, String), DeclarationId)>() + + map + .iter() + .map(|((owner, name), value)| { + owner.as_str().len() + name.capacity() + value.as_str().len() + }) + .sum::() + } + fn field_capacity(field: &ResolvedFieldDeclaration) -> usize { + field.id.as_str().len() + + field.name.capacity() + + resolved_type_owned_capacity(&field.ty) + } + let declaration_bytes = self + .declarations + .iter() + .map(|(id, declaration)| { + id.as_str().len() + + declaration.id.as_str().len() + + declaration.name.capacity() + + declaration + .owner + .as_ref() + .map_or(0, |owner| owner.as_str().len()) + }) + .sum::(); + let field_bytes = self + .record_fields + .values() + .chain(self.case_fields.values()) + .flatten() + .map(field_capacity) + .sum::(); + let case_bytes = self + .variant_cases + .values() + .flatten() + .map(|case| { + case.id.as_str().len() + + case.name.capacity() + + case.fields.capacity() * std::mem::size_of::() + + case.fields.iter().map(field_capacity).sum::() + }) + .sum::(); + let fact_bytes = self + .type_facts_by_id + .values() + .map(|facts| facts.layout_key.capacity()) + .sum::(); + let declaration_map_backing = + self.declarations.len() * std::mem::size_of::<(DeclarationId, Declaration)>(); + let record_field_maps = self + .record_fields + .iter() + .chain(self.case_fields.iter()) + .map(|(owner, fields)| { + std::mem::size_of::<(DeclarationId, Vec)>() + + owner.as_str().len() + + fields.capacity() * std::mem::size_of::() + }) + .sum::(); + let variant_case_map = self + .variant_cases + .iter() + .map(|(owner, cases)| { + std::mem::size_of::<(DeclarationId, Vec)>() + + owner.as_str().len() + + cases.capacity() * std::mem::size_of::() + }) + .sum::(); + let type_parameter_map = self + .type_parameters + .iter() + .map(|(owner, parameters)| { + std::mem::size_of::<(DeclarationId, Vec)>() + + owner.as_str().len() + + parameters.capacity() + * std::mem::size_of::() + + parameters + .iter() + .map(|parameter| parameter.name.capacity()) + .sum::() + }) + .sum::(); + declaration_map_backing + + record_field_maps + + variant_case_map + + type_parameter_map + + declaration_bytes + + field_bytes + + case_bytes + + fact_bytes + + named_id_map_capacity(&self.types_by_name) + + named_id_map_capacity(&self.functions_by_name) + + named_id_map_capacity(&self.imports_by_key) + + named_id_map_capacity(&self.native_rust_imports_by_name) + + string_map_capacity(&self.type_facts_by_id) + + owner_name_map_capacity(&self.fields_by_owner_name) + + owner_name_map_capacity(&self.cases_by_owner_name) + } + pub(crate) fn workspace_declarations(&self) -> Vec { self.declarations.values().cloned().collect() } @@ -290,6 +998,10 @@ impl DeclarationIndex { self.imports_by_key.get(key) } + pub fn native_rust_import_id(&self, name: &str) -> Option<&DeclarationId> { + self.native_rust_imports_by_name.get(name) + } + pub fn declarations(&self) -> impl ExactSizeIterator { self.declarations.values() } @@ -311,155 +1023,309 @@ impl DeclarationIndex { visiting: &mut BTreeSet, memo: &mut BTreeMap, ) -> Option { - let identity = ty.identity_key(); - if let Some(facts) = memo.get(&identity) { - return Some(facts.clone()); + enum Frame { + Enter(ResolvedType), + Finish { + identity: String, + declaration: DeclarationId, + kind: DeclarationKind, + child_count: usize, + }, } - match ty { - ResolvedType::I64 => Some(TypeFacts { - copy: true, - contains_resource: false, - sized: true, - needs_drop: false, - layout_key: "scalar:i64".to_owned(), - }), - ResolvedType::Bool => Some(TypeFacts { - copy: true, - contains_resource: false, - sized: true, - needs_drop: false, - layout_key: "scalar:bool".to_owned(), - }), - ResolvedType::TypeParameter { .. } => None, - ResolvedType::Nominal { - declaration, - arguments, - } => { - let declaration = self.declaration(declaration)?; - let facts = match declaration.kind { - DeclarationKind::Resource if arguments.is_empty() => Some(TypeFacts { - copy: false, - contains_resource: true, - sized: true, - needs_drop: true, - layout_key: format!("resource:{}", ty.identity_key()), - }), - DeclarationKind::Record => { - let parameters = self.type_parameters.get(&declaration.id)?; - if arguments.len() != parameters.len() - || arguments.iter().any(|argument| { - !matches!(argument, ResolvedType::I64 | ResolvedType::Bool) - }) - || !visiting.insert(declaration.id.clone()) - { - return None; - } - let fields = self.record_fields.get(&declaration.id)?; - let mut copy = true; - let mut contains_resource = false; - let mut sized = true; - let mut needs_drop = false; - let mut encoded_fields = crate::bounded_output::CappedString::new(); - for field in fields { - let field_ty = - substitute_type(&field.ty, &declaration.id, arguments).ok()?; - let facts = self.compute_type_facts(&field_ty, visiting, memo)?; - copy &= facts.copy; - contains_resource |= facts.contains_resource; - sized &= facts.sized; - needs_drop |= facts.needs_drop; - write!( - encoded_fields, - "{}:{}:{}:{}", - field.id.as_str().len(), - field.id, - facts.layout_key.len(), - facts.layout_key - ) - .expect("writing to a string cannot fail"); + + #[cfg(test)] + fn frame_owned_capacity(frame: &Frame) -> usize { + match frame { + Frame::Enter(ty) => resolved_type_owned_capacity(ty), + Frame::Finish { + identity, + declaration, + .. + } => identity.capacity() + declaration.as_str().len(), + } + } + + #[cfg(test)] + fn retained_capacity( + frames: &Vec, + results: &Vec, + memo: &BTreeMap, + visiting: &BTreeSet, + ) -> usize { + type_facts_outer_baseline() + + frames.capacity() * std::mem::size_of::() + + frames.iter().map(frame_owned_capacity).sum::() + + results.capacity() * std::mem::size_of::() + + results + .iter() + .map(|facts| facts.layout_key.capacity()) + .sum::() + + memo.len() + * (std::mem::size_of::<(String, TypeFacts)>() + + std::mem::size_of::>()) + + memo + .iter() + .map(|(key, facts)| key.capacity() + facts.layout_key.capacity()) + .sum::() + + visiting.len() + * (std::mem::size_of::() + + std::mem::size_of::>()) + + visiting.iter().map(|id| id.as_str().len()).sum::() + } + + let mut frames = vec![Frame::Enter(ty.clone())]; + let mut results = Vec::::new(); + while let Some(frame) = frames.pop() { + #[cfg(test)] + note_iterative_phase_capacity( + 2, + retained_capacity(&frames, &results, memo, visiting) + frame_owned_capacity(&frame), + ); + match frame { + Frame::Enter(ty) => { + let identity = ty.identity_key(); + #[cfg(test)] + note_iterative_phase_capacity( + 2, + retained_capacity(&frames, &results, memo, visiting) + + resolved_type_owned_capacity(&ty) + + identity.capacity(), + ); + if let Some(facts) = memo.get(&identity) { + results.push(facts.clone()); + continue; + } + let scalar = match &ty { + ResolvedType::Unit => { + Some((true, false, false, "native-rust-import-result:unit")) } - visiting.remove(&declaration.id); - Some(TypeFacts { + ResolvedType::I64 => Some((true, false, false, "scalar:i64")), + ResolvedType::Bool => Some((true, false, false, "scalar:bool")), + ResolvedType::TypeParameter { .. } | ResolvedType::Nominal { .. } => None, + }; + if let Some((copy, contains_resource, needs_drop, key)) = scalar { + results.push(TypeFacts { copy, contains_resource, - sized, + sized: true, needs_drop, - layout_key: format!( - "record:{}:{}:{}:{}", - declaration.id.as_str().len(), - declaration.id, - fields.len(), - encoded_fields.into_string() - ), - }) + layout_key: key.to_owned(), + }); + continue; } - DeclarationKind::Variant => { - let parameters = self.type_parameters.get(&declaration.id)?; - if arguments.len() != parameters.len() - || arguments.iter().any(|argument| { - !matches!(argument, ResolvedType::I64 | ResolvedType::Bool) - }) - || !visiting.insert(declaration.id.clone()) - { - return None; - } - let cases = self.variant_cases.get(&declaration.id)?; - let mut encoded_cases = crate::bounded_output::CappedString::new(); - for case in cases { - write!( - encoded_cases, - "{}:{}:{}:", - case.id.as_str().len(), - case.id, - case.fields.len() - ) - .expect("writing to a string cannot fail"); - for field in &case.fields { - let field_ty = - substitute_type(&field.ty, &declaration.id, arguments).ok()?; - let facts = self.compute_type_facts(&field_ty, visiting, memo)?; - if !facts.copy || facts.contains_resource || facts.needs_drop { - return None; - } - write!( - encoded_cases, - "{}:{}:{}:{}", - field.id.as_str().len(), - field.id, - facts.layout_key.len(), - facts.layout_key - ) - .expect("writing to a string cannot fail"); - } - } - visiting.remove(&declaration.id); - Some(TypeFacts { - copy: true, - contains_resource: false, + let ResolvedType::Nominal { + declaration, + arguments, + } = ty + else { + return None; + }; + let item = self.declaration(&declaration)?; + if item.kind == DeclarationKind::Resource && arguments.is_empty() { + let facts = TypeFacts { + copy: false, + contains_resource: true, sized: true, - needs_drop: false, - layout_key: format!( - "variant:{}:{}:{}:{}", - declaration.id.as_str().len(), - declaration.id, - cases.len(), - encoded_cases.into_string() - ), + needs_drop: true, + layout_key: format!("resource:{identity}"), + }; + memo.insert(identity, facts.clone()); + results.push(facts); + continue; + } + if !matches!( + item.kind, + DeclarationKind::Record | DeclarationKind::Variant + ) { + return None; + } + let parameters = self.type_parameters.get(&declaration)?; + if arguments.len() != parameters.len() + || arguments.iter().any(|argument| { + !matches!(argument, ResolvedType::I64 | ResolvedType::Bool) }) + || !visiting.insert(declaration.clone()) + { + return None; + } + let children = match item.kind { + DeclarationKind::Record => self + .record_fields + .get(&declaration)? + .iter() + .map(|field| substitute_type(&field.ty, &declaration, &arguments).ok()) + .collect::>>()?, + DeclarationKind::Variant => self + .variant_cases + .get(&declaration)? + .iter() + .flat_map(|case| &case.fields) + .map(|field| substitute_type(&field.ty, &declaration, &arguments).ok()) + .collect::>>()?, + _ => unreachable!(), + }; + #[cfg(test)] + note_iterative_phase_capacity( + 2, + retained_capacity(&frames, &results, memo, visiting) + + identity.capacity() + + declaration.as_str().len() + + children.capacity() * std::mem::size_of::() + + children + .iter() + .map(resolved_type_owned_capacity) + .sum::(), + ); + frames.try_reserve(children.len() + 1).ok()?; + frames.push(Frame::Finish { + identity, + declaration, + kind: item.kind, + child_count: children.len(), + }); + frames.extend(children.into_iter().rev().map(Frame::Enter)); + } + Frame::Finish { + identity, + declaration, + kind, + child_count, + } => { + #[cfg(test)] + let finish_identity_bytes = identity.capacity() + declaration.as_str().len(); + let split = results.len().checked_sub(child_count)?; + let child_facts = results.drain(split..).collect::>(); + #[cfg(test)] + note_iterative_phase_capacity( + 2, + retained_capacity(&frames, &results, memo, visiting) + + finish_identity_bytes + + child_facts.capacity() * std::mem::size_of::() + + child_facts + .iter() + .map(|facts| facts.layout_key.capacity()) + .sum::(), + ); + visiting.remove(&declaration); + let mut encoded = crate::bounded_output::CappedString::new(); + match kind { + DeclarationKind::Record => { + let fields = self.record_fields.get(&declaration)?; + let mut copy = true; + let mut contains_resource = false; + let mut sized = true; + let mut needs_drop = false; + for (field, facts) in fields.iter().zip(&child_facts) { + copy &= facts.copy; + contains_resource |= facts.contains_resource; + sized &= facts.sized; + needs_drop |= facts.needs_drop; + write!( + encoded, + "{}:{}:{}:{}", + field.id.as_str().len(), + field.id, + facts.layout_key.len(), + facts.layout_key + ) + .ok()?; + } + #[cfg(test)] + let encoded_capacity = encoded.allocated_capacity(); + let facts = TypeFacts { + copy, + contains_resource, + sized, + needs_drop, + layout_key: format!( + "record:{}:{}:{}:{}", + declaration.as_str().len(), + declaration, + fields.len(), + encoded.into_string() + ), + }; + #[cfg(test)] + note_iterative_phase_capacity( + 2, + retained_capacity(&frames, &results, memo, visiting) + + finish_identity_bytes + + child_facts.capacity() * std::mem::size_of::() + + child_facts + .iter() + .map(|facts| facts.layout_key.capacity()) + .sum::() + + encoded_capacity + + facts.layout_key.capacity(), + ); + memo.insert(identity, facts.clone()); + results.push(facts); + } + DeclarationKind::Variant => { + let cases = self.variant_cases.get(&declaration)?; + let mut facts_iter = child_facts.iter(); + for case in cases { + write!( + encoded, + "{}:{}:{}:", + case.id.as_str().len(), + case.id, + case.fields.len() + ) + .ok()?; + for field in &case.fields { + let facts = facts_iter.next()?; + if !facts.copy || facts.contains_resource || facts.needs_drop { + return None; + } + write!( + encoded, + "{}:{}:{}:{}", + field.id.as_str().len(), + field.id, + facts.layout_key.len(), + facts.layout_key + ) + .ok()?; + } + } + #[cfg(test)] + let encoded_capacity = encoded.allocated_capacity(); + let facts = TypeFacts { + copy: true, + contains_resource: false, + sized: true, + needs_drop: false, + layout_key: format!( + "variant:{}:{}:{}:{}", + declaration.as_str().len(), + declaration, + cases.len(), + encoded.into_string() + ), + }; + #[cfg(test)] + note_iterative_phase_capacity( + 2, + retained_capacity(&frames, &results, memo, visiting) + + finish_identity_bytes + + child_facts.capacity() * std::mem::size_of::() + + child_facts + .iter() + .map(|facts| facts.layout_key.capacity()) + .sum::() + + encoded_capacity + + facts.layout_key.capacity(), + ); + memo.insert(identity, facts.clone()); + results.push(facts); + } + _ => unreachable!(), } - DeclarationKind::Resource - | DeclarationKind::ResourceDrop - | DeclarationKind::Field - | DeclarationKind::VariantCase - | DeclarationKind::CaseField - | DeclarationKind::Interface - | DeclarationKind::Import - | DeclarationKind::Function => None, - }?; - memo.insert(identity, facts.clone()); - Some(facts) + } } } + (results.len() == 1).then(|| results.pop().expect("type fact count checked above")) } fn populate_type_facts(&mut self) -> bool { @@ -471,7 +1337,20 @@ impl DeclarationIndex { memo.insert(ty.identity_key(), facts); } let declarations = self.types_by_name.values().cloned().collect::>(); + #[cfg(test)] + let declarations_capacity = declarations.capacity() * std::mem::size_of::() + + declarations + .iter() + .map(|id| id.as_str().len()) + .sum::(); + #[cfg(test)] + TYPE_FACTS_OUTER_BASELINE.with(|baseline| baseline.set(declarations_capacity)); for declaration in declarations { + #[cfg(test)] + note_iterative_phase_capacity( + 2, + declarations_capacity.saturating_add(declaration.as_str().len()), + ); if self .type_parameters .get(&declaration) @@ -490,6 +1369,8 @@ impl DeclarationIndex { return false; } } + #[cfg(test)] + TYPE_FACTS_OUTER_BASELINE.with(|baseline| baseline.set(0)); self.type_facts_by_id = memo; true } @@ -553,6 +1434,11 @@ impl DeclarationIndex { index .imports_by_key .insert(import.stable_id.clone(), import_id.clone()); + if import.native_rust { + index + .native_rust_imports_by_name + .insert(import.name.clone(), import_id.clone()); + } index.insert_owned_declaration( interface_id.clone(), import.name.clone(), @@ -613,6 +1499,20 @@ impl DeclarationIndex { .collect::, Diagnostic>>()?; index.type_parameters.insert(owner, parameters); } + if program + .interfaces + .iter() + .flat_map(|interface| &interface.imports) + .filter(|import| import.native_rust) + .any(|import| index.functions_by_name.contains_key(&import.name)) + { + return Err(Diagnostic::error( + "SPX-B107", + "Native Rust Interop declaration set is unsupported: symbol collision", + Span::default(), + ) + .at_path(&program.path)); + } for declaration in program.types.iter().chain(crate::prelude::declarations()) { let TypeDeclarationKind::Record { fields } = &declaration.kind else { continue; @@ -874,39 +1774,54 @@ impl DeclarationIndex { ty: &Type, parameter_owner: Option<&DeclarationId>, ) -> Option { - match ty { - Type::I64 => Some(ResolvedType::I64), - Type::Bool => Some(ResolvedType::Bool), - Type::Named { name, arguments } => { - if arguments.is_empty() { - if let Some(owner) = parameter_owner { - if let Some(parameter) = self - .type_parameters(owner)? - .iter() - .find(|parameter| parameter.name == *name) - { - return Some(ResolvedType::TypeParameter { - owner: owner.clone(), - index: parameter.index, - }); + enum Frame<'a> { + Enter(&'a Type), + Finish(DeclarationId, usize), + } + let mut frames = vec![Frame::Enter(ty)]; + let mut resolved = Vec::new(); + while let Some(frame) = frames.pop() { + match frame { + Frame::Enter(ty) => match ty { + Type::I64 => resolved.push(ResolvedType::I64), + Type::Bool => resolved.push(ResolvedType::Bool), + Type::Named { name, arguments } => { + if arguments.is_empty() { + if let Some(owner) = parameter_owner { + if let Some(parameter) = self + .type_parameters(owner)? + .iter() + .find(|parameter| parameter.name == *name) + { + resolved.push(ResolvedType::TypeParameter { + owner: owner.clone(), + index: parameter.index, + }); + continue; + } + } } + frames.push(Frame::Finish(self.type_id(name)?.clone(), arguments.len())); + frames.extend(arguments.iter().rev().map(Frame::Enter)); } + }, + Frame::Finish(declaration, count) => { + let split = resolved.len().checked_sub(count)?; + let arguments = resolved.drain(split..).collect(); + resolved.push(ResolvedType::Nominal { + declaration, + arguments, + }); } - let declaration = self.type_id(name)?.clone(); - Some(ResolvedType::Nominal { - declaration, - arguments: arguments - .iter() - .map(|argument| self.resolve_source_type(argument, parameter_owner)) - .collect::>>()?, - }) } } + (resolved.len() == 1).then(|| resolved.pop().expect("type count checked above")) } } #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum ResolvedType { + Unit, I64, Bool, TypeParameter { @@ -923,38 +1838,58 @@ impl ResolvedType { pub fn nominal_id(&self) -> Option<&DeclarationId> { match self { Self::Nominal { declaration, .. } => Some(declaration), - Self::I64 | Self::Bool | Self::TypeParameter { .. } => None, + Self::Unit | Self::I64 | Self::Bool | Self::TypeParameter { .. } => None, } } /// A name-independent key suitable as an input to future layout hashing. pub fn identity_key(&self) -> String { - match self { - Self::I64 => "i64".to_owned(), - Self::Bool => "bool".to_owned(), - Self::TypeParameter { owner, index } => { - format!("parameter:{}:{}:{index}", owner.as_str().len(), owner) - } - Self::Nominal { - declaration, - arguments, - } => { - let argument_count = arguments.len(); - let mut encoded_arguments = crate::bounded_output::CappedString::new(); - for argument in arguments { - let key = argument.identity_key(); - write!(encoded_arguments, "{}:{key}", key.len()) - .expect("writing to a string cannot fail"); - } - format!( - "nominal:{}:{}:{}:{}", - declaration.as_str().len(), - declaration, - argument_count, - encoded_arguments.into_string() - ) + enum Frame<'a> { + Enter(&'a ResolvedType), + Finish(&'a DeclarationId, usize), + } + let mut frames = vec![Frame::Enter(self)]; + let mut keys = Vec::::new(); + while let Some(frame) = frames.pop() { + match frame { + Frame::Enter(ty) => match ty { + Self::Unit => keys.push("unit".to_owned()), + Self::I64 => keys.push("i64".to_owned()), + Self::Bool => keys.push("bool".to_owned()), + Self::TypeParameter { owner, index } => keys.push(format!( + "parameter:{}:{}:{index}", + owner.as_str().len(), + owner + )), + Self::Nominal { + declaration, + arguments, + } => { + frames.push(Frame::Finish(declaration, arguments.len())); + frames.extend(arguments.iter().rev().map(Frame::Enter)); + } + }, + Frame::Finish(declaration, count) => { + let split = keys + .len() + .checked_sub(count) + .expect("type-key traversal has one result per argument"); + let mut encoded = crate::bounded_output::CappedString::new(); + for key in keys.drain(split..) { + write!(encoded, "{}:{key}", key.len()) + .expect("writing to a string cannot fail"); + } + keys.push(format!( + "nominal:{}:{}:{}:{}", + declaration.as_str().len(), + declaration, + count, + encoded.into_string() + )); + } } } + keys.pop().expect("a type always produces an identity key") } } @@ -966,13 +1901,13 @@ impl FunctionInstanceId { write!(encoded_arguments, "{}:{key}", key.len()) .expect("writing to a string cannot fail"); } - Self(format!( + Self(exact_string(format!( "semaprax.function-instance.v1:{}:{}:{}:{}", template.as_str().len(), template, arguments.len(), encoded_arguments.into_string() - )) + ))) } } @@ -984,40 +1919,67 @@ pub(crate) fn substitute_type( owner: &DeclarationId, arguments: &[ResolvedType], ) -> Result { - match template { - ResolvedType::I64 => Ok(ResolvedType::I64), - ResolvedType::Bool => Ok(ResolvedType::Bool), - ResolvedType::TypeParameter { - owner: parameter_owner, - index, - } => { - if parameter_owner != owner { - return Err(hir_error(format!( - "type template for `{owner}` contains foreign parameter owner `{parameter_owner}`" - ))); + enum Frame<'a> { + Enter(&'a ResolvedType), + Finish(&'a DeclarationId, usize), + } + let mut frames = vec![Frame::Enter(template)]; + let mut resolved = Vec::new(); + while let Some(frame) = frames.pop() { + match frame { + Frame::Enter(template) => match template { + ResolvedType::Unit => resolved.push(ResolvedType::Unit), + ResolvedType::I64 => resolved.push(ResolvedType::I64), + ResolvedType::Bool => resolved.push(ResolvedType::Bool), + ResolvedType::TypeParameter { + owner: parameter_owner, + index, + } => { + if parameter_owner != owner { + return Err(hir_error(format!( + "type template for `{owner}` contains foreign parameter owner `{parameter_owner}`" + ))); + } + resolved.push( + arguments + .get(usize::try_from(*index).map_err(|_| { + hir_error(format!("type parameter index {index} does not fit usize")) + })?) + .cloned() + .ok_or_else(|| { + hir_error(format!( + "type template for `{owner}` references missing parameter {index}" + )) + })?, + ); + } + ResolvedType::Nominal { + declaration, + arguments, + } => { + frames.push(Frame::Finish(declaration, arguments.len())); + frames.extend(arguments.iter().rev().map(Frame::Enter)); + } + }, + Frame::Finish(declaration, count) => { + let split = resolved + .len() + .checked_sub(count) + .ok_or_else(|| hir_error("type substitution traversal is incomplete"))?; + let nested = resolved.drain(split..).collect(); + resolved.push(ResolvedType::Nominal { + declaration: declaration.clone(), + arguments: nested, + }); } - arguments - .get(usize::try_from(*index).map_err(|_| { - hir_error(format!("type parameter index {index} does not fit usize")) - })?) - .cloned() - .ok_or_else(|| { - hir_error(format!( - "type template for `{owner}` references missing parameter {index}" - )) - }) } - ResolvedType::Nominal { - declaration, - arguments: nested, - } => Ok(ResolvedType::Nominal { - declaration: declaration.clone(), - arguments: nested - .iter() - .map(|argument| substitute_type(argument, owner, arguments)) - .collect::, _>>()?, - }), } + if resolved.len() != 1 { + return Err(hir_error("type substitution traversal did not settle")); + } + Ok(resolved + .pop() + .expect("substitution result count checked above")) } fn substitute_source_function_type( @@ -1025,31 +1987,46 @@ fn substitute_source_function_type( arguments: &[Type], template: &Type, ) -> Option { - match template { - Type::I64 => Some(Type::I64), - Type::Bool => Some(Type::Bool), - Type::Named { - name, - arguments: nested, - } => { - if nested.is_empty() { - if let Some(index) = function - .type_parameters - .iter() - .position(|parameter| parameter.name == *name) - { - return arguments.get(index).cloned(); + enum Frame<'a> { + Enter(&'a Type), + Finish(&'a str, usize), + } + let mut frames = vec![Frame::Enter(template)]; + let mut resolved = Vec::new(); + while let Some(frame) = frames.pop() { + match frame { + Frame::Enter(template) => match template { + Type::I64 => resolved.push(Type::I64), + Type::Bool => resolved.push(Type::Bool), + Type::Named { + name, + arguments: nested, + } => { + if nested.is_empty() { + if let Some(index) = function + .type_parameters + .iter() + .position(|parameter| parameter.name == *name) + { + resolved.push(arguments.get(index)?.clone()); + continue; + } + } + frames.push(Frame::Finish(name, nested.len())); + frames.extend(nested.iter().rev().map(Frame::Enter)); } + }, + Frame::Finish(name, count) => { + let split = resolved.len().checked_sub(count)?; + let arguments = resolved.drain(split..).collect(); + resolved.push(Type::Named { + name: name.to_owned(), + arguments, + }); } - Some(Type::Named { - name: name.clone(), - arguments: nested - .iter() - .map(|nested| substitute_source_function_type(function, arguments, nested)) - .collect::>>()?, - }) } } + (resolved.len() == 1).then(|| resolved.pop().expect("type count checked above")) } fn specialize_source_function( @@ -1226,6 +2203,28 @@ fn materialize_template_expr( .collect::>()?, } } + ResolvedExprKind::NativeRustImportCall(call) => { + ResolvedExprKind::NativeRustImportCall(ResolvedNativeRustImportCall { + expression: ExpressionId::new(execution, path), + import: call.import.clone(), + args: call + .args + .iter() + .enumerate() + .map(|(index, argument)| { + materialize_template_expr( + template, + arguments, + execution, + argument, + values, + &format!("{path}.native-rust-arg.{index}"), + ) + }) + .collect::>()?, + result: call.result.clone(), + }) + } ResolvedExprKind::Unary { op, value } => ResolvedExprKind::Unary { op: *op, value: Box::new(materialize_template_expr( @@ -1395,6 +2394,14 @@ pub struct ResolvedProgram { pub function_instances: Vec, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ResolvedNativeRustImportCall { + pub expression: ExpressionId, + pub import: DeclarationId, + pub args: Vec, + pub result: ResolvedImportResultKind, +} + impl ResolvedProgram { pub fn resolve_call_target( &self, @@ -1483,6 +2490,7 @@ pub struct ResolvedImport { pub name: String, pub interface: DeclarationId, pub import_key: String, + pub native_rust: bool, pub parameters: Vec, pub result: ResolvedImportResult, pub effects: Vec, @@ -1511,6 +2519,8 @@ pub struct ResolvedImportResult { #[derive(Clone, Debug, Eq, PartialEq)] pub enum ResolvedImportResultKind { Unit, + I64, + Bool, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -1608,6 +2618,7 @@ pub enum ResolvedExprKind { instance: Option, args: Vec, }, + NativeRustImportCall(ResolvedNativeRustImportCall), Unary { op: UnaryOp, value: Box, @@ -1766,7 +2777,7 @@ impl Availability { } } -#[derive(Clone)] +#[derive(Clone, Debug, Eq, PartialEq)] struct ValidationBinding { ty: ResolvedType, ownership: OwnershipMode, @@ -1775,6 +2786,30 @@ struct ValidationBinding { definitely_partial: BTreeSet>, } +/// Restores the most recently published ownership scope on every early +/// validation return. Iterative continuations update the publication boundary +/// before entering a direct child; isolated Block/branch/arm children leave it +/// at their outer baseline. +struct ValidationScopePublication<'a> { + target: &'a mut BTreeMap, + published: BTreeMap, + enabled: bool, +} + +impl ValidationScopePublication<'_> { + fn publish(&mut self, scope: &BTreeMap) { + if self.enabled { + self.published.clone_from(scope); + } + } +} + +impl Drop for ValidationScopePublication<'_> { + fn drop(&mut self) { + std::mem::swap(self.target, &mut self.published); + } +} + /// Verify and resolve a parsed program into deterministic HIR. /// /// Verification errors are returned unchanged. This makes the HIR boundary @@ -1860,6 +2895,7 @@ pub(crate) fn validate_core(program: &ResolvedProgram) -> Result<(), Diagnostic> HirValidator::new(program)?.validate() } +#[derive(Clone)] struct HirValidator<'a> { program: &'a ResolvedProgram, functions: BTreeMap, @@ -1996,10 +3032,25 @@ impl<'a> HirValidator<'a> { ))); } } - if import.parameters.len() != 1 - || import.parameters[0].ownership != OwnershipMode::Own - || !import.parameters[0].consumes_on_failure - || import.result.kind != ResolvedImportResultKind::Unit + let native_shape = import.native_rust + && import.parameters.len() <= 8 + && import.parameters.iter().all(|parameter| { + parameter.ownership == OwnershipMode::Value + && !parameter.consumes_on_failure + && matches!(parameter.ty, ResolvedType::I64 | ResolvedType::Bool) + }) + && matches!( + import.result.kind, + ResolvedImportResultKind::Unit + | ResolvedImportResultKind::I64 + | ResolvedImportResultKind::Bool + ); + let lifecycle_shape = !import.native_rust + && import.parameters.len() == 1 + && import.parameters[0].ownership == OwnershipMode::Own + && import.parameters[0].consumes_on_failure + && import.result.kind == ResolvedImportResultKind::Unit; + if (!native_shape && !lifecycle_shape) || import.result.ownership != OwnershipMode::Value || import.result.producer != "callee" || import.result.out_slot_initialization != "success_only" @@ -2011,15 +3062,19 @@ impl<'a> HirValidator<'a> { import.id ))); } - self.validate_type(&import.parameters[0].ty)?; - let parameter_is_resource = import.parameters[0] - .ty - .nominal_id() - .and_then(|id| self.program.declarations.declaration(id)) - .is_some_and(|item| item.kind == DeclarationKind::Resource); + for parameter in &import.parameters { + self.validate_type(¶meter.ty)?; + } + let parameter_is_resource = import.parameters.first().is_some_and(|parameter| { + parameter + .ty + .nominal_id() + .and_then(|id| self.program.declarations.declaration(id)) + .is_some_and(|item| item.kind == DeclarationKind::Resource) + }); let effects = import.effects.iter().collect::>(); let authority = import.required_authority.iter().collect::>(); - if !parameter_is_resource + if (!import.native_rust && !parameter_is_resource) || effects.len() != import.effects.len() || authority.len() != import.required_authority.len() { @@ -2043,9 +3098,26 @@ impl<'a> HirValidator<'a> { normalization, } = &import.failure { - if domain_id.is_empty() - || domain_id.len() > STATUS_DOMAIN_MAX_BYTES_V1 - || domain_id.contains('\0') + let native_domain_valid = || { + let bytes = domain_id.as_bytes(); + (2..=STATUS_DOMAIN_MAX_BYTES_V1).contains(&bytes.len()) + && bytes.first().is_some_and(|byte| { + byte.is_ascii_lowercase() || byte.is_ascii_digit() + }) + && bytes.last().is_some_and(|byte| { + byte.is_ascii_lowercase() || byte.is_ascii_digit() + }) + && bytes.iter().all(|byte| { + byte.is_ascii_lowercase() + || byte.is_ascii_digit() + || matches!(byte, b'.' | b'-') + }) + }; + if (import.native_rust && !native_domain_valid()) + || (!import.native_rust + && (domain_id.is_empty() + || domain_id.len() > STATUS_DOMAIN_MAX_BYTES_V1 + || domain_id.contains('\0'))) || *normalization != "semaprax.status.v1" { return Err(hir_error(format!( @@ -2289,6 +3361,12 @@ impl<'a> HirValidator<'a> { } } if declaration.type_parameters.is_empty() { + if field.ty == ResolvedType::Unit { + return Err(hir_error(format!( + "field `{}` uses Unit outside a native Rust import result", + field.id + ))); + } self.validate_type(&field.ty)?; if let ResolvedType::Nominal { declaration: field_declaration, @@ -2319,7 +3397,9 @@ impl<'a> HirValidator<'a> { hir_error("type parameter index does not fit usize") })?) .is_some() => {} - ResolvedType::TypeParameter { .. } | ResolvedType::Nominal { .. } => { + ResolvedType::Unit + | ResolvedType::TypeParameter { .. } + | ResolvedType::Nominal { .. } => { return Err(hir_error(format!( "field `{}` has an invalid generic copy record template", field.id @@ -2456,7 +3536,9 @@ impl<'a> HirValidator<'a> { hir_error("type parameter index does not fit usize") })?) .is_some() => {} - ResolvedType::TypeParameter { .. } | ResolvedType::Nominal { .. } => { + ResolvedType::Unit + | ResolvedType::TypeParameter { .. } + | ResolvedType::Nominal { .. } => { return Err(hir_error(format!( "field `{}` has an invalid generic copy payload template", field.id @@ -2655,12 +3737,12 @@ impl<'a> HirValidator<'a> { { Ok(()) } - ResolvedType::TypeParameter { .. } | ResolvedType::Nominal { .. } => { - Err(hir_error(format!( - "generic template `{}` has an invalid direct-scalar signature slot", - template.id - ))) - } + ResolvedType::Unit + | ResolvedType::TypeParameter { .. } + | ResolvedType::Nominal { .. } => Err(hir_error(format!( + "generic template `{}` has an invalid direct-scalar signature slot", + template.id + ))), } } @@ -2756,6 +3838,11 @@ impl<'a> HirValidator<'a> { )?; } } + ResolvedExprKind::NativeRustImportCall(_) => { + return Err(hir_error( + "generic templates cannot call native Rust imports", + )); + } ResolvedExprKind::Unary { value, .. } => self.validate_template_expr( template, execution, @@ -2891,6 +3978,11 @@ impl<'a> HirValidator<'a> { function: &ResolvedFunction, execution: &FunctionExecutionId, ) -> Result<(), Diagnostic> { + if function.return_type == ResolvedType::Unit { + return Err(hir_error( + "ordinary resolved functions cannot declare a unit result", + )); + } self.validate_type(&function.return_type)?; let permits = self .program @@ -2943,6 +4035,11 @@ impl<'a> HirValidator<'a> { } let mut scope = BTreeMap::new(); for (index, param) in function.params.iter().enumerate() { + if param.ty == ResolvedType::Unit { + return Err(hir_error( + "ordinary resolved functions cannot declare a unit parameter", + )); + } reject_nul_identity("resolved value", param.id.as_str())?; let expected = ValueId::parameter(execution, index); if param.id != expected { @@ -3040,609 +4137,1766 @@ impl<'a> HirValidator<'a> { scope: &mut BTreeMap, path: &str, ) -> Result<(), Diagnostic> { - if instance != expected { - return Err(hir_error( - "resolved record pattern has the wrong concrete instance", - )); - } - let ResolvedType::Nominal { - declaration, - arguments, - } = expected - else { - return Err(hir_error("resolved record pattern instance is not nominal")); - }; - if declaration != record - || self - .program - .declarations - .declaration(record) - .is_none_or(|item| item.kind != DeclarationKind::Record) - { - return Err(hir_error( - "resolved record pattern references a foreign record", - )); - } - let facts = self - .program - .declarations - .type_facts(expected) - .ok_or_else(|| hir_error("resolved record pattern has no exact type facts"))?; - if !facts.copy || facts.contains_resource || facts.needs_drop { - return Err(hir_error("resolved record pattern is not Copy")); + enum Frame<'a> { + Enter { + expected: ResolvedType, + record: &'a DeclarationId, + instance: &'a ResolvedType, + fields: &'a [ResolvedRecordMatchPatternField], + path: String, + }, + Fields { + expected: ResolvedType, + record: &'a DeclarationId, + fields: &'a [ResolvedRecordMatchPatternField], + declared_fields: &'a [ResolvedFieldDeclaration], + index: usize, + seen: BTreeSet, + path: String, + }, } - let declared_fields = self - .program - .declarations - .record_fields(record) - .ok_or_else(|| hir_error(format!("record `{record}` has no fields")))?; - let mut seen = BTreeSet::new(); - for (field_index, field) in fields.iter().enumerate() { - let declared = declared_fields - .iter() - .find(|candidate| candidate.id == field.field) - .ok_or_else(|| { - hir_error(format!( - "resolved record pattern contains foreign field `{}`", - field.field - )) - })?; - if !seen.insert(field.field.clone()) { - return Err(hir_error( - "resolved record pattern contains a duplicate field", - )); - } - let field_ty = substitute_type(&declared.ty, record, arguments)?; - let field_path = format!("{path}.field.{field_index}"); - match &field.pattern { - ResolvedRecordMatchFieldPattern::Binding(binding) => { - if binding.id != ValueId::local(function, &format!("{field_path}.binding")) - || binding.ty != field_ty - || binding.ownership != OwnershipMode::Value - { + let mut frames = vec![Frame::Enter { + expected: expected.clone(), + record, + instance, + fields, + path: path.to_owned(), + }]; + while let Some(frame) = frames.pop() { + match frame { + Frame::Enter { + expected, + record, + instance, + fields, + path, + } => { + if instance != &expected { return Err(hir_error( - "resolved record pattern binding is not canonical", + "resolved record pattern has the wrong concrete instance", )); } - self.insert_value(&binding.id)?; - self.validate_type(&binding.ty)?; - if scope.contains_key(&binding.id) { + let ResolvedType::Nominal { + declaration, + arguments: _, + } = &expected + else { + return Err(hir_error("resolved record pattern instance is not nominal")); + }; + if declaration != record + || self + .program + .declarations + .declaration(record) + .is_none_or(|item| item.kind != DeclarationKind::Record) + { return Err(hir_error( - "resolved record pattern binding shadows an existing value", + "resolved record pattern references a foreign record", )); } - scope.insert( - binding.id.clone(), - ValidationBinding { - ty: binding.ty.clone(), - ownership: OwnershipMode::Value, - availability: Availability::Available, - moved_places: BTreeMap::new(), - definitely_partial: BTreeSet::new(), - }, - ); + let facts = + self.program + .declarations + .type_facts(&expected) + .ok_or_else(|| { + hir_error("resolved record pattern has no exact type facts") + })?; + if !facts.copy || facts.contains_resource || facts.needs_drop { + return Err(hir_error("resolved record pattern is not Copy")); + } + let declared_fields = self + .program + .declarations + .record_fields(record) + .ok_or_else(|| hir_error(format!("record `{record}` has no fields")))?; + frames.push(Frame::Fields { + expected, + record, + fields, + declared_fields, + index: 0, + seen: BTreeSet::new(), + path, + }); } - ResolvedRecordMatchFieldPattern::Wildcard => {} - ResolvedRecordMatchFieldPattern::Record { - record, - instance, - fields, - } => self.validate_record_match_pattern( - function, - &field_ty, + Frame::Fields { + expected, record, - instance, fields, - scope, - &format!("{field_path}.record"), - )?, - } - } - if seen.len() != declared_fields.len() { - return Err(hir_error("resolved record pattern is missing fields")); - } - Ok(()) - } - - fn validate_expr( - &mut self, - function: &FunctionExecutionId, - expression: &ResolvedExpr, - scope: &mut BTreeMap, + declared_fields, + index, + mut seen, + path, + } => { + let Some(field) = fields.get(index) else { + if seen.len() != declared_fields.len() { + return Err(hir_error("resolved record pattern is missing fields")); + } + continue; + }; + let declared = declared_fields + .iter() + .find(|candidate| candidate.id == field.field) + .ok_or_else(|| { + hir_error(format!( + "resolved record pattern contains foreign field `{}`", + field.field + )) + })?; + if !seen.insert(field.field.clone()) { + return Err(hir_error( + "resolved record pattern contains a duplicate field", + )); + } + let ResolvedType::Nominal { arguments, .. } = &expected else { + unreachable!("validated record instance remains nominal") + }; + let field_ty = substitute_type(&declared.ty, record, arguments)?; + let field_path = format!("{path}.field.{index}"); + frames.push(Frame::Fields { + expected, + record, + fields, + declared_fields, + index: index + 1, + seen, + path, + }); + match &field.pattern { + ResolvedRecordMatchFieldPattern::Binding(binding) => { + if binding.id + != ValueId::local(function, &format!("{field_path}.binding")) + || binding.ty != field_ty + || binding.ownership != OwnershipMode::Value + { + return Err(hir_error( + "resolved record pattern binding is not canonical", + )); + } + self.insert_value(&binding.id)?; + self.validate_type(&binding.ty)?; + if scope.contains_key(&binding.id) { + return Err(hir_error( + "resolved record pattern binding shadows an existing value", + )); + } + scope.insert( + binding.id.clone(), + ValidationBinding { + ty: binding.ty.clone(), + ownership: OwnershipMode::Value, + availability: Availability::Available, + moved_places: BTreeMap::new(), + definitely_partial: BTreeSet::new(), + }, + ); + } + ResolvedRecordMatchFieldPattern::Wildcard => {} + ResolvedRecordMatchFieldPattern::Record { + record, + instance, + fields, + } => frames.push(Frame::Enter { + expected: field_ty, + record, + instance, + fields, + path: format!("{field_path}.record"), + }), + } + } + } + } + Ok(()) + } + + fn validate_expr( + &mut self, + function: &FunctionExecutionId, + expression: &ResolvedExpr, + scope: &mut BTreeMap, path: &str, allow_moves: bool, allowed_effects: Option<&BTreeSet>, ) -> Result<(), Diagnostic> { - reject_nul_identity("resolved expression", expression.id.as_str())?; - if expression.id != ExpressionId::new(function, path) { - return Err(hir_error(format!( - "expression `{}` has a non-canonical identity", - expression.id - ))); + self.validate_expr_iterative( + function, + expression, + scope, + path, + allow_moves, + allowed_effects, + ) + } + + #[cfg(test)] + #[allow(clippy::too_many_arguments)] + fn assert_validation_oracle( + iterative: &Result<(), Diagnostic>, + recursive: &Result<(), Diagnostic>, + iterative_validator: &Self, + recursive_validator: &Self, + iterative_scope: &BTreeMap, + recursive_scope: &BTreeMap, + path: &str, + ) { + match (iterative, recursive) { + (Ok(()), Ok(())) => {} + (Err(left), Err(right)) => { + assert_eq!(left.code, right.code, "validator code differs at {path}"); + assert_eq!( + left.severity, right.severity, + "validator severity differs at {path}" + ); + assert_eq!( + left.message, right.message, + "validator message differs at {path}" + ); + assert_eq!(left.path, right.path, "validator path differs at {path}"); + assert_eq!(left.span, right.span, "validator span differs at {path}"); + assert_eq!(left.help, right.help, "validator help differs at {path}"); + } + outcomes => panic!("validator outcomes differ at {path}: {outcomes:?}"), } - if !self.expression_ids.insert(expression.id.clone()) { - return Err(hir_error(format!( - "duplicate resolved expression identity `{}`", - expression.id - ))); + assert_eq!( + iterative_validator.expression_ids, recursive_validator.expression_ids, + "validator expression IDs differ at {path}" + ); + assert_eq!( + iterative_validator.value_ids, recursive_validator.value_ids, + "validator value IDs differ at {path}" + ); + assert_eq!( + iterative_scope, recursive_scope, + "validator scope differs at {path}" + ); + } + + #[allow(clippy::too_many_arguments)] + fn validate_expr_iterative( + &mut self, + function: &FunctionExecutionId, + expression: &ResolvedExpr, + scope: &mut BTreeMap, + path: &str, + allow_moves: bool, + allowed_effects: Option<&BTreeSet>, + ) -> Result<(), Diagnostic> { + enum Frame<'e> { + RestorePublication(bool), + Enter { + expression: &'e ResolvedExpr, + scope: BTreeMap, + path: String, + }, + Unary { + expression: &'e ResolvedExpr, + op: UnaryOp, + }, + BinaryLeft { + expression: &'e ResolvedExpr, + op: BinaryOp, + right: &'e ResolvedExpr, + path: String, + }, + BinaryRight { + expression: &'e ResolvedExpr, + op: BinaryOp, + left: &'e ResolvedExpr, + baseline: Option<(Vec, BTreeMap)>, + }, + IfCondition { + expression: &'e ResolvedExpr, + then_branch: &'e ResolvedExpr, + else_branch: &'e ResolvedExpr, + path: String, + }, + IfThen { + expression: &'e ResolvedExpr, + else_branch: &'e ResolvedExpr, + path: String, + outer: BTreeMap, + outer_ids: Vec, + }, + IfElse { + expression: &'e ResolvedExpr, + then_scope: BTreeMap, + outer: BTreeMap, + outer_ids: Vec, + }, + Project { + expression: &'e ResolvedExpr, + field: &'e DeclarationId, + }, + Try { + expression: &'e ResolvedExpr, + path: String, + option: bool, + }, + CallNext { + expression: &'e ResolvedExpr, + args: &'e [ResolvedExpr], + params: Vec, + return_type: ResolvedType, + index: usize, + scope: BTreeMap, + path: String, + }, + CallAfterArg { + expression: &'e ResolvedExpr, + args: &'e [ResolvedExpr], + params: Vec, + return_type: ResolvedType, + index: usize, + path: String, + }, + NativeNext { + expression: &'e ResolvedExpr, + args: &'e [ResolvedExpr], + params: Vec, + result: ResolvedType, + index: usize, + scope: BTreeMap, + path: String, + }, + NativeAfterArg { + expression: &'e ResolvedExpr, + args: &'e [ResolvedExpr], + params: Vec, + result: ResolvedType, + index: usize, + path: String, + }, + BlockNext { + expression: &'e ResolvedExpr, + statements: &'e [ResolvedStatement], + tail: &'e ResolvedExpr, + index: usize, + scope: BTreeMap, + outer: BTreeMap, + outer_ids: Vec, + path: String, + }, + BlockAfterLet { + expression: &'e ResolvedExpr, + statements: &'e [ResolvedStatement], + tail: &'e ResolvedExpr, + index: usize, + outer: BTreeMap, + outer_ids: Vec, + path: String, + }, + BlockTail { + expression: &'e ResolvedExpr, + outer_ids: Vec, + outer: BTreeMap, + }, + RecordNext { + expression: &'e ResolvedExpr, + fields: &'e [ResolvedFieldInitializer], + expected: Vec, + record: DeclarationId, + arguments: Vec, + seen: BTreeSet, + index: usize, + scope: BTreeMap, + path: String, + }, + RecordAfterField { + expression: &'e ResolvedExpr, + fields: &'e [ResolvedFieldInitializer], + expected: Vec, + record: DeclarationId, + arguments: Vec, + seen: BTreeSet, + index: usize, + path: String, + }, + VariantNext { + expression: &'e ResolvedExpr, + fields: &'e [ResolvedFieldInitializer], + expected: Vec, + variant: DeclarationId, + case: DeclarationId, + arguments: Vec, + seen: BTreeSet, + index: usize, + scope: BTreeMap, + path: String, + }, + VariantAfterField { + expression: &'e ResolvedExpr, + fields: &'e [ResolvedFieldInitializer], + expected: Vec, + variant: DeclarationId, + case: DeclarationId, + arguments: Vec, + seen: BTreeSet, + index: usize, + path: String, + }, + UpdateBase { + expression: &'e ResolvedExpr, + record: &'e DeclarationId, + fields: &'e [ResolvedFieldInitializer], + path: String, + }, + UpdateNext { + expression: &'e ResolvedExpr, + fields: &'e [ResolvedFieldInitializer], + expected: Vec, + record: DeclarationId, + arguments: Vec, + seen: BTreeSet, + index: usize, + scope: BTreeMap, + path: String, + ownership: OwnershipMode, + }, + UpdateAfterField { + expression: &'e ResolvedExpr, + fields: &'e [ResolvedFieldInitializer], + expected: Vec, + record: DeclarationId, + arguments: Vec, + seen: BTreeSet, + index: usize, + path: String, + ownership: OwnershipMode, + }, + MatchScrutinee { + expression: &'e ResolvedExpr, + arms: &'e [ResolvedMatchArm], + path: String, + }, + RecordMatchArm { + expression: &'e ResolvedExpr, + arm: &'e ResolvedMatchArm, + outer: BTreeMap, + outer_ids: Vec, + }, + VariantMatchNext { + expression: &'e ResolvedExpr, + arms: &'e [ResolvedMatchArm], + cases: Vec, + variant: DeclarationId, + arguments: Vec, + index: usize, + outer: BTreeMap, + outer_ids: Vec, + arm_scopes: Vec>, + covered: BTreeSet, + wildcard_seen: bool, + result: Option<(ResolvedType, OwnershipMode)>, + path: String, + }, + VariantMatchAfterArm { + expression: &'e ResolvedExpr, + arms: &'e [ResolvedMatchArm], + cases: Vec, + variant: DeclarationId, + arguments: Vec, + index: usize, + outer: BTreeMap, + outer_ids: Vec, + arm_scopes: Vec>, + covered: BTreeSet, + wildcard_seen: bool, + result: Option<(ResolvedType, OwnershipMode)>, + path: String, + }, } - self.validate_type(&expression.ty)?; - let (ty, ownership) = match &expression.kind { - ResolvedExprKind::Int(_) => (ResolvedType::I64, OwnershipMode::Value), - ResolvedExprKind::Bool(_) => (ResolvedType::Bool, OwnershipMode::Value), - ResolvedExprKind::Place(place) => { - let binding = scope.get(&place.root).ok_or_else(|| { - hir_error(format!("resolved value `{}` is out of scope", place.root)) - })?; - match (place.projections.is_empty(), binding.availability) { - (true, Availability::Available) => { - match Self::place_availability(binding, &[]) { - Availability::Available => {} - Availability::Moved => { - return Err(hir_error(format!( - "resolved value `{}` is partially moved", - place.root - ))); - } - Availability::MaybeMoved => { - return Err(hir_error(format!( - "resolved value `{}` may be partially moved", - place.root - ))); - } - } - } - (true, Availability::Moved) => { + const { assert!(std::mem::size_of::>() == 288) }; + #[cfg(test)] + fn frame_owned_capacity(frame: &Frame<'_>) -> usize { + let ids = |values: &Vec| { + values.capacity() * std::mem::size_of::() + + values.iter().map(|id| id.as_str().len()).sum::() + }; + let types = |values: &Vec| { + values.capacity() * std::mem::size_of::() + + values + .iter() + .map(resolved_type_owned_capacity) + .sum::() + }; + let scope = |scope: &BTreeMap| { + validation_scope_owned_capacity(scope) + }; + let path = match frame { + Frame::Enter { path, .. } + | Frame::BinaryLeft { path, .. } + | Frame::IfCondition { path, .. } + | Frame::IfThen { path, .. } + | Frame::Try { path, .. } + | Frame::CallNext { path, .. } + | Frame::CallAfterArg { path, .. } + | Frame::NativeNext { path, .. } + | Frame::NativeAfterArg { path, .. } + | Frame::BlockNext { path, .. } + | Frame::BlockAfterLet { path, .. } + | Frame::RecordNext { path, .. } + | Frame::RecordAfterField { path, .. } + | Frame::VariantNext { path, .. } + | Frame::VariantAfterField { path, .. } + | Frame::UpdateBase { path, .. } + | Frame::UpdateNext { path, .. } + | Frame::UpdateAfterField { path, .. } + | Frame::MatchScrutinee { path, .. } + | Frame::VariantMatchNext { path, .. } + | Frame::VariantMatchAfterArm { path, .. } => path.capacity(), + _ => 0, + }; + let retained = match frame { + Frame::Enter { scope: value, .. } => scope(value), + Frame::BinaryRight { baseline, .. } => baseline + .as_ref() + .map_or(0, |(outer_ids, value)| ids(outer_ids) + scope(value)), + Frame::IfThen { + outer, outer_ids, .. + } => scope(outer) + ids(outer_ids), + Frame::IfElse { + then_scope, + outer, + outer_ids, + .. + } => scope(then_scope) + scope(outer) + ids(outer_ids), + Frame::CallNext { + params, + return_type, + scope: value, + .. + } => { + params.capacity() * std::mem::size_of::() + + params + .iter() + .map(|param| { + param.id.as_str().len() + + param.name.capacity() + + resolved_type_owned_capacity(¶m.ty) + }) + .sum::() + + resolved_type_owned_capacity(return_type) + + scope(value) + } + Frame::CallAfterArg { + params, + return_type, + .. + } => { + params.capacity() * std::mem::size_of::() + + params + .iter() + .map(|param| { + param.id.as_str().len() + + param.name.capacity() + + resolved_type_owned_capacity(¶m.ty) + }) + .sum::() + + resolved_type_owned_capacity(return_type) + } + Frame::NativeNext { + params, + result, + scope: value, + .. + } => { + params.capacity() * std::mem::size_of::() + + params + .iter() + .map(|param| { + param.name.capacity() + resolved_type_owned_capacity(¶m.ty) + }) + .sum::() + + resolved_type_owned_capacity(result) + + scope(value) + } + Frame::NativeAfterArg { params, result, .. } => { + params.capacity() * std::mem::size_of::() + + params + .iter() + .map(|param| { + param.name.capacity() + resolved_type_owned_capacity(¶m.ty) + }) + .sum::() + + resolved_type_owned_capacity(result) + } + Frame::BlockNext { + scope: value, + outer, + outer_ids, + .. + } => scope(value) + scope(outer) + ids(outer_ids), + Frame::BlockAfterLet { + outer, outer_ids, .. + } + | Frame::BlockTail { + outer, outer_ids, .. + } => scope(outer) + ids(outer_ids), + Frame::RecordNext { + expected, + arguments, + seen, + scope: value, + .. + } + | Frame::VariantNext { + expected, + arguments, + seen, + scope: value, + .. + } + | Frame::UpdateNext { + expected, + arguments, + seen, + scope: value, + .. + } => { + expected.capacity() * std::mem::size_of::() + + expected + .iter() + .map(resolved_field_declaration_owned_capacity) + .sum::() + + types(arguments) + + seen.len() + * (std::mem::size_of::() + + std::mem::size_of::>()) + + seen.iter().map(|id| id.as_str().len()).sum::() + + scope(value) + } + Frame::RecordAfterField { + expected, + arguments, + seen, + .. + } + | Frame::VariantAfterField { + expected, + arguments, + seen, + .. + } + | Frame::UpdateAfterField { + expected, + arguments, + seen, + .. + } => { + expected.capacity() * std::mem::size_of::() + + expected + .iter() + .map(resolved_field_declaration_owned_capacity) + .sum::() + + types(arguments) + + seen.len() + * (std::mem::size_of::() + + std::mem::size_of::>()) + + seen.iter().map(|id| id.as_str().len()).sum::() + } + Frame::RecordMatchArm { + outer, outer_ids, .. + } => scope(outer) + ids(outer_ids), + Frame::VariantMatchNext { + cases, + arguments, + outer, + outer_ids, + arm_scopes, + covered, + result, + .. + } + | Frame::VariantMatchAfterArm { + cases, + arguments, + outer, + outer_ids, + arm_scopes, + covered, + result, + .. + } => { + cases.capacity() * std::mem::size_of::() + + cases + .iter() + .map(resolved_variant_case_owned_capacity) + .sum::() + + types(arguments) + + scope(outer) + + ids(outer_ids) + + arm_scopes.capacity() + * std::mem::size_of::>() + + arm_scopes.iter().map(scope).sum::() + + covered.len() + * (std::mem::size_of::() + + std::mem::size_of::>()) + + covered.iter().map(|id| id.as_str().len()).sum::() + + result + .as_ref() + .map_or(0, |(ty, _)| resolved_type_owned_capacity(ty)) + } + _ => 0, + }; + path.saturating_add(retained) + } + let initial_scope = std::mem::take(scope); + let mut publication = ValidationScopePublication { + target: scope, + published: initial_scope.clone(), + enabled: true, + }; + let mut frames = vec![Frame::Enter { + expression, + scope: initial_scope, + path: path.to_owned(), + }]; + let mut scopes = Vec::new(); + while let Some(frame) = frames.pop() { + #[cfg(test)] + note_iterative_phase_capacity( + 1, + frames.capacity() * std::mem::size_of::>() + + scopes.capacity() + * std::mem::size_of::>() + + scopes + .iter() + .map(validation_scope_owned_capacity) + .sum::() + + validation_scope_owned_capacity(&publication.published) + + frames.iter().map(frame_owned_capacity).sum::() + + frame_owned_capacity(&frame) + + self.expression_ids.len() + * (std::mem::size_of::() + + std::mem::size_of::>()) + + self + .expression_ids + .iter() + .map(|id| id.as_str().len()) + .sum::() + + self.value_ids.len() + * (std::mem::size_of::() + + std::mem::size_of::>()) + + self + .value_ids + .iter() + .map(|id| id.as_str().len()) + .sum::() + + self.functions.len() + * (std::mem::size_of::<(DeclarationId, &ResolvedFunction)>() + + std::mem::size_of::>()) + + self + .functions + .keys() + .map(|id| id.as_str().len()) + .sum::(), + ); + match frame { + Frame::RestorePublication(enabled) => publication.enabled = enabled, + Frame::Enter { + expression, + scope, + path, + } => { + reject_nul_identity("resolved expression", expression.id.as_str())?; + if expression.id != ExpressionId::new(function, &path) { return Err(hir_error(format!( - "resolved value `{}` is used after it was moved", - place.root + "expression `{}` has a non-canonical identity", + expression.id ))); } - (true, Availability::MaybeMoved) => { + if !self.expression_ids.insert(expression.id.clone()) { return Err(hir_error(format!( - "resolved value `{}` may have been moved", - place.root + "duplicate resolved expression identity `{}`", + expression.id ))); } - (false, _) => match Self::place_availability(binding, &place.projections) { - Availability::Available => {} - Availability::Moved => { - return Err(hir_error(format!( - "resolved place rooted at `{}` is partially moved", - place.root - ))); + self.validate_type(&expression.ty)?; + match &expression.kind { + ResolvedExprKind::Int(_) => { + self.finish_expr(expression, &ResolvedType::I64, OwnershipMode::Value)?; + scopes.push(scope); } - Availability::MaybeMoved => { - return Err(hir_error(format!( - "resolved place rooted at `{}` may be conditionally moved", - place.root - ))); + ResolvedExprKind::Bool(_) => { + self.finish_expr( + expression, + &ResolvedType::Bool, + OwnershipMode::Value, + )?; + scopes.push(scope); } - }, - } - self.resolve_place(place, binding)? - } - ResolvedExprKind::Call { - callee, - type_arguments, - instance, - args, - } => { - match instance { - None if !type_arguments.is_empty() => { - return Err(hir_error( - "monomorphic resolved call carries generic type arguments", - )); - } - Some(instance) - if FunctionInstanceId::derive(callee, type_arguments) != *instance => - { - return Err(hir_error( - "resolved call instance disagrees with its template and arguments", - )); - } - Some(_) if type_arguments.is_empty() => { - return Err(hir_error( - "generic resolved call has no concrete type arguments", - )); - } - None | Some(_) => {} - } - for argument in type_arguments { - if !matches!(argument, ResolvedType::I64 | ResolvedType::Bool) { - return Err(hir_error( - "resolved call has a non-scalar generic type argument", - )); - } - } - let target = self - .program - .resolve_call_target(callee, instance.as_ref()) - .ok_or_else(|| { - hir_error(format!("resolved callee `{callee}` is not indexed")) - })?; - if args.len() != target.params.len() { - return Err(hir_error(format!( - "call to `{callee}` has {} arguments but expects {}", - args.len(), - target.params.len() - ))); - } - let params = target.params.clone(); - let return_type = target.return_type.clone(); - let target_effects = target.effects.clone(); - match allowed_effects { - Some(allowed) => { - for effect in &target_effects { - if !allowed.contains(effect) { + ResolvedExprKind::Place(place) => { + let binding = scope.get(&place.root).ok_or_else(|| { + hir_error(format!( + "resolved value `{}` is out of scope", + place.root + )) + })?; + match (place.projections.is_empty(), binding.availability) { + (true, Availability::Available) => { + match Self::place_availability(binding, &[]) { + Availability::Available => {} + Availability::Moved => { + return Err(hir_error(format!( + "resolved value `{}` is partially moved", + place.root + ))) + } + Availability::MaybeMoved => { + return Err(hir_error(format!( + "resolved value `{}` may be partially moved", + place.root + ))) + } + } + } + (true, Availability::Moved) => { + return Err(hir_error(format!( + "resolved value `{}` is used after it was moved", + place.root + ))) + } + (true, Availability::MaybeMoved) => { + return Err(hir_error(format!( + "resolved value `{}` may have been moved", + place.root + ))) + } + (false, _) => { + match Self::place_availability(binding, &place.projections) { + Availability::Available => {} + Availability::Moved => { + return Err(hir_error(format!( + "resolved place rooted at `{}` is partially moved", + place.root + ))) + } + Availability::MaybeMoved => { + return Err(hir_error(format!( + "resolved place rooted at `{}` may be conditionally moved", + place.root + ))) + } + } + } + } + let (ty, ownership) = self.resolve_place(place, binding)?; + self.finish_expr(expression, &ty, ownership)?; + scopes.push(scope); + } + ResolvedExprKind::Unary { op, value } => { + frames.push(Frame::Unary { + expression, + op: *op, + }); + frames.push(Frame::Enter { + expression: value, + scope, + path: format!("{path}.value"), + }); + } + ResolvedExprKind::Call { + callee, + type_arguments, + instance, + args, + } => { + match instance { + None if !type_arguments.is_empty() => return Err(hir_error("monomorphic resolved call carries generic type arguments")), + Some(actual) if FunctionInstanceId::derive(callee, type_arguments) != *actual => return Err(hir_error("resolved call instance disagrees with its template and arguments")), + Some(_) if type_arguments.is_empty() => return Err(hir_error("generic resolved call has no concrete type arguments")), + None | Some(_) => {} + } + if type_arguments.iter().any(|argument| { + !matches!(argument, ResolvedType::I64 | ResolvedType::Bool) + }) { + return Err(hir_error( + "resolved call has a non-scalar generic type argument", + )); + } + let target = self + .program + .resolve_call_target(callee, instance.as_ref()) + .ok_or_else(|| { + hir_error(format!("resolved callee `{callee}` is not indexed")) + })?; + if args.len() != target.params.len() { return Err(hir_error(format!( - "call to `{callee}` requires undeclared effect `{effect}`" + "call to `{callee}` has {} arguments but expects {}", + args.len(), + target.params.len() + ))); + } + match allowed_effects { + Some(allowed) => { + for effect in &target.effects { + if !allowed.contains(effect) { + return Err(hir_error(format!("call to `{callee}` requires undeclared effect `{effect}`"))); + } + } + } + None if !target.effects.is_empty() => { + return Err(hir_error(format!( + "contract calls effectful function `{callee}`" + ))) + } + None => {} + } + frames.push(Frame::CallNext { + expression, + args, + params: target.params.clone(), + return_type: target.return_type.clone(), + index: 0, + scope, + path, + }); + } + ResolvedExprKind::NativeRustImportCall(call) => { + if call.expression != expression.id { + return Err(hir_error("native Rust import call has a non-canonical expression identity")); + } + let import = self + .program + .interfaces + .iter() + .flat_map(|interface| &interface.imports) + .find(|import| import.id == call.import && import.native_rust) + .ok_or_else(|| { + hir_error("native Rust import call has an unknown target") + })?; + if import.parameters.len() != call.args.len() + || import.result.kind != call.result + { + return Err(hir_error( + "native Rust import call disagrees with its declaration", + )); + } + match allowed_effects { + Some(allowed) + if import + .effects + .iter() + .any(|effect| !allowed.contains(effect)) => + { + return Err(hir_error( + "native Rust import call requires an undeclared effect", + )) + } + None if !import.effects.is_empty() => { + return Err(hir_error( + "contract calls an effectful native Rust import", + )) + } + _ => {} + } + let result = match call.result { + ResolvedImportResultKind::Unit => ResolvedType::Unit, + ResolvedImportResultKind::I64 => ResolvedType::I64, + ResolvedImportResultKind::Bool => ResolvedType::Bool, + }; + frames.push(Frame::NativeNext { + expression, + args: &call.args, + params: import.parameters.clone(), + result, + index: 0, + scope, + path, + }); + } + ResolvedExprKind::Binary { op, left, right } => { + frames.push(Frame::BinaryLeft { + expression, + op: *op, + right, + path: path.clone(), + }); + frames.push(Frame::Enter { + expression: left, + scope, + path: format!("{path}.left"), + }); + } + ResolvedExprKind::If { + condition, + then_branch, + else_branch, + } => { + frames.push(Frame::IfCondition { + expression, + then_branch, + else_branch, + path: path.clone(), + }); + frames.push(Frame::Enter { + expression: condition, + scope, + path: format!("{path}.condition"), + }); + } + ResolvedExprKind::Block { statements, tail } => { + let outer_ids = scope.keys().cloned().collect(); + let outer = scope.clone(); + frames.push(Frame::BlockNext { + expression, + statements, + tail, + index: 0, + scope, + outer, + outer_ids, + path, + }); + } + ResolvedExprKind::ConstructRecord { record, fields } => { + let declaration = self + .program + .declarations + .declaration(record) + .ok_or_else(|| { + hir_error(format!("record `{record}` is not indexed")) + })?; + if declaration.kind != DeclarationKind::Record { + return Err(hir_error(format!( + "constructor target `{record}` is not a record" + ))); + } + let expected = self + .program + .declarations + .record_fields(record) + .ok_or_else(|| { + hir_error(format!("record `{record}` has no fields")) + })? + .to_vec(); + let ResolvedType::Nominal { + declaration: instance, + arguments, + } = &expression.ty + else { + return Err(hir_error("record constructor result is not nominal")); + }; + let parameters = self + .program + .declarations + .type_parameters(record) + .ok_or_else(|| { + hir_error(format!("record `{record}` has no parameters")) + })?; + if instance != record + || arguments.len() != parameters.len() + || arguments.iter().any(|argument| { + !matches!(argument, ResolvedType::I64 | ResolvedType::Bool) + }) + { + return Err(hir_error(format!( + "constructor for `{record}` has an invalid concrete instance" + ))); + } + frames.push(Frame::RecordNext { + expression, + fields, + expected, + record: record.clone(), + arguments: arguments.clone(), + seen: BTreeSet::new(), + index: 0, + scope, + path, + }); + } + ResolvedExprKind::ConstructVariant { + variant, + case, + fields, + } => { + let ResolvedType::Nominal { + declaration: instance, + arguments, + } = &expression.ty + else { + return Err(hir_error( + "variant constructor has a non-nominal result", + )); + }; + if instance != variant { + return Err(hir_error( + "variant constructor result disagrees with its declaration", + )); + } + let declaration = + self.program.declarations.declaration(variant).ok_or_else( + || hir_error(format!("variant `{variant}` is not indexed")), + )?; + if declaration.kind != DeclarationKind::Variant { + return Err(hir_error(format!( + "constructor target `{variant}` is not a variant" ))); } + let expected = self.program.declarations.variant_cases(variant).and_then(|cases| cases.iter().find(|item| item.id == *case)).ok_or_else(|| hir_error(format!("constructor for `{variant}` contains foreign case `{case}`")))?.fields.clone(); + frames.push(Frame::VariantNext { + expression, + fields, + expected, + variant: variant.clone(), + case: case.clone(), + arguments: arguments.clone(), + seen: BTreeSet::new(), + index: 0, + scope, + path, + }); + } + ResolvedExprKind::UpdateRecord { + base, + record, + fields, + } => { + frames.push(Frame::UpdateBase { + expression, + record, + fields, + path: path.clone(), + }); + frames.push(Frame::Enter { + expression: base, + scope, + path: format!("{path}.base"), + }); + } + ResolvedExprKind::Match { scrutinee, arms } => { + frames.push(Frame::MatchScrutinee { + expression, + arms, + path: path.clone(), + }); + frames.push(Frame::Enter { + expression: scrutinee, + scope, + path: format!("{path}.scrutinee"), + }); + } + ResolvedExprKind::Project { base, field } => { + if matches!(&base.kind, ResolvedExprKind::Place(_)) { + return Err(hir_error( + "place field projections must use a resolved place path", + )); + } + frames.push(Frame::Project { expression, field }); + frames.push(Frame::Enter { + expression: base, + scope, + path: format!("{path}.base"), + }); + } + ResolvedExprKind::Try { operand, .. } => { + frames.push(Frame::Try { + expression, + path: path.clone(), + option: false, + }); + frames.push(Frame::Enter { + expression: operand, + scope, + path: format!("{path}.operand"), + }); + } + ResolvedExprKind::TryOption { operand, .. } => { + frames.push(Frame::Try { + expression, + path: path.clone(), + option: true, + }); + frames.push(Frame::Enter { + expression: operand, + scope, + path: format!("{path}.operand"), + }); } } - None if !target_effects.is_empty() => { - return Err(hir_error(format!( - "contract calls effectful function `{callee}`" - ))); + } + Frame::Unary { expression, op } => { + let scope = scopes.pop().expect("unary scope retained"); + let ResolvedExprKind::Unary { value, .. } = &expression.kind else { + unreachable!() + }; + let expected = match op { + UnaryOp::Neg => ResolvedType::I64, + UnaryOp::Not => ResolvedType::Bool, + }; + self.require_type(&value.ty, &expected, "unary operand")?; + self.finish_expr(expression, &expected, OwnershipMode::Value)?; + scopes.push(scope); + } + Frame::CallNext { + expression, + args, + params, + return_type, + index, + scope, + path, + } => { + if index == args.len() { + let ownership = + self.expected_ownership(&return_type, OwnershipMode::Own)?; + self.finish_expr(expression, &return_type, ownership)?; + scopes.push(scope); + } else { + frames.push(Frame::CallAfterArg { + expression, + args, + params, + return_type, + index, + path: path.clone(), + }); + frames.push(Frame::Enter { + expression: &args[index], + scope, + path: format!("{path}.arg.{index}"), + }); } - None => {} } - for (index, (argument, param)) in args.iter().zip(¶ms).enumerate() { - self.validate_expr( - function, - argument, - scope, - &format!("{path}.arg.{index}"), - allow_moves, - allowed_effects, - )?; + Frame::CallAfterArg { + expression, + args, + params, + return_type, + index, + path, + } => { + let mut scope = scopes.pop().expect("call argument scope retained"); + publication.publish(&scope); + let argument = &args[index]; + let param = ¶ms[index]; self.require_type(&argument.ty, ¶m.ty, "call argument")?; self.validate_argument_ownership(argument.ownership, param)?; if self.argument_transfers(param)? { if !allow_moves { + let ResolvedExprKind::Call { callee, .. } = &expression.kind else { + unreachable!() + }; return Err(hir_error(format!( "contract cannot transfer ownership to `{callee}`" ))); } - self.mark_value_sources_moved(argument, scope)?; + self.mark_value_sources_moved(argument, &mut scope)?; + publication.publish(&scope); } + frames.push(Frame::CallNext { + expression, + args, + params, + return_type, + index: index + 1, + scope, + path, + }); } - let ownership = self.expected_ownership(&return_type, OwnershipMode::Own)?; - (return_type, ownership) - } - ResolvedExprKind::Unary { op, value } => { - self.validate_expr( - function, - value, - scope, - &format!("{path}.value"), - allow_moves, - allowed_effects, - )?; - let expected = match op { - UnaryOp::Neg => ResolvedType::I64, - UnaryOp::Not => ResolvedType::Bool, - }; - self.require_type(&value.ty, &expected, "unary operand")?; - (expected, OwnershipMode::Value) - } - ResolvedExprKind::Binary { op, left, right } => { - self.validate_expr( - function, - left, + Frame::NativeNext { + expression, + args, + params, + result, + index, scope, - &format!("{path}.left"), - allow_moves, - allowed_effects, - )?; - if matches!(op, BinaryOp::And | BinaryOp::Or) { - let baseline_ids = scope.keys().cloned().collect::>(); - let mut conditional_scope = scope.clone(); - self.validate_expr( - function, - right, - &mut conditional_scope, - &format!("{path}.right"), - allow_moves, - allowed_effects, - )?; - Self::join_conditional(scope, &conditional_scope, &baseline_ids); - } else { - self.validate_expr( - function, - right, - scope, - &format!("{path}.right"), - allow_moves, - allowed_effects, - )?; + path, + } => { + if index == args.len() { + self.finish_expr(expression, &result, OwnershipMode::Value)?; + scopes.push(scope); + } else { + frames.push(Frame::NativeAfterArg { + expression, + args, + params, + result, + index, + path: path.clone(), + }); + frames.push(Frame::Enter { + expression: &args[index], + scope, + path: format!("{path}.native-rust-arg.{index}"), + }); + } } - let output = match op { - BinaryOp::Add - | BinaryOp::Sub - | BinaryOp::Mul - | BinaryOp::Div - | BinaryOp::Rem => { - self.require_type(&left.ty, &ResolvedType::I64, "binary operand")?; - self.require_type(&right.ty, &ResolvedType::I64, "binary operand")?; - ResolvedType::I64 + Frame::NativeAfterArg { + expression, + args, + params, + result, + index, + path, + } => { + let scope = scopes.pop().expect("native argument scope retained"); + publication.publish(&scope); + let argument = &args[index]; + let parameter = ¶ms[index]; + self.require_type(&argument.ty, ¶meter.ty, "native Rust import argument")?; + if argument.ownership != OwnershipMode::Value + || parameter.ownership != OwnershipMode::Value + { + return Err(hir_error( + "native Rust import arguments must use value ownership", + )); } - BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge => { - self.require_type(&left.ty, &ResolvedType::I64, "comparison operand")?; - self.require_type(&right.ty, &ResolvedType::I64, "comparison operand")?; - ResolvedType::Bool + frames.push(Frame::NativeNext { + expression, + args, + params, + result, + index: index + 1, + scope, + path, + }); + } + Frame::BinaryLeft { + expression, + op, + right, + path, + } => { + let left_scope = scopes.pop().expect("binary left scope retained"); + publication.publish(&left_scope); + let baseline = if matches!(op, BinaryOp::And | BinaryOp::Or) { + Some((left_scope.keys().cloned().collect(), left_scope.clone())) + } else { + None + }; + frames.push(Frame::BinaryRight { + expression, + op, + left: match &expression.kind { + ResolvedExprKind::Binary { left, .. } => left, + _ => unreachable!(), + }, + baseline, + }); + if matches!(op, BinaryOp::And | BinaryOp::Or) { + let enabled = publication.enabled; + publication.enabled = false; + frames.push(Frame::RestorePublication(enabled)); } - BinaryOp::And | BinaryOp::Or => { - self.require_type(&left.ty, &ResolvedType::Bool, "boolean operand")?; - self.require_type(&right.ty, &ResolvedType::Bool, "boolean operand")?; - ResolvedType::Bool + frames.push(Frame::Enter { + expression: right, + scope: left_scope, + path: format!("{path}.right"), + }); + } + Frame::BinaryRight { + expression, + op, + left, + baseline, + } => { + let mut scope = scopes.pop().expect("binary right scope retained"); + let direct = baseline.is_none(); + if let Some((ids, mut parent)) = baseline { + Self::join_conditional(&mut parent, &scope, &ids); + scope = parent; } - BinaryOp::Eq | BinaryOp::Ne => { - self.require_type(&left.ty, &right.ty, "equality operands")?; - ResolvedType::Bool + if direct || matches!(op, BinaryOp::And | BinaryOp::Or) { + publication.publish(&scope); } - }; - (output, OwnershipMode::Value) - } - ResolvedExprKind::Block { statements, tail } => { - let mut block_scope = scope.clone(); - for (index, statement) in statements.iter().enumerate() { - match statement { - ResolvedStatement::Let { binding, value, .. } => { - let statement_path = format!("{path}.s{index}"); - self.validate_expr( - function, - value, - &mut block_scope, - &format!("{statement_path}.value"), - allow_moves, - allowed_effects, - )?; - if binding.id != ValueId::local(function, &statement_path) { - return Err(hir_error(format!( - "local `{}` has a non-canonical identity", - binding.id - ))); - } - self.insert_value(&binding.id)?; - self.require_type(&binding.ty, &value.ty, "local binding")?; - if binding.ownership != value.ownership { - return Err(hir_error(format!( - "local `{}` has inconsistent ownership", - binding.id - ))); - } - self.validate_declared_ownership(&binding.ty, binding.ownership)?; - if self.is_owned_resource(&binding.ty, binding.ownership)? { - if !allow_moves { - return Err(hir_error( - "contract cannot transfer ownership into a local binding", - )); - } - self.mark_value_sources_moved(value, &mut block_scope)?; - } - block_scope.insert( - binding.id.clone(), - ValidationBinding { - ty: binding.ty.clone(), - ownership: binding.ownership, - availability: Availability::Available, - moved_places: BTreeMap::new(), - definitely_partial: BTreeSet::new(), - }, - ); + let ResolvedExprKind::Binary { right, .. } = &expression.kind else { + unreachable!() + }; + let output = match op { + BinaryOp::Add + | BinaryOp::Sub + | BinaryOp::Mul + | BinaryOp::Div + | BinaryOp::Rem => { + self.require_type(&left.ty, &ResolvedType::I64, "binary operand")?; + self.require_type(&right.ty, &ResolvedType::I64, "binary operand")?; + ResolvedType::I64 } - } + BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge => { + self.require_type(&left.ty, &ResolvedType::I64, "comparison operand")?; + self.require_type(&right.ty, &ResolvedType::I64, "comparison operand")?; + ResolvedType::Bool + } + BinaryOp::And | BinaryOp::Or => { + self.require_type(&left.ty, &ResolvedType::Bool, "boolean operand")?; + self.require_type(&right.ty, &ResolvedType::Bool, "boolean operand")?; + ResolvedType::Bool + } + BinaryOp::Eq | BinaryOp::Ne => { + self.require_type(&left.ty, &right.ty, "equality operands")?; + ResolvedType::Bool + } + }; + self.finish_expr(expression, &output, OwnershipMode::Value)?; + scopes.push(scope); } - self.validate_expr( - function, - tail, - &mut block_scope, - &format!("{path}.tail"), - allow_moves, - allowed_effects, - )?; - let outer_ids = scope.keys().cloned().collect::>(); - Self::merge_availability(scope, &block_scope, &outer_ids); - (tail.ty.clone(), tail.ownership) - } - ResolvedExprKind::If { - condition, - then_branch, - else_branch, - } => { - self.validate_expr( - function, - condition, - scope, - &format!("{path}.condition"), - allow_moves, - allowed_effects, - )?; - self.require_type(&condition.ty, &ResolvedType::Bool, "if condition")?; - let outer_ids = scope.keys().cloned().collect::>(); - let mut then_scope = scope.clone(); - let mut else_scope = scope.clone(); - self.validate_expr( - function, + Frame::IfCondition { + expression, then_branch, - &mut then_scope, - &format!("{path}.then"), - allow_moves, - allowed_effects, - )?; - self.validate_expr( - function, else_branch, - &mut else_scope, - &format!("{path}.else"), - allow_moves, - allowed_effects, - )?; - Self::join_branches(scope, &then_scope, &else_scope, &outer_ids); - self.require_type(&then_branch.ty, &else_branch.ty, "if branches")?; - if then_branch.ownership != else_branch.ownership { - return Err(hir_error("if branches have inconsistent ownership")); + path, + } => { + let outer = scopes.pop().expect("if condition scope retained"); + publication.publish(&outer); + let ResolvedExprKind::If { condition, .. } = &expression.kind else { + unreachable!() + }; + self.require_type(&condition.ty, &ResolvedType::Bool, "if condition")?; + let outer_ids = outer.keys().cloned().collect(); + frames.push(Frame::IfThen { + expression, + else_branch, + path: path.clone(), + outer: outer.clone(), + outer_ids, + }); + let enabled = publication.enabled; + publication.enabled = false; + frames.push(Frame::RestorePublication(enabled)); + frames.push(Frame::Enter { + expression: then_branch, + scope: outer, + path: format!("{path}.then"), + }); } - (then_branch.ty.clone(), then_branch.ownership) - } - ResolvedExprKind::ConstructRecord { record, fields } => { - let declaration = self - .program - .declarations - .declaration(record) - .ok_or_else(|| hir_error(format!("record `{record}` is not indexed")))?; - if declaration.kind != DeclarationKind::Record { - return Err(hir_error(format!( - "constructor target `{record}` is not a record" - ))); + Frame::IfThen { + expression, + else_branch, + path, + outer, + outer_ids, + } => { + let then_scope = scopes.pop().expect("if then scope retained"); + frames.push(Frame::IfElse { + expression, + then_scope, + outer: outer.clone(), + outer_ids, + }); + let enabled = publication.enabled; + publication.enabled = false; + frames.push(Frame::RestorePublication(enabled)); + frames.push(Frame::Enter { + expression: else_branch, + scope: outer, + path: format!("{path}.else"), + }); } - let expected_fields = self - .program - .declarations - .record_fields(record) - .ok_or_else(|| hir_error(format!("record `{record}` has no fields")))? - .to_vec(); - let ResolvedType::Nominal { - declaration: instance_record, - arguments, - } = &expression.ty - else { - return Err(hir_error("record constructor result is not nominal")); - }; - let parameters = self - .program - .declarations - .type_parameters(record) - .ok_or_else(|| hir_error(format!("record `{record}` has no parameters")))?; - if instance_record != record - || arguments.len() != parameters.len() - || arguments - .iter() - .any(|argument| !matches!(argument, ResolvedType::I64 | ResolvedType::Bool)) - { - return Err(hir_error(format!( - "constructor for `{record}` has an invalid concrete instance" - ))); + Frame::IfElse { + expression, + then_scope, + mut outer, + outer_ids, + } => { + let else_scope = scopes.pop().expect("if else scope retained"); + Self::join_branches(&mut outer, &then_scope, &else_scope, &outer_ids); + publication.publish(&outer); + let ResolvedExprKind::If { + then_branch, + else_branch, + .. + } = &expression.kind + else { + unreachable!() + }; + self.require_type(&then_branch.ty, &else_branch.ty, "if branches")?; + if then_branch.ownership != else_branch.ownership { + return Err(hir_error("if branches have inconsistent ownership")); + } + self.finish_expr(expression, &then_branch.ty, then_branch.ownership)?; + scopes.push(outer); } - let mut seen = BTreeSet::new(); - for (index, initializer) in fields.iter().enumerate() { - let field = expected_fields - .iter() - .find(|field| field.id == initializer.field) - .ok_or_else(|| { - hir_error(format!( - "constructor for `{record}` contains foreign field `{}`", - initializer.field - )) - })?; - if !seen.insert(initializer.field.clone()) { + Frame::BlockNext { + expression, + statements, + tail, + index, + scope, + outer, + outer_ids, + path, + } => { + if index == statements.len() { + frames.push(Frame::BlockTail { + expression, + outer_ids, + outer, + }); + let enabled = publication.enabled; + publication.enabled = false; + frames.push(Frame::RestorePublication(enabled)); + frames.push(Frame::Enter { + expression: tail, + scope, + path: format!("{path}.tail"), + }); + } else { + let ResolvedStatement::Let { value, .. } = &statements[index]; + frames.push(Frame::BlockAfterLet { + expression, + statements, + tail, + index, + outer, + outer_ids, + path: path.clone(), + }); + let enabled = publication.enabled; + publication.enabled = false; + frames.push(Frame::RestorePublication(enabled)); + frames.push(Frame::Enter { + expression: value, + scope, + path: format!("{path}.s{index}.value"), + }); + } + } + Frame::BlockAfterLet { + expression, + statements, + tail, + index, + outer, + outer_ids, + path, + } => { + let mut scope = scopes.pop().expect("block let scope retained"); + let ResolvedStatement::Let { binding, value, .. } = &statements[index]; + let statement_path = format!("{path}.s{index}"); + if binding.id != ValueId::local(function, &statement_path) { return Err(hir_error(format!( - "constructor for `{record}` repeats field `{}`", - initializer.field + "local `{}` has a non-canonical identity", + binding.id ))); } - self.validate_expr( - function, - &initializer.value, + self.insert_value(&binding.id)?; + self.require_type(&binding.ty, &value.ty, "local binding")?; + if binding.ownership != value.ownership { + return Err(hir_error(format!( + "local `{}` has inconsistent ownership", + binding.id + ))); + } + self.validate_declared_ownership(&binding.ty, binding.ownership)?; + if self.is_owned_resource(&binding.ty, binding.ownership)? { + if !allow_moves { + return Err(hir_error( + "contract cannot transfer ownership into a local binding", + )); + } + self.mark_value_sources_moved(value, &mut scope)?; + } + scope.insert( + binding.id.clone(), + ValidationBinding { + ty: binding.ty.clone(), + ownership: binding.ownership, + availability: Availability::Available, + moved_places: BTreeMap::new(), + definitely_partial: BTreeSet::new(), + }, + ); + frames.push(Frame::BlockNext { + expression, + statements, + tail, + index: index + 1, scope, - &format!("{path}.field.{index}.value"), - allow_moves, - allowed_effects, - )?; - let field_ty = substitute_type(&field.ty, record, arguments)?; + outer, + outer_ids, + path, + }); + } + Frame::BlockTail { + expression, + outer_ids, + mut outer, + } => { + let block_scope = scopes.pop().expect("block tail scope retained"); + Self::merge_availability(&mut outer, &block_scope, &outer_ids); + publication.publish(&outer); + let ResolvedExprKind::Block { tail, .. } = &expression.kind else { + unreachable!() + }; + self.finish_expr(expression, &tail.ty, tail.ownership)?; + scopes.push(outer); + } + Frame::RecordNext { + expression, + fields, + expected, + record, + arguments, + seen, + index, + scope, + path, + } => { + if index == fields.len() { + if seen.len() != expected.len() { + return Err(hir_error(format!( + "constructor for `{record}` is missing required fields" + ))); + } + let ownership = + self.expected_ownership(&expression.ty, OwnershipMode::Own)?; + self.finish_expr(expression, &expression.ty, ownership)?; + scopes.push(scope); + } else { + let initializer = &fields[index]; + let mut seen = seen; + if !expected.iter().any(|field| field.id == initializer.field) { + return Err(hir_error(format!( + "constructor for `{record}` contains foreign field `{}`", + initializer.field + ))); + } + if !seen.insert(initializer.field.clone()) { + return Err(hir_error(format!( + "constructor for `{record}` repeats field `{}`", + initializer.field + ))); + } + frames.push(Frame::RecordAfterField { + expression, + fields, + expected, + record, + arguments, + seen, + index, + path: path.clone(), + }); + frames.push(Frame::Enter { + expression: &initializer.value, + scope, + path: format!("{path}.field.{index}.value"), + }); + } + } + Frame::RecordAfterField { + expression, + fields, + expected, + record, + arguments, + seen, + index, + path, + } => { + let mut scope = scopes.pop().expect("record field scope retained"); + publication.publish(&scope); + let initializer = &fields[index]; + let declared = expected + .iter() + .find(|field| field.id == initializer.field) + .expect("field authenticated before child"); + let field_ty = substitute_type(&declared.ty, &record, &arguments)?; self.require_type(&initializer.value.ty, &field_ty, "record field")?; - let expected = self.expected_ownership(&field_ty, OwnershipMode::Own)?; - if initializer.value.ownership != expected { + let ownership = self.expected_ownership(&field_ty, OwnershipMode::Own)?; + if initializer.value.ownership != ownership { return Err(hir_error(format!( "field `{}` has incompatible ownership", initializer.field ))); } - if expected == OwnershipMode::Own { + if ownership == OwnershipMode::Own { if !allow_moves { return Err(hir_error( "contract cannot transfer ownership into a record", )); } - self.mark_value_sources_moved(&initializer.value, scope)?; + self.mark_value_sources_moved(&initializer.value, &mut scope)?; + publication.publish(&scope); } + frames.push(Frame::RecordNext { + expression, + fields, + expected, + record, + arguments, + seen, + index: index + 1, + scope, + path, + }); } - if seen.len() != expected_fields.len() { - return Err(hir_error(format!( - "constructor for `{record}` is missing required fields" - ))); - } - let ty = expression.ty.clone(); - let ownership = self.expected_ownership(&ty, OwnershipMode::Own)?; - (ty, ownership) - } - ResolvedExprKind::ConstructVariant { - variant, - case, - fields, - } => { - let ResolvedType::Nominal { - declaration: instance_variant, + Frame::VariantNext { + expression, + fields, + expected, + variant, + case, arguments, - } = &expression.ty - else { - return Err(hir_error("variant constructor has a non-nominal result")); - }; - if instance_variant != variant { - return Err(hir_error( - "variant constructor result disagrees with its declaration", - )); - } - let declaration = self - .program - .declarations - .declaration(variant) - .ok_or_else(|| hir_error(format!("variant `{variant}` is not indexed")))?; - if declaration.kind != DeclarationKind::Variant { - return Err(hir_error(format!( - "constructor target `{variant}` is not a variant" - ))); - } - let declared_case = self - .program - .declarations - .variant_cases(variant) - .and_then(|cases| cases.iter().find(|item| item.id == *case)) - .ok_or_else(|| { - hir_error(format!( - "constructor for `{variant}` contains foreign case `{case}`" - )) - })?; - let expected_fields = declared_case.fields.clone(); - let mut seen = BTreeSet::new(); - for (index, initializer) in fields.iter().enumerate() { - let field = expected_fields - .iter() - .find(|field| field.id == initializer.field) - .ok_or_else(|| { - hir_error(format!( + seen, + index, + scope, + path, + } => { + if index == fields.len() { + if seen.len() != expected.len() { + return Err(hir_error(format!( + "constructor for `{case}` is missing required payload fields" + ))); + } + self.finish_expr(expression, &expression.ty, OwnershipMode::Value)?; + scopes.push(scope); + } else { + let initializer = &fields[index]; + let mut seen = seen; + if !expected.iter().any(|field| field.id == initializer.field) { + return Err(hir_error(format!( "constructor for `{case}` contains foreign field `{}`", initializer.field - )) - })?; - if !seen.insert(initializer.field.clone()) { - return Err(hir_error(format!( - "constructor for `{case}` repeats field `{}`", - initializer.field - ))); + ))); + } + if !seen.insert(initializer.field.clone()) { + return Err(hir_error(format!( + "constructor for `{case}` repeats field `{}`", + initializer.field + ))); + } + frames.push(Frame::VariantAfterField { + expression, + fields, + expected, + variant, + case, + arguments, + seen, + index, + path: path.clone(), + }); + frames.push(Frame::Enter { + expression: &initializer.value, + scope, + path: format!("{path}.field.{index}.value"), + }); } - self.validate_expr( - function, - &initializer.value, - scope, - &format!("{path}.field.{index}.value"), - allow_moves, - allowed_effects, - )?; - let field_ty = substitute_type(&field.ty, variant, arguments)?; + } + Frame::VariantAfterField { + expression, + fields, + expected, + variant, + case, + arguments, + seen, + index, + path, + } => { + let scope = scopes.pop().expect("variant field scope retained"); + publication.publish(&scope); + let initializer = &fields[index]; + let declared = expected + .iter() + .find(|field| field.id == initializer.field) + .expect("variant field authenticated before child"); + let field_ty = substitute_type(&declared.ty, &variant, &arguments)?; self.require_type(&initializer.value.ty, &field_ty, "variant payload field")?; if initializer.value.ownership != OwnershipMode::Value { return Err(hir_error(format!( @@ -3650,219 +5904,487 @@ impl<'a> HirValidator<'a> { initializer.field ))); } + frames.push(Frame::VariantNext { + expression, + fields, + expected, + variant, + case, + arguments, + seen, + index: index + 1, + scope, + path, + }); } - if seen.len() != expected_fields.len() { - return Err(hir_error(format!( - "constructor for `{case}` is missing required payload fields" - ))); - } - (expression.ty.clone(), OwnershipMode::Value) - } - ResolvedExprKind::Match { scrutinee, arms } => { - self.validate_expr( - function, - scrutinee, - scope, - &format!("{path}.scrutinee"), - allow_moves, - allowed_effects, - )?; - let ResolvedType::Nominal { - declaration: matched_type, - arguments, - } = &scrutinee.ty - else { - return Err(hir_error("resolved match scrutinee is not nominal")); - }; - let matched_kind = self - .program - .declarations - .declaration(matched_type) - .map(|item| item.kind); - if matched_kind == Some(DeclarationKind::Record) { - if scrutinee.ownership != OwnershipMode::Value { - return Err(hir_error("resolved record match scrutinee is not Copy")); + Frame::UpdateBase { + expression, + record, + fields, + path, + } => { + let mut scope = scopes.pop().expect("update base scope retained"); + publication.publish(&scope); + let ResolvedExprKind::UpdateRecord { base, .. } = &expression.kind else { + unreachable!() + }; + let declaration = self + .program + .declarations + .declaration(record) + .ok_or_else(|| hir_error(format!("record `{record}` is not indexed")))?; + if declaration.kind != DeclarationKind::Record { + return Err(hir_error(format!( + "record update target `{record}` is not a record" + ))); } - let [arm] = arms.as_slice() else { - return Err(hir_error( - "resolved irrefutable record match must have exactly one arm", - )); + let ResolvedType::Nominal { + declaration: instance, + arguments, + } = &base.ty + else { + return Err(hir_error("record update base is not nominal")); }; - let outer_ids = scope.keys().cloned().collect::>(); - let mut arm_scope = scope.clone(); - match &arm.pattern { - ResolvedMatchPattern::Wildcard => {} - ResolvedMatchPattern::Record { - record, - instance, - fields, - } => self.validate_record_match_pattern( - function, - &scrutinee.ty, - record, - instance, - fields, - &mut arm_scope, - &format!("{path}.arm.0.record"), - )?, - ResolvedMatchPattern::Variant { .. } => { - return Err(hir_error( - "resolved variant pattern has a record scrutinee", - )); - } + let parameters = self + .program + .declarations + .type_parameters(record) + .ok_or_else(|| hir_error(format!("record `{record}` has no parameters")))?; + if instance != record + || arguments.len() != parameters.len() + || arguments.iter().any(|argument| { + !matches!(argument, ResolvedType::I64 | ResolvedType::Bool) + }) + { + return Err(hir_error(format!( + "record update for `{record}` has an invalid concrete instance" + ))); } - self.validate_expr( - function, - &arm.value, - &mut arm_scope, - &format!("{path}.arm.0.value"), - allow_moves, - allowed_effects, - )?; - if !matches!(arm.value.ty, ResolvedType::I64 | ResolvedType::Bool) { - return Err(hir_error( - "resolved record match arm must produce i64 or bool", - )); + let ownership = self.expected_ownership(&base.ty, OwnershipMode::Own)?; + if base.ownership != ownership { + return Err(hir_error(format!( + "record update base for `{record}` has incompatible ownership" + ))); } - for id in outer_ids { - if let Some(state) = arm_scope.get(&id) { - scope.insert(id, state.clone()); + if ownership == OwnershipMode::Own { + if !allow_moves { + return Err(hir_error( + "contract cannot transfer ownership from a record update base", + )); } + self.mark_value_sources_moved(base, &mut scope)?; + publication.publish(&scope); } - self.require_type(&expression.ty, &arm.value.ty, "record match expression")?; - if expression.ownership != arm.value.ownership { - return Err(hir_error( - "resolved record match expression has inconsistent ownership", - )); - } - return Ok(()); - } - let variant = matched_type; - if scrutinee.ownership != OwnershipMode::Value - || self + let expected = self .program .declarations - .declaration(variant) - .is_none_or(|item| item.kind != DeclarationKind::Variant) - { - return Err(hir_error( - "resolved match scrutinee is not a concrete Copy variant", - )); - } - let cases = self - .program - .declarations - .variant_cases(variant) - .ok_or_else(|| hir_error(format!("variant `{variant}` has no cases")))? - .to_vec(); - if arms.is_empty() { - return Err(hir_error("resolved match has no arms")); + .record_fields(record) + .ok_or_else(|| hir_error(format!("record `{record}` has no fields")))? + .to_vec(); + frames.push(Frame::UpdateNext { + expression, + fields, + expected, + record: record.clone(), + arguments: arguments.clone(), + seen: BTreeSet::new(), + index: 0, + scope, + path, + ownership, + }); } - let outer_ids = scope.keys().cloned().collect::>(); - let mut arm_scopes = Vec::with_capacity(arms.len()); - let mut covered = BTreeSet::new(); - let mut wildcard_seen = false; - let mut result = None::<(ResolvedType, OwnershipMode)>; - for (arm_index, arm) in arms.iter().enumerate() { - let mut arm_scope = scope.clone(); - match &arm.pattern { - ResolvedMatchPattern::Wildcard => { - if wildcard_seen || covered.len() == cases.len() { - return Err(hir_error( - "resolved match has an unreachable wildcard", - )); - } - wildcard_seen = true; + Frame::UpdateNext { + expression, + fields, + expected, + record, + arguments, + seen, + index, + scope, + path, + ownership, + } => { + if index == fields.len() { + let ResolvedExprKind::UpdateRecord { base, .. } = &expression.kind else { + unreachable!() + }; + self.finish_expr(expression, &base.ty, ownership)?; + scopes.push(scope); + } else { + let initializer = &fields[index]; + let mut seen = seen; + if !expected.iter().any(|field| field.id == initializer.field) { + return Err(hir_error(format!( + "update for `{record}` contains foreign field `{}`", + initializer.field + ))); } - ResolvedMatchPattern::Variant { - variant: pattern_variant, - case, + if !seen.insert(initializer.field.clone()) { + return Err(hir_error(format!( + "update for `{record}` repeats field `{}`", + initializer.field + ))); + } + frames.push(Frame::UpdateAfterField { + expression, fields, - } => { - if wildcard_seen - || pattern_variant != variant - || !covered.insert(case.clone()) - { + expected, + record, + arguments, + seen, + index, + path: path.clone(), + ownership, + }); + frames.push(Frame::Enter { + expression: &initializer.value, + scope, + path: format!("{path}.field.{index}.value"), + }); + } + } + Frame::UpdateAfterField { + expression, + fields, + expected, + record, + arguments, + seen, + index, + path, + ownership, + } => { + let mut scope = scopes.pop().expect("update field scope retained"); + publication.publish(&scope); + let initializer = &fields[index]; + let declared = expected + .iter() + .find(|field| field.id == initializer.field) + .expect("update field authenticated before child"); + let field_ty = substitute_type(&declared.ty, &record, &arguments)?; + self.require_type(&initializer.value.ty, &field_ty, "record replacement")?; + let expected_ownership = + self.expected_ownership(&field_ty, OwnershipMode::Own)?; + if initializer.value.ownership != expected_ownership { + return Err(hir_error(format!( + "replacement field `{}` has incompatible ownership", + initializer.field + ))); + } + if expected_ownership == OwnershipMode::Own { + if !allow_moves { + return Err(hir_error( + "contract cannot transfer ownership into a record replacement", + )); + } + self.mark_value_sources_moved(&initializer.value, &mut scope)?; + publication.publish(&scope); + } + frames.push(Frame::UpdateNext { + expression, + fields, + expected, + record, + arguments, + seen, + index: index + 1, + scope, + path, + ownership, + }); + } + Frame::MatchScrutinee { + expression, + arms, + path, + } => { + let outer = scopes.pop().expect("match scrutinee scope retained"); + publication.publish(&outer); + let ResolvedExprKind::Match { scrutinee, .. } = &expression.kind else { + unreachable!() + }; + let ResolvedType::Nominal { + declaration: matched, + arguments, + } = &scrutinee.ty + else { + return Err(hir_error("resolved match scrutinee is not nominal")); + }; + let kind = self + .program + .declarations + .declaration(matched) + .map(|item| item.kind); + let outer_ids = outer.keys().cloned().collect::>(); + if kind == Some(DeclarationKind::Record) { + if scrutinee.ownership != OwnershipMode::Value { + return Err(hir_error("resolved record match scrutinee is not Copy")); + } + let [arm] = arms else { + return Err(hir_error( + "resolved irrefutable record match must have exactly one arm", + )); + }; + let mut arm_scope = outer.clone(); + match &arm.pattern { + ResolvedMatchPattern::Wildcard => {} + ResolvedMatchPattern::Record { + record, + instance, + fields, + } => self.validate_record_match_pattern( + function, + &scrutinee.ty, + record, + instance, + fields, + &mut arm_scope, + &format!("{path}.arm.0.record"), + )?, + ResolvedMatchPattern::Variant { .. } => { return Err(hir_error( - "resolved match has an unreachable or foreign case pattern", - )); + "resolved variant pattern has a record scrutinee", + )) } - let declared_case = - cases.iter().find(|item| item.id == *case).ok_or_else(|| { - hir_error(format!( - "resolved match references foreign case `{case}`" - )) - })?; - let mut seen_fields = BTreeSet::new(); - for (field_index, pattern_field) in fields.iter().enumerate() { - let declared_field = declared_case - .fields + } + frames.push(Frame::RecordMatchArm { + expression, + arm, + outer, + outer_ids, + }); + let enabled = publication.enabled; + publication.enabled = false; + frames.push(Frame::RestorePublication(enabled)); + frames.push(Frame::Enter { + expression: &arm.value, + scope: arm_scope, + path: format!("{path}.arm.0.value"), + }); + } else { + if scrutinee.ownership != OwnershipMode::Value + || kind != Some(DeclarationKind::Variant) + { + return Err(hir_error( + "resolved match scrutinee is not a concrete Copy variant", + )); + } + let cases = self + .program + .declarations + .variant_cases(matched) + .ok_or_else(|| hir_error(format!("variant `{matched}` has no cases")))? + .to_vec(); + if arms.is_empty() { + return Err(hir_error("resolved match has no arms")); + } + frames.push(Frame::VariantMatchNext { + expression, + arms, + cases, + variant: matched.clone(), + arguments: arguments.clone(), + index: 0, + outer, + outer_ids, + arm_scopes: Vec::with_capacity(arms.len()), + covered: BTreeSet::new(), + wildcard_seen: false, + result: None, + path, + }); + } + } + Frame::RecordMatchArm { + expression, + arm, + mut outer, + outer_ids, + } => { + let arm_scope = scopes.pop().expect("record match arm scope retained"); + if !matches!(arm.value.ty, ResolvedType::I64 | ResolvedType::Bool) { + return Err(hir_error( + "resolved record match arm must produce i64 or bool", + )); + } + for id in outer_ids { + if let Some(state) = arm_scope.get(&id) { + outer.insert(id, state.clone()); + } + } + publication.publish(&outer); + self.finish_expr(expression, &arm.value.ty, arm.value.ownership)?; + scopes.push(outer); + } + Frame::VariantMatchNext { + expression, + arms, + cases, + variant, + arguments, + index, + outer, + outer_ids, + arm_scopes, + mut covered, + mut wildcard_seen, + result, + path, + } => { + if index == arms.len() { + if !wildcard_seen && covered.len() != cases.len() { + return Err(hir_error("resolved match is not exhaustive")); + } + let (ty, ownership) = + result.ok_or_else(|| hir_error("resolved match has no result"))?; + let mut final_scope = outer; + if let Some((first, rest)) = arm_scopes.split_first() { + let mut joined = first.clone(); + for arm_scope in rest { + Self::join_conditional(&mut joined, arm_scope, &outer_ids); + } + Self::merge_availability(&mut final_scope, &joined, &outer_ids); + } + publication.publish(&final_scope); + self.finish_expr(expression, &ty, ownership)?; + scopes.push(final_scope); + } else { + let arm = &arms[index]; + let mut arm_scope = outer.clone(); + match &arm.pattern { + ResolvedMatchPattern::Wildcard => { + if wildcard_seen || covered.len() == cases.len() { + return Err(hir_error( + "resolved match has an unreachable wildcard", + )); + } + wildcard_seen = true; + } + ResolvedMatchPattern::Variant { + variant: pattern_variant, + case, + fields, + } => { + if wildcard_seen + || pattern_variant != &variant + || !covered.insert(case.clone()) + { + return Err(hir_error( + "resolved match has an unreachable or foreign case pattern", + )); + } + let declared_case = cases .iter() - .find(|item| item.id == pattern_field.field) + .find(|item| item.id == *case) .ok_or_else(|| { hir_error(format!( - "resolved pattern contains foreign field `{}`", - pattern_field.field + "resolved match references foreign case `{case}`" )) })?; - let binding_ty = - substitute_type(&declared_field.ty, variant, arguments)?; - if !seen_fields.insert(pattern_field.field.clone()) - || pattern_field.binding.id - != ValueId::local( - function, - &format!( - "{path}.arm.{arm_index}.binding.{field_index}" - ), - ) - || pattern_field.binding.ty != binding_ty - || pattern_field.binding.ownership != OwnershipMode::Value - { - return Err(hir_error( - "resolved match pattern field or binding is invalid", - )); + let mut seen = BTreeSet::new(); + for (field_index, field) in fields.iter().enumerate() { + let declared = declared_case + .fields + .iter() + .find(|item| item.id == field.field) + .ok_or_else(|| { + hir_error(format!( + "resolved pattern contains foreign field `{}`", + field.field + )) + })?; + let binding_ty = + substitute_type(&declared.ty, &variant, &arguments)?; + if !seen.insert(field.field.clone()) + || field.binding.id + != ValueId::local( + function, + &format!( + "{path}.arm.{index}.binding.{field_index}" + ), + ) + || field.binding.ty != binding_ty + || field.binding.ownership != OwnershipMode::Value + { + return Err(hir_error( + "resolved match pattern field or binding is invalid", + )); + } + self.insert_value(&field.binding.id)?; + self.validate_type(&field.binding.ty)?; + if arm_scope.contains_key(&field.binding.id) { + return Err(hir_error("resolved match pattern binding shadows an existing value")); + } + arm_scope.insert( + field.binding.id.clone(), + ValidationBinding { + ty: field.binding.ty.clone(), + ownership: OwnershipMode::Value, + availability: Availability::Available, + moved_places: BTreeMap::new(), + definitely_partial: BTreeSet::new(), + }, + ); } - self.insert_value(&pattern_field.binding.id)?; - self.validate_type(&pattern_field.binding.ty)?; - if arm_scope.contains_key(&pattern_field.binding.id) { + if seen.len() != declared_case.fields.len() { return Err(hir_error( - "resolved match pattern binding shadows an existing value", + "resolved match pattern is missing payload fields", )); } - arm_scope.insert( - pattern_field.binding.id.clone(), - ValidationBinding { - ty: pattern_field.binding.ty.clone(), - ownership: OwnershipMode::Value, - availability: Availability::Available, - moved_places: BTreeMap::new(), - definitely_partial: BTreeSet::new(), - }, - ); } - if seen_fields.len() != declared_case.fields.len() { + ResolvedMatchPattern::Record { .. } => { return Err(hir_error( - "resolved match pattern is missing payload fields", - )); + "resolved record pattern has a variant scrutinee", + )) } } - ResolvedMatchPattern::Record { .. } => { - return Err(hir_error( - "resolved record pattern has a variant scrutinee", - )); - } + frames.push(Frame::VariantMatchAfterArm { + expression, + arms, + cases, + variant, + arguments, + index, + outer, + outer_ids, + arm_scopes, + covered, + wildcard_seen, + result, + path: path.clone(), + }); + let enabled = publication.enabled; + publication.enabled = false; + frames.push(Frame::RestorePublication(enabled)); + frames.push(Frame::Enter { + expression: &arm.value, + scope: arm_scope, + path: format!("{path}.arm.{index}.value"), + }); } - self.validate_expr( - function, - &arm.value, - &mut arm_scope, - &format!("{path}.arm.{arm_index}.value"), - allow_moves, - allowed_effects, - )?; - if let Some((expected_ty, expected_ownership)) = &result { - self.require_type(&arm.value.ty, expected_ty, "match arm")?; - if arm.value.ownership != *expected_ownership { + } + Frame::VariantMatchAfterArm { + expression, + arms, + cases, + variant, + arguments, + index, + outer, + outer_ids, + mut arm_scopes, + covered, + wildcard_seen, + mut result, + path, + } => { + let arm_scope = scopes.pop().expect("variant match arm scope retained"); + let arm = &arms[index]; + if let Some((ty, ownership)) = &result { + self.require_type(&arm.value.ty, ty, "match arm")?; + if arm.value.ownership != *ownership { return Err(hir_error( "resolved match arms have inconsistent ownership", )); @@ -3871,2448 +6393,5648 @@ impl<'a> HirValidator<'a> { result = Some((arm.value.ty.clone(), arm.value.ownership)); } arm_scopes.push(arm_scope); + frames.push(Frame::VariantMatchNext { + expression, + arms, + cases, + variant, + arguments, + index: index + 1, + outer, + outer_ids, + arm_scopes, + covered, + wildcard_seen, + result, + path, + }); } - if !wildcard_seen && covered.len() != cases.len() { - return Err(hir_error("resolved match is not exhaustive")); + Frame::Project { expression, field } => { + let scope = scopes.pop().expect("projection scope retained"); + let ResolvedExprKind::Project { base, .. } = &expression.kind else { + unreachable!() + }; + let projected = self.field_type_for_type(&base.ty, field)?; + let ownership = self.expected_ownership(&projected, base.ownership)?; + self.finish_expr(expression, &projected, ownership)?; + scopes.push(scope); } - if let Some((first, rest)) = arm_scopes.split_first() { - let mut joined = first.clone(); - for arm_scope in rest { - Self::join_conditional(&mut joined, arm_scope, &outer_ids); - } - Self::merge_availability(scope, &joined, &outer_ids); + Frame::Try { + expression, + path, + option, + } => { + let scope = scopes.pop().expect("try scope retained"); + self.finish_try_expr(function, expression, &scope, &path, option)?; + scopes.push(scope); } - result.ok_or_else(|| hir_error("resolved match has no result"))? } - ResolvedExprKind::Try { + } + if scopes.len() != 1 { + return Err(hir_error("iterative HIR validator lost its scope stack")); + } + publication.publish(&scopes.pop().expect("root validation scope retained")); + Ok(()) + } + + fn finish_expr( + &self, + expression: &ResolvedExpr, + ty: &ResolvedType, + ownership: OwnershipMode, + ) -> Result<(), Diagnostic> { + self.require_type(&expression.ty, ty, "expression")?; + if expression.ownership != ownership { + return Err(hir_error(format!( + "expression `{}` has inconsistent ownership", + expression.id + ))); + } + Ok(()) + } + + fn finish_try_expr( + &self, + function: &FunctionExecutionId, + expression: &ResolvedExpr, + scope: &BTreeMap, + path: &str, + option_profile: bool, + ) -> Result<(), Diagnostic> { + if option_profile { + let ResolvedExprKind::TryOption { operand, - result, - ok_case, - ok_field, - err_case, - err_field, + option, + some_case, + some_field, + none_case, residual_type, - } => { - self.validate_expr( - function, - operand, - scope, - &format!("{path}.operand"), - allow_moves, - allowed_effects, - )?; - if !path.starts_with("body") { - return Err(hir_error( - "resolved `?` is outside the executable function body", - )); - } - if scope.values().any(|binding| { - self.program - .declarations - .type_facts(&binding.ty) - .is_some_and(|facts| facts.contains_resource) - }) { - return Err(hir_error( - "resolved `?` has a live resource binding in the bounded Copy-only profile", - )); - } - if result.as_str() != crate::prelude::RESULT_ID - || ok_case.as_str() != crate::prelude::RESULT_OK_ID - || ok_field.as_str() != crate::prelude::RESULT_OK_VALUE_ID - || err_case.as_str() != crate::prelude::RESULT_ERR_ID - || err_field.as_str() != crate::prelude::RESULT_ERR_ERROR_ID - { - return Err(hir_error( - "resolved `?` does not authenticate the compiler-owned Result shape", - )); + } = &expression.kind + else { + unreachable!() + }; + if !path.starts_with("body") { + return Err(hir_error( + "resolved Option `?` is outside the executable function body", + )); + } + if scope.values().any(|binding| { + self.program + .declarations + .type_facts(&binding.ty) + .is_some_and(|facts| facts.contains_resource) + }) { + return Err(hir_error( + "resolved Option `?` has a live resource binding in the bounded Copy-only profile", + )); + } + if option.as_str() != crate::prelude::OPTION_ID + || some_case.as_str() != crate::prelude::OPTION_SOME_ID + || some_field.as_str() != crate::prelude::OPTION_SOME_VALUE_ID + || none_case.as_str() != crate::prelude::OPTION_NONE_ID + { + return Err(hir_error( + "resolved Option `?` does not authenticate the compiler-owned Option shape", + )); + } + for id in [option, some_case, some_field, none_case] { + if self + .program + .declarations + .declaration(id) + .is_none_or(|declaration| { + declaration.identity_origin != IdentityOrigin::CompilerOwned + }) + { + return Err(hir_error(format!( + "resolved Option `?` identity `{id}` is not compiler-owned" + ))); } - let ResolvedType::Nominal { - declaration: operand_result, + } + let ( + ResolvedType::Nominal { + declaration: operand_option, arguments: operand_arguments, - } = &operand.ty - else { - return Err(hir_error("resolved `?` operand is not nominal Result")); - }; - let ResolvedType::Nominal { - declaration: residual_result, + }, + ResolvedType::Nominal { + declaration: residual_option, arguments: residual_arguments, - } = residual_type - else { - return Err(hir_error("resolved `?` residual is not nominal Result")); - }; - if operand_result != result - || residual_result != result - || operand_arguments.len() != 2 - || residual_arguments.len() != 2 - || operand_arguments - .iter() - .chain(residual_arguments) - .any(|argument| !matches!(argument, ResolvedType::I64 | ResolvedType::Bool)) - { - return Err(hir_error( - "resolved `?` has invalid concrete Result instances", - )); - } - let enclosing_return = self - .execution_function(function) - .map(|candidate| &candidate.return_type) - .ok_or_else(|| hir_error("resolved `?` has no enclosing function"))?; - self.require_type(residual_type, enclosing_return, "`?` residual")?; - self.require_type(&expression.ty, &operand_arguments[0], "`?` success value")?; - self.require_type( - &operand_arguments[1], - &residual_arguments[1], - "`?` residual error", - )?; - if expression.ownership != OwnershipMode::Value { - return Err(hir_error("resolved `?` success value is not Copy")); - } - (expression.ty.clone(), OwnershipMode::Value) + }, + ) = (&operand.ty, residual_type) + else { + return Err(hir_error( + "resolved Option `?` operand or residual is not nominal Option", + )); + }; + if operand_option != option + || residual_option != option + || operand_arguments.len() != 1 + || residual_arguments.len() != 1 + || operand_arguments + .iter() + .chain(residual_arguments) + .any(|argument| !matches!(argument, ResolvedType::I64 | ResolvedType::Bool)) + { + return Err(hir_error( + "resolved Option `?` has invalid concrete Option instances", + )); } - ResolvedExprKind::TryOption { + let enclosing = self + .execution_function(function) + .map(|candidate| &candidate.return_type) + .ok_or_else(|| hir_error("resolved Option `?` has no enclosing function"))?; + self.require_type(residual_type, enclosing, "Option `?` residual")?; + self.require_type( + &expression.ty, + &operand_arguments[0], + "Option `?` success value", + )?; + if expression.ownership != OwnershipMode::Value { + return Err(hir_error("resolved Option `?` success value is not Copy")); + } + Ok(()) + } else { + let ResolvedExprKind::Try { operand, - option, - some_case, - some_field, - none_case, + result, + ok_case, + ok_field, + err_case, + err_field, residual_type, - } => { - self.validate_expr( - function, - operand, - scope, - &format!("{path}.operand"), - allow_moves, - allowed_effects, - )?; - if !path.starts_with("body") { - return Err(hir_error( - "resolved Option `?` is outside the executable function body", - )); - } - if scope.values().any(|binding| { - self.program - .declarations - .type_facts(&binding.ty) - .is_some_and(|facts| facts.contains_resource) - }) { - return Err(hir_error( - "resolved Option `?` has a live resource binding in the bounded Copy-only profile", - )); - } - if option.as_str() != crate::prelude::OPTION_ID - || some_case.as_str() != crate::prelude::OPTION_SOME_ID - || some_field.as_str() != crate::prelude::OPTION_SOME_VALUE_ID - || none_case.as_str() != crate::prelude::OPTION_NONE_ID - { - return Err(hir_error( - "resolved Option `?` does not authenticate the compiler-owned Option shape", - )); - } - for id in [option, some_case, some_field, none_case] { - if self - .program - .declarations - .declaration(id) - .is_none_or(|declaration| { - declaration.identity_origin != IdentityOrigin::CompilerOwned - }) - { - return Err(hir_error(format!( - "resolved Option `?` identity `{id}` is not compiler-owned" - ))); - } - } - let ResolvedType::Nominal { - declaration: operand_option, + } = &expression.kind + else { + unreachable!() + }; + if !path.starts_with("body") { + return Err(hir_error( + "resolved `?` is outside the executable function body", + )); + } + if scope.values().any(|binding| { + self.program + .declarations + .type_facts(&binding.ty) + .is_some_and(|facts| facts.contains_resource) + }) { + return Err(hir_error( + "resolved `?` has a live resource binding in the bounded Copy-only profile", + )); + } + if result.as_str() != crate::prelude::RESULT_ID + || ok_case.as_str() != crate::prelude::RESULT_OK_ID + || ok_field.as_str() != crate::prelude::RESULT_OK_VALUE_ID + || err_case.as_str() != crate::prelude::RESULT_ERR_ID + || err_field.as_str() != crate::prelude::RESULT_ERR_ERROR_ID + { + return Err(hir_error( + "resolved `?` does not authenticate the compiler-owned Result shape", + )); + } + let ( + ResolvedType::Nominal { + declaration: operand_result, arguments: operand_arguments, - } = &operand.ty - else { - return Err(hir_error( - "resolved Option `?` operand is not nominal Option", - )); - }; - let ResolvedType::Nominal { - declaration: residual_option, + }, + ResolvedType::Nominal { + declaration: residual_result, arguments: residual_arguments, - } = residual_type - else { - return Err(hir_error( - "resolved Option `?` residual is not nominal Option", - )); - }; - if operand_option != option - || residual_option != option - || operand_arguments.len() != 1 - || residual_arguments.len() != 1 - || operand_arguments - .iter() - .chain(residual_arguments) - .any(|argument| !matches!(argument, ResolvedType::I64 | ResolvedType::Bool)) - { - return Err(hir_error( - "resolved Option `?` has invalid concrete Option instances", - )); + }, + ) = (&operand.ty, residual_type) + else { + return Err(hir_error( + "resolved `?` operand or residual is not nominal Result", + )); + }; + if operand_result != result + || residual_result != result + || operand_arguments.len() != 2 + || residual_arguments.len() != 2 + || operand_arguments + .iter() + .chain(residual_arguments) + .any(|argument| !matches!(argument, ResolvedType::I64 | ResolvedType::Bool)) + { + return Err(hir_error( + "resolved `?` has invalid concrete Result instances", + )); + } + let enclosing = self + .execution_function(function) + .map(|candidate| &candidate.return_type) + .ok_or_else(|| hir_error("resolved `?` has no enclosing function"))?; + self.require_type(residual_type, enclosing, "`?` residual")?; + self.require_type(&expression.ty, &operand_arguments[0], "`?` success value")?; + self.require_type( + &operand_arguments[1], + &residual_arguments[1], + "`?` residual error", + )?; + if expression.ownership != OwnershipMode::Value { + return Err(hir_error("resolved `?` success value is not Copy")); + } + Ok(()) + } + } + + #[cfg(test)] + #[allow(dead_code)] + fn validate_expr_recursive_reference( + &mut self, + function: &FunctionExecutionId, + expression: &ResolvedExpr, + scope: &mut BTreeMap, + path: &str, + allow_moves: bool, + allowed_effects: Option<&BTreeSet>, + ) -> Result<(), Diagnostic> { + if matches!(expression.kind, ResolvedExprKind::Unary { .. }) { + let mut unary = Vec::new(); + let mut current = expression; + let mut current_path = path.to_owned(); + while let ResolvedExprKind::Unary { op, value } = ¤t.kind { + reject_nul_identity("resolved expression", current.id.as_str())?; + if current.id != ExpressionId::new(function, ¤t_path) { + return Err(hir_error(format!( + "expression `{}` has a non-canonical identity", + current.id + ))); } - let enclosing_return = self - .execution_function(function) - .map(|candidate| &candidate.return_type) - .ok_or_else(|| hir_error("resolved Option `?` has no enclosing function"))?; - self.require_type(residual_type, enclosing_return, "Option `?` residual")?; - self.require_type( - &expression.ty, - &operand_arguments[0], - "Option `?` success value", - )?; - if expression.ownership != OwnershipMode::Value { - return Err(hir_error("resolved Option `?` success value is not Copy")); + if !self.expression_ids.insert(current.id.clone()) { + return Err(hir_error(format!( + "duplicate resolved expression identity `{}`", + current.id + ))); } - (expression.ty.clone(), OwnershipMode::Value) + self.validate_type(¤t.ty)?; + unary.push((current, *op)); + current = value; + current_path.push_str(".value"); } - ResolvedExprKind::UpdateRecord { - base, - record, - fields, - } => { - self.validate_expr( - function, - base, - scope, - &format!("{path}.base"), - allow_moves, - allowed_effects, - )?; - let declaration = self - .program - .declarations - .declaration(record) - .ok_or_else(|| hir_error(format!("record `{record}` is not indexed")))?; - if declaration.kind != DeclarationKind::Record { + self.validate_expr_recursive_reference( + function, + current, + scope, + ¤t_path, + allow_moves, + allowed_effects, + )?; + let mut operand = current; + for (expression, op) in unary.into_iter().rev() { + let expected = match op { + UnaryOp::Neg => ResolvedType::I64, + UnaryOp::Not => ResolvedType::Bool, + }; + self.require_type(&operand.ty, &expected, "unary operand")?; + self.require_type(&expression.ty, &expected, "expression")?; + if expression.ownership != OwnershipMode::Value { return Err(hir_error(format!( - "record update target `{record}` is not a record" + "expression `{}` has inconsistent ownership", + expression.id ))); } - let ty = base.ty.clone(); - let ResolvedType::Nominal { - declaration: instance_record, - arguments, - } = &ty - else { - return Err(hir_error("record update base is not nominal")); - }; - let parameters = self - .program - .declarations - .type_parameters(record) - .ok_or_else(|| hir_error(format!("record `{record}` has no parameters")))?; - if instance_record != record - || arguments.len() != parameters.len() - || arguments - .iter() - .any(|argument| !matches!(argument, ResolvedType::I64 | ResolvedType::Bool)) - { - return Err(hir_error(format!( - "record update for `{record}` has an invalid concrete instance" - ))); + operand = expression; + } + return Ok(()); + } + reject_nul_identity("resolved expression", expression.id.as_str())?; + if expression.id != ExpressionId::new(function, path) { + return Err(hir_error(format!( + "expression `{}` has a non-canonical identity", + expression.id + ))); + } + if !self.expression_ids.insert(expression.id.clone()) { + return Err(hir_error(format!( + "duplicate resolved expression identity `{}`", + expression.id + ))); + } + self.validate_type(&expression.ty)?; + + let (ty, ownership) = match &expression.kind { + ResolvedExprKind::Int(_) => (ResolvedType::I64, OwnershipMode::Value), + ResolvedExprKind::Bool(_) => (ResolvedType::Bool, OwnershipMode::Value), + ResolvedExprKind::Place(place) => { + let binding = scope.get(&place.root).ok_or_else(|| { + hir_error(format!("resolved value `{}` is out of scope", place.root)) + })?; + match (place.projections.is_empty(), binding.availability) { + (true, Availability::Available) => { + match Self::place_availability(binding, &[]) { + Availability::Available => {} + Availability::Moved => { + return Err(hir_error(format!( + "resolved value `{}` is partially moved", + place.root + ))); + } + Availability::MaybeMoved => { + return Err(hir_error(format!( + "resolved value `{}` may be partially moved", + place.root + ))); + } + } + } + (true, Availability::Moved) => { + return Err(hir_error(format!( + "resolved value `{}` is used after it was moved", + place.root + ))); + } + (true, Availability::MaybeMoved) => { + return Err(hir_error(format!( + "resolved value `{}` may have been moved", + place.root + ))); + } + (false, _) => match Self::place_availability(binding, &place.projections) { + Availability::Available => {} + Availability::Moved => { + return Err(hir_error(format!( + "resolved place rooted at `{}` is partially moved", + place.root + ))); + } + Availability::MaybeMoved => { + return Err(hir_error(format!( + "resolved place rooted at `{}` may be conditionally moved", + place.root + ))); + } + }, } - self.require_type(&base.ty, &ty, "record update base")?; - let ownership = self.expected_ownership(&ty, OwnershipMode::Own)?; - if base.ownership != ownership { - return Err(hir_error(format!( - "record update base for `{record}` has incompatible ownership" - ))); + self.resolve_place(place, binding)? + } + ResolvedExprKind::Call { + callee, + type_arguments, + instance, + args, + } => { + match instance { + None if !type_arguments.is_empty() => { + return Err(hir_error( + "monomorphic resolved call carries generic type arguments", + )); + } + Some(instance) + if FunctionInstanceId::derive(callee, type_arguments) != *instance => + { + return Err(hir_error( + "resolved call instance disagrees with its template and arguments", + )); + } + Some(_) if type_arguments.is_empty() => { + return Err(hir_error( + "generic resolved call has no concrete type arguments", + )); + } + None | Some(_) => {} } - if ownership == OwnershipMode::Own { - if !allow_moves { + for argument in type_arguments { + if !matches!(argument, ResolvedType::I64 | ResolvedType::Bool) { return Err(hir_error( - "contract cannot transfer ownership from a record update base", + "resolved call has a non-scalar generic type argument", )); } - self.mark_value_sources_moved(base, scope)?; } - - let expected_fields = self + let target = self .program - .declarations - .record_fields(record) - .ok_or_else(|| hir_error(format!("record `{record}` has no fields")))? - .to_vec(); - let mut seen = BTreeSet::new(); - for (index, initializer) in fields.iter().enumerate() { - let field = expected_fields - .iter() - .find(|field| field.id == initializer.field) - .ok_or_else(|| { - hir_error(format!( - "update for `{record}` contains foreign field `{}`", - initializer.field - )) - })?; - if !seen.insert(initializer.field.clone()) { + .resolve_call_target(callee, instance.as_ref()) + .ok_or_else(|| { + hir_error(format!("resolved callee `{callee}` is not indexed")) + })?; + if args.len() != target.params.len() { + return Err(hir_error(format!( + "call to `{callee}` has {} arguments but expects {}", + args.len(), + target.params.len() + ))); + } + let params = target.params.clone(); + let return_type = target.return_type.clone(); + let target_effects = target.effects.clone(); + match allowed_effects { + Some(allowed) => { + for effect in &target_effects { + if !allowed.contains(effect) { + return Err(hir_error(format!( + "call to `{callee}` requires undeclared effect `{effect}`" + ))); + } + } + } + None if !target_effects.is_empty() => { return Err(hir_error(format!( - "update for `{record}` repeats field `{}`", - initializer.field + "contract calls effectful function `{callee}`" ))); } - self.validate_expr( + None => {} + } + for (index, (argument, param)) in args.iter().zip(¶ms).enumerate() { + self.validate_expr_recursive_reference( function, - &initializer.value, + argument, scope, - &format!("{path}.field.{index}.value"), + &format!("{path}.arg.{index}"), allow_moves, allowed_effects, )?; - let field_ty = substitute_type(&field.ty, record, arguments)?; - self.require_type(&initializer.value.ty, &field_ty, "record replacement")?; - let expected = self.expected_ownership(&field_ty, OwnershipMode::Own)?; - if initializer.value.ownership != expected { - return Err(hir_error(format!( - "replacement field `{}` has incompatible ownership", - initializer.field - ))); - } - if expected == OwnershipMode::Own { + self.require_type(&argument.ty, ¶m.ty, "call argument")?; + self.validate_argument_ownership(argument.ownership, param)?; + if self.argument_transfers(param)? { if !allow_moves { - return Err(hir_error( - "contract cannot transfer ownership into a record replacement", - )); + return Err(hir_error(format!( + "contract cannot transfer ownership to `{callee}`" + ))); } - self.mark_value_sources_moved(&initializer.value, scope)?; + self.mark_value_sources_moved(argument, scope)?; } } - (ty, ownership) + let ownership = self.expected_ownership(&return_type, OwnershipMode::Own)?; + (return_type, ownership) } - ResolvedExprKind::Project { base, field } => { - if matches!(&base.kind, ResolvedExprKind::Place(_)) { + ResolvedExprKind::NativeRustImportCall(call) => { + if call.expression != expression.id { return Err(hir_error( - "place field projections must use a resolved place path", + "native Rust import call has a non-canonical expression identity", + )); + } + let import = self + .program + .interfaces + .iter() + .flat_map(|interface| &interface.imports) + .find(|import| import.id == call.import && import.native_rust) + .ok_or_else(|| hir_error("native Rust import call has an unknown target"))?; + if import.parameters.len() != call.args.len() || import.result.kind != call.result { + return Err(hir_error( + "native Rust import call disagrees with its declaration", )); } - self.validate_expr( + match allowed_effects { + Some(allowed) => { + if import + .effects + .iter() + .any(|effect| !allowed.contains(effect)) + { + return Err(hir_error( + "native Rust import call requires an undeclared effect", + )); + } + } + None if !import.effects.is_empty() => { + return Err(hir_error("contract calls an effectful native Rust import")); + } + None => {} + } + for (index, (argument, parameter)) in + call.args.iter().zip(&import.parameters).enumerate() + { + self.validate_expr_recursive_reference( + function, + argument, + scope, + &format!("{path}.native-rust-arg.{index}"), + allow_moves, + allowed_effects, + )?; + self.require_type(&argument.ty, ¶meter.ty, "native Rust import argument")?; + if argument.ownership != OwnershipMode::Value + || parameter.ownership != OwnershipMode::Value + { + return Err(hir_error( + "native Rust import arguments must use value ownership", + )); + } + } + let result = match call.result { + ResolvedImportResultKind::Unit => ResolvedType::Unit, + ResolvedImportResultKind::I64 => ResolvedType::I64, + ResolvedImportResultKind::Bool => ResolvedType::Bool, + }; + (result, OwnershipMode::Value) + } + ResolvedExprKind::Unary { .. } => unreachable!("unary chain handled above"), + ResolvedExprKind::Binary { op, left, right } => { + self.validate_expr_recursive_reference( function, - base, + left, scope, - &format!("{path}.base"), + &format!("{path}.left"), allow_moves, allowed_effects, )?; - let projected = self.field_type_for_type(&base.ty, field)?; - let ownership = self.expected_ownership(&projected, base.ownership)?; - (projected, ownership) - } - }; - - self.require_type(&expression.ty, &ty, "expression")?; - if expression.ownership != ownership { - return Err(hir_error(format!( - "expression `{}` has inconsistent ownership", - expression.id - ))); - } - Ok(()) - } - - fn resolve_place( - &self, - place: &Place, - binding: &ValidationBinding, - ) -> Result<(ResolvedType, OwnershipMode), Diagnostic> { - let mut ty = binding.ty.clone(); - let mut ownership = binding.ownership; - for projection in &place.projections { - match projection { - PlaceProjection::Field(field) => { - ty = self.field_type_for_type(&ty, field)?; - ownership = self.expected_ownership(&ty, ownership)?; - } - PlaceProjection::VariantField { .. } => { - return Err(hir_error( - "variant-field projections are not valid before variant HIR lands", - )); + if matches!(op, BinaryOp::And | BinaryOp::Or) { + let baseline_ids = scope.keys().cloned().collect::>(); + let mut conditional_scope = scope.clone(); + self.validate_expr_recursive_reference( + function, + right, + &mut conditional_scope, + &format!("{path}.right"), + allow_moves, + allowed_effects, + )?; + Self::join_conditional(scope, &conditional_scope, &baseline_ids); + } else { + self.validate_expr_recursive_reference( + function, + right, + scope, + &format!("{path}.right"), + allow_moves, + allowed_effects, + )?; } - } - } - Ok((ty, ownership)) - } - - fn field_type_for_type( - &self, - ty: &ResolvedType, - field: &DeclarationId, - ) -> Result { - let ResolvedType::Nominal { - declaration, - arguments, - } = ty - else { - return Err(hir_error(format!( - "field `{field}` projects from a non-record type" - ))); - }; - if self - .program - .declarations - .declaration(declaration) - .is_none_or(|item| item.kind != DeclarationKind::Record) - { - return Err(hir_error(format!( - "field `{field}` projects from a non-record nominal type" - ))); - } - let parameters = self - .program - .declarations - .type_parameters(declaration) - .ok_or_else(|| hir_error(format!("record `{declaration}` has no parameters")))?; - if arguments.len() != parameters.len() - || arguments - .iter() - .any(|argument| !matches!(argument, ResolvedType::I64 | ResolvedType::Bool)) - { - return Err(hir_error(format!( - "field `{field}` projects from an invalid concrete record instance" - ))); - } - let template = self - .program - .declarations - .record_fields(declaration) - .and_then(|fields| fields.iter().find(|candidate| candidate.id == *field)) - .ok_or_else(|| { - hir_error(format!( - "field `{field}` does not belong to record `{declaration}`" - )) - })?; - substitute_type(&template.ty, declaration, arguments) - } - - fn argument_transfers(&self, param: &ResolvedParam) -> Result { - self.is_owned_resource(¶m.ty, param.ownership) - } - - fn is_owned_resource( - &self, - ty: &ResolvedType, - ownership: OwnershipMode, - ) -> Result { - self.program - .declarations - .type_facts(ty) - .map(|facts| !facts.copy && ownership == OwnershipMode::Own) - .ok_or_else(|| { - hir_error(format!( - "type `{}` has no semantic facts", - ty.identity_key() - )) - }) - } - - fn mark_value_sources_moved( - &self, - expression: &ResolvedExpr, - scope: &mut BTreeMap, - ) -> Result<(), Diagnostic> { - match &expression.kind { - ResolvedExprKind::Place(place) => { - let Some(binding) = scope.get(&place.root) else { - // A block result may be backed by a local whose lexical - // scope ended after the expression was validated. Its - // transfer cannot affect any still-visible root. - return Ok(()); + let output = match op { + BinaryOp::Add + | BinaryOp::Sub + | BinaryOp::Mul + | BinaryOp::Div + | BinaryOp::Rem => { + self.require_type(&left.ty, &ResolvedType::I64, "binary operand")?; + self.require_type(&right.ty, &ResolvedType::I64, "binary operand")?; + ResolvedType::I64 + } + BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge => { + self.require_type(&left.ty, &ResolvedType::I64, "comparison operand")?; + self.require_type(&right.ty, &ResolvedType::I64, "comparison operand")?; + ResolvedType::Bool + } + BinaryOp::And | BinaryOp::Or => { + self.require_type(&left.ty, &ResolvedType::Bool, "boolean operand")?; + self.require_type(&right.ty, &ResolvedType::Bool, "boolean operand")?; + ResolvedType::Bool + } + BinaryOp::Eq | BinaryOp::Ne => { + self.require_type(&left.ty, &right.ty, "equality operands")?; + ResolvedType::Bool + } }; - let (place_ty, place_ownership) = self.resolve_place(place, binding)?; - let should_move = self.is_owned_resource(&place_ty, place_ownership)? - && Self::place_availability(binding, &place.projections) - == Availability::Available; - if should_move { - let binding = scope.get_mut(&place.root).ok_or_else(|| { - hir_error(format!( - "resolved value `{}` disappeared during ownership validation", - place.root - )) - })?; - if place.projections.is_empty() { - binding.availability = Availability::Moved; - } else { - binding - .moved_places - .insert(place.projections.clone(), Availability::Moved); + (output, OwnershipMode::Value) + } + ResolvedExprKind::Block { statements, tail } => { + let mut block_scope = scope.clone(); + for (index, statement) in statements.iter().enumerate() { + match statement { + ResolvedStatement::Let { binding, value, .. } => { + let statement_path = format!("{path}.s{index}"); + self.validate_expr_recursive_reference( + function, + value, + &mut block_scope, + &format!("{statement_path}.value"), + allow_moves, + allowed_effects, + )?; + if binding.id != ValueId::local(function, &statement_path) { + return Err(hir_error(format!( + "local `{}` has a non-canonical identity", + binding.id + ))); + } + self.insert_value(&binding.id)?; + self.require_type(&binding.ty, &value.ty, "local binding")?; + if binding.ownership != value.ownership { + return Err(hir_error(format!( + "local `{}` has inconsistent ownership", + binding.id + ))); + } + self.validate_declared_ownership(&binding.ty, binding.ownership)?; + if self.is_owned_resource(&binding.ty, binding.ownership)? { + if !allow_moves { + return Err(hir_error( + "contract cannot transfer ownership into a local binding", + )); + } + self.mark_value_sources_moved(value, &mut block_scope)?; + } + block_scope.insert( + binding.id.clone(), + ValidationBinding { + ty: binding.ty.clone(), + ownership: binding.ownership, + availability: Availability::Available, + moved_places: BTreeMap::new(), + definitely_partial: BTreeSet::new(), + }, + ); + } } } - } - ResolvedExprKind::Block { tail, .. } => { - self.mark_value_sources_moved(tail, scope)?; + self.validate_expr_recursive_reference( + function, + tail, + &mut block_scope, + &format!("{path}.tail"), + allow_moves, + allowed_effects, + )?; + let outer_ids = scope.keys().cloned().collect::>(); + Self::merge_availability(scope, &block_scope, &outer_ids); + (tail.ty.clone(), tail.ownership) } ResolvedExprKind::If { + condition, then_branch, else_branch, - .. } => { - let ids = scope.keys().cloned().collect::>(); + self.validate_expr_recursive_reference( + function, + condition, + scope, + &format!("{path}.condition"), + allow_moves, + allowed_effects, + )?; + self.require_type(&condition.ty, &ResolvedType::Bool, "if condition")?; + let outer_ids = scope.keys().cloned().collect::>(); let mut then_scope = scope.clone(); let mut else_scope = scope.clone(); - self.mark_value_sources_moved(then_branch, &mut then_scope)?; - self.mark_value_sources_moved(else_branch, &mut else_scope)?; - Self::join_branches(scope, &then_scope, &else_scope, &ids); - } - ResolvedExprKind::Match { arms, .. } => { - let ids = scope.keys().cloned().collect::>(); - let mut arm_scopes = Vec::with_capacity(arms.len()); - for arm in arms { - let mut arm_scope = scope.clone(); - self.mark_value_sources_moved(&arm.value, &mut arm_scope)?; - arm_scopes.push(arm_scope); - } - if let Some((first, rest)) = arm_scopes.split_first() { - let mut joined = first.clone(); - for arm_scope in rest { - Self::join_conditional(&mut joined, arm_scope, &ids); - } - Self::merge_availability(scope, &joined, &ids); + self.validate_expr_recursive_reference( + function, + then_branch, + &mut then_scope, + &format!("{path}.then"), + allow_moves, + allowed_effects, + )?; + self.validate_expr_recursive_reference( + function, + else_branch, + &mut else_scope, + &format!("{path}.else"), + allow_moves, + allowed_effects, + )?; + Self::join_branches(scope, &then_scope, &else_scope, &outer_ids); + self.require_type(&then_branch.ty, &else_branch.ty, "if branches")?; + if then_branch.ownership != else_branch.ownership { + return Err(hir_error("if branches have inconsistent ownership")); } + (then_branch.ty.clone(), then_branch.ownership) } - ResolvedExprKind::Project { base, .. } => { - self.mark_value_sources_moved(base, scope)?; - } - ResolvedExprKind::Int(_) - | ResolvedExprKind::Bool(_) - | ResolvedExprKind::Call { .. } - | ResolvedExprKind::Unary { .. } - | ResolvedExprKind::Binary { .. } - | ResolvedExprKind::ConstructRecord { .. } - | ResolvedExprKind::ConstructVariant { .. } - | ResolvedExprKind::Try { .. } - | ResolvedExprKind::TryOption { .. } - | ResolvedExprKind::UpdateRecord { .. } => {} - } - Ok(()) - } - - fn merge_availability( - target: &mut BTreeMap, - source: &BTreeMap, - ids: &[ValueId], - ) { - for id in ids { - if let (Some(target), Some(source)) = (target.get_mut(id), source.get(id)) { - target.availability = source.availability; - target.moved_places.clone_from(&source.moved_places); - target - .definitely_partial - .clone_from(&source.definitely_partial); - } - } - } - - fn join_conditional( - baseline: &mut BTreeMap, - conditional: &BTreeMap, - ids: &[ValueId], - ) { - for id in ids { - if let (Some(baseline), Some(conditional)) = (baseline.get_mut(id), conditional.get(id)) - { - let moved_places = Self::join_moved_places(baseline, conditional); - let definitely_partial = Self::join_definitely_partial(baseline, conditional); - baseline.availability = baseline.availability.join(conditional.availability); - baseline.moved_places = moved_places; - baseline.definitely_partial = definitely_partial; - } - } - } - - fn join_branches( - target: &mut BTreeMap, - then_scope: &BTreeMap, - else_scope: &BTreeMap, - ids: &[ValueId], - ) { - for id in ids { - if let (Some(target), Some(then_value), Some(else_value)) = - (target.get_mut(id), then_scope.get(id), else_scope.get(id)) - { - target.availability = then_value.availability.join(else_value.availability); - target.moved_places = Self::join_moved_places(then_value, else_value); - target.definitely_partial = Self::join_definitely_partial(then_value, else_value); - } - } - } - - fn place_availability( - binding: &ValidationBinding, - requested: &[PlaceProjection], - ) -> Availability { - if binding.availability != Availability::Available { - return binding.availability; - } - let mut maybe_moved = false; - for (moved, state) in &binding.moved_places { - if path_is_prefix(moved, requested) || path_is_prefix(requested, moved) { - if *state == Availability::Moved { - return Availability::Moved; + ResolvedExprKind::ConstructRecord { record, fields } => { + let declaration = self + .program + .declarations + .declaration(record) + .ok_or_else(|| hir_error(format!("record `{record}` is not indexed")))?; + if declaration.kind != DeclarationKind::Record { + return Err(hir_error(format!( + "constructor target `{record}` is not a record" + ))); } - maybe_moved = true; - } - } - if binding - .definitely_partial - .iter() - .any(|partial| path_is_prefix(requested, partial)) - { - return Availability::Moved; - } - if maybe_moved { - Availability::MaybeMoved - } else { - Availability::Available - } - } - - fn join_moved_places( - left: &ValidationBinding, - right: &ValidationBinding, - ) -> BTreeMap, Availability> { - left.moved_places - .keys() - .chain(right.moved_places.keys()) - .cloned() - .collect::>() - .into_iter() - .filter_map(|path| { - let left = left - .moved_places - .get(&path) - .copied() - .unwrap_or(Availability::Available); - let right = right - .moved_places - .get(&path) - .copied() - .unwrap_or(Availability::Available); - let state = left.join(right); - (state != Availability::Available).then_some((path, state)) - }) - .collect() - } - - fn join_definitely_partial( - left: &ValidationBinding, - right: &ValidationBinding, - ) -> BTreeSet> { - let mut candidates = BTreeSet::new(); - for path in left - .moved_places - .keys() - .chain(right.moved_places.keys()) - .chain(left.definitely_partial.iter()) - .chain(right.definitely_partial.iter()) - { - for length in 0..=path.len() { - candidates.insert(path[..length].to_vec()); - } - } - candidates - .into_iter() - .filter(|path| { - Self::place_availability(left, path) == Availability::Moved - && Self::place_availability(right, path) == Availability::Moved - }) - .collect() - } - - fn validate_type(&self, ty: &ResolvedType) -> Result<(), Diagnostic> { - match ty { - ResolvedType::I64 | ResolvedType::Bool => Ok(()), - ResolvedType::TypeParameter { .. } => Err(hir_error( - "uninstantiated type parameters are not valid in executable HIR", - )), - ResolvedType::Nominal { - declaration, - arguments, - } => { - let kind = self + let expected_fields = self .program .declarations - .declaration(declaration) - .map(|item| item.kind) - .filter(|kind| { - matches!( - kind, - DeclarationKind::Resource - | DeclarationKind::Record - | DeclarationKind::Variant - ) - }) - .ok_or_else(|| { - hir_error(format!( - "nominal type `{declaration}` is not a resolved type declaration" - )) - })?; + .record_fields(record) + .ok_or_else(|| hir_error(format!("record `{record}` has no fields")))? + .to_vec(); + let ResolvedType::Nominal { + declaration: instance_record, + arguments, + } = &expression.ty + else { + return Err(hir_error("record constructor result is not nominal")); + }; let parameters = self .program .declarations - .type_parameters(declaration) - .ok_or_else(|| { - hir_error(format!("nominal type `{declaration}` has no parameters")) - })?; - if arguments.len() != parameters.len() { + .type_parameters(record) + .ok_or_else(|| hir_error(format!("record `{record}` has no parameters")))?; + if instance_record != record + || arguments.len() != parameters.len() + || arguments + .iter() + .any(|argument| !matches!(argument, ResolvedType::I64 | ResolvedType::Bool)) + { return Err(hir_error(format!( - "nominal type `{declaration}` has incorrect argument arity" + "constructor for `{record}` has an invalid concrete instance" ))); } - if !arguments.is_empty() - && (!matches!(kind, DeclarationKind::Record | DeclarationKind::Variant) - || arguments.iter().any(|argument| { - !matches!(argument, ResolvedType::I64 | ResolvedType::Bool) - })) - { + let mut seen = BTreeSet::new(); + for (index, initializer) in fields.iter().enumerate() { + let field = expected_fields + .iter() + .find(|field| field.id == initializer.field) + .ok_or_else(|| { + hir_error(format!( + "constructor for `{record}` contains foreign field `{}`", + initializer.field + )) + })?; + if !seen.insert(initializer.field.clone()) { + return Err(hir_error(format!( + "constructor for `{record}` repeats field `{}`", + initializer.field + ))); + } + self.validate_expr_recursive_reference( + function, + &initializer.value, + scope, + &format!("{path}.field.{index}.value"), + allow_moves, + allowed_effects, + )?; + let field_ty = substitute_type(&field.ty, record, arguments)?; + self.require_type(&initializer.value.ty, &field_ty, "record field")?; + let expected = self.expected_ownership(&field_ty, OwnershipMode::Own)?; + if initializer.value.ownership != expected { + return Err(hir_error(format!( + "field `{}` has incompatible ownership", + initializer.field + ))); + } + if expected == OwnershipMode::Own { + if !allow_moves { + return Err(hir_error( + "contract cannot transfer ownership into a record", + )); + } + self.mark_value_sources_moved(&initializer.value, scope)?; + } + } + if seen.len() != expected_fields.len() { return Err(hir_error(format!( - "nominal type `{declaration}` has unsupported generic arguments" + "constructor for `{record}` is missing required fields" ))); } - for argument in arguments { - self.validate_type(argument)?; - } - self.program.declarations.type_facts(ty).ok_or_else(|| { - hir_error(format!( - "type `{}` has no semantic facts", - ty.identity_key() - )) - })?; - Ok(()) + let ty = expression.ty.clone(); + let ownership = self.expected_ownership(&ty, OwnershipMode::Own)?; + (ty, ownership) } - } - } - - fn validate_declared_ownership( - &self, - ty: &ResolvedType, - ownership: OwnershipMode, - ) -> Result<(), Diagnostic> { - let facts = self.program.declarations.type_facts(ty).ok_or_else(|| { - hir_error(format!( - "type `{}` has no semantic facts", - ty.identity_key() - )) - })?; - if (facts.copy && ownership != OwnershipMode::Value) - || (!facts.copy && ownership == OwnershipMode::Value) - { - return Err(hir_error(format!( - "type `{}` has an invalid ownership mode", - ty.identity_key() - ))); - } - Ok(()) - } - - fn validate_argument_ownership( - &self, - actual: OwnershipMode, - param: &ResolvedParam, - ) -> Result<(), Diagnostic> { - let facts = self - .program - .declarations - .type_facts(¶m.ty) - .ok_or_else(|| { - hir_error(format!( - "type `{}` has no semantic facts", - param.ty.identity_key() - )) - })?; - let valid = if facts.copy { - actual == OwnershipMode::Value && param.ownership == OwnershipMode::Value - } else { - match param.ownership { - OwnershipMode::Own => actual == OwnershipMode::Own, - OwnershipMode::Borrow => true, - OwnershipMode::Shared => actual == OwnershipMode::Shared, - OwnershipMode::Value => false, - } - }; - if valid { - Ok(()) - } else { - Err(hir_error(format!( - "argument ownership is incompatible with parameter `{}`", - param.id - ))) - } - } - - fn expected_ownership( - &self, - ty: &ResolvedType, - non_copy: OwnershipMode, - ) -> Result { - self.program - .declarations - .type_facts(ty) - .map(|facts| { - if facts.copy { - OwnershipMode::Value - } else { - non_copy - } - }) - .ok_or_else(|| { - hir_error(format!( - "type `{}` has no semantic facts", - ty.identity_key() - )) - }) - } - - fn require_type( - &self, - actual: &ResolvedType, - expected: &ResolvedType, - context: &str, - ) -> Result<(), Diagnostic> { - if actual == expected { - Ok(()) - } else { - Err(hir_error(format!( - "{context} has inconsistent resolved types" - ))) - } - } - - fn insert_value(&mut self, id: &ValueId) -> Result<(), Diagnostic> { - reject_nul_identity("resolved value", id.as_str())?; - if self.value_ids.insert(id.clone()) { - Ok(()) - } else { - Err(hir_error(format!( - "duplicate resolved value identity `{id}`" - ))) - } - } -} - -fn validate_nul_free_identities(program: &ResolvedProgram) -> Result<(), Diagnostic> { - reject_nul_identity("resolved entry point", program.entrypoint.as_str())?; - - for (key, declaration) in &program.declarations.declarations { - reject_nul_identity("declaration index key", key.as_str())?; - reject_nul_identity( - declaration_identity_subject(declaration.kind), - declaration.id.as_str(), - )?; - if let Some(owner) = &declaration.owner { - reject_nul_identity("resolved declaration owner", owner.as_str())?; - } - } - for id in program.declarations.types_by_name.values() { - reject_nul_identity("resolved type lookup", id.as_str())?; - } - for id in program.declarations.functions_by_name.values() { - reject_nul_identity("resolved function lookup", id.as_str())?; - } - for ((owner, _), field) in &program.declarations.fields_by_owner_name { - reject_nul_identity("resolved field owner lookup", owner.as_str())?; - reject_nul_identity("resolved field lookup", field.as_str())?; - } - for ((owner, _), case) in &program.declarations.cases_by_owner_name { - reject_nul_identity("resolved variant owner lookup", owner.as_str())?; - reject_nul_identity("resolved variant case lookup", case.as_str())?; - } - for (owner, fields) in &program.declarations.record_fields { - reject_nul_identity("resolved record-field owner", owner.as_str())?; - for field in fields { - reject_nul_identity("resolved field", field.id.as_str())?; - audit_resolved_type(&field.ty)?; - } - } - for (owner, cases) in &program.declarations.variant_cases { - reject_nul_identity("resolved variant-case owner", owner.as_str())?; - for case in cases { - reject_nul_identity("resolved variant case", case.id.as_str())?; - for field in &case.fields { - reject_nul_identity("resolved case field", field.id.as_str())?; - audit_resolved_type(&field.ty)?; - } - } - } - for (case, fields) in &program.declarations.case_fields { - reject_nul_identity("resolved case-field owner", case.as_str())?; - for field in fields { - reject_nul_identity("resolved case field", field.id.as_str())?; - audit_resolved_type(&field.ty)?; - } - } - for (key, import) in &program.declarations.imports_by_key { - reject_nul_identity("resolved logical import key", key)?; - reject_nul_identity("resolved import lookup", import.as_str())?; - } - - for declaration in &program.types { - let subject = match declaration.kind { - ResolvedTypeDeclarationKind::Resource { .. } => "resolved resource", - ResolvedTypeDeclarationKind::Record { .. } => "resolved record", - ResolvedTypeDeclarationKind::Variant { .. } => "resolved variant", - }; - reject_nul_identity(subject, declaration.id.as_str())?; - match &declaration.kind { - ResolvedTypeDeclarationKind::Resource { drop } => { - reject_nul_identity("resolved resource lifecycle", drop.id.as_str())?; - if let ResolvedResourceDropKind::Imported { import, import_key } = &drop.kind { - reject_nul_identity("resolved lifecycle import", import.as_str())?; - reject_nul_identity("resolved lifecycle logical import key", import_key)?; + ResolvedExprKind::ConstructVariant { + variant, + case, + fields, + } => { + let ResolvedType::Nominal { + declaration: instance_variant, + arguments, + } = &expression.ty + else { + return Err(hir_error("variant constructor has a non-nominal result")); + }; + if instance_variant != variant { + return Err(hir_error( + "variant constructor result disagrees with its declaration", + )); } - } - ResolvedTypeDeclarationKind::Record { fields } => { - for field in fields { - reject_nul_identity("resolved field", field.id.as_str())?; - audit_resolved_type(&field.ty)?; + let declaration = self + .program + .declarations + .declaration(variant) + .ok_or_else(|| hir_error(format!("variant `{variant}` is not indexed")))?; + if declaration.kind != DeclarationKind::Variant { + return Err(hir_error(format!( + "constructor target `{variant}` is not a variant" + ))); } - } - ResolvedTypeDeclarationKind::Variant { cases } => { - for case in cases { - reject_nul_identity("resolved variant case", case.id.as_str())?; - for field in &case.fields { - reject_nul_identity("resolved case field", field.id.as_str())?; - audit_resolved_type(&field.ty)?; + let declared_case = self + .program + .declarations + .variant_cases(variant) + .and_then(|cases| cases.iter().find(|item| item.id == *case)) + .ok_or_else(|| { + hir_error(format!( + "constructor for `{variant}` contains foreign case `{case}`" + )) + })?; + let expected_fields = declared_case.fields.clone(); + let mut seen = BTreeSet::new(); + for (index, initializer) in fields.iter().enumerate() { + let field = expected_fields + .iter() + .find(|field| field.id == initializer.field) + .ok_or_else(|| { + hir_error(format!( + "constructor for `{case}` contains foreign field `{}`", + initializer.field + )) + })?; + if !seen.insert(initializer.field.clone()) { + return Err(hir_error(format!( + "constructor for `{case}` repeats field `{}`", + initializer.field + ))); + } + self.validate_expr_recursive_reference( + function, + &initializer.value, + scope, + &format!("{path}.field.{index}.value"), + allow_moves, + allowed_effects, + )?; + let field_ty = substitute_type(&field.ty, variant, arguments)?; + self.require_type(&initializer.value.ty, &field_ty, "variant payload field")?; + if initializer.value.ownership != OwnershipMode::Value { + return Err(hir_error(format!( + "variant payload field `{}` is not a Copy value", + initializer.field + ))); } } - } - } - } - for interface in &program.interfaces { - reject_nul_identity("resolved interface", interface.id.as_str())?; - for import in &interface.imports { - reject_nul_identity("resolved import", import.id.as_str())?; - reject_nul_identity("resolved import owner", import.interface.as_str())?; - reject_nul_identity("resolved logical import key", &import.import_key)?; - for parameter in &import.parameters { - audit_resolved_type(¶meter.ty)?; - } - } - } - for function in &program.functions { - reject_nul_identity("resolved function", function.id.as_str())?; - for parameter in &function.params { - reject_nul_identity("resolved value", parameter.id.as_str())?; - audit_resolved_type(¶meter.ty)?; - } - reject_nul_identity("resolved value", function.result_id.as_str())?; - audit_resolved_type(&function.return_type)?; - for expression in function - .requires - .iter() - .chain(std::iter::once(&function.body)) - .chain(&function.ensures) - { - audit_resolved_expression(expression)?; - } - } - Ok(()) -} - -/// Reject target-neutral attached metadata containing identities that cannot -/// cross C-string-backed backend and trace boundaries losslessly. -/// -/// This is intentionally narrower than semantic inventory/plan validation so -/// independent replayers can call it without trusting either canonical builder. -pub(crate) fn validate_attached_identity_references( - program: &ResolvedProgram, -) -> Result<(), Diagnostic> { - for function in &program.functions { - audit_cleanup_inventory(&function.cleanup)?; - audit_cleanup_plan(&function.cleanup_plan)?; - } - Ok(()) -} - -fn audit_resolved_type(root: &ResolvedType) -> Result<(), Diagnostic> { - let mut pending = vec![root]; - while let Some(ty) = pending.pop() { - match ty { - ResolvedType::I64 | ResolvedType::Bool => {} - ResolvedType::TypeParameter { owner, .. } => { - reject_nul_identity("resolved type-parameter owner", owner.as_str())?; - } - ResolvedType::Nominal { - declaration, - arguments, - } => { - reject_nul_identity("resolved nominal type", declaration.as_str())?; - pending.extend(arguments); - } - } - } - Ok(()) -} - -fn audit_resolved_record_match_pattern( - record: &DeclarationId, - instance: &ResolvedType, - fields: &[ResolvedRecordMatchPatternField], -) -> Result<(), Diagnostic> { - reject_nul_identity("resolved record match", record.as_str())?; - audit_resolved_type(instance)?; - for field in fields { - reject_nul_identity("resolved record match field", field.field.as_str())?; - match &field.pattern { - ResolvedRecordMatchFieldPattern::Binding(binding) => { - reject_nul_identity("resolved record match binding", binding.id.as_str())?; - audit_resolved_type(&binding.ty)?; - } - ResolvedRecordMatchFieldPattern::Wildcard => {} - ResolvedRecordMatchFieldPattern::Record { - record, - instance, - fields, - } => audit_resolved_record_match_pattern(record, instance, fields)?, - } - } - Ok(()) -} - -fn audit_resolved_expression(root: &ResolvedExpr) -> Result<(), Diagnostic> { - let mut pending = vec![root]; - while let Some(expression) = pending.pop() { - reject_nul_identity("resolved expression", expression.id.as_str())?; - audit_resolved_type(&expression.ty)?; - match &expression.kind { - ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) => {} - ResolvedExprKind::Place(place) => audit_hir_place(place)?, - ResolvedExprKind::Call { callee, args, .. } => { - reject_nul_identity("resolved call target", callee.as_str())?; - pending.extend(args); - } - ResolvedExprKind::Unary { value, .. } => pending.push(value), - ResolvedExprKind::Binary { left, right, .. } => { - pending.push(right); - pending.push(left); - } - ResolvedExprKind::Block { statements, tail } => { - pending.push(tail); - for statement in statements.iter().rev() { - let ResolvedStatement::Let { binding, value, .. } = statement; - reject_nul_identity("resolved value", binding.id.as_str())?; - audit_resolved_type(&binding.ty)?; - pending.push(value); - } - } - ResolvedExprKind::If { - condition, - then_branch, - else_branch, - } => { - pending.push(else_branch); - pending.push(then_branch); - pending.push(condition); - } - ResolvedExprKind::ConstructRecord { record, fields } => { - reject_nul_identity("resolved record constructor", record.as_str())?; - for field in fields.iter().rev() { - reject_nul_identity("resolved record initializer field", field.field.as_str())?; - pending.push(&field.value); - } - } - ResolvedExprKind::ConstructVariant { - variant, - case, - fields, - } => { - reject_nul_identity("resolved variant constructor", variant.as_str())?; - reject_nul_identity("resolved variant case", case.as_str())?; - for field in fields.iter().rev() { - reject_nul_identity("resolved case initializer field", field.field.as_str())?; - pending.push(&field.value); - } + if seen.len() != expected_fields.len() { + return Err(hir_error(format!( + "constructor for `{case}` is missing required payload fields" + ))); + } + (expression.ty.clone(), OwnershipMode::Value) } ResolvedExprKind::Match { scrutinee, arms } => { - for arm in arms.iter().rev() { + self.validate_expr_recursive_reference( + function, + scrutinee, + scope, + &format!("{path}.scrutinee"), + allow_moves, + allowed_effects, + )?; + let ResolvedType::Nominal { + declaration: matched_type, + arguments, + } = &scrutinee.ty + else { + return Err(hir_error("resolved match scrutinee is not nominal")); + }; + let matched_kind = self + .program + .declarations + .declaration(matched_type) + .map(|item| item.kind); + if matched_kind == Some(DeclarationKind::Record) { + if scrutinee.ownership != OwnershipMode::Value { + return Err(hir_error("resolved record match scrutinee is not Copy")); + } + let [arm] = arms.as_slice() else { + return Err(hir_error( + "resolved irrefutable record match must have exactly one arm", + )); + }; + let outer_ids = scope.keys().cloned().collect::>(); + let mut arm_scope = scope.clone(); match &arm.pattern { ResolvedMatchPattern::Wildcard => {} - ResolvedMatchPattern::Variant { - variant, - case, - fields, - } => { - reject_nul_identity("resolved match variant", variant.as_str())?; - reject_nul_identity("resolved match case", case.as_str())?; - for field in fields { - reject_nul_identity("resolved match field", field.field.as_str())?; - reject_nul_identity( - "resolved match binding", - field.binding.id.as_str(), - )?; - audit_resolved_type(&field.binding.ty)?; - } - } ResolvedMatchPattern::Record { record, instance, fields, - } => audit_resolved_record_match_pattern(record, instance, fields)?, + } => self.validate_record_match_pattern( + function, + &scrutinee.ty, + record, + instance, + fields, + &mut arm_scope, + &format!("{path}.arm.0.record"), + )?, + ResolvedMatchPattern::Variant { .. } => { + return Err(hir_error( + "resolved variant pattern has a record scrutinee", + )); + } } - pending.push(&arm.value); + self.validate_expr_recursive_reference( + function, + &arm.value, + &mut arm_scope, + &format!("{path}.arm.0.value"), + allow_moves, + allowed_effects, + )?; + if !matches!(arm.value.ty, ResolvedType::I64 | ResolvedType::Bool) { + return Err(hir_error( + "resolved record match arm must produce i64 or bool", + )); + } + for id in outer_ids { + if let Some(state) = arm_scope.get(&id) { + scope.insert(id, state.clone()); + } + } + self.require_type(&expression.ty, &arm.value.ty, "record match expression")?; + if expression.ownership != arm.value.ownership { + return Err(hir_error( + "resolved record match expression has inconsistent ownership", + )); + } + return Ok(()); } - pending.push(scrutinee); - } - ResolvedExprKind::Try { - operand, - result, - ok_case, - ok_field, - err_case, - err_field, - residual_type, - } => { - reject_nul_identity("resolved `?` Result", result.as_str())?; - reject_nul_identity("resolved `?` Ok case", ok_case.as_str())?; - reject_nul_identity("resolved `?` Ok field", ok_field.as_str())?; - reject_nul_identity("resolved `?` Err case", err_case.as_str())?; - reject_nul_identity("resolved `?` Err field", err_field.as_str())?; - audit_resolved_type(residual_type)?; - pending.push(operand); - } - ResolvedExprKind::TryOption { - operand, - option, - some_case, - some_field, - none_case, - residual_type, - } => { - reject_nul_identity("resolved Option `?` Option", option.as_str())?; - reject_nul_identity("resolved Option `?` Some case", some_case.as_str())?; - reject_nul_identity("resolved Option `?` Some field", some_field.as_str())?; - reject_nul_identity("resolved Option `?` None case", none_case.as_str())?; - audit_resolved_type(residual_type)?; - pending.push(operand); - } - ResolvedExprKind::UpdateRecord { - base, - record, - fields, - } => { - reject_nul_identity("resolved record update", record.as_str())?; - for field in fields.iter().rev() { - reject_nul_identity("resolved record replacement field", field.field.as_str())?; - pending.push(&field.value); - } - pending.push(base); - } - ResolvedExprKind::Project { base, field } => { - reject_nul_identity("resolved projected field", field.as_str())?; - pending.push(base); - } - } - } - Ok(()) -} - -fn audit_hir_place(place: &Place) -> Result<(), Diagnostic> { - reject_nul_identity("resolved place root", place.root.as_str())?; - for projection in &place.projections { - match projection { - PlaceProjection::Field(field) => { - reject_nul_identity("resolved place field", field.as_str())?; - } - PlaceProjection::VariantField { case, field } => { - reject_nul_identity("resolved place variant case", case.as_str())?; - reject_nul_identity("resolved place variant field", field.as_str())?; - } - } - } - Ok(()) -} - -fn audit_field_liveness_shape(root: &crate::cleanup::FieldLivenessShape) -> Result<(), Diagnostic> { - let mut pending = vec![root]; - while let Some(shape) = pending.pop() { - match shape { - crate::cleanup::FieldLivenessShape::NoDrop => {} - crate::cleanup::FieldLivenessShape::Leaf { lifecycle, .. } => { - reject_nul_identity("cleanup lifecycle", lifecycle.as_str())?; - } - crate::cleanup::FieldLivenessShape::Record { - declaration, - fields, - } => { - reject_nul_identity("cleanup record", declaration.as_str())?; - for field in fields.iter().rev() { - reject_nul_identity("cleanup field", field.field.as_str())?; - pending.push(&field.shape); - } - } - } - } - Ok(()) -} - -fn audit_inventory_place(place: &crate::cleanup::CleanupPlace) -> Result<(), Diagnostic> { - for projection in &place.projections { - reject_nul_identity("cleanup inventory projection", projection.as_str())?; - } - Ok(()) -} - -fn audit_cleanup_inventory(inventory: &CleanupInventory) -> Result<(), Diagnostic> { - for slot in &inventory.slots { - match &slot.origin { - crate::cleanup::CleanupStorageOrigin::Parameter { value, .. } - | crate::cleanup::CleanupStorageOrigin::Binding { value } - | crate::cleanup::CleanupStorageOrigin::ProvisionalResult { value } => { - reject_nul_identity("cleanup inventory value", value.as_str())?; - } - crate::cleanup::CleanupStorageOrigin::Temporary { expression } => { - reject_nul_identity("cleanup inventory expression", expression.as_str())?; - } - } - audit_resolved_type(&slot.ty)?; - audit_field_liveness_shape(&slot.shape)?; - } - for flag in &inventory.flags { - audit_inventory_place(&flag.place)?; - reject_nul_identity("cleanup inventory lifecycle", flag.lifecycle.as_str())?; - } - Ok(()) -} - -fn audit_plan_storage(storage: &crate::cleanup_plan::StorageId) -> Result<(), Diagnostic> { - match storage { - crate::cleanup_plan::StorageId::Value(value) => { - reject_nul_identity("cleanup-plan value storage", value.as_str())?; - } - crate::cleanup_plan::StorageId::Temporary(expression) => { - reject_nul_identity("cleanup-plan temporary storage", expression.as_str())?; - } - crate::cleanup_plan::StorageId::CallArgument { - call, - value_expression, - .. - } => { - reject_nul_identity("cleanup-plan call-argument call", call.as_str())?; - reject_nul_identity( - "cleanup-plan call-argument value", - value_expression.as_str(), - )?; - } - crate::cleanup_plan::StorageId::ProvisionalResult => {} - } - Ok(()) -} - -fn audit_plan_place(place: &crate::cleanup_plan::CleanupPlace) -> Result<(), Diagnostic> { - audit_plan_storage(&place.storage)?; - for projection in &place.projections { - reject_nul_identity("cleanup-plan projection", projection.as_str())?; - } - Ok(()) -} - -fn audit_status_source(source: &crate::cleanup_plan::StatusSourceId) -> Result<(), Diagnostic> { - reject_nul_identity("cleanup-plan status expression", source.expression.as_str()) -} - -fn audit_result_source( - source: &crate::cleanup_plan::CleanupResultSource, -) -> Result<(), Diagnostic> { - match source { - crate::cleanup_plan::CleanupResultSource::Scalar { expression } => { - reject_nul_identity("cleanup-plan scalar result", expression.as_str())?; - } - crate::cleanup_plan::CleanupResultSource::Owned { storage } => { - audit_plan_place(storage)?; - } - } - Ok(()) -} - -fn audit_cleanup_plan(plan: &CleanupPlan) -> Result<(), Diagnostic> { - for place in &plan.entry_state.live_owned_parameters { - audit_plan_place(place)?; - } - for slot in &plan.slots { - audit_plan_storage(&slot.storage)?; - audit_resolved_type(&slot.ty)?; - audit_field_liveness_shape(&slot.field_liveness_shape)?; - } - for source in &plan.status_sources { - audit_status_source(&source.id)?; - if let crate::cleanup_plan::StatusProducer::PropagatedCall { callee } = &source.producer { - reject_nul_identity("cleanup-plan propagated callee", callee.as_str())?; - } - } - for block in &plan.blocks { - for transition in &block.transitions { - match transition { - crate::cleanup_plan::CleanupTransition::Initialize { at, destination } => { - reject_nul_identity("cleanup-plan initialize expression", at.as_str())?; - audit_plan_place(destination)?; - } - crate::cleanup_plan::CleanupTransition::Transfer { - at, - source, - destination, - } => { - reject_nul_identity("cleanup-plan transfer expression", at.as_str())?; - audit_plan_place(source)?; - audit_plan_place(destination)?; - } - crate::cleanup_plan::CleanupTransition::CallCommit { call, arguments } => { - reject_nul_identity("cleanup-plan committed call", call.as_str())?; - for argument in arguments { - audit_plan_place(&argument.source)?; - } + let variant = matched_type; + if scrutinee.ownership != OwnershipMode::Value + || self + .program + .declarations + .declaration(variant) + .is_none_or(|item| item.kind != DeclarationKind::Variant) + { + return Err(hir_error( + "resolved match scrutinee is not a concrete Copy variant", + )); } - crate::cleanup_plan::CleanupTransition::SelectFailure { source } => { - audit_status_source(source)?; + let cases = self + .program + .declarations + .variant_cases(variant) + .ok_or_else(|| hir_error(format!("variant `{variant}` has no cases")))? + .to_vec(); + if arms.is_empty() { + return Err(hir_error("resolved match has no arms")); } - crate::cleanup_plan::CleanupTransition::StageCopyResult { source } => { - match source { - crate::cleanup_plan::StagedCopyResultSource::Body { - expression, - instance, - } => { - reject_nul_identity( - "cleanup-plan staged body expression", - expression.as_str(), - )?; - audit_resolved_type(instance)?; - } - crate::cleanup_plan::StagedCopyResultSource::TryResidual { - expression, - operand, - source_instance, - target_instance, - result, - ok_case, - ok_field, - err_case, - err_field, - } => { - reject_nul_identity( - "cleanup-plan staged `?` expression", - expression.as_str(), - )?; - reject_nul_identity( - "cleanup-plan staged `?` operand", - operand.as_str(), - )?; - audit_resolved_type(source_instance)?; - audit_resolved_type(target_instance)?; - for (kind, declaration) in [ - ("Result", result), - ("Ok case", ok_case), - ("Ok field", ok_field), - ("Err case", err_case), - ("Err field", err_field), - ] { - reject_nul_identity( - &format!("cleanup-plan staged `?` {kind}"), - declaration.as_str(), - )?; + let outer_ids = scope.keys().cloned().collect::>(); + let mut arm_scopes = Vec::with_capacity(arms.len()); + let mut covered = BTreeSet::new(); + let mut wildcard_seen = false; + let mut result = None::<(ResolvedType, OwnershipMode)>; + for (arm_index, arm) in arms.iter().enumerate() { + let mut arm_scope = scope.clone(); + match &arm.pattern { + ResolvedMatchPattern::Wildcard => { + if wildcard_seen || covered.len() == cases.len() { + return Err(hir_error( + "resolved match has an unreachable wildcard", + )); } + wildcard_seen = true; } - crate::cleanup_plan::StagedCopyResultSource::TryOptionNone { - expression, - operand, - source_instance, - target_instance, - option, - some_case, - some_field, - none_case, + ResolvedMatchPattern::Variant { + variant: pattern_variant, + case, + fields, } => { - reject_nul_identity( - "cleanup-plan staged Option `?` expression", - expression.as_str(), - )?; - reject_nul_identity( - "cleanup-plan staged Option `?` operand", - operand.as_str(), - )?; - audit_resolved_type(source_instance)?; - audit_resolved_type(target_instance)?; - for (kind, declaration) in [ - ("Option", option), - ("Some case", some_case), - ("Some field", some_field), - ("None case", none_case), - ] { - reject_nul_identity( - &format!("cleanup-plan staged Option `?` {kind}"), - declaration.as_str(), - )?; + if wildcard_seen + || pattern_variant != variant + || !covered.insert(case.clone()) + { + return Err(hir_error( + "resolved match has an unreachable or foreign case pattern", + )); + } + let declared_case = + cases.iter().find(|item| item.id == *case).ok_or_else(|| { + hir_error(format!( + "resolved match references foreign case `{case}`" + )) + })?; + let mut seen_fields = BTreeSet::new(); + for (field_index, pattern_field) in fields.iter().enumerate() { + let declared_field = declared_case + .fields + .iter() + .find(|item| item.id == pattern_field.field) + .ok_or_else(|| { + hir_error(format!( + "resolved pattern contains foreign field `{}`", + pattern_field.field + )) + })?; + let binding_ty = + substitute_type(&declared_field.ty, variant, arguments)?; + if !seen_fields.insert(pattern_field.field.clone()) + || pattern_field.binding.id + != ValueId::local( + function, + &format!( + "{path}.arm.{arm_index}.binding.{field_index}" + ), + ) + || pattern_field.binding.ty != binding_ty + || pattern_field.binding.ownership != OwnershipMode::Value + { + return Err(hir_error( + "resolved match pattern field or binding is invalid", + )); + } + self.insert_value(&pattern_field.binding.id)?; + self.validate_type(&pattern_field.binding.ty)?; + if arm_scope.contains_key(&pattern_field.binding.id) { + return Err(hir_error( + "resolved match pattern binding shadows an existing value", + )); + } + arm_scope.insert( + pattern_field.binding.id.clone(), + ValidationBinding { + ty: pattern_field.binding.ty.clone(), + ownership: OwnershipMode::Value, + availability: Availability::Available, + moved_places: BTreeMap::new(), + definitely_partial: BTreeSet::new(), + }, + ); + } + if seen_fields.len() != declared_case.fields.len() { + return Err(hir_error( + "resolved match pattern is missing payload fields", + )); } } + ResolvedMatchPattern::Record { .. } => { + return Err(hir_error( + "resolved record pattern has a variant scrutinee", + )); + } } + self.validate_expr_recursive_reference( + function, + &arm.value, + &mut arm_scope, + &format!("{path}.arm.{arm_index}.value"), + allow_moves, + allowed_effects, + )?; + if let Some((expected_ty, expected_ownership)) = &result { + self.require_type(&arm.value.ty, expected_ty, "match arm")?; + if arm.value.ownership != *expected_ownership { + return Err(hir_error( + "resolved match arms have inconsistent ownership", + )); + } + } else { + result = Some((arm.value.ty.clone(), arm.value.ownership)); + } + arm_scopes.push(arm_scope); } - } - } - } - for edge in &plan.edges { - match &edge.condition { - crate::cleanup_plan::EdgeCondition::Always => {} - crate::cleanup_plan::EdgeCondition::BooleanResult(expression, _) => { - reject_nul_identity("cleanup-plan boolean expression", expression.as_str())?; - } - crate::cleanup_plan::EdgeCondition::VariantCase { - scrutinee, case, .. - } => { - reject_nul_identity("cleanup-plan match scrutinee", scrutinee.as_str())?; - reject_nul_identity("cleanup-plan variant case", case.as_str())?; - } - crate::cleanup_plan::EdgeCondition::StatusZero(source) - | crate::cleanup_plan::EdgeCondition::StatusNonzero(source) => { - audit_status_source(source)?; - } - } - } - for region in &plan.regions { - for storage in ®ion.slots { - audit_plan_storage(storage)?; - } - } - for exit in &plan.exits { - for finalizer in &exit.finalize_in_order { - audit_plan_place(&finalizer.source)?; - reject_nul_identity( - "cleanup-plan finalizer lifecycle", - finalizer.lifecycle_id.as_str(), - )?; - } - match &exit.continuation { - crate::cleanup_plan::ExitContinuation::Continue(_) - | crate::cleanup_plan::ExitContinuation::ReturnUnit => {} - crate::cleanup_plan::ExitContinuation::CommitResult { source } => { - audit_result_source(source)?; - } - crate::cleanup_plan::ExitContinuation::ReturnFailure { source } => { - audit_status_source(source)?; - } - } - } - Ok(()) -} - -fn declaration_identity_subject(kind: DeclarationKind) -> &'static str { - match kind { - DeclarationKind::Resource => "resolved resource declaration", - DeclarationKind::ResourceDrop => "resolved resource lifecycle declaration", - DeclarationKind::Record => "resolved record declaration", - DeclarationKind::Field => "resolved field declaration", - DeclarationKind::Variant => "resolved variant declaration", - DeclarationKind::VariantCase => "resolved variant case declaration", - DeclarationKind::CaseField => "resolved case field declaration", - DeclarationKind::Interface => "resolved interface declaration", - DeclarationKind::Import => "resolved import declaration", - DeclarationKind::Function => "resolved function declaration", - } -} - -fn reject_nul_identity(subject: &str, value: &str) -> Result<(), Diagnostic> { - if value.contains('\0') { - Err(hir_error(format!("{subject} identity contains NUL"))) - } else { - Ok(()) - } -} - -fn path_is_prefix(prefix: &[T], path: &[T]) -> bool { - prefix.len() <= path.len() && prefix.iter().zip(path).all(|(left, right)| left == right) -} - -fn resolved_lifecycle_effects( - program: &ResolvedProgram, - ty: &ResolvedType, -) -> Result, Diagnostic> { - fn collect( - program: &ResolvedProgram, - ty: &ResolvedType, - visiting: &mut BTreeSet, - effects: &mut BTreeSet, - ) -> Result<(), Diagnostic> { - let Some(id) = ty.nominal_id() else { - return Ok(()); - }; - if !visiting.insert(id.clone()) { - return Ok(()); - } - let declaration = program - .types - .iter() - .find(|item| item.id == *id) - .ok_or_else(|| hir_error(format!("type `{id}` has no lifecycle declaration")))?; - match &declaration.kind { - ResolvedTypeDeclarationKind::Resource { drop } => { - if let ResolvedResourceDropKind::Imported { import, .. } = &drop.kind { - let resolved = program - .interfaces - .iter() - .flat_map(|interface| &interface.imports) - .find(|item| item.id == *import) - .ok_or_else(|| { - hir_error(format!( - "resource `{id}` references missing import `{import}`" - )) - })?; - effects.extend(resolved.effects.iter().cloned()); - } - } - ResolvedTypeDeclarationKind::Record { fields } => { - for field in fields { - collect(program, &field.ty, visiting, effects)?; + if !wildcard_seen && covered.len() != cases.len() { + return Err(hir_error("resolved match is not exhaustive")); } - } - ResolvedTypeDeclarationKind::Variant { cases } => { - for case in cases { - for field in &case.fields { - collect(program, &field.ty, visiting, effects)?; + if let Some((first, rest)) = arm_scopes.split_first() { + let mut joined = first.clone(); + for arm_scope in rest { + Self::join_conditional(&mut joined, arm_scope, &outer_ids); } + Self::merge_availability(scope, &joined, &outer_ids); } + result.ok_or_else(|| hir_error("resolved match has no result"))? } - } - visiting.remove(id); - Ok(()) - } - - let mut effects = BTreeSet::new(); - collect(program, ty, &mut BTreeSet::new(), &mut effects)?; - Ok(effects) -} - -fn visit_resolved_calls( - expression: &ResolvedExpr, - visit: &mut impl FnMut(&DeclarationId, Option<&FunctionInstanceId>, &[ResolvedType]), -) { - match &expression.kind { - ResolvedExprKind::Call { - callee, - instance, - type_arguments, - args, - } => { - visit(callee, instance.as_ref(), type_arguments); - for arg in args { - visit_resolved_calls(arg, visit); - } - } - ResolvedExprKind::Unary { value, .. } - | ResolvedExprKind::Try { operand: value, .. } - | ResolvedExprKind::TryOption { operand: value, .. } - | ResolvedExprKind::Project { base: value, .. } => visit_resolved_calls(value, visit), - ResolvedExprKind::Binary { left, right, .. } => { - visit_resolved_calls(left, visit); - visit_resolved_calls(right, visit); - } - ResolvedExprKind::Block { statements, tail } => { - for statement in statements { - match statement { - ResolvedStatement::Let { value, .. } => visit_resolved_calls(value, visit), + ResolvedExprKind::Try { + operand, + result, + ok_case, + ok_field, + err_case, + err_field, + residual_type, + } => { + self.validate_expr_recursive_reference( + function, + operand, + scope, + &format!("{path}.operand"), + allow_moves, + allowed_effects, + )?; + if !path.starts_with("body") { + return Err(hir_error( + "resolved `?` is outside the executable function body", + )); } - } - visit_resolved_calls(tail, visit); - } - ResolvedExprKind::If { - condition, - then_branch, - else_branch, - } => { - visit_resolved_calls(condition, visit); - visit_resolved_calls(then_branch, visit); - visit_resolved_calls(else_branch, visit); - } - ResolvedExprKind::ConstructRecord { fields, .. } => { - for field in fields { - visit_resolved_calls(&field.value, visit); - } - } - ResolvedExprKind::ConstructVariant { fields, .. } => { - for field in fields { - visit_resolved_calls(&field.value, visit); - } - } - ResolvedExprKind::Match { scrutinee, arms } => { - visit_resolved_calls(scrutinee, visit); - for arm in arms { - visit_resolved_calls(&arm.value, visit); - } - } - ResolvedExprKind::UpdateRecord { base, fields, .. } => { - visit_resolved_calls(base, visit); - for field in fields { - visit_resolved_calls(&field.value, visit); - } - } - ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) | ResolvedExprKind::Place(_) => {} - } -} - -#[allow(dead_code, reason = "private Workspace Semantic Graph Phase-A seam")] -pub(crate) fn workspace_call_edges( - program: &ResolvedProgram, -) -> BTreeSet<(DeclarationId, DeclarationId)> { - let mut edges = BTreeSet::new(); - for function in &program.functions { - for expression in function - .requires - .iter() - .chain(std::iter::once(&function.body)) - .chain(&function.ensures) - { - visit_resolved_calls(expression, &mut |callee, _, _| { - edges.insert((function.id.clone(), callee.clone())); - }); - } - } - edges -} - -#[allow(dead_code, reason = "private Workspace Semantic Graph Phase-A seam")] -pub(crate) fn workspace_expression_identity(owner: &DeclarationId, path: &str) -> String { - ExpressionId::new(&FunctionExecutionId::Monomorphic(owner.clone()), path) - .as_str() - .to_owned() -} - -#[allow(dead_code, reason = "private Workspace Semantic Graph Phase-A seam")] -pub(crate) fn workspace_call_sites( - program: &ResolvedProgram, -) -> Vec<(DeclarationId, String, DeclarationId)> { - fn walk( - owner: &DeclarationId, - expression: &ResolvedExpr, - sites: &mut Vec<(DeclarationId, String, DeclarationId)>, - ) { - match &expression.kind { - ResolvedExprKind::Call { callee, args, .. } => { - sites.push(( - owner.clone(), - expression.id.as_str().to_owned(), - callee.clone(), - )); - for argument in args { - walk(owner, argument, sites); + if scope.values().any(|binding| { + self.program + .declarations + .type_facts(&binding.ty) + .is_some_and(|facts| facts.contains_resource) + }) { + return Err(hir_error( + "resolved `?` has a live resource binding in the bounded Copy-only profile", + )); } - } - ResolvedExprKind::Unary { value, .. } - | ResolvedExprKind::Try { operand: value, .. } - | ResolvedExprKind::TryOption { operand: value, .. } - | ResolvedExprKind::Project { base: value, .. } => walk(owner, value, sites), - ResolvedExprKind::Binary { left, right, .. } => { - walk(owner, left, sites); - walk(owner, right, sites); - } - ResolvedExprKind::Block { statements, tail } => { - for statement in statements { - match statement { - ResolvedStatement::Let { value, .. } => walk(owner, value, sites), - } + if result.as_str() != crate::prelude::RESULT_ID + || ok_case.as_str() != crate::prelude::RESULT_OK_ID + || ok_field.as_str() != crate::prelude::RESULT_OK_VALUE_ID + || err_case.as_str() != crate::prelude::RESULT_ERR_ID + || err_field.as_str() != crate::prelude::RESULT_ERR_ERROR_ID + { + return Err(hir_error( + "resolved `?` does not authenticate the compiler-owned Result shape", + )); } - walk(owner, tail, sites); - } - ResolvedExprKind::If { - condition, - then_branch, - else_branch, - } => { - walk(owner, condition, sites); - walk(owner, then_branch, sites); - walk(owner, else_branch, sites); - } - ResolvedExprKind::ConstructRecord { fields, .. } - | ResolvedExprKind::ConstructVariant { fields, .. } => { - for field in fields { - walk(owner, &field.value, sites); + let ResolvedType::Nominal { + declaration: operand_result, + arguments: operand_arguments, + } = &operand.ty + else { + return Err(hir_error("resolved `?` operand is not nominal Result")); + }; + let ResolvedType::Nominal { + declaration: residual_result, + arguments: residual_arguments, + } = residual_type + else { + return Err(hir_error("resolved `?` residual is not nominal Result")); + }; + if operand_result != result + || residual_result != result + || operand_arguments.len() != 2 + || residual_arguments.len() != 2 + || operand_arguments + .iter() + .chain(residual_arguments) + .any(|argument| !matches!(argument, ResolvedType::I64 | ResolvedType::Bool)) + { + return Err(hir_error( + "resolved `?` has invalid concrete Result instances", + )); } - } - ResolvedExprKind::Match { scrutinee, arms } => { - walk(owner, scrutinee, sites); - for arm in arms { - walk(owner, &arm.value, sites); + let enclosing_return = self + .execution_function(function) + .map(|candidate| &candidate.return_type) + .ok_or_else(|| hir_error("resolved `?` has no enclosing function"))?; + self.require_type(residual_type, enclosing_return, "`?` residual")?; + self.require_type(&expression.ty, &operand_arguments[0], "`?` success value")?; + self.require_type( + &operand_arguments[1], + &residual_arguments[1], + "`?` residual error", + )?; + if expression.ownership != OwnershipMode::Value { + return Err(hir_error("resolved `?` success value is not Copy")); } + (expression.ty.clone(), OwnershipMode::Value) } - ResolvedExprKind::UpdateRecord { base, fields, .. } => { - walk(owner, base, sites); - for field in fields { - walk(owner, &field.value, sites); + ResolvedExprKind::TryOption { + operand, + option, + some_case, + some_field, + none_case, + residual_type, + } => { + self.validate_expr_recursive_reference( + function, + operand, + scope, + &format!("{path}.operand"), + allow_moves, + allowed_effects, + )?; + if !path.starts_with("body") { + return Err(hir_error( + "resolved Option `?` is outside the executable function body", + )); } - } - ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) | ResolvedExprKind::Place(_) => {} - } - } - - let mut sites = Vec::new(); - for function in &program.functions { - for expression in function - .requires - .iter() - .chain(std::iter::once(&function.body)) - .chain(&function.ensures) - { - walk(&function.id, expression, &mut sites); + if scope.values().any(|binding| { + self.program + .declarations + .type_facts(&binding.ty) + .is_some_and(|facts| facts.contains_resource) + }) { + return Err(hir_error( + "resolved Option `?` has a live resource binding in the bounded Copy-only profile", + )); + } + if option.as_str() != crate::prelude::OPTION_ID + || some_case.as_str() != crate::prelude::OPTION_SOME_ID + || some_field.as_str() != crate::prelude::OPTION_SOME_VALUE_ID + || none_case.as_str() != crate::prelude::OPTION_NONE_ID + { + return Err(hir_error( + "resolved Option `?` does not authenticate the compiler-owned Option shape", + )); + } + for id in [option, some_case, some_field, none_case] { + if self + .program + .declarations + .declaration(id) + .is_none_or(|declaration| { + declaration.identity_origin != IdentityOrigin::CompilerOwned + }) + { + return Err(hir_error(format!( + "resolved Option `?` identity `{id}` is not compiler-owned" + ))); + } + } + let ResolvedType::Nominal { + declaration: operand_option, + arguments: operand_arguments, + } = &operand.ty + else { + return Err(hir_error( + "resolved Option `?` operand is not nominal Option", + )); + }; + let ResolvedType::Nominal { + declaration: residual_option, + arguments: residual_arguments, + } = residual_type + else { + return Err(hir_error( + "resolved Option `?` residual is not nominal Option", + )); + }; + if operand_option != option + || residual_option != option + || operand_arguments.len() != 1 + || residual_arguments.len() != 1 + || operand_arguments + .iter() + .chain(residual_arguments) + .any(|argument| !matches!(argument, ResolvedType::I64 | ResolvedType::Bool)) + { + return Err(hir_error( + "resolved Option `?` has invalid concrete Option instances", + )); + } + let enclosing_return = self + .execution_function(function) + .map(|candidate| &candidate.return_type) + .ok_or_else(|| hir_error("resolved Option `?` has no enclosing function"))?; + self.require_type(residual_type, enclosing_return, "Option `?` residual")?; + self.require_type( + &expression.ty, + &operand_arguments[0], + "Option `?` success value", + )?; + if expression.ownership != OwnershipMode::Value { + return Err(hir_error("resolved Option `?` success value is not Copy")); + } + (expression.ty.clone(), OwnershipMode::Value) + } + ResolvedExprKind::UpdateRecord { + base, + record, + fields, + } => { + self.validate_expr_recursive_reference( + function, + base, + scope, + &format!("{path}.base"), + allow_moves, + allowed_effects, + )?; + let declaration = self + .program + .declarations + .declaration(record) + .ok_or_else(|| hir_error(format!("record `{record}` is not indexed")))?; + if declaration.kind != DeclarationKind::Record { + return Err(hir_error(format!( + "record update target `{record}` is not a record" + ))); + } + let ty = base.ty.clone(); + let ResolvedType::Nominal { + declaration: instance_record, + arguments, + } = &ty + else { + return Err(hir_error("record update base is not nominal")); + }; + let parameters = self + .program + .declarations + .type_parameters(record) + .ok_or_else(|| hir_error(format!("record `{record}` has no parameters")))?; + if instance_record != record + || arguments.len() != parameters.len() + || arguments + .iter() + .any(|argument| !matches!(argument, ResolvedType::I64 | ResolvedType::Bool)) + { + return Err(hir_error(format!( + "record update for `{record}` has an invalid concrete instance" + ))); + } + self.require_type(&base.ty, &ty, "record update base")?; + let ownership = self.expected_ownership(&ty, OwnershipMode::Own)?; + if base.ownership != ownership { + return Err(hir_error(format!( + "record update base for `{record}` has incompatible ownership" + ))); + } + if ownership == OwnershipMode::Own { + if !allow_moves { + return Err(hir_error( + "contract cannot transfer ownership from a record update base", + )); + } + self.mark_value_sources_moved(base, scope)?; + } + + let expected_fields = self + .program + .declarations + .record_fields(record) + .ok_or_else(|| hir_error(format!("record `{record}` has no fields")))? + .to_vec(); + let mut seen = BTreeSet::new(); + for (index, initializer) in fields.iter().enumerate() { + let field = expected_fields + .iter() + .find(|field| field.id == initializer.field) + .ok_or_else(|| { + hir_error(format!( + "update for `{record}` contains foreign field `{}`", + initializer.field + )) + })?; + if !seen.insert(initializer.field.clone()) { + return Err(hir_error(format!( + "update for `{record}` repeats field `{}`", + initializer.field + ))); + } + self.validate_expr_recursive_reference( + function, + &initializer.value, + scope, + &format!("{path}.field.{index}.value"), + allow_moves, + allowed_effects, + )?; + let field_ty = substitute_type(&field.ty, record, arguments)?; + self.require_type(&initializer.value.ty, &field_ty, "record replacement")?; + let expected = self.expected_ownership(&field_ty, OwnershipMode::Own)?; + if initializer.value.ownership != expected { + return Err(hir_error(format!( + "replacement field `{}` has incompatible ownership", + initializer.field + ))); + } + if expected == OwnershipMode::Own { + if !allow_moves { + return Err(hir_error( + "contract cannot transfer ownership into a record replacement", + )); + } + self.mark_value_sources_moved(&initializer.value, scope)?; + } + } + (ty, ownership) + } + ResolvedExprKind::Project { base, field } => { + if matches!(&base.kind, ResolvedExprKind::Place(_)) { + return Err(hir_error( + "place field projections must use a resolved place path", + )); + } + self.validate_expr_recursive_reference( + function, + base, + scope, + &format!("{path}.base"), + allow_moves, + allowed_effects, + )?; + let projected = self.field_type_for_type(&base.ty, field)?; + let ownership = self.expected_ownership(&projected, base.ownership)?; + (projected, ownership) + } + }; + + self.require_type(&expression.ty, &ty, "expression")?; + if expression.ownership != ownership { + return Err(hir_error(format!( + "expression `{}` has inconsistent ownership", + expression.id + ))); } + Ok(()) } - for template in &program.function_templates { - for expression in template - .requires - .iter() - .chain(std::iter::once(&template.body)) - .chain(&template.ensures) - { - walk(&template.id, expression, &mut sites); + + fn resolve_place( + &self, + place: &Place, + binding: &ValidationBinding, + ) -> Result<(ResolvedType, OwnershipMode), Diagnostic> { + let mut ty = binding.ty.clone(); + let mut ownership = binding.ownership; + for projection in &place.projections { + match projection { + PlaceProjection::Field(field) => { + ty = self.field_type_for_type(&ty, field)?; + ownership = self.expected_ownership(&ty, ownership)?; + } + PlaceProjection::VariantField { .. } => { + return Err(hir_error( + "variant-field projections are not valid before variant HIR lands", + )); + } + } } + Ok((ty, ownership)) } - sites -} - -fn hir_error(message: impl Into) -> Diagnostic { - Diagnostic::io("SPX-H006", message) -} -struct Resolver<'a> { - program: &'a Program, - declarations: DeclarationIndex, -} - -impl Resolver<'_> { - fn resolve(self) -> Result { - let entrypoint = self + fn field_type_for_type( + &self, + ty: &ResolvedType, + field: &DeclarationId, + ) -> Result { + let ResolvedType::Nominal { + declaration, + arguments, + } = ty + else { + return Err(hir_error(format!( + "field `{field}` projects from a non-record type" + ))); + }; + if self .program - .functions - .iter() - .find(|function| function.name == "main") - .map(|function| DeclarationId::new(function.stable_id.clone())) + .declarations + .declaration(declaration) + .is_none_or(|item| item.kind != DeclarationKind::Record) + { + return Err(hir_error(format!( + "field `{field}` projects from a non-record nominal type" + ))); + } + let parameters = self + .program + .declarations + .type_parameters(declaration) + .ok_or_else(|| hir_error(format!("record `{declaration}` has no parameters")))?; + if arguments.len() != parameters.len() + || arguments + .iter() + .any(|argument| !matches!(argument, ResolvedType::I64 | ResolvedType::Bool)) + { + return Err(hir_error(format!( + "field `{field}` projects from an invalid concrete record instance" + ))); + } + let template = self + .program + .declarations + .record_fields(declaration) + .and_then(|fields| fields.iter().find(|candidate| candidate.id == *field)) .ok_or_else(|| { - self.error( - "SPX-H005", - "verified program has no resolved entry point", - Span::default(), - ) + hir_error(format!( + "field `{field}` does not belong to record `{declaration}`" + )) })?; - self.validate_record_layouts()?; - let types = self - .program - .types - .iter() - .chain(crate::prelude::declarations()) - .map(|declaration| { - let id = DeclarationId::new(declaration.stable_id.clone()); - let kind = match &declaration.kind { - TypeDeclarationKind::Resource { lifecycles } => { - let lifecycle = lifecycles.first().ok_or_else(|| { - self.error( - "SPX-H006", - format!("resource `{id}` has no resolved lifecycle"), - declaration.span, - ) - })?; - let lifecycle_id = DeclarationId::new( - lifecycle.stable_id.clone().ok_or_else(|| { - self.error( - "SPX-H006", - format!("resource `{id}` lifecycle has no identity"), - lifecycle.span, - ) - })?, - ); - let drop_kind = match &lifecycle.kind { - ResourceLifecycleKind::Trivial => ResolvedResourceDropKind::Trivial, - ResourceLifecycleKind::Imported { import_key } => { - let import = self - .declarations - .import_id(import_key) - .cloned() - .ok_or_else(|| { - self.error( - "SPX-H006", - format!( - "resource `{id}` lifecycle references unknown import key `{import_key}`" - ), - lifecycle.span, - ) - })?; - ResolvedResourceDropKind::Imported { - import, - import_key: import_key.clone(), - } - } + substitute_type(&template.ty, declaration, arguments) + } + + fn argument_transfers(&self, param: &ResolvedParam) -> Result { + self.is_owned_resource(¶m.ty, param.ownership) + } + + fn is_owned_resource( + &self, + ty: &ResolvedType, + ownership: OwnershipMode, + ) -> Result { + self.program + .declarations + .type_facts(ty) + .map(|facts| !facts.copy && ownership == OwnershipMode::Own) + .ok_or_else(|| { + hir_error(format!( + "type `{}` has no semantic facts", + ty.identity_key() + )) + }) + } + + fn mark_value_sources_moved( + &self, + expression: &ResolvedExpr, + scope: &mut BTreeMap, + ) -> Result<(), Diagnostic> { + enum Frame<'a> { + Enter(&'a ResolvedExpr, usize), + AfterThen { + else_branch: &'a ResolvedExpr, + parent: usize, + then_scope: usize, + ids: Vec, + }, + AfterElse { + parent: usize, + else_scope: usize, + ids: Vec, + then_bindings: BTreeMap, + }, + AfterMatchArm { + arms: &'a [ResolvedMatchArm], + index: usize, + parent: usize, + arm_scope: usize, + ids: Vec, + arm_scopes: Vec>, + }, + } + let root = std::mem::take(scope); + let mut scopes = vec![root]; + let mut frames = vec![Frame::Enter(expression, 0)]; + while let Some(frame) = frames.pop() { + match frame { + Frame::Enter(expression, scope_index) => match &expression.kind { + ResolvedExprKind::Place(place) => { + let Some(binding) = scopes[scope_index].get(&place.root) else { + continue; }; - ResolvedTypeDeclarationKind::Resource { - drop: ResolvedResourceDrop { - id: lifecycle_id, - kind: drop_kind, - }, + let (place_ty, place_ownership) = self.resolve_place(place, binding)?; + let should_move = self.is_owned_resource(&place_ty, place_ownership)? + && Self::place_availability(binding, &place.projections) + == Availability::Available; + if should_move { + let binding = + scopes[scope_index].get_mut(&place.root).ok_or_else(|| { + hir_error(format!( + "resolved value `{}` disappeared during ownership validation", + place.root + )) + })?; + if place.projections.is_empty() { + binding.availability = Availability::Moved; + } else { + binding + .moved_places + .insert(place.projections.clone(), Availability::Moved); + } } } - TypeDeclarationKind::Record { .. } => { - let fields = self - .declarations - .record_fields(&id) - .ok_or_else(|| { - self.error( - "SPX-H006", - format!("record `{id}` has no resolved fields"), - declaration.span, - ) - })? - .to_vec(); - ResolvedTypeDeclarationKind::Record { fields } + ResolvedExprKind::Block { tail, .. } => { + frames.push(Frame::Enter(tail, scope_index)); } - TypeDeclarationKind::Variant { .. } => { - let cases = self - .declarations - .variant_cases(&id) - .ok_or_else(|| { - self.error( - "SPX-H006", - format!("variant `{id}` has no resolved cases"), - declaration.span, - ) - })? - .to_vec(); - ResolvedTypeDeclarationKind::Variant { cases } + ResolvedExprKind::If { + then_branch, + else_branch, + .. + } => { + let ids = scopes[scope_index].keys().cloned().collect::>(); + let then_scope = scopes.len(); + scopes.push(scopes[scope_index].clone()); + frames.push(Frame::AfterThen { + else_branch, + parent: scope_index, + then_scope, + ids, + }); + frames.push(Frame::Enter(then_branch, then_scope)); } - }; - Ok(ResolvedTypeDeclaration { - type_parameters: self - .declarations - .type_parameters(&id) - .ok_or_else(|| { - self.error( - "SPX-H006", - format!("type `{id}` has no parameter metadata"), - declaration.span, - ) - })? - .to_vec(), - id, - name: declaration.name.clone(), - kind, - span: declaration.span, - }) - }) - .collect::, Diagnostic>>()?; - let interfaces = self - .program - .interfaces - .iter() - .map(|interface| { - let interface_id = DeclarationId::new(interface.stable_id.clone()); - let imports = interface - .imports - .iter() - .map(|import| { - let parameters = import - .params - .iter() - .map(|param| { - Ok(ResolvedImportParameter { - name: param.name.clone(), - ty: self.resolve_type(¶m.ty, param.span)?, - ownership: param.mode.into(), - consumes_on_failure: param.name == import.consumes, - }) - }) - .collect::, Diagnostic>>()?; - let failure = match &import.failure { - ImportFailure::Infallible => ResolvedImportFailure::Infallible, - ImportFailure::Status { domain_id } => ResolvedImportFailure::Status { - domain_id: domain_id.clone(), - normalization: "semaprax.status.v1", - }, - }; - Ok(ResolvedImport { - id: DeclarationId::new(import.stable_id.clone()), - name: import.name.clone(), - interface: interface_id.clone(), - import_key: import.stable_id.clone(), - parameters, - result: ResolvedImportResult { - kind: ResolvedImportResultKind::Unit, - ownership: OwnershipMode::Value, - producer: "callee", - out_slot_initialization: "success_only", - ownership_transfer: "final_zero_status_commit", - }, - effects: import.effects.clone(), - required_authority: import.effects.clone(), - failure, - span: import.span, - }) - }) - .collect::, Diagnostic>>()?; - Ok(ResolvedInterface { - id: interface_id, - name: interface.name.clone(), - permits: interface.permits.clone(), - imports, - span: interface.span, - }) - }) - .collect::, Diagnostic>>()?; - let functions = self - .program - .functions - .iter() - .filter(|function| function.type_parameters.is_empty()) - .map(|function| self.resolve_function(function)) - .collect::>()?; - let function_templates = self - .program - .functions - .iter() - .filter(|function| !function.type_parameters.is_empty()) - .map(|function| self.resolve_function_template(function)) - .collect::>()?; - let function_instances = self.discover_function_instances()?; - let mut resolved = ResolvedProgram { - module: self.program.module.clone(), - permits: self.program.permits.clone(), - entrypoint, - declarations: self.declarations, - types, - interfaces, - function_templates, - functions, - function_instances, - }; - let inventories = resolved - .functions - .iter() - .map(|function| crate::cleanup::build_inventory(&resolved, function)) - .collect::, _>>()?; - for (function, inventory) in resolved.functions.iter_mut().zip(inventories) { - function.cleanup = inventory; - } - let instance_inventories = resolved - .function_instances - .iter() - .map(|instance| crate::cleanup::build_inventory(&resolved, &instance.function)) - .collect::, _>>()?; - for (instance, inventory) in resolved - .function_instances - .iter_mut() - .zip(instance_inventories) - { - instance.function.cleanup = inventory; - } - let cleanup_plans = resolved - .functions - .iter() - .map(|function| crate::cleanup_plan::build_plan(&resolved, function)) - .collect::, _>>()?; - for (function, cleanup_plan) in resolved.functions.iter_mut().zip(cleanup_plans) { - function.cleanup_plan = cleanup_plan; + ResolvedExprKind::Match { arms, .. } => { + if let Some(first) = arms.first() { + let ids = scopes[scope_index].keys().cloned().collect::>(); + let arm_scope = scopes.len(); + scopes.push(scopes[scope_index].clone()); + frames.push(Frame::AfterMatchArm { + arms, + index: 0, + parent: scope_index, + arm_scope, + ids, + arm_scopes: Vec::with_capacity(arms.len()), + }); + frames.push(Frame::Enter(&first.value, arm_scope)); + } + } + ResolvedExprKind::Project { base, .. } => { + frames.push(Frame::Enter(base, scope_index)); + } + ResolvedExprKind::Int(_) + | ResolvedExprKind::Bool(_) + | ResolvedExprKind::Call { .. } + | ResolvedExprKind::NativeRustImportCall(_) + | ResolvedExprKind::Unary { .. } + | ResolvedExprKind::Binary { .. } + | ResolvedExprKind::ConstructRecord { .. } + | ResolvedExprKind::ConstructVariant { .. } + | ResolvedExprKind::Try { .. } + | ResolvedExprKind::TryOption { .. } + | ResolvedExprKind::UpdateRecord { .. } => {} + }, + Frame::AfterThen { + else_branch, + parent, + then_scope, + ids, + } => { + debug_assert_eq!(then_scope + 1, scopes.len()); + let then_bindings = scopes.pop().expect("active move branch retained"); + let else_scope = scopes.len(); + scopes.push(scopes[parent].clone()); + frames.push(Frame::AfterElse { + parent, + else_scope, + ids, + then_bindings, + }); + frames.push(Frame::Enter(else_branch, else_scope)); + } + Frame::AfterElse { + parent, + else_scope, + ids, + then_bindings, + } => { + debug_assert_eq!(else_scope + 1, scopes.len()); + let else_bindings = scopes.pop().expect("active move branch retained"); + Self::join_branches(&mut scopes[parent], &then_bindings, &else_bindings, &ids); + } + Frame::AfterMatchArm { + arms, + index, + parent, + arm_scope, + ids, + mut arm_scopes, + } => { + debug_assert_eq!(arm_scope + 1, scopes.len()); + arm_scopes.push(scopes.pop().expect("active match move branch retained")); + let next = index + 1; + if let Some(arm) = arms.get(next) { + let arm_scope = scopes.len(); + scopes.push(scopes[parent].clone()); + frames.push(Frame::AfterMatchArm { + arms, + index: next, + parent, + arm_scope, + ids, + arm_scopes, + }); + frames.push(Frame::Enter(&arm.value, arm_scope)); + } else if let Some((first, rest)) = arm_scopes.split_first() { + let mut joined = first.clone(); + for arm_scope in rest { + Self::join_conditional(&mut joined, arm_scope, &ids); + } + Self::merge_availability(&mut scopes[parent], &joined, &ids); + } + } + } } - let instance_cleanup_plans = resolved - .function_instances - .iter() - .map(|instance| crate::cleanup_plan::build_plan(&resolved, &instance.function)) - .collect::, _>>()?; - for (instance, cleanup_plan) in resolved - .function_instances - .iter_mut() - .zip(instance_cleanup_plans) - { - instance.function.cleanup_plan = cleanup_plan; + *scope = scopes.pop().expect("root move scope retained"); + Ok(()) + } + + fn merge_availability( + target: &mut BTreeMap, + source: &BTreeMap, + ids: &[ValueId], + ) { + for id in ids { + if let (Some(target), Some(source)) = (target.get_mut(id), source.get(id)) { + target.availability = source.availability; + target.moved_places.clone_from(&source.moved_places); + target + .definitely_partial + .clone_from(&source.definitely_partial); + } } - validate(&resolved)?; - Ok(resolved) } - fn validate_record_layouts(&self) -> Result<(), Diagnostic> { - for declaration in &self.program.types { - if !matches!(&declaration.kind, TypeDeclarationKind::Record { .. }) { - continue; + fn join_conditional( + baseline: &mut BTreeMap, + conditional: &BTreeMap, + ids: &[ValueId], + ) { + for id in ids { + if let (Some(baseline), Some(conditional)) = (baseline.get_mut(id), conditional.get(id)) + { + let moved_places = Self::join_moved_places(baseline, conditional); + let definitely_partial = Self::join_definitely_partial(baseline, conditional); + baseline.availability = baseline.availability.join(conditional.availability); + baseline.moved_places = moved_places; + baseline.definitely_partial = definitely_partial; } - if !declaration.type_parameters.is_empty() { - continue; + } + } + + fn join_branches( + target: &mut BTreeMap, + then_scope: &BTreeMap, + else_scope: &BTreeMap, + ids: &[ValueId], + ) { + for id in ids { + if let (Some(target), Some(then_value), Some(else_value)) = + (target.get_mut(id), then_scope.get(id), else_scope.get(id)) + { + target.availability = then_value.availability.join(else_value.availability); + target.moved_places = Self::join_moved_places(then_value, else_value); + target.definitely_partial = Self::join_definitely_partial(then_value, else_value); } - let ty = ResolvedType::Nominal { - declaration: DeclarationId::new(declaration.stable_id.clone()), - arguments: Vec::new(), - }; - if self.declarations.type_facts(&ty).is_none() { - return Err(self.error( - "SPX-T217", - format!( - "record `{}` has an illegal by-value recursive layout", - declaration.name - ), - declaration.span, - )); + } + } + + fn place_availability( + binding: &ValidationBinding, + requested: &[PlaceProjection], + ) -> Availability { + if binding.availability != Availability::Available { + return binding.availability; + } + let mut maybe_moved = false; + for (moved, state) in &binding.moved_places { + if path_is_prefix(moved, requested) || path_is_prefix(requested, moved) { + if *state == Availability::Moved { + return Availability::Moved; + } + maybe_moved = true; } } - Ok(()) + if binding + .definitely_partial + .iter() + .any(|partial| path_is_prefix(requested, partial)) + { + return Availability::Moved; + } + if maybe_moved { + Availability::MaybeMoved + } else { + Availability::Available + } } - fn resolve_function( - &self, - function: &crate::ast::Function, - ) -> Result { - let template_id = DeclarationId::new(function.stable_id.clone()); - let function_scope = FunctionExecutionId::Monomorphic(template_id.clone()); - self.resolve_function_in_scope(function, &function_scope, template_id) + fn join_moved_places( + left: &ValidationBinding, + right: &ValidationBinding, + ) -> BTreeMap, Availability> { + left.moved_places + .keys() + .chain(right.moved_places.keys()) + .cloned() + .collect::>() + .into_iter() + .filter_map(|path| { + let left = left + .moved_places + .get(&path) + .copied() + .unwrap_or(Availability::Available); + let right = right + .moved_places + .get(&path) + .copied() + .unwrap_or(Availability::Available); + let state = left.join(right); + (state != Availability::Available).then_some((path, state)) + }) + .collect() } - fn resolve_function_template( - &self, - function: &crate::ast::Function, - ) -> Result { - let function_id = DeclarationId::new(function.stable_id.clone()); - let function_scope = FunctionExecutionId::Monomorphic(function_id.clone()); - let type_parameters = function - .type_parameters - .iter() - .enumerate() - .map(|(index, parameter)| { - Ok(ResolvedTypeParameterDeclaration { - name: parameter.name.clone(), - index: u32::try_from(index).map_err(|_| { - self.error( - "SPX-H006", - format!("function `{}` has too many type parameters", function.name), - parameter.span, - ) - })?, - span: parameter.span, - }) + fn join_definitely_partial( + left: &ValidationBinding, + right: &ValidationBinding, + ) -> BTreeSet> { + let mut candidates = BTreeSet::new(); + for path in left + .moved_places + .keys() + .chain(right.moved_places.keys()) + .chain(left.definitely_partial.iter()) + .chain(right.definitely_partial.iter()) + { + for length in 0..=path.len() { + candidates.insert(path[..length].to_vec()); + } + } + candidates + .into_iter() + .filter(|path| { + Self::place_availability(left, path) == Availability::Moved + && Self::place_availability(right, path) == Availability::Moved }) - .collect::, Diagnostic>>()?; - let mut bindings = BTreeMap::new(); - let params = function - .params - .iter() - .enumerate() - .map(|(index, param)| { - let ty = self.resolve_function_type(function, ¶m.ty, param.span)?; - let id = ValueId::parameter(&function_scope, index); - bindings.insert( - param.name.clone(), - Binding { - id: id.clone(), - ty: ty.clone(), - ownership: OwnershipMode::Value, + .collect() + } + + fn validate_type(&self, ty: &ResolvedType) -> Result<(), Diagnostic> { + enum Frame<'a> { + Enter(&'a ResolvedType), + Finish(&'a ResolvedType), + } + let mut frames = vec![Frame::Enter(ty)]; + while let Some(frame) = frames.pop() { + match frame { + Frame::Enter(ResolvedType::Unit | ResolvedType::I64 | ResolvedType::Bool) => {} + Frame::Enter(ResolvedType::TypeParameter { .. }) => { + return Err(hir_error( + "uninstantiated type parameters are not valid in executable HIR", + )); + } + Frame::Enter( + ty @ ResolvedType::Nominal { + declaration, + arguments, }, - ); - Ok(ResolvedParam { - id, - name: param.name.clone(), - ownership: OwnershipMode::Value, - ty, - span: param.span, - }) - }) - .collect::, Diagnostic>>()?; - let return_type = - self.resolve_function_type(function, &function.return_type, function.span)?; - let result_id = ValueId::result(&function_scope); - let requires = function - .requires - .iter() - .enumerate() - .map(|(index, expression)| { - self.resolve_expr( - &function_scope, - expression, - &bindings, - &format!("requires.{index}"), - ) - }) - .collect::>()?; - let body = self.resolve_expr(&function_scope, &function.body, &bindings, "body")?; - let mut ensures_bindings = bindings; - ensures_bindings.insert( - "result".to_owned(), - Binding { - id: result_id.clone(), - ty: return_type.clone(), - ownership: OwnershipMode::Value, - }, - ); - let ensures = function - .ensures - .iter() - .enumerate() - .map(|(index, expression)| { - self.resolve_expr( - &function_scope, - expression, - &ensures_bindings, - &format!("ensures.{index}"), - ) - }) - .collect::>()?; - Ok(ResolvedFunctionTemplate { - id: function_id, - name: function.name.clone(), - type_parameters, - params, - result_id, - return_type, - effects: function.effects.clone(), - requires, - ensures, - body, - span: function.span, - }) + ) => { + let kind = self + .program + .declarations + .declaration(declaration) + .map(|item| item.kind) + .filter(|kind| { + matches!( + kind, + DeclarationKind::Resource + | DeclarationKind::Record + | DeclarationKind::Variant + ) + }) + .ok_or_else(|| { + hir_error(format!( + "nominal type `{declaration}` is not a resolved type declaration" + )) + })?; + let parameters = self + .program + .declarations + .type_parameters(declaration) + .ok_or_else(|| { + hir_error(format!("nominal type `{declaration}` has no parameters")) + })?; + if arguments.len() != parameters.len() { + return Err(hir_error(format!( + "nominal type `{declaration}` has incorrect argument arity" + ))); + } + if !arguments.is_empty() + && (!matches!(kind, DeclarationKind::Record | DeclarationKind::Variant) + || arguments.iter().any(|argument| { + !matches!(argument, ResolvedType::I64 | ResolvedType::Bool) + })) + { + return Err(hir_error(format!( + "nominal type `{declaration}` has unsupported generic arguments" + ))); + } + frames.push(Frame::Finish(ty)); + for argument in arguments.iter().rev() { + frames.push(Frame::Enter(argument)); + } + } + Frame::Finish(ty) => { + self.program.declarations.type_facts(ty).ok_or_else(|| { + hir_error(format!( + "type `{}` has no semantic facts", + ty.identity_key() + )) + })?; + } + } + } + Ok(()) } - fn discover_function_instances(&self) -> Result, Diagnostic> { - let mut calls = Vec::new(); - for function in self - .program - .functions - .iter() - .filter(|function| function.type_parameters.is_empty()) + fn validate_declared_ownership( + &self, + ty: &ResolvedType, + ownership: OwnershipMode, + ) -> Result<(), Diagnostic> { + let facts = self.program.declarations.type_facts(ty).ok_or_else(|| { + hir_error(format!( + "type `{}` has no semantic facts", + ty.identity_key() + )) + })?; + if (facts.copy && ownership != OwnershipMode::Value) + || (!facts.copy && ownership == OwnershipMode::Value) { - for expression in function - .requires - .iter() - .chain(std::iter::once(&function.body)) - .chain(&function.ensures) - { - expression.visit_call_instances(&mut |name, arguments, span| { - calls.push((name.to_owned(), arguments.to_vec(), span)); - }); - } + return Err(hir_error(format!( + "type `{}` has an invalid ownership mode", + ty.identity_key() + ))); } + Ok(()) + } - let mut seen = BTreeSet::new(); - let mut instances = Vec::new(); - for (name, source_arguments, span) in calls { - let Some(template) = self - .program - .functions - .iter() - .find(|function| function.name == name && !function.type_parameters.is_empty()) - else { - continue; - }; - let type_arguments = source_arguments - .iter() - .map(|argument| self.resolve_type(argument, span)) - .collect::, _>>()?; - let template_id = DeclarationId::new(template.stable_id.clone()); - let id = FunctionInstanceId::derive(&template_id, &type_arguments); - if !seen.insert(id.clone()) { - continue; + fn validate_argument_ownership( + &self, + actual: OwnershipMode, + param: &ResolvedParam, + ) -> Result<(), Diagnostic> { + let facts = self + .program + .declarations + .type_facts(¶m.ty) + .ok_or_else(|| { + hir_error(format!( + "type `{}` has no semantic facts", + param.ty.identity_key() + )) + })?; + let valid = if facts.copy { + actual == OwnershipMode::Value && param.ownership == OwnershipMode::Value + } else { + match param.ownership { + OwnershipMode::Own => actual == OwnershipMode::Own, + OwnershipMode::Borrow => true, + OwnershipMode::Shared => actual == OwnershipMode::Shared, + OwnershipMode::Value => false, } - let specialized = - specialize_source_function(template, &source_arguments).ok_or_else(|| { - self.error( - "SPX-H006", - format!("generic function `{}` specialization failed", template.name), - span, - ) - })?; - let execution = FunctionExecutionId::Generic(id.clone()); - let function = - self.resolve_function_in_scope(&specialized, &execution, template_id.clone())?; - instances.push(ResolvedFunctionInstance { - id, - template: template_id, - type_arguments, - function, - }); + }; + if valid { + Ok(()) + } else { + Err(hir_error(format!( + "argument ownership is incompatible with parameter `{}`", + param.id + ))) } - Ok(instances) } - fn resolve_function_in_scope( + fn expected_ownership( &self, - function: &crate::ast::Function, - function_scope: &FunctionExecutionId, - function_id: DeclarationId, - ) -> Result { - let mut bindings = BTreeMap::new(); - let params = function - .params - .iter() - .enumerate() - .map(|(index, param)| { - let ty = self.resolve_type(¶m.ty, param.span)?; - let id = ValueId::parameter(function_scope, index); - let ownership = param.mode.into(); - bindings.insert( - param.name.clone(), - Binding { - id: id.clone(), - ty: ty.clone(), - ownership, - }, - ); - Ok(ResolvedParam { - id, - name: param.name.clone(), - ownership, - ty, - span: param.span, - }) - }) - .collect::, Diagnostic>>()?; - let return_type = self.resolve_type(&function.return_type, function.span)?; - let result_id = ValueId::result(function_scope); - - let requires = function - .requires - .iter() - .enumerate() - .map(|(index, expression)| { - self.resolve_expr( - function_scope, - expression, - &bindings, - &format!("requires.{index}"), - ) + ty: &ResolvedType, + non_copy: OwnershipMode, + ) -> Result { + self.program + .declarations + .type_facts(ty) + .map(|facts| { + if facts.copy { + OwnershipMode::Value + } else { + non_copy + } }) - .collect::>()?; - let body = self.resolve_expr(function_scope, &function.body, &bindings, "body")?; + .ok_or_else(|| { + hir_error(format!( + "type `{}` has no semantic facts", + ty.identity_key() + )) + }) + } - let mut ensures_bindings = bindings; - ensures_bindings.insert( - "result".to_owned(), - Binding { - id: result_id.clone(), - ty: return_type.clone(), - ownership: self.expression_ownership( - &return_type, - OwnershipMode::Own, - function.span, - )?, - }, - ); - let ensures = function - .ensures + fn require_type( + &self, + actual: &ResolvedType, + expected: &ResolvedType, + context: &str, + ) -> Result<(), Diagnostic> { + if actual == expected { + Ok(()) + } else { + Err(hir_error(format!( + "{context} has inconsistent resolved types" + ))) + } + } + + fn insert_value(&mut self, id: &ValueId) -> Result<(), Diagnostic> { + reject_nul_identity("resolved value", id.as_str())?; + if self.value_ids.insert(id.clone()) { + Ok(()) + } else { + Err(hir_error(format!( + "duplicate resolved value identity `{id}`" + ))) + } + } +} + +fn validate_nul_free_identities(program: &ResolvedProgram) -> Result<(), Diagnostic> { + reject_nul_identity("resolved entry point", program.entrypoint.as_str())?; + + for (key, declaration) in &program.declarations.declarations { + reject_nul_identity("declaration index key", key.as_str())?; + reject_nul_identity( + declaration_identity_subject(declaration.kind), + declaration.id.as_str(), + )?; + if let Some(owner) = &declaration.owner { + reject_nul_identity("resolved declaration owner", owner.as_str())?; + } + } + for id in program.declarations.types_by_name.values() { + reject_nul_identity("resolved type lookup", id.as_str())?; + } + for id in program.declarations.functions_by_name.values() { + reject_nul_identity("resolved function lookup", id.as_str())?; + } + for ((owner, _), field) in &program.declarations.fields_by_owner_name { + reject_nul_identity("resolved field owner lookup", owner.as_str())?; + reject_nul_identity("resolved field lookup", field.as_str())?; + } + for ((owner, _), case) in &program.declarations.cases_by_owner_name { + reject_nul_identity("resolved variant owner lookup", owner.as_str())?; + reject_nul_identity("resolved variant case lookup", case.as_str())?; + } + for (owner, fields) in &program.declarations.record_fields { + reject_nul_identity("resolved record-field owner", owner.as_str())?; + for field in fields { + reject_nul_identity("resolved field", field.id.as_str())?; + audit_resolved_type(&field.ty)?; + } + } + for (owner, cases) in &program.declarations.variant_cases { + reject_nul_identity("resolved variant-case owner", owner.as_str())?; + for case in cases { + reject_nul_identity("resolved variant case", case.id.as_str())?; + for field in &case.fields { + reject_nul_identity("resolved case field", field.id.as_str())?; + audit_resolved_type(&field.ty)?; + } + } + } + for (case, fields) in &program.declarations.case_fields { + reject_nul_identity("resolved case-field owner", case.as_str())?; + for field in fields { + reject_nul_identity("resolved case field", field.id.as_str())?; + audit_resolved_type(&field.ty)?; + } + } + for (key, import) in &program.declarations.imports_by_key { + reject_nul_identity("resolved logical import key", key)?; + reject_nul_identity("resolved import lookup", import.as_str())?; + } + + for declaration in &program.types { + let subject = match declaration.kind { + ResolvedTypeDeclarationKind::Resource { .. } => "resolved resource", + ResolvedTypeDeclarationKind::Record { .. } => "resolved record", + ResolvedTypeDeclarationKind::Variant { .. } => "resolved variant", + }; + reject_nul_identity(subject, declaration.id.as_str())?; + match &declaration.kind { + ResolvedTypeDeclarationKind::Resource { drop } => { + reject_nul_identity("resolved resource lifecycle", drop.id.as_str())?; + if let ResolvedResourceDropKind::Imported { import, import_key } = &drop.kind { + reject_nul_identity("resolved lifecycle import", import.as_str())?; + reject_nul_identity("resolved lifecycle logical import key", import_key)?; + } + } + ResolvedTypeDeclarationKind::Record { fields } => { + for field in fields { + reject_nul_identity("resolved field", field.id.as_str())?; + audit_resolved_type(&field.ty)?; + } + } + ResolvedTypeDeclarationKind::Variant { cases } => { + for case in cases { + reject_nul_identity("resolved variant case", case.id.as_str())?; + for field in &case.fields { + reject_nul_identity("resolved case field", field.id.as_str())?; + audit_resolved_type(&field.ty)?; + } + } + } + } + } + for interface in &program.interfaces { + reject_nul_identity("resolved interface", interface.id.as_str())?; + for import in &interface.imports { + reject_nul_identity("resolved import", import.id.as_str())?; + reject_nul_identity("resolved import owner", import.interface.as_str())?; + reject_nul_identity("resolved logical import key", &import.import_key)?; + for parameter in &import.parameters { + audit_resolved_type(¶meter.ty)?; + } + } + } + for function in &program.functions { + reject_nul_identity("resolved function", function.id.as_str())?; + for parameter in &function.params { + reject_nul_identity("resolved value", parameter.id.as_str())?; + audit_resolved_type(¶meter.ty)?; + } + reject_nul_identity("resolved value", function.result_id.as_str())?; + audit_resolved_type(&function.return_type)?; + for expression in function + .requires .iter() - .enumerate() - .map(|(index, expression)| { - self.resolve_expr( - function_scope, - expression, - &ensures_bindings, - &format!("ensures.{index}"), - ) - }) - .collect::>()?; + .chain(std::iter::once(&function.body)) + .chain(&function.ensures) + { + audit_resolved_expression(expression)?; + } + } + Ok(()) +} - Ok(ResolvedFunction { - id: function_id, - name: function.name.clone(), - params, - result_id, - return_type, - effects: function.effects.clone(), - requires, - ensures, - body, - cleanup: CleanupInventory::unresolved(), - cleanup_plan: CleanupPlan::unresolved(), - span: function.span, - }) +/// Reject target-neutral attached metadata containing identities that cannot +/// cross C-string-backed backend and trace boundaries losslessly. +/// +/// This is intentionally narrower than semantic inventory/plan validation so +/// independent replayers can call it without trusting either canonical builder. +pub(crate) fn validate_attached_identity_references( + program: &ResolvedProgram, +) -> Result<(), Diagnostic> { + for function in &program.functions { + audit_cleanup_inventory(&function.cleanup)?; + audit_cleanup_plan(&function.cleanup_plan)?; } + Ok(()) +} - fn resolve_type(&self, ty: &Type, span: Span) -> Result { +fn audit_resolved_type(root: &ResolvedType) -> Result<(), Diagnostic> { + let mut pending = vec![root]; + while let Some(ty) = pending.pop() { match ty { - Type::I64 => Ok(ResolvedType::I64), - Type::Bool => Ok(ResolvedType::Bool), - Type::Named { name, arguments } => { - let declaration = self.declarations.type_id(name).cloned().ok_or_else(|| { - self.error("SPX-H001", format!("unresolved type `{name}`"), span) - })?; - let arguments = arguments - .iter() - .map(|argument| self.resolve_type(argument, span)) - .collect::, _>>()?; - let parameters = - self.declarations - .type_parameters(&declaration) + ResolvedType::Unit | ResolvedType::I64 | ResolvedType::Bool => {} + ResolvedType::TypeParameter { owner, .. } => { + reject_nul_identity("resolved type-parameter owner", owner.as_str())?; + } + ResolvedType::Nominal { + declaration, + arguments, + } => { + reject_nul_identity("resolved nominal type", declaration.as_str())?; + pending.extend(arguments); + } + } + } + Ok(()) +} + +fn audit_resolved_record_match_pattern( + record: &DeclarationId, + instance: &ResolvedType, + fields: &[ResolvedRecordMatchPatternField], +) -> Result<(), Diagnostic> { + reject_nul_identity("resolved record match", record.as_str())?; + audit_resolved_type(instance)?; + for field in fields { + reject_nul_identity("resolved record match field", field.field.as_str())?; + match &field.pattern { + ResolvedRecordMatchFieldPattern::Binding(binding) => { + reject_nul_identity("resolved record match binding", binding.id.as_str())?; + audit_resolved_type(&binding.ty)?; + } + ResolvedRecordMatchFieldPattern::Wildcard => {} + ResolvedRecordMatchFieldPattern::Record { + record, + instance, + fields, + } => audit_resolved_record_match_pattern(record, instance, fields)?, + } + } + Ok(()) +} + +fn audit_resolved_expression(root: &ResolvedExpr) -> Result<(), Diagnostic> { + let mut pending = vec![root]; + while let Some(expression) = pending.pop() { + reject_nul_identity("resolved expression", expression.id.as_str())?; + audit_resolved_type(&expression.ty)?; + match &expression.kind { + ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) => {} + ResolvedExprKind::Place(place) => audit_hir_place(place)?, + ResolvedExprKind::Call { callee, args, .. } => { + reject_nul_identity("resolved call target", callee.as_str())?; + pending.extend(args); + } + ResolvedExprKind::NativeRustImportCall(call) => { + reject_nul_identity("resolved native Rust import target", call.import.as_str())?; + if call.expression != expression.id { + return Err(hir_error( + "resolved native Rust import call identity is inconsistent", + )); + } + pending.extend(&call.args); + } + ResolvedExprKind::Unary { value, .. } => pending.push(value), + ResolvedExprKind::Binary { left, right, .. } => { + pending.push(right); + pending.push(left); + } + ResolvedExprKind::Block { statements, tail } => { + pending.push(tail); + for statement in statements.iter().rev() { + let ResolvedStatement::Let { binding, value, .. } = statement; + reject_nul_identity("resolved value", binding.id.as_str())?; + audit_resolved_type(&binding.ty)?; + pending.push(value); + } + } + ResolvedExprKind::If { + condition, + then_branch, + else_branch, + } => { + pending.push(else_branch); + pending.push(then_branch); + pending.push(condition); + } + ResolvedExprKind::ConstructRecord { record, fields } => { + reject_nul_identity("resolved record constructor", record.as_str())?; + for field in fields.iter().rev() { + reject_nul_identity("resolved record initializer field", field.field.as_str())?; + pending.push(&field.value); + } + } + ResolvedExprKind::ConstructVariant { + variant, + case, + fields, + } => { + reject_nul_identity("resolved variant constructor", variant.as_str())?; + reject_nul_identity("resolved variant case", case.as_str())?; + for field in fields.iter().rev() { + reject_nul_identity("resolved case initializer field", field.field.as_str())?; + pending.push(&field.value); + } + } + ResolvedExprKind::Match { scrutinee, arms } => { + for arm in arms.iter().rev() { + match &arm.pattern { + ResolvedMatchPattern::Wildcard => {} + ResolvedMatchPattern::Variant { + variant, + case, + fields, + } => { + reject_nul_identity("resolved match variant", variant.as_str())?; + reject_nul_identity("resolved match case", case.as_str())?; + for field in fields { + reject_nul_identity("resolved match field", field.field.as_str())?; + reject_nul_identity( + "resolved match binding", + field.binding.id.as_str(), + )?; + audit_resolved_type(&field.binding.ty)?; + } + } + ResolvedMatchPattern::Record { + record, + instance, + fields, + } => audit_resolved_record_match_pattern(record, instance, fields)?, + } + pending.push(&arm.value); + } + pending.push(scrutinee); + } + ResolvedExprKind::Try { + operand, + result, + ok_case, + ok_field, + err_case, + err_field, + residual_type, + } => { + reject_nul_identity("resolved `?` Result", result.as_str())?; + reject_nul_identity("resolved `?` Ok case", ok_case.as_str())?; + reject_nul_identity("resolved `?` Ok field", ok_field.as_str())?; + reject_nul_identity("resolved `?` Err case", err_case.as_str())?; + reject_nul_identity("resolved `?` Err field", err_field.as_str())?; + audit_resolved_type(residual_type)?; + pending.push(operand); + } + ResolvedExprKind::TryOption { + operand, + option, + some_case, + some_field, + none_case, + residual_type, + } => { + reject_nul_identity("resolved Option `?` Option", option.as_str())?; + reject_nul_identity("resolved Option `?` Some case", some_case.as_str())?; + reject_nul_identity("resolved Option `?` Some field", some_field.as_str())?; + reject_nul_identity("resolved Option `?` None case", none_case.as_str())?; + audit_resolved_type(residual_type)?; + pending.push(operand); + } + ResolvedExprKind::UpdateRecord { + base, + record, + fields, + } => { + reject_nul_identity("resolved record update", record.as_str())?; + for field in fields.iter().rev() { + reject_nul_identity("resolved record replacement field", field.field.as_str())?; + pending.push(&field.value); + } + pending.push(base); + } + ResolvedExprKind::Project { base, field } => { + reject_nul_identity("resolved projected field", field.as_str())?; + pending.push(base); + } + } + } + Ok(()) +} + +fn audit_hir_place(place: &Place) -> Result<(), Diagnostic> { + reject_nul_identity("resolved place root", place.root.as_str())?; + for projection in &place.projections { + match projection { + PlaceProjection::Field(field) => { + reject_nul_identity("resolved place field", field.as_str())?; + } + PlaceProjection::VariantField { case, field } => { + reject_nul_identity("resolved place variant case", case.as_str())?; + reject_nul_identity("resolved place variant field", field.as_str())?; + } + } + } + Ok(()) +} + +fn audit_field_liveness_shape(root: &crate::cleanup::FieldLivenessShape) -> Result<(), Diagnostic> { + let mut pending = vec![root]; + while let Some(shape) = pending.pop() { + match shape { + crate::cleanup::FieldLivenessShape::NoDrop => {} + crate::cleanup::FieldLivenessShape::Leaf { lifecycle, .. } => { + reject_nul_identity("cleanup lifecycle", lifecycle.as_str())?; + } + crate::cleanup::FieldLivenessShape::Record { + declaration, + fields, + } => { + reject_nul_identity("cleanup record", declaration.as_str())?; + for field in fields.iter().rev() { + reject_nul_identity("cleanup field", field.field.as_str())?; + pending.push(&field.shape); + } + } + } + } + Ok(()) +} + +fn audit_inventory_place(place: &crate::cleanup::CleanupPlace) -> Result<(), Diagnostic> { + for projection in &place.projections { + reject_nul_identity("cleanup inventory projection", projection.as_str())?; + } + Ok(()) +} + +fn audit_cleanup_inventory(inventory: &CleanupInventory) -> Result<(), Diagnostic> { + for slot in &inventory.slots { + match &slot.origin { + crate::cleanup::CleanupStorageOrigin::Parameter { value, .. } + | crate::cleanup::CleanupStorageOrigin::Binding { value } + | crate::cleanup::CleanupStorageOrigin::ProvisionalResult { value } => { + reject_nul_identity("cleanup inventory value", value.as_str())?; + } + crate::cleanup::CleanupStorageOrigin::Temporary { expression } => { + reject_nul_identity("cleanup inventory expression", expression.as_str())?; + } + } + audit_resolved_type(&slot.ty)?; + audit_field_liveness_shape(&slot.shape)?; + } + for flag in &inventory.flags { + audit_inventory_place(&flag.place)?; + reject_nul_identity("cleanup inventory lifecycle", flag.lifecycle.as_str())?; + } + Ok(()) +} + +fn audit_plan_storage(storage: &crate::cleanup_plan::StorageId) -> Result<(), Diagnostic> { + match storage { + crate::cleanup_plan::StorageId::Value(value) => { + reject_nul_identity("cleanup-plan value storage", value.as_str())?; + } + crate::cleanup_plan::StorageId::Temporary(expression) => { + reject_nul_identity("cleanup-plan temporary storage", expression.as_str())?; + } + crate::cleanup_plan::StorageId::CallArgument { + call, + value_expression, + .. + } => { + reject_nul_identity("cleanup-plan call-argument call", call.as_str())?; + reject_nul_identity( + "cleanup-plan call-argument value", + value_expression.as_str(), + )?; + } + crate::cleanup_plan::StorageId::ProvisionalResult => {} + } + Ok(()) +} + +fn audit_plan_place(place: &crate::cleanup_plan::CleanupPlace) -> Result<(), Diagnostic> { + audit_plan_storage(&place.storage)?; + for projection in &place.projections { + reject_nul_identity("cleanup-plan projection", projection.as_str())?; + } + Ok(()) +} + +fn audit_status_source(source: &crate::cleanup_plan::StatusSourceId) -> Result<(), Diagnostic> { + reject_nul_identity("cleanup-plan status expression", source.expression.as_str()) +} + +fn audit_result_source( + source: &crate::cleanup_plan::CleanupResultSource, +) -> Result<(), Diagnostic> { + match source { + crate::cleanup_plan::CleanupResultSource::Scalar { expression } => { + reject_nul_identity("cleanup-plan scalar result", expression.as_str())?; + } + crate::cleanup_plan::CleanupResultSource::Owned { storage } => { + audit_plan_place(storage)?; + } + } + Ok(()) +} + +fn audit_cleanup_plan(plan: &CleanupPlan) -> Result<(), Diagnostic> { + for place in &plan.entry_state.live_owned_parameters { + audit_plan_place(place)?; + } + for slot in &plan.slots { + audit_plan_storage(&slot.storage)?; + audit_resolved_type(&slot.ty)?; + audit_field_liveness_shape(&slot.field_liveness_shape)?; + } + for source in &plan.status_sources { + audit_status_source(&source.id)?; + if let crate::cleanup_plan::StatusProducer::PropagatedCall { callee } = &source.producer { + reject_nul_identity("cleanup-plan propagated callee", callee.as_str())?; + } + } + for block in &plan.blocks { + for transition in &block.transitions { + match transition { + crate::cleanup_plan::CleanupTransition::Initialize { at, destination } => { + reject_nul_identity("cleanup-plan initialize expression", at.as_str())?; + audit_plan_place(destination)?; + } + crate::cleanup_plan::CleanupTransition::Transfer { + at, + source, + destination, + } => { + reject_nul_identity("cleanup-plan transfer expression", at.as_str())?; + audit_plan_place(source)?; + audit_plan_place(destination)?; + } + crate::cleanup_plan::CleanupTransition::CallCommit { call, arguments } => { + reject_nul_identity("cleanup-plan committed call", call.as_str())?; + for argument in arguments { + audit_plan_place(&argument.source)?; + } + } + crate::cleanup_plan::CleanupTransition::SelectFailure { source } => { + audit_status_source(source)?; + } + crate::cleanup_plan::CleanupTransition::StageCopyResult { source } => { + match source { + crate::cleanup_plan::StagedCopyResultSource::Body { + expression, + instance, + } => { + reject_nul_identity( + "cleanup-plan staged body expression", + expression.as_str(), + )?; + audit_resolved_type(instance)?; + } + crate::cleanup_plan::StagedCopyResultSource::TryResidual { + expression, + operand, + source_instance, + target_instance, + result, + ok_case, + ok_field, + err_case, + err_field, + } => { + reject_nul_identity( + "cleanup-plan staged `?` expression", + expression.as_str(), + )?; + reject_nul_identity( + "cleanup-plan staged `?` operand", + operand.as_str(), + )?; + audit_resolved_type(source_instance)?; + audit_resolved_type(target_instance)?; + for (kind, declaration) in [ + ("Result", result), + ("Ok case", ok_case), + ("Ok field", ok_field), + ("Err case", err_case), + ("Err field", err_field), + ] { + reject_nul_identity( + &format!("cleanup-plan staged `?` {kind}"), + declaration.as_str(), + )?; + } + } + crate::cleanup_plan::StagedCopyResultSource::TryOptionNone { + expression, + operand, + source_instance, + target_instance, + option, + some_case, + some_field, + none_case, + } => { + reject_nul_identity( + "cleanup-plan staged Option `?` expression", + expression.as_str(), + )?; + reject_nul_identity( + "cleanup-plan staged Option `?` operand", + operand.as_str(), + )?; + audit_resolved_type(source_instance)?; + audit_resolved_type(target_instance)?; + for (kind, declaration) in [ + ("Option", option), + ("Some case", some_case), + ("Some field", some_field), + ("None case", none_case), + ] { + reject_nul_identity( + &format!("cleanup-plan staged Option `?` {kind}"), + declaration.as_str(), + )?; + } + } + } + } + } + } + } + for edge in &plan.edges { + match &edge.condition { + crate::cleanup_plan::EdgeCondition::Always => {} + crate::cleanup_plan::EdgeCondition::BooleanResult(expression, _) => { + reject_nul_identity("cleanup-plan boolean expression", expression.as_str())?; + } + crate::cleanup_plan::EdgeCondition::VariantCase { + scrutinee, case, .. + } => { + reject_nul_identity("cleanup-plan match scrutinee", scrutinee.as_str())?; + reject_nul_identity("cleanup-plan variant case", case.as_str())?; + } + crate::cleanup_plan::EdgeCondition::StatusZero(source) + | crate::cleanup_plan::EdgeCondition::StatusNonzero(source) => { + audit_status_source(source)?; + } + } + } + for region in &plan.regions { + for storage in ®ion.slots { + audit_plan_storage(storage)?; + } + } + for exit in &plan.exits { + for finalizer in &exit.finalize_in_order { + audit_plan_place(&finalizer.source)?; + reject_nul_identity( + "cleanup-plan finalizer lifecycle", + finalizer.lifecycle_id.as_str(), + )?; + } + match &exit.continuation { + crate::cleanup_plan::ExitContinuation::Continue(_) + | crate::cleanup_plan::ExitContinuation::ReturnUnit => {} + crate::cleanup_plan::ExitContinuation::CommitResult { source } => { + audit_result_source(source)?; + } + crate::cleanup_plan::ExitContinuation::ReturnFailure { source } => { + audit_status_source(source)?; + } + } + } + Ok(()) +} + +fn declaration_identity_subject(kind: DeclarationKind) -> &'static str { + match kind { + DeclarationKind::Resource => "resolved resource declaration", + DeclarationKind::ResourceDrop => "resolved resource lifecycle declaration", + DeclarationKind::Record => "resolved record declaration", + DeclarationKind::Field => "resolved field declaration", + DeclarationKind::Variant => "resolved variant declaration", + DeclarationKind::VariantCase => "resolved variant case declaration", + DeclarationKind::CaseField => "resolved case field declaration", + DeclarationKind::Interface => "resolved interface declaration", + DeclarationKind::Import => "resolved import declaration", + DeclarationKind::Function => "resolved function declaration", + } +} + +fn reject_nul_identity(subject: &str, value: &str) -> Result<(), Diagnostic> { + if value.contains('\0') { + Err(hir_error(format!("{subject} identity contains NUL"))) + } else { + Ok(()) + } +} + +fn path_is_prefix(prefix: &[T], path: &[T]) -> bool { + prefix.len() <= path.len() && prefix.iter().zip(path).all(|(left, right)| left == right) +} + +fn resolved_lifecycle_effects( + program: &ResolvedProgram, + ty: &ResolvedType, +) -> Result, Diagnostic> { + fn collect( + program: &ResolvedProgram, + ty: &ResolvedType, + visiting: &mut BTreeSet, + effects: &mut BTreeSet, + ) -> Result<(), Diagnostic> { + let Some(id) = ty.nominal_id() else { + return Ok(()); + }; + if !visiting.insert(id.clone()) { + return Ok(()); + } + let declaration = program + .types + .iter() + .find(|item| item.id == *id) + .ok_or_else(|| hir_error(format!("type `{id}` has no lifecycle declaration")))?; + match &declaration.kind { + ResolvedTypeDeclarationKind::Resource { drop } => { + if let ResolvedResourceDropKind::Imported { import, .. } = &drop.kind { + let resolved = program + .interfaces + .iter() + .flat_map(|interface| &interface.imports) + .find(|item| item.id == *import) + .ok_or_else(|| { + hir_error(format!( + "resource `{id}` references missing import `{import}`" + )) + })?; + effects.extend(resolved.effects.iter().cloned()); + } + } + ResolvedTypeDeclarationKind::Record { fields } => { + for field in fields { + collect(program, &field.ty, visiting, effects)?; + } + } + ResolvedTypeDeclarationKind::Variant { cases } => { + for case in cases { + for field in &case.fields { + collect(program, &field.ty, visiting, effects)?; + } + } + } + } + visiting.remove(id); + Ok(()) + } + + let mut effects = BTreeSet::new(); + collect(program, ty, &mut BTreeSet::new(), &mut effects)?; + Ok(effects) +} + +fn visit_resolved_calls( + expression: &ResolvedExpr, + visit: &mut impl FnMut(&DeclarationId, Option<&FunctionInstanceId>, &[ResolvedType]), +) { + match &expression.kind { + ResolvedExprKind::Call { + callee, + instance, + type_arguments, + args, + } => { + visit(callee, instance.as_ref(), type_arguments); + for arg in args { + visit_resolved_calls(arg, visit); + } + } + ResolvedExprKind::NativeRustImportCall(call) => { + for arg in &call.args { + visit_resolved_calls(arg, visit); + } + } + ResolvedExprKind::Unary { value, .. } + | ResolvedExprKind::Try { operand: value, .. } + | ResolvedExprKind::TryOption { operand: value, .. } + | ResolvedExprKind::Project { base: value, .. } => visit_resolved_calls(value, visit), + ResolvedExprKind::Binary { left, right, .. } => { + visit_resolved_calls(left, visit); + visit_resolved_calls(right, visit); + } + ResolvedExprKind::Block { statements, tail } => { + for statement in statements { + match statement { + ResolvedStatement::Let { value, .. } => visit_resolved_calls(value, visit), + } + } + visit_resolved_calls(tail, visit); + } + ResolvedExprKind::If { + condition, + then_branch, + else_branch, + } => { + visit_resolved_calls(condition, visit); + visit_resolved_calls(then_branch, visit); + visit_resolved_calls(else_branch, visit); + } + ResolvedExprKind::ConstructRecord { fields, .. } => { + for field in fields { + visit_resolved_calls(&field.value, visit); + } + } + ResolvedExprKind::ConstructVariant { fields, .. } => { + for field in fields { + visit_resolved_calls(&field.value, visit); + } + } + ResolvedExprKind::Match { scrutinee, arms } => { + visit_resolved_calls(scrutinee, visit); + for arm in arms { + visit_resolved_calls(&arm.value, visit); + } + } + ResolvedExprKind::UpdateRecord { base, fields, .. } => { + visit_resolved_calls(base, visit); + for field in fields { + visit_resolved_calls(&field.value, visit); + } + } + ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) | ResolvedExprKind::Place(_) => {} + } +} + +#[allow(dead_code, reason = "private Workspace Semantic Graph Phase-A seam")] +pub(crate) fn workspace_call_edges( + program: &ResolvedProgram, +) -> BTreeSet<(DeclarationId, DeclarationId)> { + let mut edges = BTreeSet::new(); + for function in &program.functions { + for expression in function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures) + { + visit_resolved_calls(expression, &mut |callee, _, _| { + edges.insert((function.id.clone(), callee.clone())); + }); + } + } + edges +} + +#[allow(dead_code, reason = "private Workspace Semantic Graph Phase-A seam")] +pub(crate) fn workspace_expression_identity(owner: &DeclarationId, path: &str) -> String { + ExpressionId::new(&FunctionExecutionId::Monomorphic(owner.clone()), path) + .as_str() + .to_owned() +} + +#[allow(dead_code, reason = "private Workspace Semantic Graph Phase-A seam")] +pub(crate) fn workspace_call_sites( + program: &ResolvedProgram, +) -> Vec<(DeclarationId, String, DeclarationId)> { + fn walk( + owner: &DeclarationId, + expression: &ResolvedExpr, + sites: &mut Vec<(DeclarationId, String, DeclarationId)>, + ) { + match &expression.kind { + ResolvedExprKind::Call { callee, args, .. } => { + sites.push(( + owner.clone(), + expression.id.as_str().to_owned(), + callee.clone(), + )); + for argument in args { + walk(owner, argument, sites); + } + } + ResolvedExprKind::NativeRustImportCall(call) => { + for argument in &call.args { + walk(owner, argument, sites); + } + } + ResolvedExprKind::Unary { value, .. } + | ResolvedExprKind::Try { operand: value, .. } + | ResolvedExprKind::TryOption { operand: value, .. } + | ResolvedExprKind::Project { base: value, .. } => walk(owner, value, sites), + ResolvedExprKind::Binary { left, right, .. } => { + walk(owner, left, sites); + walk(owner, right, sites); + } + ResolvedExprKind::Block { statements, tail } => { + for statement in statements { + match statement { + ResolvedStatement::Let { value, .. } => walk(owner, value, sites), + } + } + walk(owner, tail, sites); + } + ResolvedExprKind::If { + condition, + then_branch, + else_branch, + } => { + walk(owner, condition, sites); + walk(owner, then_branch, sites); + walk(owner, else_branch, sites); + } + ResolvedExprKind::ConstructRecord { fields, .. } + | ResolvedExprKind::ConstructVariant { fields, .. } => { + for field in fields { + walk(owner, &field.value, sites); + } + } + ResolvedExprKind::Match { scrutinee, arms } => { + walk(owner, scrutinee, sites); + for arm in arms { + walk(owner, &arm.value, sites); + } + } + ResolvedExprKind::UpdateRecord { base, fields, .. } => { + walk(owner, base, sites); + for field in fields { + walk(owner, &field.value, sites); + } + } + ResolvedExprKind::Int(_) | ResolvedExprKind::Bool(_) | ResolvedExprKind::Place(_) => {} + } + } + + let mut sites = Vec::new(); + for function in &program.functions { + for expression in function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures) + { + walk(&function.id, expression, &mut sites); + } + } + for template in &program.function_templates { + for expression in template + .requires + .iter() + .chain(std::iter::once(&template.body)) + .chain(&template.ensures) + { + walk(&template.id, expression, &mut sites); + } + } + sites +} + +fn hir_error(message: impl Into) -> Diagnostic { + Diagnostic::io("SPX-H006", message) +} + +struct Resolver<'a> { + program: &'a Program, + declarations: DeclarationIndex, +} + +impl Resolver<'_> { + fn resolve(self) -> Result { + let entrypoint = self + .program + .functions + .iter() + .find(|function| function.name == "main") + .map(|function| DeclarationId::new(function.stable_id.clone())) + .ok_or_else(|| { + self.error( + "SPX-H005", + "verified program has no resolved entry point", + Span::default(), + ) + })?; + self.validate_record_layouts()?; + let types = self + .program + .types + .iter() + .chain(crate::prelude::declarations()) + .map(|declaration| { + let id = DeclarationId::new(declaration.stable_id.clone()); + let kind = match &declaration.kind { + TypeDeclarationKind::Resource { lifecycles } => { + let lifecycle = lifecycles.first().ok_or_else(|| { + self.error( + "SPX-H006", + format!("resource `{id}` has no resolved lifecycle"), + declaration.span, + ) + })?; + let lifecycle_id = DeclarationId::new( + lifecycle.stable_id.clone().ok_or_else(|| { + self.error( + "SPX-H006", + format!("resource `{id}` lifecycle has no identity"), + lifecycle.span, + ) + })?, + ); + let drop_kind = match &lifecycle.kind { + ResourceLifecycleKind::Trivial => ResolvedResourceDropKind::Trivial, + ResourceLifecycleKind::Imported { import_key } => { + let import = self + .declarations + .import_id(import_key) + .cloned() + .ok_or_else(|| { + self.error( + "SPX-H006", + format!( + "resource `{id}` lifecycle references unknown import key `{import_key}`" + ), + lifecycle.span, + ) + })?; + ResolvedResourceDropKind::Imported { + import, + import_key: import_key.clone(), + } + } + }; + ResolvedTypeDeclarationKind::Resource { + drop: ResolvedResourceDrop { + id: lifecycle_id, + kind: drop_kind, + }, + } + } + TypeDeclarationKind::Record { .. } => { + let fields = self + .declarations + .record_fields(&id) + .ok_or_else(|| { + self.error( + "SPX-H006", + format!("record `{id}` has no resolved fields"), + declaration.span, + ) + })? + .to_vec(); + ResolvedTypeDeclarationKind::Record { fields } + } + TypeDeclarationKind::Variant { .. } => { + let cases = self + .declarations + .variant_cases(&id) + .ok_or_else(|| { + self.error( + "SPX-H006", + format!("variant `{id}` has no resolved cases"), + declaration.span, + ) + })? + .to_vec(); + ResolvedTypeDeclarationKind::Variant { cases } + } + }; + Ok(ResolvedTypeDeclaration { + type_parameters: self + .declarations + .type_parameters(&id) + .ok_or_else(|| { + self.error( + "SPX-H006", + format!("type `{id}` has no parameter metadata"), + declaration.span, + ) + })? + .to_vec(), + id, + name: declaration.name.clone(), + kind, + span: declaration.span, + }) + }) + .collect::, Diagnostic>>()?; + let interfaces = self + .program + .interfaces + .iter() + .map(|interface| { + let interface_id = DeclarationId::new(interface.stable_id.clone()); + let imports = interface + .imports + .iter() + .map(|import| { + let parameters = import + .params + .iter() + .map(|param| { + Ok(ResolvedImportParameter { + name: param.name.clone(), + ty: self.resolve_type(¶m.ty, param.span)?, + ownership: param.mode.into(), + consumes_on_failure: param.name == import.consumes, + }) + }) + .collect::, Diagnostic>>()?; + let failure = match &import.failure { + ImportFailure::Infallible => ResolvedImportFailure::Infallible, + ImportFailure::Status { domain_id } => ResolvedImportFailure::Status { + domain_id: domain_id.clone(), + normalization: "semaprax.status.v1", + }, + }; + Ok(ResolvedImport { + id: DeclarationId::new(import.stable_id.clone()), + name: import.name.clone(), + interface: interface_id.clone(), + import_key: import.stable_id.clone(), + native_rust: import.native_rust, + parameters, + result: ResolvedImportResult { + kind: match import.result { + crate::ast::ImportResult::Unit => { + ResolvedImportResultKind::Unit + } + crate::ast::ImportResult::I64 => ResolvedImportResultKind::I64, + crate::ast::ImportResult::Bool => { + ResolvedImportResultKind::Bool + } + }, + ownership: OwnershipMode::Value, + producer: "callee", + out_slot_initialization: "success_only", + ownership_transfer: "final_zero_status_commit", + }, + effects: import.effects.clone(), + required_authority: import.effects.clone(), + failure, + span: import.span, + }) + }) + .collect::, Diagnostic>>()?; + Ok(ResolvedInterface { + id: interface_id, + name: interface.name.clone(), + permits: interface.permits.clone(), + imports, + span: interface.span, + }) + }) + .collect::, Diagnostic>>()?; + let functions = self + .program + .functions + .iter() + .filter(|function| function.type_parameters.is_empty()) + .map(|function| self.resolve_function(function)) + .collect::>()?; + let function_templates = self + .program + .functions + .iter() + .filter(|function| !function.type_parameters.is_empty()) + .map(|function| self.resolve_function_template(function)) + .collect::>()?; + let function_instances = self.discover_function_instances()?; + let mut resolved = ResolvedProgram { + module: self.program.module.clone(), + permits: self.program.permits.clone(), + entrypoint, + declarations: self.declarations, + types, + interfaces, + function_templates, + functions, + function_instances, + }; + let inventories = resolved + .functions + .iter() + .map(|function| crate::cleanup::build_inventory(&resolved, function)) + .collect::, _>>()?; + for (function, inventory) in resolved.functions.iter_mut().zip(inventories) { + function.cleanup = inventory; + } + let instance_inventories = resolved + .function_instances + .iter() + .map(|instance| crate::cleanup::build_inventory(&resolved, &instance.function)) + .collect::, _>>()?; + for (instance, inventory) in resolved + .function_instances + .iter_mut() + .zip(instance_inventories) + { + instance.function.cleanup = inventory; + } + let cleanup_plans = resolved + .functions + .iter() + .map(|function| crate::cleanup_plan::build_plan(&resolved, function)) + .collect::, _>>()?; + for (function, cleanup_plan) in resolved.functions.iter_mut().zip(cleanup_plans) { + function.cleanup_plan = cleanup_plan; + } + let instance_cleanup_plans = resolved + .function_instances + .iter() + .map(|instance| crate::cleanup_plan::build_plan(&resolved, &instance.function)) + .collect::, _>>()?; + for (instance, cleanup_plan) in resolved + .function_instances + .iter_mut() + .zip(instance_cleanup_plans) + { + instance.function.cleanup_plan = cleanup_plan; + } + validate(&resolved)?; + Ok(resolved) + } + + fn validate_record_layouts(&self) -> Result<(), Diagnostic> { + for declaration in &self.program.types { + if !matches!(&declaration.kind, TypeDeclarationKind::Record { .. }) { + continue; + } + if !declaration.type_parameters.is_empty() { + continue; + } + let ty = ResolvedType::Nominal { + declaration: DeclarationId::new(declaration.stable_id.clone()), + arguments: Vec::new(), + }; + if self.declarations.type_facts(&ty).is_none() { + return Err(self.error( + "SPX-T217", + format!( + "record `{}` has an illegal by-value recursive layout", + declaration.name + ), + declaration.span, + )); + } + } + Ok(()) + } + + fn resolve_function( + &self, + function: &crate::ast::Function, + ) -> Result { + let template_id = DeclarationId::new(function.stable_id.clone()); + let function_scope = FunctionExecutionId::Monomorphic(template_id.clone()); + self.resolve_function_in_scope(function, &function_scope, template_id) + } + + fn resolve_function_template( + &self, + function: &crate::ast::Function, + ) -> Result { + let function_id = DeclarationId::new(function.stable_id.clone()); + let function_scope = FunctionExecutionId::Monomorphic(function_id.clone()); + let type_parameters = function + .type_parameters + .iter() + .enumerate() + .map(|(index, parameter)| { + Ok(ResolvedTypeParameterDeclaration { + name: parameter.name.clone(), + index: u32::try_from(index).map_err(|_| { + self.error( + "SPX-H006", + format!("function `{}` has too many type parameters", function.name), + parameter.span, + ) + })?, + span: parameter.span, + }) + }) + .collect::, Diagnostic>>()?; + let mut bindings = BTreeMap::new(); + let params = function + .params + .iter() + .enumerate() + .map(|(index, param)| { + let ty = self.resolve_function_type(function, ¶m.ty, param.span)?; + let id = ValueId::parameter(&function_scope, index); + bindings.insert( + param.name.clone(), + Binding { + id: id.clone(), + ty: ty.clone(), + ownership: OwnershipMode::Value, + }, + ); + Ok(ResolvedParam { + id, + name: param.name.clone(), + ownership: OwnershipMode::Value, + ty, + span: param.span, + }) + }) + .collect::, Diagnostic>>()?; + let return_type = + self.resolve_function_type(function, &function.return_type, function.span)?; + let result_id = ValueId::result(&function_scope); + let requires = function + .requires + .iter() + .enumerate() + .map(|(index, expression)| { + self.resolve_expr( + &function_scope, + expression, + &bindings, + &format!("requires.{index}"), + ) + }) + .collect::>()?; + let body = self.resolve_expr(&function_scope, &function.body, &bindings, "body")?; + let mut ensures_bindings = bindings; + ensures_bindings.insert( + "result".to_owned(), + Binding { + id: result_id.clone(), + ty: return_type.clone(), + ownership: OwnershipMode::Value, + }, + ); + let ensures = function + .ensures + .iter() + .enumerate() + .map(|(index, expression)| { + self.resolve_expr( + &function_scope, + expression, + &ensures_bindings, + &format!("ensures.{index}"), + ) + }) + .collect::>()?; + Ok(ResolvedFunctionTemplate { + id: function_id, + name: function.name.clone(), + type_parameters, + params, + result_id, + return_type, + effects: function.effects.clone(), + requires, + ensures, + body, + span: function.span, + }) + } + + fn discover_function_instances(&self) -> Result, Diagnostic> { + let mut calls = Vec::new(); + for function in self + .program + .functions + .iter() + .filter(|function| function.type_parameters.is_empty()) + { + for expression in function + .requires + .iter() + .chain(std::iter::once(&function.body)) + .chain(&function.ensures) + { + expression.visit_call_instances(&mut |name, arguments, span| { + calls.push((name.to_owned(), arguments.to_vec(), span)); + }); + } + } + + let mut seen = BTreeSet::new(); + let mut instances = Vec::new(); + for (name, source_arguments, span) in calls { + let Some(template) = self + .program + .functions + .iter() + .find(|function| function.name == name && !function.type_parameters.is_empty()) + else { + continue; + }; + let type_arguments = source_arguments + .iter() + .map(|argument| self.resolve_type(argument, span)) + .collect::, _>>()?; + let template_id = DeclarationId::new(template.stable_id.clone()); + let id = FunctionInstanceId::derive(&template_id, &type_arguments); + if !seen.insert(id.clone()) { + continue; + } + let specialized = + specialize_source_function(template, &source_arguments).ok_or_else(|| { + self.error( + "SPX-H006", + format!("generic function `{}` specialization failed", template.name), + span, + ) + })?; + let execution = FunctionExecutionId::Generic(id.clone()); + let function = + self.resolve_function_in_scope(&specialized, &execution, template_id.clone())?; + instances.push(ResolvedFunctionInstance { + id, + template: template_id, + type_arguments, + function, + }); + } + Ok(instances) + } + + fn resolve_function_in_scope( + &self, + function: &crate::ast::Function, + function_scope: &FunctionExecutionId, + function_id: DeclarationId, + ) -> Result { + let mut bindings = BTreeMap::new(); + let params = function + .params + .iter() + .enumerate() + .map(|(index, param)| { + let ty = self.resolve_type(¶m.ty, param.span)?; + let id = ValueId::parameter(function_scope, index); + let ownership = param.mode.into(); + bindings.insert( + param.name.clone(), + Binding { + id: id.clone(), + ty: ty.clone(), + ownership, + }, + ); + Ok(ResolvedParam { + id, + name: param.name.clone(), + ownership, + ty, + span: param.span, + }) + }) + .collect::, Diagnostic>>()?; + let return_type = self.resolve_type(&function.return_type, function.span)?; + let result_id = ValueId::result(function_scope); + + let requires = function + .requires + .iter() + .enumerate() + .map(|(index, expression)| { + self.resolve_expr( + function_scope, + expression, + &bindings, + &format!("requires.{index}"), + ) + }) + .collect::>()?; + let body = self.resolve_expr(function_scope, &function.body, &bindings, "body")?; + + let mut ensures_bindings = bindings; + ensures_bindings.insert( + "result".to_owned(), + Binding { + id: result_id.clone(), + ty: return_type.clone(), + ownership: self.expression_ownership( + &return_type, + OwnershipMode::Own, + function.span, + )?, + }, + ); + let ensures = function + .ensures + .iter() + .enumerate() + .map(|(index, expression)| { + self.resolve_expr( + function_scope, + expression, + &ensures_bindings, + &format!("ensures.{index}"), + ) + }) + .collect::>()?; + + Ok(ResolvedFunction { + id: function_id, + name: function.name.clone(), + params, + result_id, + return_type, + effects: function.effects.clone(), + requires, + ensures, + body, + cleanup: CleanupInventory::unresolved(), + cleanup_plan: CleanupPlan::unresolved(), + span: function.span, + }) + } + + fn resolve_type(&self, ty: &Type, span: Span) -> Result { + enum Frame<'a> { + Enter(&'a Type), + Arguments { + declaration: DeclarationId, + arguments: &'a [Type], + index: usize, + resolved: Vec, + }, + } + let mut frames = vec![Frame::Enter(ty)]; + let mut result = None; + while let Some(frame) = frames.pop() { + match frame { + Frame::Enter(Type::I64) => result = Some(ResolvedType::I64), + Frame::Enter(Type::Bool) => result = Some(ResolvedType::Bool), + Frame::Enter(Type::Named { name, arguments }) => { + let declaration = + self.declarations.type_id(name).cloned().ok_or_else(|| { + self.error("SPX-H001", format!("unresolved type `{name}`"), span) + })?; + frames.push(Frame::Arguments { + declaration, + arguments, + index: 0, + resolved: Vec::with_capacity(arguments.len()), + }); + } + Frame::Arguments { + declaration, + arguments, + index, + mut resolved, + } => { + if index != 0 { + resolved.push(result.take().expect("resolved child type retained")); + } + if let Some(argument) = arguments.get(index) { + frames.push(Frame::Arguments { + declaration, + arguments, + index: index + 1, + resolved, + }); + frames.push(Frame::Enter(argument)); + } else { + let parameters = self + .declarations + .type_parameters(&declaration) + .ok_or_else(|| { + self.error( + "SPX-H006", + format!("type `{declaration}` has no parameter metadata"), + span, + ) + })?; + if resolved.len() != parameters.len() + || (!resolved.is_empty() + && resolved.iter().any(|argument| { + !matches!(argument, ResolvedType::I64 | ResolvedType::Bool) + })) + { + return Err(self.error( + "SPX-H006", + format!("type `{declaration}` has invalid concrete arguments"), + span, + )); + } + result = Some(ResolvedType::Nominal { + declaration, + arguments: resolved, + }); + } + } + } + } + Ok(result.expect("root type resolution produces a value")) + } + + fn resolve_function_type( + &self, + function: &crate::ast::Function, + ty: &Type, + span: Span, + ) -> Result { + if let Type::Named { name, arguments } = ty { + if arguments.is_empty() { + if let Some(index) = function + .type_parameters + .iter() + .position(|parameter| parameter.name == *name) + { + return Ok(ResolvedType::TypeParameter { + owner: DeclarationId::new(function.stable_id.clone()), + index: u32::try_from(index).map_err(|_| { + self.error( + "SPX-H006", + format!( + "function `{}` type parameter index does not fit u32", + function.name + ), + span, + ) + })?, + }); + } + } + } + self.resolve_type(ty, span) + } + + #[allow(clippy::too_many_arguments)] + fn resolve_record_match_pattern( + &self, + function: &FunctionExecutionId, + expected: &ResolvedType, + type_name: &str, + fields: &[crate::ast::RecordMatchPatternField], + bindings: &mut BTreeMap, + path: &str, + span: Span, + ) -> Result { + enum Frame<'a> { + Enter { + expected: ResolvedType, + type_name: &'a str, + fields: &'a [crate::ast::RecordMatchPatternField], + path: String, + span: Span, + }, + Fields { + expected: ResolvedType, + record: DeclarationId, + arguments: Vec, + templates: &'a [ResolvedFieldDeclaration], + fields: &'a [crate::ast::RecordMatchPatternField], + index: usize, + resolved: Vec, + path: String, + }, + AfterNested { + expected: ResolvedType, + record: DeclarationId, + arguments: Vec, + templates: &'a [ResolvedFieldDeclaration], + fields: &'a [crate::ast::RecordMatchPatternField], + index: usize, + resolved: Vec, + path: String, + field: DeclarationId, + }, + } + let mut frames = vec![Frame::Enter { + expected: expected.clone(), + type_name, + fields, + path: path.to_owned(), + span, + }]; + let mut results = Vec::new(); + while let Some(frame) = frames.pop() { + match frame { + Frame::Enter { + expected, + type_name, + fields, + path, + span, + } => { + let ResolvedType::Nominal { + declaration: record, + arguments, + } = &expected + else { + return Err(self.error( + "SPX-H001", + "record pattern has a non-record concrete instance", + span, + )); + }; + if self.declarations.type_id(type_name) != Some(record) + || self + .declarations + .declaration(record) + .is_none_or(|item| item.kind != DeclarationKind::Record) + { + return Err(self.error( + "SPX-H001", + format!("record pattern `{type_name}` does not match `{record}`"), + span, + )); + } + let templates = self.declarations.record_fields(record).ok_or_else(|| { + self.error("SPX-H006", "record pattern has no fields", span) + })?; + let record = record.clone(); + let arguments = arguments.clone(); + frames.push(Frame::Fields { + expected, + record, + arguments, + templates, + fields, + index: 0, + resolved: Vec::with_capacity(fields.len()), + path, + }); + } + Frame::Fields { + expected, + record, + arguments, + templates, + fields, + index, + mut resolved, + path, + } => { + let Some(field) = fields.get(index) else { + results.push(ResolvedMatchPattern::Record { + record, + instance: expected, + fields: resolved, + }); + continue; + }; + let field_id = self + .declarations + .field_id(&record, &field.name) + .cloned() + .ok_or_else(|| { + self.error( + "SPX-H001", + format!( + "unresolved record pattern field `{record}.{}`", + field.name + ), + field.span, + ) + })?; + let template = templates + .iter() + .find(|candidate| candidate.id == field_id) + .ok_or_else(|| { + self.error( + "SPX-H006", + format!("record pattern field `{field_id}` has no template"), + field.span, + ) + })?; + let field_ty = substitute_type(&template.ty, &record, &arguments)?; + let field_path = format!("{path}.field.{index}"); + match &field.pattern { + crate::ast::RecordMatchFieldPattern::Binding { name, span } => { + let binding = ResolvedBinding { + id: ValueId::local(function, &format!("{field_path}.binding")), + name: name.clone(), + ownership: OwnershipMode::Value, + ty: field_ty.clone(), + span: *span, + }; + bindings.insert( + name.clone(), + Binding { + id: binding.id.clone(), + ty: field_ty, + ownership: OwnershipMode::Value, + }, + ); + resolved.push(ResolvedRecordMatchPatternField { + field: field_id, + pattern: ResolvedRecordMatchFieldPattern::Binding(binding), + }); + frames.push(Frame::Fields { + expected, + record, + arguments, + templates, + fields, + index: index + 1, + resolved, + path, + }); + } + crate::ast::RecordMatchFieldPattern::Wildcard { .. } => { + resolved.push(ResolvedRecordMatchPatternField { + field: field_id, + pattern: ResolvedRecordMatchFieldPattern::Wildcard, + }); + frames.push(Frame::Fields { + expected, + record, + arguments, + templates, + fields, + index: index + 1, + resolved, + path, + }); + } + crate::ast::RecordMatchFieldPattern::Record { + type_name, + fields: nested, + span, + .. + } => { + frames.push(Frame::AfterNested { + expected, + record, + arguments, + templates, + fields, + index, + resolved, + path: path.clone(), + field: field_id, + }); + frames.push(Frame::Enter { + expected: field_ty, + type_name, + fields: nested, + path: format!("{field_path}.record"), + span: *span, + }); + } + } + } + Frame::AfterNested { + expected, + record, + arguments, + templates, + fields, + index, + mut resolved, + path, + field, + } => { + let ResolvedMatchPattern::Record { + record: nested_record, + instance, + fields: nested_fields, + } = results.pop().expect("nested record result retained") + else { + unreachable!("nested resolver returns a record pattern") + }; + resolved.push(ResolvedRecordMatchPatternField { + field, + pattern: ResolvedRecordMatchFieldPattern::Record { + record: nested_record, + instance, + fields: nested_fields, + }, + }); + frames.push(Frame::Fields { + expected, + record, + arguments, + templates, + fields, + index: index + 1, + resolved, + path, + }); + } + } + } + Ok(results.pop().expect("root record pattern result retained")) + } + + fn resolve_expr( + &self, + function: &FunctionExecutionId, + expr: &Expr, + bindings: &BTreeMap, + path: &str, + ) -> Result { + self.resolve_expr_iterative(function, expr, bindings, path) + } + + fn resolve_expr_iterative( + &self, + function: &FunctionExecutionId, + expr: &Expr, + bindings: &BTreeMap, + path: &str, + ) -> Result { + enum Frame<'expr> { + Enter { + expr: &'expr Expr, + bindings: Rc>, + path: String, + }, + FinishNativeCall { + span: Span, + path: String, + import: DeclarationId, + argument_count: usize, + }, + FinishCall { + span: Span, + path: String, + callee: DeclarationId, + type_arguments: Vec, + instance: Option, + return_source_type: Type, + target_span: Span, + argument_count: usize, + }, + ChildNext { + children: &'expr [Expr], + index: usize, + bindings: Rc>, + path: String, + segment: &'static str, + }, + FinishUnary { + span: Span, + path: String, + op: UnaryOp, + }, + FinishBinary { + span: Span, + path: String, + op: BinaryOp, + }, + AfterBinaryLeft { + span: Span, + path: String, + op: BinaryOp, + right: &'expr Expr, + bindings: Rc>, + }, + BlockNext { + span: Span, + path: String, + statements: &'expr [Statement], + tail: &'expr Expr, + index: usize, + scope: Rc>, + resolved: Vec, + }, + BlockAfterLet { + span: Span, + path: String, + statements: &'expr [Statement], + tail: &'expr Expr, + index: usize, + scope: Rc>, + resolved: Vec, + }, + FinishBlock { + span: Span, + path: String, + statements: Vec, + }, + FinishIf { + span: Span, + path: String, + }, + AfterIfCondition { + span: Span, + path: String, + then_branch: &'expr Expr, + else_branch: &'expr Expr, + bindings: Rc>, + }, + AfterIfThen { + span: Span, + path: String, + else_branch: &'expr Expr, + bindings: Rc>, + }, + RecordNext { + span: Span, + path: String, + type_name: &'expr str, + record: DeclarationId, + arguments: Vec, + fields: &'expr [crate::ast::FieldInitializer], + index: usize, + bindings: Rc>, + resolved: Vec, + }, + RecordAfterField { + span: Span, + path: String, + type_name: &'expr str, + record: DeclarationId, + arguments: Vec, + fields: &'expr [crate::ast::FieldInitializer], + index: usize, + bindings: Rc>, + resolved: Vec, + field: DeclarationId, + }, + VariantNext { + span: Span, + path: String, + type_name: &'expr str, + case_name: &'expr str, + variant: DeclarationId, + case: DeclarationId, + type_arguments: &'expr [Type], + fields: &'expr [crate::ast::FieldInitializer], + index: usize, + bindings: Rc>, + resolved: Vec, + }, + VariantAfterField { + span: Span, + path: String, + type_name: &'expr str, + case_name: &'expr str, + variant: DeclarationId, + case: DeclarationId, + type_arguments: &'expr [Type], + fields: &'expr [crate::ast::FieldInitializer], + index: usize, + bindings: Rc>, + resolved: Vec, + field: DeclarationId, + }, + AfterMatchScrutinee { + span: Span, + path: String, + arms: &'expr [crate::ast::MatchArm], + bindings: Rc>, + }, + MatchNext { + span: Span, + path: String, + arms: &'expr [crate::ast::MatchArm], + index: usize, + bindings: Rc>, + scrutinee: ResolvedExpr, + matched_type: DeclarationId, + instance_arguments: Vec, + matched_kind: DeclarationKind, + resolved: Vec, + }, + MatchAfterArm { + span: Span, + path: String, + arms: &'expr [crate::ast::MatchArm], + index: usize, + bindings: Rc>, + scrutinee: ResolvedExpr, + matched_type: DeclarationId, + instance_arguments: Vec, + matched_kind: DeclarationKind, + resolved: Vec, + pattern: ResolvedMatchPattern, + }, + FinishTry { + span: Span, + path: String, + }, + AfterUpdateBase { + span: Span, + path: String, + fields: &'expr [crate::ast::FieldInitializer], + bindings: Rc>, + }, + UpdateNext { + span: Span, + path: String, + base: ResolvedExpr, + record: DeclarationId, + fields: &'expr [crate::ast::FieldInitializer], + index: usize, + bindings: Rc>, + resolved: Vec, + }, + UpdateAfterField { + span: Span, + path: String, + base: ResolvedExpr, + record: DeclarationId, + fields: &'expr [crate::ast::FieldInitializer], + index: usize, + bindings: Rc>, + resolved: Vec, + field: DeclarationId, + }, + FinishProject { + span: Span, + path: String, + field: &'expr str, + }, + } + + fn take_results(results: &mut Vec, count: usize) -> Vec { + let start = results + .len() + .checked_sub(count) + .expect("expression continuation retains every child result"); + results.split_off(start) + } + + #[cfg(test)] + fn frame_owned_capacity(frame: &Frame<'_>) -> usize { + let path = match frame { + Frame::Enter { path, .. } + | Frame::FinishNativeCall { path, .. } + | Frame::FinishCall { path, .. } + | Frame::ChildNext { path, .. } + | Frame::FinishUnary { path, .. } + | Frame::FinishBinary { path, .. } + | Frame::AfterBinaryLeft { path, .. } + | Frame::BlockNext { path, .. } + | Frame::BlockAfterLet { path, .. } + | Frame::FinishBlock { path, .. } + | Frame::FinishIf { path, .. } + | Frame::AfterIfCondition { path, .. } + | Frame::AfterIfThen { path, .. } + | Frame::RecordNext { path, .. } + | Frame::RecordAfterField { path, .. } + | Frame::VariantNext { path, .. } + | Frame::VariantAfterField { path, .. } + | Frame::AfterMatchScrutinee { path, .. } + | Frame::MatchNext { path, .. } + | Frame::MatchAfterArm { path, .. } + | Frame::FinishTry { path, .. } + | Frame::AfterUpdateBase { path, .. } + | Frame::UpdateNext { path, .. } + | Frame::UpdateAfterField { path, .. } + | Frame::FinishProject { path, .. } => path.capacity(), + }; + let scope = match frame { + Frame::Enter { bindings, .. } + | Frame::ChildNext { bindings, .. } + | Frame::AfterBinaryLeft { bindings, .. } + | Frame::AfterIfCondition { bindings, .. } + | Frame::AfterIfThen { bindings, .. } + | Frame::RecordNext { bindings, .. } + | Frame::RecordAfterField { bindings, .. } + | Frame::VariantNext { bindings, .. } + | Frame::VariantAfterField { bindings, .. } + | Frame::AfterMatchScrutinee { bindings, .. } + | Frame::MatchNext { bindings, .. } + | Frame::MatchAfterArm { bindings, .. } + | Frame::AfterUpdateBase { bindings, .. } + | Frame::UpdateNext { bindings, .. } + | Frame::UpdateAfterField { bindings, .. } => { + resolver_scope_owned_capacity(bindings) + } + Frame::BlockNext { scope, .. } | Frame::BlockAfterLet { scope, .. } => { + resolver_scope_owned_capacity(scope) + } + _ => 0, + }; + let retained = match frame { + Frame::FinishCall { + type_arguments, + return_source_type, + .. + } => { + type_arguments.capacity() * std::mem::size_of::() + + type_arguments + .iter() + .map(resolved_type_owned_capacity) + .sum::() + + match return_source_type { + Type::I64 | Type::Bool => 0, + Type::Named { name, arguments } => { + name.capacity() + arguments.capacity() * std::mem::size_of::() + } + } + } + Frame::BlockNext { resolved, .. } + | Frame::BlockAfterLet { resolved, .. } + | Frame::FinishBlock { + statements: resolved, + .. + } => { + resolved.capacity() * std::mem::size_of::() + + resolved + .iter() + .map(resolved_statement_owned_capacity) + .sum::() + } + Frame::RecordNext { + arguments, + resolved, + .. + } + | Frame::RecordAfterField { + arguments, + resolved, + .. + } => { + arguments.capacity() * std::mem::size_of::() + + arguments + .iter() + .map(resolved_type_owned_capacity) + .sum::() + + resolved.capacity() * std::mem::size_of::() + + resolved + .iter() + .map(resolved_field_initializer_owned_capacity) + .sum::() + } + Frame::VariantNext { resolved, .. } | Frame::VariantAfterField { resolved, .. } => { + resolved.capacity() * std::mem::size_of::() + + resolved + .iter() + .map(resolved_field_initializer_owned_capacity) + .sum::() + } + Frame::MatchNext { + scrutinee, + instance_arguments, + resolved, + .. + } + | Frame::MatchAfterArm { + scrutinee, + instance_arguments, + resolved, + .. + } => { + resolved_expr_owned_capacity(scrutinee) + + instance_arguments.capacity() * std::mem::size_of::() + + instance_arguments + .iter() + .map(resolved_type_owned_capacity) + .sum::() + + resolved.capacity() * std::mem::size_of::() + + resolved + .iter() + .map(resolved_match_arm_owned_capacity) + .sum::() + } + Frame::UpdateNext { base, resolved, .. } + | Frame::UpdateAfterField { base, resolved, .. } => { + resolved_expr_owned_capacity(base) + + resolved.capacity() * std::mem::size_of::() + + resolved + .iter() + .map(resolved_field_initializer_owned_capacity) + .sum::() + } + _ => 0, + }; + path.saturating_add(scope).saturating_add(retained) + } + + const { assert!(std::mem::size_of::>() == 552) }; + + let mut frames = vec![Frame::Enter { + expr, + bindings: Rc::new(bindings.clone()), + path: path.to_owned(), + }]; + let mut results = Vec::new(); + + while let Some(frame) = frames.pop() { + #[cfg(test)] + note_iterative_phase_capacity( + 0, + frames.capacity() * std::mem::size_of::>() + + results.capacity() * std::mem::size_of::() + + results + .iter() + .map(resolved_expr_owned_capacity) + .sum::() + + frames.iter().map(frame_owned_capacity).sum::() + + frame_owned_capacity(&frame), + ); + match frame { + Frame::Enter { + expr, + bindings, + path, + } => match &expr.kind { + ExprKind::Int(value) => results.push(ResolvedExpr { + id: ExpressionId::new(function, &path), + ty: ResolvedType::I64, + ownership: OwnershipMode::Value, + kind: ResolvedExprKind::Int(*value), + span: expr.span, + }), + ExprKind::Bool(value) => results.push(ResolvedExpr { + id: ExpressionId::new(function, &path), + ty: ResolvedType::Bool, + ownership: OwnershipMode::Value, + kind: ResolvedExprKind::Bool(*value), + span: expr.span, + }), + ExprKind::Var(name) => { + let binding = bindings.get(name).ok_or_else(|| { + self.error("SPX-H002", format!("unresolved value `{name}`"), expr.span) + })?; + results.push(ResolvedExpr { + id: ExpressionId::new(function, &path), + ty: binding.ty.clone(), + ownership: binding.ownership, + kind: ResolvedExprKind::Place(Place { + root: binding.id.clone(), + projections: Vec::new(), + }), + span: expr.span, + }); + } + ExprKind::Call { + name, + type_arguments, + args, + } => { + if let Some(import_id) = + self.declarations.native_rust_import_id(name).cloned() + { + let import = self + .program + .interfaces + .iter() + .flat_map(|interface| &interface.imports) + .find(|import| import.stable_id == import_id.as_str()) + .expect("native Rust import index is built from source imports"); + if !type_arguments.is_empty() || args.len() != import.params.len() { + return Err(self.error( + "SPX-B107", + "Native Rust Interop declaration set is unsupported: scalar value signature required", + expr.span, + )); + } + frames.push(Frame::FinishNativeCall { + span: expr.span, + path: path.clone(), + import: import_id, + argument_count: args.len(), + }); + frames.push(Frame::ChildNext { + children: args, + index: 0, + bindings, + path, + segment: "native-rust-arg", + }); + } else { + let template = self + .declarations + .function_id(name) + .cloned() + .ok_or_else(|| { + self.error( + "SPX-H003", + format!("unresolved function `{name}`"), + expr.span, + ) + })?; + let target = self + .program + .functions + .iter() + .find(|function| function.stable_id == template.as_str()) + .ok_or_else(|| { + self.error( + "SPX-H003", + format!( + "function identity `{template}` has no declaration" + ), + expr.span, + ) + })?; + let resolved_arguments = type_arguments + .iter() + .map(|argument| self.resolve_type(argument, expr.span)) + .collect::, _>>()?; + let (instance, return_source_type) = if target + .type_parameters + .is_empty() + { + if !resolved_arguments.is_empty() { + return Err(self.error( + "SPX-H006", + format!( + "monomorphic function `{template}` has type arguments" + ), + expr.span, + )); + } + (None, target.return_type.clone()) + } else { + if resolved_arguments.len() != target.type_parameters.len() + || resolved_arguments.iter().any(|argument| { + !matches!(argument, ResolvedType::I64 | ResolvedType::Bool) + }) + { + return Err(self.error( + "SPX-H006", + format!( + "generic function `{template}` has invalid type arguments" + ), + expr.span, + )); + } + let instance = + FunctionInstanceId::derive(&template, &resolved_arguments); + let return_type = substitute_source_function_type( + target, + type_arguments, + &target.return_type, + ) + .ok_or_else(|| { + self.error( + "SPX-H006", + format!( + "generic function `{template}` return substitution failed" + ), + expr.span, + ) + })?; + (Some(instance), return_type) + }; + frames.push(Frame::FinishCall { + span: expr.span, + path: path.clone(), + callee: template, + type_arguments: resolved_arguments, + instance, + return_source_type, + target_span: target.span, + argument_count: args.len(), + }); + frames.push(Frame::ChildNext { + children: args, + index: 0, + bindings, + path, + segment: "arg", + }); + } + } + ExprKind::Unary { op, value } => { + frames.push(Frame::FinishUnary { + span: expr.span, + path: path.clone(), + op: *op, + }); + frames.push(Frame::Enter { + expr: value, + bindings, + path: format!("{path}.value"), + }); + } + ExprKind::Binary { op, left, right } => { + frames.push(Frame::AfterBinaryLeft { + span: expr.span, + path: path.clone(), + op: *op, + right, + bindings: bindings.clone(), + }); + frames.push(Frame::Enter { + expr: left, + bindings, + path: format!("{path}.left"), + }); + } + ExprKind::Block { statements, tail } => { + frames.push(Frame::BlockNext { + span: expr.span, + path, + statements, + tail, + index: 0, + scope: bindings, + resolved: Vec::with_capacity(statements.len()), + }); + } + ExprKind::If { + condition, + then_branch, + else_branch, + } => { + frames.push(Frame::AfterIfCondition { + span: expr.span, + path: path.clone(), + then_branch, + else_branch, + bindings: bindings.clone(), + }); + frames.push(Frame::Enter { + expr: condition, + bindings, + path: format!("{path}.condition"), + }); + } + ExprKind::ConstructRecord { + type_name, + type_arguments, + fields, + .. + } => { + let record = + self.declarations + .type_id(type_name) + .cloned() + .ok_or_else(|| { + self.error( + "SPX-H001", + format!("unresolved record `{type_name}`"), + expr.span, + ) + })?; + if self + .declarations + .declaration(&record) + .is_none_or(|item| item.kind != DeclarationKind::Record) + { + return Err(self.error( + "SPX-H001", + format!("constructor target `{type_name}` is not a record"), + expr.span, + )); + } + let arguments = type_arguments + .iter() + .map(|argument| self.resolve_type(argument, expr.span)) + .collect::, _>>()?; + let parameters = + self.declarations.type_parameters(&record).ok_or_else(|| { + self.error( + "SPX-H006", + format!("record `{record}` has no parameter metadata"), + expr.span, + ) + })?; + if arguments.len() != parameters.len() + || arguments.iter().any(|argument| { + !matches!(argument, ResolvedType::I64 | ResolvedType::Bool) + }) + { + return Err(self.error( + "SPX-H006", + format!("record `{record}` has invalid concrete arguments"), + expr.span, + )); + } + frames.push(Frame::RecordNext { + span: expr.span, + path, + type_name, + record, + arguments, + fields, + index: 0, + bindings, + resolved: Vec::with_capacity(fields.len()), + }); + } + ExprKind::ConstructVariant { + type_name, + type_arguments, + case_name, + fields, + .. + } => { + let variant = + self.declarations + .type_id(type_name) + .cloned() + .ok_or_else(|| { + self.error( + "SPX-H001", + format!("unresolved variant `{type_name}`"), + expr.span, + ) + })?; + if self + .declarations + .declaration(&variant) + .is_none_or(|item| item.kind != DeclarationKind::Variant) + { + return Err(self.error( + "SPX-H001", + format!("constructor target `{type_name}` is not a variant"), + expr.span, + )); + } + let case = self + .declarations + .case_id(&variant, case_name) + .cloned() + .ok_or_else(|| { + self.error( + "SPX-H001", + format!("unresolved case `{type_name}::{case_name}`"), + expr.span, + ) + })?; + frames.push(Frame::VariantNext { + span: expr.span, + path, + type_name, + case_name, + variant, + case, + type_arguments, + fields, + index: 0, + bindings, + resolved: Vec::with_capacity(fields.len()), + }); + } + ExprKind::Match { scrutinee, arms } => { + frames.push(Frame::AfterMatchScrutinee { + span: expr.span, + path: path.clone(), + arms, + bindings: bindings.clone(), + }); + frames.push(Frame::Enter { + expr: scrutinee, + bindings, + path: format!("{path}.scrutinee"), + }); + } + ExprKind::Try { operand } => { + frames.push(Frame::FinishTry { + span: expr.span, + path: path.clone(), + }); + frames.push(Frame::Enter { + expr: operand, + bindings, + path: format!("{path}.operand"), + }); + } + ExprKind::UpdateRecord { base, fields } => { + frames.push(Frame::AfterUpdateBase { + span: expr.span, + path: path.clone(), + fields, + bindings: bindings.clone(), + }); + frames.push(Frame::Enter { + expr: base, + bindings, + path: format!("{path}.base"), + }); + } + ExprKind::Project { base, field, .. } => { + frames.push(Frame::FinishProject { + span: expr.span, + path: path.clone(), + field, + }); + frames.push(Frame::Enter { + expr: base, + bindings, + path: format!("{path}.base"), + }); + } + }, + Frame::FinishNativeCall { + span, + path, + import, + argument_count, + } => { + let args = take_results(&mut results, argument_count); + let source_import = self + .program + .interfaces + .iter() + .flat_map(|interface| &interface.imports) + .find(|candidate| candidate.stable_id == import.as_str()) + .expect("native Rust import identity remains indexed"); + for (argument, parameter) in args.iter().zip(&source_import.params) { + if argument.ty != self.resolve_type(¶meter.ty, parameter.span)? { + return Err(self.error( + "SPX-B107", + "Native Rust Interop declaration set is unsupported: scalar value signature required", + argument.span, + )); + } + } + let result = match source_import.result { + crate::ast::ImportResult::Unit => ResolvedImportResultKind::Unit, + crate::ast::ImportResult::I64 => ResolvedImportResultKind::I64, + crate::ast::ImportResult::Bool => ResolvedImportResultKind::Bool, + }; + let ty = match result { + ResolvedImportResultKind::Unit => ResolvedType::Unit, + ResolvedImportResultKind::I64 => ResolvedType::I64, + ResolvedImportResultKind::Bool => ResolvedType::Bool, + }; + results.push(ResolvedExpr { + id: ExpressionId::new(function, &path), + ty, + ownership: OwnershipMode::Value, + kind: ResolvedExprKind::NativeRustImportCall( + ResolvedNativeRustImportCall { + expression: ExpressionId::new(function, &path), + import, + args, + result, + }, + ), + span, + }); + } + Frame::FinishCall { + span, + path, + callee, + type_arguments, + instance, + return_source_type, + target_span, + argument_count, + } => { + let args = take_results(&mut results, argument_count); + let ty = self.resolve_type(&return_source_type, target_span)?; + let ownership = + self.expression_ownership(&ty, OwnershipMode::Own, target_span)?; + results.push(ResolvedExpr { + id: ExpressionId::new(function, &path), + ty, + ownership, + kind: ResolvedExprKind::Call { + callee, + type_arguments, + instance, + args, + }, + span, + }); + } + Frame::ChildNext { + children, + index, + bindings, + path, + segment, + } => { + if index < children.len() { + frames.push(Frame::ChildNext { + children, + index: index + 1, + bindings: bindings.clone(), + path: path.clone(), + segment, + }); + frames.push(Frame::Enter { + expr: &children[index], + bindings, + path: format!("{path}.{segment}.{index}"), + }); + } + } + Frame::FinishUnary { span, path, op } => { + let value = results.pop().expect("unary child result retained"); + let ty = match op { + UnaryOp::Neg => ResolvedType::I64, + UnaryOp::Not => ResolvedType::Bool, + }; + results.push(ResolvedExpr { + id: ExpressionId::new(function, &path), + ty, + ownership: OwnershipMode::Value, + kind: ResolvedExprKind::Unary { + op, + value: Box::new(value), + }, + span, + }); + } + Frame::FinishBinary { span, path, op } => { + let mut children = take_results(&mut results, 2).into_iter(); + let left = children.next().expect("binary left result retained"); + let right = children.next().expect("binary right result retained"); + let ty = match op { + BinaryOp::Add + | BinaryOp::Sub + | BinaryOp::Mul + | BinaryOp::Div + | BinaryOp::Rem => ResolvedType::I64, + BinaryOp::Eq + | BinaryOp::Ne + | BinaryOp::Lt + | BinaryOp::Le + | BinaryOp::Gt + | BinaryOp::Ge + | BinaryOp::And + | BinaryOp::Or => ResolvedType::Bool, + }; + results.push(ResolvedExpr { + id: ExpressionId::new(function, &path), + ty, + ownership: OwnershipMode::Value, + kind: ResolvedExprKind::Binary { + op, + left: Box::new(left), + right: Box::new(right), + }, + span, + }); + } + Frame::AfterBinaryLeft { + span, + path, + op, + right, + bindings, + } => { + frames.push(Frame::FinishBinary { + span, + path: path.clone(), + op, + }); + frames.push(Frame::Enter { + expr: right, + bindings, + path: format!("{path}.right"), + }); + } + Frame::BlockNext { + span, + path, + statements, + tail, + index, + scope, + resolved, + } => { + if index == statements.len() { + frames.push(Frame::FinishBlock { + span, + path: path.clone(), + statements: resolved, + }); + frames.push(Frame::Enter { + expr: tail, + bindings: scope, + path: format!("{path}.tail"), + }); + } else { + let Statement::Let { value, .. } = &statements[index]; + frames.push(Frame::BlockAfterLet { + span, + path: path.clone(), + statements, + tail, + index, + scope: scope.clone(), + resolved, + }); + frames.push(Frame::Enter { + expr: value, + bindings: scope, + path: format!("{path}.s{index}.value"), + }); + } + } + Frame::BlockAfterLet { + span, + path, + statements, + tail, + index, + mut scope, + mut resolved, + } => { + let value = results.pop().expect("let value result retained"); + let Statement::Let { + name, + name_span, + span: statement_span, + .. + } = &statements[index]; + let statement_path = format!("{path}.s{index}"); + let binding = ResolvedBinding { + id: ValueId::local(function, &statement_path), + name: name.clone(), + ownership: value.ownership, + ty: value.ty.clone(), + span: *name_span, + }; + Rc::make_mut(&mut scope).insert( + name.clone(), + Binding { + id: binding.id.clone(), + ty: binding.ty.clone(), + ownership: binding.ownership, + }, + ); + resolved.push(ResolvedStatement::Let { + binding, + value, + span: *statement_span, + }); + frames.push(Frame::BlockNext { + span, + path, + statements, + tail, + index: index + 1, + scope, + resolved, + }); + } + Frame::FinishBlock { + span, + path, + statements, + } => { + let tail = results.pop().expect("block tail result retained"); + let ty = tail.ty.clone(); + let ownership = tail.ownership; + results.push(ResolvedExpr { + id: ExpressionId::new(function, &path), + ty, + ownership, + kind: ResolvedExprKind::Block { + statements, + tail: Box::new(tail), + }, + span, + }); + } + Frame::FinishIf { span, path } => { + let mut children = take_results(&mut results, 3).into_iter(); + let condition = children.next().expect("if condition retained"); + let then_branch = children.next().expect("if then branch retained"); + let else_branch = children.next().expect("if else branch retained"); + let ty = then_branch.ty.clone(); + let ownership = then_branch.ownership; + results.push(ResolvedExpr { + id: ExpressionId::new(function, &path), + ty, + ownership, + kind: ResolvedExprKind::If { + condition: Box::new(condition), + then_branch: Box::new(then_branch), + else_branch: Box::new(else_branch), + }, + span, + }); + } + Frame::AfterIfCondition { + span, + path, + then_branch, + else_branch, + bindings, + } => { + frames.push(Frame::AfterIfThen { + span, + path: path.clone(), + else_branch, + bindings: bindings.clone(), + }); + frames.push(Frame::Enter { + expr: then_branch, + bindings, + path: format!("{path}.then"), + }); + } + Frame::AfterIfThen { + span, + path, + else_branch, + bindings, + } => { + frames.push(Frame::FinishIf { + span, + path: path.clone(), + }); + frames.push(Frame::Enter { + expr: else_branch, + bindings, + path: format!("{path}.else"), + }); + } + Frame::RecordNext { + span, + path, + type_name, + record, + arguments, + fields, + index, + bindings, + resolved, + } => { + if index == fields.len() { + let ty = ResolvedType::Nominal { + declaration: record.clone(), + arguments, + }; + let ownership = self.expression_ownership(&ty, OwnershipMode::Own, span)?; + results.push(ResolvedExpr { + id: ExpressionId::new(function, &path), + ty, + ownership, + kind: ResolvedExprKind::ConstructRecord { + record, + fields: resolved, + }, + span, + }); + } else { + let initializer = &fields[index]; + let field = self + .declarations + .field_id(&record, &initializer.name) + .cloned() + .ok_or_else(|| { + self.error( + "SPX-H001", + format!( + "unresolved field `{}.{}`", + type_name, initializer.name + ), + initializer.name_span, + ) + })?; + frames.push(Frame::RecordAfterField { + span, + path: path.clone(), + type_name, + record, + arguments, + fields, + index, + bindings: bindings.clone(), + resolved, + field, + }); + frames.push(Frame::Enter { + expr: &initializer.value, + bindings, + path: format!("{path}.field.{index}.value"), + }); + } + } + Frame::RecordAfterField { + span, + path, + type_name, + record, + arguments, + fields, + index, + bindings, + mut resolved, + field, + } => { + let value = results.pop().expect("record field result retained"); + resolved.push(ResolvedFieldInitializer { field, value }); + frames.push(Frame::RecordNext { + span, + path, + type_name, + record, + arguments, + fields, + index: index + 1, + bindings, + resolved, + }); + } + Frame::VariantNext { + span, + path, + type_name, + case_name, + variant, + case, + type_arguments, + fields, + index, + bindings, + resolved, + } => { + if index == fields.len() { + let arguments = type_arguments + .iter() + .map(|argument| self.resolve_type(argument, span)) + .collect::, _>>()?; + results.push(ResolvedExpr { + id: ExpressionId::new(function, &path), + ty: ResolvedType::Nominal { + declaration: variant.clone(), + arguments, + }, + ownership: OwnershipMode::Value, + kind: ResolvedExprKind::ConstructVariant { + variant, + case, + fields: resolved, + }, + span, + }); + } else { + let initializer = &fields[index]; + let field = self + .declarations + .field_id(&case, &initializer.name) + .cloned() + .ok_or_else(|| { + self.error( + "SPX-H001", + format!( + "unresolved payload field `{type_name}::{case_name}.{}`", + initializer.name + ), + initializer.name_span, + ) + })?; + frames.push(Frame::VariantAfterField { + span, + path: path.clone(), + type_name, + case_name, + variant, + case, + type_arguments, + fields, + index, + bindings: bindings.clone(), + resolved, + field, + }); + frames.push(Frame::Enter { + expr: &initializer.value, + bindings, + path: format!("{path}.field.{index}.value"), + }); + } + } + Frame::VariantAfterField { + span, + path, + type_name, + case_name, + variant, + case, + type_arguments, + fields, + index, + bindings, + mut resolved, + field, + } => { + let value = results.pop().expect("variant field result retained"); + resolved.push(ResolvedFieldInitializer { field, value }); + frames.push(Frame::VariantNext { + span, + path, + type_name, + case_name, + variant, + case, + type_arguments, + fields, + index: index + 1, + bindings, + resolved, + }); + } + Frame::AfterMatchScrutinee { + span, + path, + arms, + bindings, + } => { + let scrutinee = results.pop().expect("match scrutinee retained"); + let ResolvedType::Nominal { + declaration: matched_type, + arguments, + } = &scrutinee.ty + else { + return Err(self.error( + "SPX-H001", + "cannot resolve match on a non-record/non-variant value", + span, + )); + }; + let matched_kind = self + .declarations + .declaration(matched_type) + .map(|item| item.kind) + .filter(|kind| { + matches!(kind, DeclarationKind::Record | DeclarationKind::Variant) + }) + .ok_or_else(|| { + self.error( + "SPX-H001", + "cannot resolve match on a non-record/non-variant value", + span, + ) + })?; + let matched_type = matched_type.clone(); + let instance_arguments = arguments.clone(); + frames.push(Frame::MatchNext { + span, + path, + arms, + index: 0, + bindings, + scrutinee, + matched_type, + instance_arguments, + matched_kind, + resolved: Vec::with_capacity(arms.len()), + }); + } + Frame::MatchNext { + span, + path, + arms, + index, + bindings, + scrutinee, + matched_type, + instance_arguments, + matched_kind, + resolved, + } => { + if index == arms.len() { + let first = resolved.first().ok_or_else(|| { + self.error("SPX-H006", "resolved match has no arms", span) + })?; + let ty = first.value.ty.clone(); + let ownership = first.value.ownership; + results.push(ResolvedExpr { + id: ExpressionId::new(function, &path), + ty, + ownership, + kind: ResolvedExprKind::Match { + scrutinee: Box::new(scrutinee), + arms: resolved, + }, + span, + }); + } else { + let arm = &arms[index]; + let mut arm_bindings = bindings.clone(); + let pattern = match &arm.pattern { + MatchPattern::Wildcard { .. } => ResolvedMatchPattern::Wildcard, + MatchPattern::Variant { + case_name, fields, .. + } => { + if matched_kind != DeclarationKind::Variant { + return Err(self.error( + "SPX-H001", + "variant pattern has a record scrutinee", + arm.span, + )); + } + let case = self + .declarations + .case_id(&matched_type, case_name) + .cloned() + .ok_or_else(|| { + self.error( + "SPX-H001", + format!( + "unresolved case `{matched_type}::{case_name}`" + ), + arm.span, + ) + })?; + let mut resolved_fields = Vec::with_capacity(fields.len()); + for (field_index, field) in fields.iter().enumerate() { + let field_id = self + .declarations + .field_id(&case, &field.name) + .cloned() + .ok_or_else(|| { + self.error( + "SPX-H001", + format!( + "unresolved pattern field `{case}.{}`", + field.name + ), + field.span, + ) + })?; + let field_template = self + .declarations + .case_fields(&case) + .and_then(|items| { + items.iter().find(|item| item.id == field_id) + }) + .map(|item| item.ty.clone()) + .ok_or_else(|| { + self.error( + "SPX-H001", + format!("pattern field `{field_id}` has no type"), + field.span, + ) + })?; + let field_ty = substitute_type( + &field_template, + &matched_type, + &instance_arguments, + )?; + let binding = ResolvedBinding { + id: ValueId::local( + function, + &format!("{path}.arm.{index}.binding.{field_index}"), + ), + name: field.binding.clone(), + ownership: OwnershipMode::Value, + ty: field_ty.clone(), + span: field.binding_span, + }; + Rc::make_mut(&mut arm_bindings).insert( + field.binding.clone(), + Binding { + id: binding.id.clone(), + ty: field_ty, + ownership: OwnershipMode::Value, + }, + ); + resolved_fields.push(ResolvedMatchPatternField { + field: field_id, + binding, + }); + } + ResolvedMatchPattern::Variant { + variant: matched_type.clone(), + case, + fields: resolved_fields, + } + } + MatchPattern::Record { + type_name, + fields, + span: pattern_span, + .. + } => { + if matched_kind != DeclarationKind::Record { + return Err(self.error( + "SPX-H001", + "record pattern has a variant scrutinee", + arm.span, + )); + } + self.resolve_record_match_pattern( + function, + &scrutinee.ty, + type_name, + fields, + Rc::make_mut(&mut arm_bindings), + &format!("{path}.arm.{index}.record"), + *pattern_span, + )? + } + }; + frames.push(Frame::MatchAfterArm { + span, + path: path.clone(), + arms, + index, + bindings, + scrutinee, + matched_type, + instance_arguments, + matched_kind, + resolved, + pattern, + }); + frames.push(Frame::Enter { + expr: &arm.value, + bindings: arm_bindings, + path: format!("{path}.arm.{index}.value"), + }); + } + } + Frame::MatchAfterArm { + span, + path, + arms, + index, + bindings, + scrutinee, + matched_type, + instance_arguments, + matched_kind, + mut resolved, + pattern, + } => { + let value = results.pop().expect("match arm value retained"); + resolved.push(ResolvedMatchArm { + pattern, + value, + span: arms[index].span, + }); + frames.push(Frame::MatchNext { + span, + path, + arms, + index: index + 1, + bindings, + scrutinee, + matched_type, + instance_arguments, + matched_kind, + resolved, + }); + } + Frame::FinishTry { span, path } => { + let operand = results.pop().expect("try operand retained"); + let operand_type = operand.ty.clone(); + let ResolvedType::Nominal { + declaration, + arguments, + } = &operand_type + else { + return Err(self.error( + "SPX-H006", + "resolved `?` operand is not the ordinary Result", + span, + )); + }; + let target = self + .program + .functions + .iter() + .find(|candidate| { + matches!( + function, + FunctionExecutionId::Monomorphic(declaration) + if candidate.stable_id == declaration.as_str() + ) + }) .ok_or_else(|| { self.error( "SPX-H006", - format!("type `{declaration}` has no parameter metadata"), + format!("resolved `?` has unknown enclosing function `{function}`"), span, ) })?; - if arguments.len() != parameters.len() - || (!arguments.is_empty() - && arguments.iter().any(|argument| { - !matches!(argument, ResolvedType::I64 | ResolvedType::Bool) - })) - { - return Err(self.error( - "SPX-H006", - format!("type `{declaration}` has invalid concrete arguments"), - span, - )); - } - Ok(ResolvedType::Nominal { - declaration, - arguments, - }) - } - } - } - - fn resolve_function_type( - &self, - function: &crate::ast::Function, - ty: &Type, - span: Span, - ) -> Result { - if let Type::Named { name, arguments } = ty { - if arguments.is_empty() { - if let Some(index) = function - .type_parameters - .iter() - .position(|parameter| parameter.name == *name) - { - return Ok(ResolvedType::TypeParameter { - owner: DeclarationId::new(function.stable_id.clone()), - index: u32::try_from(index).map_err(|_| { - self.error( - "SPX-H006", - format!( - "function `{}` type parameter index does not fit u32", - function.name + let residual_type = self.resolve_type(&target.return_type, target.span)?; + let (kind, ty) = match (declaration.as_str(), arguments.as_slice()) { + (crate::prelude::RESULT_ID, [ok_type, _]) => ( + ResolvedExprKind::Try { + operand: Box::new(operand), + result: DeclarationId::new(crate::prelude::RESULT_ID), + ok_case: DeclarationId::new(crate::prelude::RESULT_OK_ID), + ok_field: DeclarationId::new(crate::prelude::RESULT_OK_VALUE_ID), + err_case: DeclarationId::new(crate::prelude::RESULT_ERR_ID), + err_field: DeclarationId::new(crate::prelude::RESULT_ERR_ERROR_ID), + residual_type, + }, + ok_type.clone(), + ), + (crate::prelude::OPTION_ID, [some_type]) => ( + ResolvedExprKind::TryOption { + operand: Box::new(operand), + option: DeclarationId::new(crate::prelude::OPTION_ID), + some_case: DeclarationId::new(crate::prelude::OPTION_SOME_ID), + some_field: DeclarationId::new( + crate::prelude::OPTION_SOME_VALUE_ID, ), + none_case: DeclarationId::new(crate::prelude::OPTION_NONE_ID), + residual_type, + }, + some_type.clone(), + ), + _ => { + return Err(self.error( + "SPX-H006", + "resolved `?` operand is not an ordinary Result or Option", span, - ) - })?, + )); + } + }; + results.push(ResolvedExpr { + id: ExpressionId::new(function, &path), + ty, + ownership: OwnershipMode::Value, + kind, + span, }); } - } - } - self.resolve_type(ty, span) - } - - #[allow(clippy::too_many_arguments)] - fn resolve_record_match_pattern( - &self, - function: &FunctionExecutionId, - expected: &ResolvedType, - type_name: &str, - fields: &[crate::ast::RecordMatchPatternField], - bindings: &mut BTreeMap, - path: &str, - span: Span, - ) -> Result { - let ResolvedType::Nominal { - declaration: record, - arguments, - } = expected - else { - return Err(self.error( - "SPX-H001", - "record pattern has a non-record concrete instance", - span, - )); - }; - let named_record = self.declarations.type_id(type_name); - if named_record != Some(record) - || self - .declarations - .declaration(record) - .is_none_or(|item| item.kind != DeclarationKind::Record) - { - return Err(self.error( - "SPX-H001", - format!("record pattern `{type_name}` does not match `{record}`"), - span, - )); - } - let templates = self - .declarations - .record_fields(record) - .ok_or_else(|| self.error("SPX-H006", "record pattern has no fields", span))?; - let mut resolved_fields = Vec::with_capacity(fields.len()); - for (field_index, field) in fields.iter().enumerate() { - let field_id = self - .declarations - .field_id(record, &field.name) - .cloned() - .ok_or_else(|| { - self.error( - "SPX-H001", - format!("unresolved record pattern field `{record}.{}`", field.name), - field.span, - ) - })?; - let template = templates - .iter() - .find(|candidate| candidate.id == field_id) - .ok_or_else(|| { - self.error( - "SPX-H006", - format!("record pattern field `{field_id}` has no template"), - field.span, - ) - })?; - let field_ty = substitute_type(&template.ty, record, arguments)?; - let field_path = format!("{path}.field.{field_index}"); - let pattern = match &field.pattern { - crate::ast::RecordMatchFieldPattern::Binding { name, span } => { - let binding = ResolvedBinding { - id: ValueId::local(function, &format!("{field_path}.binding")), - name: name.clone(), - ownership: OwnershipMode::Value, - ty: field_ty.clone(), - span: *span, + Frame::AfterUpdateBase { + span, + path, + fields, + bindings, + } => { + let base = results.pop().expect("record update base retained"); + let ResolvedType::Nominal { + declaration: record, + .. + } = &base.ty + else { + return Err(self.error( + "SPX-H001", + "cannot resolve a record update on a non-record value", + span, + )); }; - bindings.insert( - name.clone(), - Binding { - id: binding.id.clone(), - ty: field_ty, - ownership: OwnershipMode::Value, - }, - ); - ResolvedRecordMatchFieldPattern::Binding(binding) - } - crate::ast::RecordMatchFieldPattern::Wildcard { .. } => { - ResolvedRecordMatchFieldPattern::Wildcard + if self + .declarations + .declaration(record) + .is_none_or(|item| item.kind != DeclarationKind::Record) + { + return Err(self.error( + "SPX-H001", + "cannot resolve a record update on a non-record value", + span, + )); + } + let record = record.clone(); + frames.push(Frame::UpdateNext { + span, + path, + base, + record, + fields, + index: 0, + bindings, + resolved: Vec::with_capacity(fields.len()), + }); } - crate::ast::RecordMatchFieldPattern::Record { - type_name, + Frame::UpdateNext { + span, + path, + base, + record, fields, + index, + bindings, + resolved, + } => { + if index == fields.len() { + let ty = base.ty.clone(); + let ownership = self.expression_ownership(&ty, OwnershipMode::Own, span)?; + results.push(ResolvedExpr { + id: ExpressionId::new(function, &path), + ty, + ownership, + kind: ResolvedExprKind::UpdateRecord { + base: Box::new(base), + record, + fields: resolved, + }, + span, + }); + } else { + let initializer = &fields[index]; + let field = self + .declarations + .field_id(&record, &initializer.name) + .cloned() + .ok_or_else(|| { + self.error( + "SPX-H001", + format!( + "unresolved replacement field `{}.{}`", + record, initializer.name + ), + initializer.name_span, + ) + })?; + frames.push(Frame::UpdateAfterField { + span, + path: path.clone(), + base, + record, + fields, + index, + bindings: bindings.clone(), + resolved, + field, + }); + frames.push(Frame::Enter { + expr: &initializer.value, + bindings, + path: format!("{path}.field.{index}.value"), + }); + } + } + Frame::UpdateAfterField { span, - .. + path, + base, + record, + fields, + index, + bindings, + mut resolved, + field, } => { - let ResolvedMatchPattern::Record { + let value = results.pop().expect("record replacement result retained"); + resolved.push(ResolvedFieldInitializer { field, value }); + frames.push(Frame::UpdateNext { + span, + path, + base, record, - instance, - fields, - } = self.resolve_record_match_pattern( - function, - &field_ty, - type_name, fields, + index: index + 1, bindings, - &format!("{field_path}.record"), - *span, - )? + resolved, + }); + } + Frame::FinishProject { span, path, field } => { + let base = results.pop().expect("projection base retained"); + let ResolvedType::Nominal { + declaration: record, + arguments, + } = &base.ty else { - unreachable!("record resolver returns a record pattern"); + return Err(self.error( + "SPX-H001", + format!("cannot resolve field `{field}` on a non-record value"), + span, + )); }; - ResolvedRecordMatchFieldPattern::Record { - record, - instance, - fields, + if self + .declarations + .declaration(record) + .is_none_or(|item| item.kind != DeclarationKind::Record) + { + return Err(self.error( + "SPX-H001", + format!("cannot resolve field `{field}` on a non-record value"), + span, + )); } + let field_id = self + .declarations + .field_id(record, field) + .cloned() + .ok_or_else(|| { + self.error( + "SPX-H001", + format!("unresolved field `{field}` on record `{record}`"), + span, + ) + })?; + let field_ty = self + .declarations + .record_fields(record) + .and_then(|fields| fields.iter().find(|item| item.id == field_id)) + .map(|item| item.ty.clone()) + .ok_or_else(|| { + self.error( + "SPX-H001", + format!("field `{field_id}` has no resolved type"), + span, + ) + })?; + let field_ty = substitute_type(&field_ty, record, arguments)?; + let ownership = self.expression_ownership(&field_ty, base.ownership, span)?; + let kind = match &base.kind { + ResolvedExprKind::Place(place) => { + let mut place = place.clone(); + place + .projections + .push(PlaceProjection::Field(field_id.clone())); + ResolvedExprKind::Place(place) + } + _ => ResolvedExprKind::Project { + base: Box::new(base), + field: field_id, + }, + }; + results.push(ResolvedExpr { + id: ExpressionId::new(function, &path), + ty: field_ty, + ownership, + kind, + span, + }); } - }; - resolved_fields.push(ResolvedRecordMatchPatternField { - field: field_id, - pattern, - }); + } } - Ok(ResolvedMatchPattern::Record { - record: record.clone(), - instance: expected.clone(), - fields: resolved_fields, + + if results.len() != 1 { + return Err(self.error( + "SPX-H006", + "iterative expression resolver finished with an invalid result stack", + expr.span, + )); + } + results.pop().ok_or_else(|| { + self.error( + "SPX-H006", + "iterative expression resolver lost its root result", + expr.span, + ) }) } - fn resolve_expr( + #[cfg(test)] + #[allow(dead_code)] + fn resolve_expr_recursive_reference( &self, function: &FunctionExecutionId, expr: &Expr, @@ -6349,6 +12071,67 @@ impl Resolver<'_> { type_arguments, args, } => { + if let Some(import_id) = self.declarations.native_rust_import_id(name).cloned() { + let import = self + .program + .interfaces + .iter() + .flat_map(|interface| &interface.imports) + .find(|import| import.stable_id == import_id.as_str()) + .expect("native Rust import index is built from source imports"); + if !type_arguments.is_empty() || args.len() != import.params.len() { + return Err(self.error( + "SPX-B107", + "Native Rust Interop declaration set is unsupported: scalar value signature required", + expr.span, + )); + } + let args = args + .iter() + .enumerate() + .map(|(index, argument)| { + self.resolve_expr_recursive_reference( + function, + argument, + bindings, + &format!("{path}.native-rust-arg.{index}"), + ) + }) + .collect::, _>>()?; + for (argument, parameter) in args.iter().zip(&import.params) { + if argument.ty != self.resolve_type(¶meter.ty, parameter.span)? { + return Err(self.error( + "SPX-B107", + "Native Rust Interop declaration set is unsupported: scalar value signature required", + argument.span, + )); + } + } + let result = match import.result { + crate::ast::ImportResult::Unit => ResolvedImportResultKind::Unit, + crate::ast::ImportResult::I64 => ResolvedImportResultKind::I64, + crate::ast::ImportResult::Bool => ResolvedImportResultKind::Bool, + }; + let ty = match result { + ResolvedImportResultKind::Unit => ResolvedType::Unit, + ResolvedImportResultKind::I64 => ResolvedType::I64, + ResolvedImportResultKind::Bool => ResolvedType::Bool, + }; + return Ok(ResolvedExpr { + id, + ty, + ownership: OwnershipMode::Value, + kind: ResolvedExprKind::NativeRustImportCall( + ResolvedNativeRustImportCall { + expression: ExpressionId::new(function, path), + import: import_id, + args, + result, + }, + ), + span: expr.span, + }); + } let template = self .declarations .function_id(name) @@ -6416,7 +12199,7 @@ impl Resolver<'_> { .iter() .enumerate() .map(|(index, argument)| { - self.resolve_expr( + self.resolve_expr_recursive_reference( function, argument, bindings, @@ -6438,25 +12221,52 @@ impl Resolver<'_> { ) } ExprKind::Unary { op, value } => { - let value = - self.resolve_expr(function, value, bindings, &format!("{path}.value"))?; - let ty = match op { - UnaryOp::Neg => ResolvedType::I64, - UnaryOp::Not => ResolvedType::Bool, - }; - ( - ResolvedExprKind::Unary { - op: *op, - value: Box::new(value), - }, - ty, - OwnershipMode::Value, - ) + // Peel this linear family without consuming resolver frames. + // The general expression-frame conversion handles the other + // recursive families separately; this fast path preserves the + // exact canonical `.value` identity chain. + let mut unary = Vec::new(); + unary.push((*op, expr.span, path.to_owned())); + let mut leaf = value.as_ref(); + let mut leaf_path = format!("{path}.value"); + while let ExprKind::Unary { op, value } = &leaf.kind { + unary.push((*op, leaf.span, leaf_path.clone())); + leaf = value; + leaf_path.push_str(".value"); + } + let mut resolved = + self.resolve_expr_recursive_reference(function, leaf, bindings, &leaf_path)?; + for (op, span, unary_path) in unary.into_iter().rev() { + let ty = match op { + UnaryOp::Neg => ResolvedType::I64, + UnaryOp::Not => ResolvedType::Bool, + }; + resolved = ResolvedExpr { + id: ExpressionId::new(function, &unary_path), + ty, + ownership: OwnershipMode::Value, + kind: ResolvedExprKind::Unary { + op, + value: Box::new(resolved), + }, + span, + }; + } + return Ok(resolved); } ExprKind::Binary { op, left, right } => { - let left = self.resolve_expr(function, left, bindings, &format!("{path}.left"))?; - let right = - self.resolve_expr(function, right, bindings, &format!("{path}.right"))?; + let left = self.resolve_expr_recursive_reference( + function, + left, + bindings, + &format!("{path}.left"), + )?; + let right = self.resolve_expr_recursive_reference( + function, + right, + bindings, + &format!("{path}.right"), + )?; let ty = match op { BinaryOp::Add | BinaryOp::Sub @@ -6494,7 +12304,7 @@ impl Resolver<'_> { value, span, } => { - let value = self.resolve_expr( + let value = self.resolve_expr_recursive_reference( function, value, &scope, @@ -6523,7 +12333,12 @@ impl Resolver<'_> { } } } - let tail = self.resolve_expr(function, tail, &scope, &format!("{path}.tail"))?; + let tail = self.resolve_expr_recursive_reference( + function, + tail, + &scope, + &format!("{path}.tail"), + )?; let ty = tail.ty.clone(); let ownership = tail.ownership; ( @@ -6540,12 +12355,24 @@ impl Resolver<'_> { then_branch, else_branch, } => { - let condition = - self.resolve_expr(function, condition, bindings, &format!("{path}.condition"))?; - let then_branch = - self.resolve_expr(function, then_branch, bindings, &format!("{path}.then"))?; - let else_branch = - self.resolve_expr(function, else_branch, bindings, &format!("{path}.else"))?; + let condition = self.resolve_expr_recursive_reference( + function, + condition, + bindings, + &format!("{path}.condition"), + )?; + let then_branch = self.resolve_expr_recursive_reference( + function, + then_branch, + bindings, + &format!("{path}.then"), + )?; + let else_branch = self.resolve_expr_recursive_reference( + function, + else_branch, + bindings, + &format!("{path}.else"), + )?; let ty = then_branch.ty.clone(); let ownership = then_branch.ownership; ( @@ -6621,7 +12448,7 @@ impl Resolver<'_> { initializer.name_span, ) })?; - let value = self.resolve_expr( + let value = self.resolve_expr_recursive_reference( function, &initializer.value, bindings, @@ -6699,7 +12526,7 @@ impl Resolver<'_> { initializer.name_span, ) })?; - let value = self.resolve_expr( + let value = self.resolve_expr_recursive_reference( function, &initializer.value, bindings, @@ -6725,8 +12552,12 @@ impl Resolver<'_> { ) } ExprKind::Match { scrutinee, arms } => { - let scrutinee = - self.resolve_expr(function, scrutinee, bindings, &format!("{path}.scrutinee"))?; + let scrutinee = self.resolve_expr_recursive_reference( + function, + scrutinee, + bindings, + &format!("{path}.scrutinee"), + )?; let ResolvedType::Nominal { declaration: matched_type, arguments, @@ -6866,7 +12697,7 @@ impl Resolver<'_> { )? } }; - let value = self.resolve_expr( + let value = self.resolve_expr_recursive_reference( function, &arm.value, &arm_bindings, @@ -6893,8 +12724,12 @@ impl Resolver<'_> { ) } ExprKind::Try { operand } => { - let operand = - self.resolve_expr(function, operand, bindings, &format!("{path}.operand"))?; + let operand = self.resolve_expr_recursive_reference( + function, + operand, + bindings, + &format!("{path}.operand"), + )?; let operand_type = operand.ty.clone(); let ResolvedType::Nominal { declaration, @@ -6962,7 +12797,12 @@ impl Resolver<'_> { } } ExprKind::UpdateRecord { base, fields } => { - let base = self.resolve_expr(function, base, bindings, &format!("{path}.base"))?; + let base = self.resolve_expr_recursive_reference( + function, + base, + bindings, + &format!("{path}.base"), + )?; let ResolvedType::Nominal { declaration: record, arguments: _, @@ -7002,7 +12842,7 @@ impl Resolver<'_> { initializer.name_span, ) })?; - let value = self.resolve_expr( + let value = self.resolve_expr_recursive_reference( function, &initializer.value, bindings, @@ -7023,7 +12863,12 @@ impl Resolver<'_> { ) } ExprKind::Project { base, field, .. } => { - let base = self.resolve_expr(function, base, bindings, &format!("{path}.base"))?; + let base = self.resolve_expr_recursive_reference( + function, + base, + bindings, + &format!("{path}.base"), + )?; let ResolvedType::Nominal { declaration: record, arguments, @@ -7129,8 +12974,437 @@ impl Resolver<'_> { } } +#[cfg(test)] +mod iterative_validator_tests { + use std::collections::{BTreeMap, BTreeSet}; + use std::path::Path; + + use super::*; + use crate::{hir, parse}; + + #[test] + fn iterative_resolver_matches_recursive_reference_outside_builder_accounting() { + let source = r#" +module test.resolver_oracle; +permit { host.echo } +@id("choice") +variant Choice { + @id("choice.a") A { @id("choice.a.v") v: i64, }, + @id("choice.b") B, +} + +@id("pair") +record Pair { + @id("pair.a") a: i64, + @id("pair.b") b: i64, +} +@id("host.echo.interface") +interface HostEcho permits { host.echo } { + @id("host.echo") import rust fn host_echo(value: i64) -> i64 + effects { host.echo } + failure status "host.echo.v1"; +} +@id("callee") fn callee(a: i64, b: i64) -> i64 { a + b } +@id("identity") fn identity(value: T) -> T { value } +@id("option_use") fn option_use(value: Option) -> Option { + let checked = value?; + Option::Some { value: checked > 0 } +} +@id("result_use") fn result_use(value: Result) -> Result { + let checked = value?; + Result::Ok { value: checked > 0 } +} +@id("exercise") fn exercise(flag: bool, choice: Choice, pair: Pair) -> i64 + uses { host.echo } +{ + let x = callee(1, 2); + let native = host_echo(identity(x)); + let rebuilt = if flag && !false { Choice::A { v: Pair { a: native, b: 3 }.a } } else { choice }; + let y = pair with { b: 4 }.b; + match rebuilt { Choice::A { v } => y + v, Choice::B {} => -y, } +} +@id("main") fn main() -> i64 { 0 } +"#; + let parsed = parse(source, Path::new("resolver-oracle.spx")).unwrap(); + let resolved = hir::resolve(&parsed).unwrap(); + let resolver = Resolver { + program: &parsed, + declarations: DeclarationIndex::from_verified(&parsed).unwrap(), + }; + for source_function in &parsed.functions { + let Some(resolved_function) = resolved + .functions + .iter() + .find(|function| function.id.as_str() == source_function.stable_id) + else { + continue; + }; + let execution = FunctionExecutionId::Monomorphic(resolved_function.id.clone()); + let bindings = source_function + .params + .iter() + .zip(&resolved_function.params) + .map(|(source, resolved)| { + ( + source.name.clone(), + Binding { + id: resolved.id.clone(), + ty: resolved.ty.clone(), + ownership: resolved.ownership, + }, + ) + }) + .collect(); + let iterative = resolver.resolve_expr_iterative( + &execution, + &source_function.body, + &bindings, + "body", + ); + let recursive = resolver.resolve_expr_recursive_reference( + &execution, + &source_function.body, + &bindings, + "body", + ); + match (iterative, recursive) { + (Ok(iterative), Ok(recursive)) => assert_eq!(iterative, recursive), + (Err(iterative), Err(recursive)) => { + assert_eq!(iterative.code, recursive.code); + assert_eq!(iterative.severity, recursive.severity); + assert_eq!(iterative.message, recursive.message); + assert_eq!(iterative.path, recursive.path); + assert_eq!(iterative.span, recursive.span); + assert_eq!(iterative.help, recursive.help); + } + (iterative, recursive) => panic!( + "resolver oracle outcome differs: iterative={iterative:?}, recursive={recursive:?}" + ), + } + } + + let invalid = parse( + "module test.resolver_invalid; @id(\"main\") fn main() -> i64 { missing }", + Path::new("resolver-invalid.spx"), + ) + .unwrap(); + let execution = FunctionExecutionId::Monomorphic(DeclarationId::new("main")); + let iterative = resolver.resolve_expr_iterative( + &execution, + &invalid.functions[0].body, + &BTreeMap::new(), + "body", + ); + let recursive = resolver.resolve_expr_recursive_reference( + &execution, + &invalid.functions[0].body, + &BTreeMap::new(), + "body", + ); + let (Err(iterative), Err(recursive)) = (iterative, recursive) else { + panic!("unresolved-value oracle must fail in both evaluators") + }; + assert_eq!(iterative.code, recursive.code); + assert_eq!(iterative.severity, recursive.severity); + assert_eq!(iterative.message, recursive.message); + assert_eq!(iterative.path, recursive.path); + assert_eq!(iterative.span, recursive.span); + assert_eq!(iterative.help, recursive.help); + } + + const SOURCE: &str = r#" +module test.validator_oracle_hostiles; +permit { host.echo } + +@id("token.type") +resource Token { @id("token.drop") drop trivial; } + +@id("owned.box") +record OwnedBox { @id("owned.box.token") token: Token, } + +@id("choice.type") +variant Choice { @id("choice.a") A, @id("choice.b") B, } + +@id("host.echo.interface") +interface HostEcho permits { host.echo } { + @id("host.echo") + import rust fn host_echo(value: i64) -> i64 + effects { host.echo } + failure status "host.echo.v1"; +} + +@id("token.consume") +fn consume(token: own Token) -> i64 { 1 } + +@id("token.consume_bool") +fn consume_bool(token: own Token) -> bool { true } + +@id("hostile.call") +fn call_hostile(token: own Token) -> i64 { consume(token) } + +@id("hostile.native") +fn native_hostile(token: own Token, value: i64) -> i64 + uses { host.echo } +{ host_echo(value) } + +@id("hostile.construct") +fn construct_hostile(token: own Token) -> OwnedBox { OwnedBox { token: token } } + +@id("hostile.update") +fn update_hostile(input: own OwnedBox, token: own Token) -> OwnedBox { + input with { token: token } +} + +@id("hostile.block_statement") +fn block_statement_hostile(token: own Token) -> i64 { + let used = consume(token); + used +} + +@id("hostile.block_tail") +fn block_tail_hostile(token: own Token) -> i64 { + let zero = 0; + consume(token) +} + +@id("hostile.if") +fn if_hostile(flag: bool, token: own Token) -> i64 { + if flag { consume(token) } else { 0 } +} + +@id("hostile.lazy") +fn lazy_hostile(flag: bool, token: own Token) -> bool { + flag && consume_bool(token) +} + +@id("hostile.match") +fn match_hostile(choice: Choice, token: own Token) -> i64 { + match choice { Choice::A {} => consume(token), Choice::B {} => 0, } +} + +@id("app.main") +fn main() -> i64 { 0 } +"#; + + fn program() -> ResolvedProgram { + hir::resolve(&parse(SOURCE, Path::new("validator-oracle-hostiles.spx")).unwrap()).unwrap() + } + + fn function_index(program: &ResolvedProgram, id: &str) -> usize { + program + .functions + .iter() + .position(|function| function.id.as_str() == id) + .unwrap() + } + + fn tail_mut(function: &mut ResolvedFunction) -> &mut ResolvedExpr { + let ResolvedExprKind::Block { tail, .. } = &mut function.body.kind else { + panic!("fixture function body must remain a block") + }; + tail + } + + fn validation_scope(function: &ResolvedFunction) -> BTreeMap { + function + .params + .iter() + .map(|param| { + ( + param.id.clone(), + ValidationBinding { + ty: param.ty.clone(), + ownership: param.ownership, + availability: Availability::Available, + moved_places: BTreeMap::new(), + definitely_partial: BTreeSet::new(), + }, + ) + }) + .collect() + } + + fn validate_expression_hostile( + program: &ResolvedProgram, + function_id: &str, + expression: &ResolvedExpr, + path: &str, + ) -> BTreeMap { + let function = &program.functions[function_index(program, function_id)]; + let execution = FunctionExecutionId::Monomorphic(function.id.clone()); + let mut scope = validation_scope(function); + let mut recursive_scope = scope.clone(); + let mut validator = HirValidator::new(program).unwrap(); + let mut recursive_validator = validator.clone(); + let allowed_effects = function.effects.iter().cloned().collect(); + let recursive = recursive_validator.validate_expr_recursive_reference( + &execution, + expression, + &mut recursive_scope, + path, + true, + Some(&allowed_effects), + ); + let iterative = validator.validate_expr_iterative( + &execution, + expression, + &mut scope, + path, + true, + Some(&allowed_effects), + ); + HirValidator::assert_validation_oracle( + &iterative, + &recursive, + &validator, + &recursive_validator, + &scope, + &recursive_scope, + path, + ); + let diagnostic = iterative.unwrap_err(); + assert_eq!(diagnostic.code, "SPX-H006", "{function_id}"); + function + .params + .iter() + .map(|param| { + ( + param.name.clone(), + scope.get(¶m.id).unwrap().availability, + ) + }) + .collect() + } + + #[test] + fn validator_oracle_preserves_direct_child_scope_on_late_errors() { + for (function_id, expected_token, expected_input) in [ + ("hostile.call", Availability::Moved, None), + ("hostile.native", Availability::Available, None), + ("hostile.construct", Availability::Moved, None), + ( + "hostile.update", + Availability::Moved, + Some(Availability::Moved), + ), + ] { + let mut hostile = program(); + let index = function_index(&hostile, function_id); + tail_mut(&mut hostile.functions[index]).ownership = match function_id { + "hostile.construct" | "hostile.update" => OwnershipMode::Value, + "hostile.call" | "hostile.native" => OwnershipMode::Borrow, + _ => unreachable!(), + }; + let expression = tail_mut(&mut hostile.functions[index]).clone(); + let scope = + validate_expression_hostile(&hostile, function_id, &expression, "body.tail"); + assert_eq!(scope["token"], expected_token); + if let Some(expected) = expected_input { + assert_eq!(scope["input"], expected); + } + } + } + + #[test] + fn validator_oracle_suppresses_failed_block_branch_lazy_and_match_child_scopes() { + for function_id in [ + "hostile.block_statement", + "hostile.block_tail", + "hostile.if", + "hostile.lazy", + "hostile.match", + ] { + let mut hostile = program(); + let index = function_index(&hostile, function_id); + let body = &mut hostile.functions[index].body; + match function_id { + "hostile.block_statement" => { + let ResolvedExprKind::Block { statements, .. } = &mut body.kind else { + unreachable!() + }; + let ResolvedStatement::Let { binding, .. } = &mut statements[0]; + binding.ty = ResolvedType::Bool; + } + "hostile.block_tail" => { + tail_mut(&mut hostile.functions[index]).ownership = OwnershipMode::Borrow; + } + "hostile.if" => { + let ResolvedExprKind::If { then_branch, .. } = + &mut tail_mut(&mut hostile.functions[index]).kind + else { + unreachable!() + }; + then_branch.ownership = OwnershipMode::Borrow; + } + "hostile.lazy" => { + let ResolvedExprKind::Binary { right, .. } = + &mut tail_mut(&mut hostile.functions[index]).kind + else { + unreachable!() + }; + right.ownership = OwnershipMode::Borrow; + } + "hostile.match" => { + let ResolvedExprKind::Match { arms, .. } = + &mut tail_mut(&mut hostile.functions[index]).kind + else { + unreachable!() + }; + arms[0].value.ownership = OwnershipMode::Borrow; + } + _ => unreachable!(), + } + let expression = hostile.functions[index].body.clone(); + let scope = validate_expression_hostile(&hostile, function_id, &expression, "body"); + assert_eq!(scope["token"], Availability::Available, "{function_id}"); + } + } + + #[test] + fn validator_oracle_handles_an_exact_depth_512_late_error_with_a_nonempty_scope() { + fn run() { + const UNARY_NODES: usize = 510; + let source = format!( + "module test.validator_depth; @id(\"token.type\") resource Token {{ @id(\"token.drop\") drop trivial; }} @id(\"token.consume\") fn consume(token: own Token) -> i64 {{ 1 }} @id(\"hostile.depth\") fn deep(token: own Token) -> i64 {{ {}consume(token) }} @id(\"app.main\") fn main() -> i64 {{ 0 }}", + "-".repeat(UNARY_NODES) + ); + let mut hostile = + hir::resolve(&parse(&source, Path::new("validator-depth-hostile.spx")).unwrap()) + .unwrap(); + let index = function_index(&hostile, "hostile.depth"); + let expression = tail_mut(&mut hostile.functions[index]); + let mut depth = 0; + let mut cursor = &*expression; + loop { + depth += 1; + match &cursor.kind { + ResolvedExprKind::Unary { value, .. } => cursor = value, + ResolvedExprKind::Call { args, .. } => cursor = &args[0], + ResolvedExprKind::Place(_) => break, + _ => panic!("unexpected exact-depth fixture shape"), + } + } + assert_eq!(depth, 512); + expression.ownership = OwnershipMode::Borrow; + let expression = expression.clone(); + let scope = + validate_expression_hostile(&hostile, "hostile.depth", &expression, "body.tail"); + assert_eq!(scope["token"], Availability::Moved); + } + + std::thread::Builder::new() + .name("validator-depth-oracle".to_owned()) + .stack_size(16 * 1024 * 1024) + .spawn(run) + .unwrap() + .join() + .unwrap(); + } +} + #[cfg(test)] mod record_tests { + use std::fmt::Write as _; use std::path::Path; use super::{validate, DeclarationId, ResolvedType, ResolvedTypeDeclarationKind}; @@ -7549,6 +13823,27 @@ fn main() -> i64 { helper(1) } assert_eq!(validate(&program).unwrap_err().code, "SPX-H006"); } + #[test] + fn validator_rejects_unit_in_an_ordinary_record_field_and_index() { + let mut program = record_program(); + let ResolvedTypeDeclarationKind::Record { fields } = &mut program.types[0].kind else { + panic!("Node must be a record"); + }; + fields[0].ty = ResolvedType::Unit; + program + .declarations + .record_fields + .get_mut(&DeclarationId::new("node.type")) + .unwrap()[0] + .ty = ResolvedType::Unit; + + let error = validate(&program).unwrap_err(); + assert_eq!(error.code, "SPX-H006"); + assert!(error + .message + .contains("uses Unit outside a native Rust import result")); + } + #[test] fn validator_rejects_a_field_owned_by_the_wrong_record() { let mut program = record_program(); @@ -7561,4 +13856,125 @@ fn main() -> i64 { helper(1) } assert_eq!(validate(&program).unwrap_err().code, "SPX-H006"); } + + #[test] + fn iterative_resolver_and_validator_report_allocated_vec_capacity() { + let source = "module capacity.hir; @id(\"capacity.choose\") fn choose(value: i64) -> i64 { if value == 0 { value } else { value + 1 } } @id(\"app.main\") fn main() -> i64 { choose(0) }"; + let parsed = crate::parse(source, std::path::Path::new("capacity-hir.spx")).unwrap(); + crate::source_verify::reset_capacity_high_water(); + super::reset_iterative_phase_capacity_high_water(); + let resolved = super::resolve(&parsed).unwrap(); + validate(&resolved).unwrap(); + let water = super::iterative_phase_capacity_high_water(); + assert!(water[0] >= std::mem::size_of::()); + assert!(water[1] > 0); + assert!(water[2] > 0); + assert!(crate::source_verify::capacity_high_water() > 0); + } + + #[test] + fn type_facts_capacity_high_water_covers_layered_and_wide_hostiles() { + use sha2::{Digest, Sha256}; + + fn layered(resource: bool, levels: usize) -> String { + let mut source = String::from("module capacity.typefacts.layers;\n\n"); + if resource { + source.push_str( + "@id(\"layer.r0\")\nresource R0 {\n @id(\"layer.r0.drop\")\n drop trivial;\n}\n\n", + ); + } else { + source.push_str( + "@id(\"layer.r0\")\nrecord R0 {\n @id(\"layer.r0.value\")\n value: i64,\n}\n\n", + ); + } + for level in 1..=levels { + writeln!( + source, + "@id(\"layer.r{level}\")\nrecord R{level} {{\n @id(\"layer.r{level}.a\")\n a: R{},\n @id(\"layer.r{level}.b\")\n b: R{},\n}}\n", + level - 1, + level - 1 + ) + .unwrap(); + } + source.push_str("@id(\"app.main\")\nfn main() -> i64 { 0 }\n"); + source + } + + fn resolve_type_facts_peak(source: &str, name: &str) -> (String, usize) { + let parsed = crate::parse(source, std::path::Path::new(name)).unwrap(); + let canonical = crate::format::canonical(&parsed); + super::reset_iterative_phase_capacity_high_water(); + super::resolve(&parsed).unwrap(); + ( + format!("sha256:{:x}", Sha256::digest(canonical.as_bytes())), + super::iterative_phase_capacity_high_water()[2], + ) + } + + let scalar = layered(false, 12); + let resource = layered(true, 12); + let mut wide = String::from("module capacity.typefacts.wide;\n\n"); + for index in 0..514 { + writeln!( + wide, + "@id(\"wide.r{index}\")\nrecord R{index} {{\n @id(\"wide.r{index}.value\")\n value: i64,\n}}\n" + ) + .unwrap(); + } + wide.push_str("@id(\"app.main\")\nfn main() -> i64 { 0 }\n"); + let mut chain = String::from( + "module capacity.typefacts.chain;\n\n@id(\"chain.r0\")\nrecord R0 {\n @id(\"chain.r0.value\")\n value: i64,\n}\n\n", + ); + for index in 1..514 { + writeln!( + chain, + "@id(\"chain.r{index}\")\nrecord R{index} {{\n @id(\"chain.r{index}.next\")\n next: R{},\n}}\n", + index - 1 + ) + .unwrap(); + } + chain.push_str("@id(\"app.main\")\nfn main() -> i64 { 0 }\n"); + + let observed = [ + resolve_type_facts_peak(&scalar, "typefacts-layered-scalar.spx"), + resolve_type_facts_peak(&resource, "typefacts-layered-resource.spx"), + resolve_type_facts_peak(&wide, "typefacts-wide.spx"), + resolve_type_facts_peak(&chain, "typefacts-chain.spx"), + ]; + let expected = [ + ( + "sha256:cfa16985be87d169c3fb81d5958126347ec82b4c1afed878e2d98d1fbfe72c80", + 1_741_515, + 669_965_618, + ), + ( + "sha256:461611e4315e312330af0285273568e5d09cd8e5770a35dcf66a82783aa15ae6", + 1_397_458, + 2_886_293_140, + ), + ( + "sha256:dc19474b86def3eaf6e3c60cc2224694e6aa7cf2811cca6115943c11102f95fc", + 96_838, + 122_429_248, + ), + ( + "sha256:d2692d4883957575ee95df8f9ee7057343599e1da945c386cedea714c716f66d", + 6_273_598, + 31_588_832_202, + ), + ]; + for ((digest, actual), (expected_digest, expected_actual, envelope)) in + observed.into_iter().zip(expected) + { + assert_eq!(digest, expected_digest, "canonical hostile fixture drifted"); + assert_eq!( + actual, expected_actual, + "TypeFacts owned-capacity peak drifted" + ); + assert!( + actual <= envelope, + "TypeFacts observed total exceeded retained_upper + TypeFacts phase" + ); + } + } } diff --git a/src/impact.rs b/src/impact.rs index 135540b..27d4381 100644 --- a/src/impact.rs +++ b/src/impact.rs @@ -189,6 +189,8 @@ fn build_report_with_complete_limits( let candidate = hir::resolve(preflight.candidate())?; hir::validate(&before).map_err(|error| vec![error])?; hir::validate(&candidate).map_err(|error| vec![error])?; + graph::reject_native_rust_imports(&before).map_err(|error| vec![error])?; + graph::reject_native_rust_imports(&candidate).map_err(|error| vec![error])?; let base_schema = graph::graph_schema(&before); let candidate_schema = graph::graph_schema(&candidate); if base_schema != candidate_schema { diff --git a/src/lib.rs b/src/lib.rs index 5c0921e..2beba01 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ pub mod cleanup_plan; pub mod codegen; pub mod conformance; pub mod diagnostic; +pub mod economic_agent; pub mod format; pub mod graph; pub mod hir; @@ -29,6 +30,8 @@ pub mod owned_resource_corpus; pub mod parser; pub mod patch; pub mod patch_evidence; +#[allow(dead_code, reason = "path-included by the unpublished native builder")] +mod private_capacity_contract; pub mod quality_route; pub mod repair; pub mod review; diff --git a/src/parser.rs b/src/parser.rs index 865f795..9685dbc 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2,10 +2,10 @@ use std::path::Path; use crate::ast::{ BinaryOp, Expr, ExprKind, FieldDeclaration, FieldInitializer, Function, ImportDeclaration, - ImportFailure, InterfaceDeclaration, MatchArm, MatchPattern, MatchPatternField, ModuleUse, - ModuleUseKind, Param, ParamMode, Program, ResourceLifecycleDeclaration, ResourceLifecycleKind, - Span, Statement, Type, TypeDeclaration, TypeDeclarationKind, TypeParameterDeclaration, UnaryOp, - VariantCaseDeclaration, + ImportFailure, ImportResult, InterfaceDeclaration, MatchArm, MatchPattern, MatchPatternField, + ModuleUse, ModuleUseKind, Param, ParamMode, Program, ResourceLifecycleDeclaration, + ResourceLifecycleKind, Span, Statement, Type, TypeDeclaration, TypeDeclarationKind, + TypeParameterDeclaration, UnaryOp, VariantCaseDeclaration, }; use crate::diagnostic::Diagnostic; use crate::lexer::{lex, Token, TokenKind}; @@ -248,6 +248,12 @@ impl Parser { } let import_id = self.stable_id_attribute()?; let import_start = self.keyword("import")?.span; + let native_rust = if self.at_keyword("rust") { + self.bump(); + true + } else { + false + }; self.keyword("fn")?; let (import_name, import_name_span) = self.ident("import name")?; self.expect(&TokenKind::LParen, "`(` after import name")?; @@ -282,34 +288,55 @@ impl Parser { } self.expect(&TokenKind::RParen, "`)` after import parameters")?; self.expect(&TokenKind::Arrow, "`->` before import result")?; - self.keyword("unit")?; - self.keyword("effects")?; - let effects = self.effect_set()?; - self.keyword("failure")?; - let failure = if self.at_keyword("infallible") { + let result = if self.at_keyword("unit") { self.bump(); - ImportFailure::Infallible - } else if self.at_keyword("status") { + ImportResult::Unit + } else if native_rust && self.at_keyword("i64") { self.bump(); - let domain_id = match self.bump().kind.clone() { - TokenKind::String(value) => value, - _ => { - return Err(self.error_previous( - "SPX-P106", - "expected status-domain string after `failure status`", - )); - } - }; - ImportFailure::Status { domain_id } + ImportResult::I64 + } else if native_rust && self.at_keyword("bool") { + self.bump(); + ImportResult::Bool } else { - return Err(self.error_here( - "SPX-P106", - "expected `infallible` or `status` after `failure`", - )); + return Err(self.error_here("SPX-P106", "expected admitted import result type")); + }; + self.keyword("effects")?; + let effects = self.effect_set()?; + let failure = { + if native_rust && !self.at_keyword("failure") { + return Err(self.error_here("SPX-P106", "expected keyword `failure`")); + } + self.keyword("failure")?; + if self.at_keyword("infallible") { + self.bump(); + ImportFailure::Infallible + } else if self.at_keyword("status") { + self.bump(); + let domain_id = match self.bump().kind.clone() { + TokenKind::String(value) => value, + _ => { + return Err(self.error_previous( + "SPX-P106", + "expected status-domain string after `failure status`", + )); + } + }; + ImportFailure::Status { domain_id } + } else { + return Err(self.error_here( + "SPX-P106", + "expected `infallible` or `status` after `failure`", + )); + } + }; + let (consumes, consumes_span) = if native_rust { + (String::new(), import_start) + } else { + self.keyword("consumes")?; + let consumed = self.ident("consumed parameter name")?; + self.keyword("always")?; + consumed }; - self.keyword("consumes")?; - let (consumes, consumes_span) = self.ident("consumed parameter name")?; - self.keyword("always")?; let end = self .expect(&TokenKind::Semicolon, "`;` after import contract")? .span; @@ -321,7 +348,9 @@ impl Parser { explicit_id: import_explicit_id, name: import_name, name_span: import_name_span, + native_rust, params, + result, effects, failure, consumes, diff --git a/src/private_capacity_contract.rs b/src/private_capacity_contract.rs new file mode 100644 index 0000000..001d556 --- /dev/null +++ b/src/private_capacity_contract.rs @@ -0,0 +1,422 @@ +//! Unpublished allocation contracts shared with the private native builder. +//! +//! This file is path-included by the unpublished builder so the opaque HIR +//! declaration-index allowance cannot drift from the root-side proof. + +pub(crate) const PRELUDE_CAPACITY_IDENTITIES: [&str; 9] = [ + "core.option", + "core.option.none", + "core.option.some", + "core.option.some.value", + "core.result", + "core.result.ok", + "core.result.ok.value", + "core.result.err", + "core.result.err.error", +]; + +pub(crate) fn declaration_index_upper( + canonical_source_bytes: usize, + type_count: usize, + interface_count: usize, + function_count: usize, + type_facts_layout_upper: usize, +) -> Option { + let declarations = type_count + .checked_add(interface_count)? + .checked_add(function_count)?; + let per_declaration = std::mem::size_of::() + .checked_mul(8)? + .checked_add(640)?; + let compiler_owned_prelude = + std::mem::size_of::().checked_mul(12)?; + // A TypeFacts layout key may embed child keys for each authored field. + // The root builder independently pre-rejects exponential declaration-DAG + // expansion; source² is a checked upper for all retained index key/value + // bytes after that admission and covers the compiler-owned prelude rows. + std::mem::size_of::() + .checked_add(compiler_owned_prelude)? + .checked_add(canonical_source_bytes.checked_mul(4)?)? + .checked_add(type_facts_layout_upper)? + .checked_add(declarations.max(1).checked_mul(per_declaration)?) +} + +pub(crate) fn type_facts_layout_upper( + canonical_source_bytes: usize, + type_count: usize, + maximum_type_occurrences: usize, +) -> Option { + let occurrence_bytes = canonical_source_bytes + .checked_add(canonical_source_bytes.checked_ilog10().unwrap_or(0) as usize * 4)? + .checked_add("variant::record::resource:".len())?; + maximum_type_occurrences + .checked_mul(occurrence_bytes)? + .checked_mul(type_count.max(1)) +} + +fn resolved_type_owned_capacity(ty: &crate::hir::ResolvedType) -> Option { + match ty { + crate::hir::ResolvedType::Unit + | crate::hir::ResolvedType::I64 + | crate::hir::ResolvedType::Bool => Some(0), + crate::hir::ResolvedType::TypeParameter { owner, .. } => Some(owner.as_str().len()), + crate::hir::ResolvedType::Nominal { + declaration, + arguments, + } => arguments + .iter() + .try_fold(declaration.as_str().len(), |bytes, argument| { + bytes.checked_add(resolved_type_owned_capacity(argument)?) + })? + .checked_add( + arguments + .capacity() + .checked_mul(std::mem::size_of::())?, + ), + } +} + +#[allow( + unreachable_patterns, + reason = "non-exhaustive across the builder crate boundary" +)] +fn shape_owned_capacity(shape: &crate::cleanup::FieldLivenessShape) -> Option { + match shape { + crate::cleanup::FieldLivenessShape::NoDrop => Some(0), + crate::cleanup::FieldLivenessShape::Leaf { lifecycle, .. } => { + Some(lifecycle.as_str().len()) + } + crate::cleanup::FieldLivenessShape::Record { + declaration, + fields, + } => fields + .iter() + .try_fold(declaration.as_str().len(), |bytes, field| { + bytes + .checked_add(field.field.as_str().len())? + .checked_add(shape_owned_capacity(&field.shape)?) + })? + .checked_add( + fields + .capacity() + .checked_mul(std::mem::size_of::())?, + ), + _ => None, + } +} + +#[allow( + unreachable_patterns, + reason = "non-exhaustive across the builder crate boundary" +)] +pub(crate) fn cleanup_inventory_owned_capacity( + inventory: &crate::cleanup::CleanupInventory, +) -> Option { + let slots = inventory.slots.iter().try_fold(0usize, |bytes, slot| { + let origin = match &slot.origin { + crate::cleanup::CleanupStorageOrigin::Parameter { value, .. } + | crate::cleanup::CleanupStorageOrigin::Binding { value } + | crate::cleanup::CleanupStorageOrigin::ProvisionalResult { value } => { + value.as_str().len() + } + crate::cleanup::CleanupStorageOrigin::Temporary { expression } => { + expression.as_str().len() + } + _ => return None, + }; + bytes + .checked_add(origin)? + .checked_add(resolved_type_owned_capacity(&slot.ty)?)? + .checked_add(shape_owned_capacity(&slot.shape)?) + })?; + let flags = inventory.flags.iter().try_fold(0usize, |bytes, flag| { + let projections = flag + .place + .projections + .iter() + .try_fold(0usize, |bytes, id| bytes.checked_add(id.as_str().len()))?; + bytes + .checked_add(flag.lifecycle.as_str().len())? + .checked_add( + flag.place + .projections + .capacity() + .checked_mul(std::mem::size_of::())?, + )? + .checked_add(projections) + })?; + inventory + .slots + .capacity() + .checked_mul(std::mem::size_of::())? + .checked_add( + inventory + .flags + .capacity() + .checked_mul(std::mem::size_of::())?, + )? + .checked_add( + inventory + .entry_state + .live_owned_parameters + .capacity() + .checked_mul(std::mem::size_of::())?, + )? + .checked_add(slots)? + .checked_add(flags) +} + +fn storage_owned_capacity(storage: &crate::cleanup_plan::StorageId) -> Option { + match storage { + crate::cleanup_plan::StorageId::Value(value) => Some(value.as_str().len()), + crate::cleanup_plan::StorageId::Temporary(expression) => Some(expression.as_str().len()), + crate::cleanup_plan::StorageId::CallArgument { + call, + value_expression, + .. + } => call + .as_str() + .len() + .checked_add(value_expression.as_str().len()), + crate::cleanup_plan::StorageId::ProvisionalResult => Some(0), + } +} + +fn cleanup_place_owned_capacity(place: &crate::cleanup_plan::CleanupPlace) -> Option { + place + .projections + .iter() + .try_fold( + storage_owned_capacity(&place.storage)?, + |bytes, projection| bytes.checked_add(projection.as_str().len()), + )? + .checked_add( + place + .projections + .capacity() + .checked_mul(std::mem::size_of::())?, + ) +} + +fn staged_result_owned_capacity( + source: &crate::cleanup_plan::StagedCopyResultSource, +) -> Option { + use crate::cleanup_plan::StagedCopyResultSource; + match source { + StagedCopyResultSource::Body { + expression, + instance, + } => expression + .as_str() + .len() + .checked_add(resolved_type_owned_capacity(instance)?), + StagedCopyResultSource::TryResidual { + expression, + operand, + source_instance, + target_instance, + result, + ok_case, + ok_field, + err_case, + err_field, + } => [ + expression.as_str().len(), + operand.as_str().len(), + result.as_str().len(), + ok_case.as_str().len(), + ok_field.as_str().len(), + err_case.as_str().len(), + err_field.as_str().len(), + ] + .into_iter() + .try_fold(0usize, usize::checked_add)? + .checked_add(resolved_type_owned_capacity(source_instance)?)? + .checked_add(resolved_type_owned_capacity(target_instance)?), + StagedCopyResultSource::TryOptionNone { + expression, + operand, + source_instance, + target_instance, + option, + some_case, + some_field, + none_case, + } => [ + expression.as_str().len(), + operand.as_str().len(), + option.as_str().len(), + some_case.as_str().len(), + some_field.as_str().len(), + none_case.as_str().len(), + ] + .into_iter() + .try_fold(0usize, usize::checked_add)? + .checked_add(resolved_type_owned_capacity(source_instance)?)? + .checked_add(resolved_type_owned_capacity(target_instance)?), + } +} + +pub(crate) fn cleanup_plan_owned_capacity( + plan: &crate::cleanup_plan::CleanupPlan, +) -> Option { + use crate::cleanup_plan::{ + CleanupResultSource, CleanupTerminator, CleanupTransition, EdgeCondition, ExitContinuation, + StatusProducer, + }; + let status_id = |id: &crate::cleanup_plan::StatusSourceId| id.expression.as_str().len(); + let mut bytes = [ + plan.entry_state + .live_owned_parameters + .capacity() + .checked_mul(std::mem::size_of::())?, + plan.slots + .capacity() + .checked_mul(std::mem::size_of::())?, + plan.status_sources + .capacity() + .checked_mul(std::mem::size_of::())?, + plan.blocks + .capacity() + .checked_mul(std::mem::size_of::())?, + plan.edges + .capacity() + .checked_mul(std::mem::size_of::())?, + plan.regions + .capacity() + .checked_mul(std::mem::size_of::())?, + plan.exits + .capacity() + .checked_mul(std::mem::size_of::())?, + ] + .into_iter() + .try_fold(0usize, usize::checked_add)?; + bytes = bytes.checked_add( + plan.entry_state + .live_owned_parameters + .iter() + .try_fold(0usize, |bytes, place| { + bytes.checked_add(cleanup_place_owned_capacity(place)?) + })?, + )?; + bytes = bytes.checked_add(plan.slots.iter().try_fold(0usize, |bytes, slot| { + bytes.checked_add( + storage_owned_capacity(&slot.storage)? + .checked_add(resolved_type_owned_capacity(&slot.ty)?)? + .checked_add(shape_owned_capacity(&slot.field_liveness_shape)?)?, + ) + })?)?; + for status in &plan.status_sources { + bytes = bytes.checked_add(status_id(&status.id))?; + bytes = bytes.checked_add(match &status.producer { + StatusProducer::PropagatedCall { callee } => callee.as_str().len(), + StatusProducer::CheckedArithmetic { + normalized_cases, .. + } => normalized_cases + .capacity() + .checked_mul(std::mem::size_of::())?, + StatusProducer::ContractFalse { .. } => 0, + })?; + } + for block in &plan.blocks { + bytes = bytes.checked_add( + block + .transitions + .capacity() + .checked_mul(std::mem::size_of::())?, + )?; + for transition in &block.transitions { + let transition_bytes = match transition { + CleanupTransition::Initialize { at, destination } => at + .as_str() + .len() + .checked_add(cleanup_place_owned_capacity(destination)?)?, + CleanupTransition::Transfer { + at, + source, + destination, + } => at + .as_str() + .len() + .checked_add(cleanup_place_owned_capacity(source)?)? + .checked_add(cleanup_place_owned_capacity(destination)?)?, + CleanupTransition::CallCommit { call, arguments } => call + .as_str() + .len() + .checked_add(arguments.capacity().checked_mul(std::mem::size_of::< + crate::cleanup_plan::CallArgumentTransfer, + >())?)? + .checked_add(arguments.iter().try_fold(0usize, |bytes, argument| { + bytes.checked_add(cleanup_place_owned_capacity(&argument.source)?) + })?)?, + CleanupTransition::SelectFailure { source } => status_id(source), + // Staged-copy metadata identities/types are also represented + // in the owning HIR expression; charge its full inline value + // plus one source-derived identity payload here. + CleanupTransition::StageCopyResult { source } => { + staged_result_owned_capacity(source)? + } + }; + bytes = bytes.checked_add(transition_bytes)?; + } + if let CleanupTerminator::Branch(edges) = &block.terminator { + bytes = bytes.checked_add( + edges + .capacity() + .checked_mul(std::mem::size_of::())?, + )?; + } + } + for edge in &plan.edges { + bytes = bytes.checked_add(match &edge.condition { + EdgeCondition::Always => 0, + EdgeCondition::BooleanResult(expression, _) => expression.as_str().len(), + EdgeCondition::VariantCase { + scrutinee, case, .. + } => scrutinee.as_str().len().checked_add(case.as_str().len())?, + EdgeCondition::StatusZero(source) | EdgeCondition::StatusNonzero(source) => { + status_id(source) + } + })?; + } + for region in &plan.regions { + bytes = bytes.checked_add( + region + .slots + .capacity() + .checked_mul(std::mem::size_of::())?, + )?; + bytes = bytes.checked_add(region.slots.iter().try_fold(0usize, |bytes, storage| { + bytes.checked_add(storage_owned_capacity(storage)?) + })?)?; + } + for exit in &plan.exits { + bytes = bytes.checked_add( + exit.leaves_regions + .capacity() + .checked_mul(std::mem::size_of::())?, + )?; + bytes = bytes.checked_add( + exit.finalize_in_order + .capacity() + .checked_mul(std::mem::size_of::())?, + )?; + bytes = bytes.checked_add(exit.finalize_in_order.iter().try_fold( + 0usize, + |bytes, action| { + bytes + .checked_add(cleanup_place_owned_capacity(&action.source)?)? + .checked_add(action.lifecycle_id.as_str().len()) + }, + )?)?; + bytes = bytes.checked_add(match &exit.continuation { + ExitContinuation::Continue(_) | ExitContinuation::ReturnUnit => 0, + ExitContinuation::CommitResult { source } => match source { + CleanupResultSource::Scalar { expression } => expression.as_str().len(), + CleanupResultSource::Owned { storage } => cleanup_place_owned_capacity(storage)?, + }, + ExitContinuation::ReturnFailure { source } => status_id(source), + })?; + } + Some(bytes) +} diff --git a/src/repair.rs b/src/repair.rs index 5c54899..571fed5 100644 --- a/src/repair.rs +++ b/src/repair.rs @@ -684,6 +684,11 @@ fn collect_calls( collect_calls(argument, known, calls, call_sites); } } + ResolvedExprKind::NativeRustImportCall(call) => { + for argument in &call.args { + collect_calls(argument, known, calls, call_sites); + } + } ResolvedExprKind::Unary { value, .. } => collect_calls(value, known, calls, call_sites), ResolvedExprKind::Binary { left, right, .. } => { collect_calls(left, known, calls, call_sites); diff --git a/src/review.rs b/src/review.rs index 8242e09..c16b6ec 100644 --- a/src/review.rs +++ b/src/review.rs @@ -419,6 +419,8 @@ fn build_from_preflight_with_limits( let candidate_resolved = hir::resolve(preflight.candidate())?; hir::validate(&before_resolved).map_err(|error| vec![error])?; hir::validate(&candidate_resolved).map_err(|error| vec![error])?; + graph::reject_native_rust_imports(&before_resolved).map_err(|error| vec![error])?; + graph::reject_native_rust_imports(&candidate_resolved).map_err(|error| vec![error])?; Ok((before_resolved, candidate_resolved)) }; let (before_resolved, candidate_resolved) = if let Some(limit) = max_candidate_bytes { diff --git a/src/source_verify.rs b/src/source_verify.rs index 3212d98..8695f7e 100644 --- a/src/source_verify.rs +++ b/src/source_verify.rs @@ -2,13 +2,95 @@ use std::collections::{BTreeSet, HashMap, HashSet}; use crate::ast::{ BinaryOp, Expr, ExprKind, FieldDeclaration, Function, ImportDeclaration, ImportFailure, - InterfaceDeclaration, MatchPattern, ParamMode, Program, RecordMatchFieldPattern, - RecordMatchPatternField, ResourceLifecycleKind, Span, Statement, Type, TypeDeclaration, - TypeDeclarationKind, UnaryOp, VariantCaseDeclaration, + ImportResult, InterfaceDeclaration, MatchPattern, Param, ParamMode, Program, + RecordMatchFieldPattern, RecordMatchPatternField, ResourceLifecycleKind, Span, Statement, Type, + TypeDeclaration, TypeDeclarationKind, UnaryOp, VariantCaseDeclaration, }; use crate::conformance::STATUS_DOMAIN_MAX_BYTES_V1; use crate::diagnostic::Diagnostic; +#[cfg(test)] +thread_local! { + static SOURCE_VERIFY_CAPACITY_HIGH_WATER: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +#[cfg(test)] +pub(crate) fn reset_capacity_high_water() { + SOURCE_VERIFY_CAPACITY_HIGH_WATER.with(|water| water.set(0)); +} + +#[cfg(test)] +pub(crate) fn capacity_high_water() -> usize { + SOURCE_VERIFY_CAPACITY_HIGH_WATER.with(std::cell::Cell::get) +} + +#[cfg(test)] +fn note_capacity_high_water(bytes: usize) { + SOURCE_VERIFY_CAPACITY_HIGH_WATER.with(|water| water.set(water.get().max(bytes))); +} + +#[cfg(test)] +fn binding_owned_capacity(binding: &Binding) -> usize { + let moved = binding + .moved_places + .iter() + .fold(0usize, |bytes, (place, _)| { + bytes + + std::mem::size_of::<(Vec, Availability)>() + + place.capacity() * std::mem::size_of::() + + place.iter().map(String::capacity).sum::() + }); + let partial = binding + .definitely_partial + .iter() + .fold(0usize, |bytes, place| { + bytes + + std::mem::size_of::>() + + place.capacity() * std::mem::size_of::() + + place.iter().map(String::capacity).sum::() + }); + binding + .moved_places + .capacity() + .saturating_mul(std::mem::size_of::<(Vec, Availability)>()) + .saturating_add( + binding + .definitely_partial + .capacity() + .saturating_mul(std::mem::size_of::>()), + ) + .saturating_add(ast_type_owned_capacity(&binding.ty)) + .saturating_add(moved) + .saturating_add(partial) +} + +#[cfg(test)] +fn ast_type_owned_capacity(ty: &Type) -> usize { + match ty { + Type::I64 | Type::Bool => 0, + Type::Named { name, arguments } => name + .capacity() + .saturating_add(arguments.capacity() * std::mem::size_of::()) + .saturating_add(arguments.iter().map(ast_type_owned_capacity).sum::()), + } +} + +#[cfg(test)] +fn scope_owned_capacity(scope: &VerifierScope) -> usize { + scope + .bindings + .capacity() + .saturating_mul(std::mem::size_of::<(String, Binding)>()) + .saturating_add( + scope + .bindings + .iter() + .fold(0usize, |bytes, (name, binding)| { + bytes + name.capacity() + binding_owned_capacity(binding) + }), + ) +} + #[derive(Clone, Debug)] struct Binding { ty: Type, @@ -16,6 +98,7 @@ struct Binding { availability: Availability, moved_places: HashMap, Availability>, definitely_partial: HashSet>, + native_unit_discard: bool, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -39,6 +122,23 @@ impl Availability { struct CheckedValue { ty: Type, mode: ParamMode, + native_unit: bool, +} + +fn reject_native_unit_value( + program: &Program, + expression: &Expr, + value: &CheckedValue, + diagnostics: &mut Vec, +) { + if value.native_unit && !matches!(expression.kind, ExprKind::Var(_)) { + diagnostics.push(error( + program, + "SPX-B107", + "Native Rust Interop declaration set is unsupported: scalar value signature required", + expression.span, + )); + } } impl CheckedValue { @@ -46,6 +146,7 @@ impl CheckedValue { Self { ty, mode: ParamMode::Value, + native_unit: false, } } @@ -55,7 +156,11 @@ impl CheckedValue { } else { ParamMode::Value }; - Self { ty, mode } + Self { + ty, + mode, + native_unit: false, + } } } @@ -112,33 +217,46 @@ impl<'a> TypeTable<'a> { arguments: &[Type], template: &Type, ) -> Option { - match template { - Type::I64 => Some(Type::I64), - Type::Bool => Some(Type::Bool), - Type::Named { - name, - arguments: nested, - } => { - if nested.is_empty() { - if let Some(index) = declaration - .type_parameters - .iter() - .position(|parameter| parameter.name == *name) - { - return arguments.get(index).cloned(); + enum Frame<'a> { + Enter(&'a Type), + Finish(&'a str, usize), + } + let mut frames = vec![Frame::Enter(template)]; + let mut resolved = Vec::new(); + while let Some(frame) = frames.pop() { + match frame { + Frame::Enter(template) => match template { + Type::I64 => resolved.push(Type::I64), + Type::Bool => resolved.push(Type::Bool), + Type::Named { + name, + arguments: nested, + } => { + if nested.is_empty() { + if let Some(index) = declaration + .type_parameters + .iter() + .position(|parameter| parameter.name == *name) + { + resolved.push(arguments.get(index)?.clone()); + continue; + } + } + frames.push(Frame::Finish(name, nested.len())); + frames.extend(nested.iter().rev().map(Frame::Enter)); } + }, + Frame::Finish(name, count) => { + let split = resolved.len().checked_sub(count)?; + let nested = resolved.drain(split..).collect(); + resolved.push(Type::Named { + name: name.to_owned(), + arguments: nested, + }); } - Some(Type::Named { - name: name.clone(), - arguments: nested - .iter() - .map(|argument| { - Self::substitute_variant_type(declaration, arguments, argument) - }) - .collect::>>()?, - }) } } + (resolved.len() == 1).then(|| resolved.pop().expect("type count checked above")) } fn contains_resource(&self, ty: &Type) -> bool { @@ -155,42 +273,51 @@ impl<'a> TypeTable<'a> { } fn contains_resource_inner(&self, ty: &Type, visiting: &mut HashSet) -> bool { - let Type::Named { name, arguments } = ty else { - return false; - }; - let Some(declaration) = self.declaration(name) else { - return false; - }; - match &declaration.kind { - TypeDeclarationKind::Resource { .. } => true, - TypeDeclarationKind::Record { fields } => { - let instance = ty.to_string(); - if !visiting.insert(instance.clone()) { - return true; - } - let contains = fields.iter().any(|field| { - Self::substitute_variant_type(declaration, arguments, &field.ty) - .is_none_or(|field_ty| self.contains_resource_inner(&field_ty, visiting)) - }); - visiting.remove(&instance); - contains - } - TypeDeclarationKind::Variant { cases } => { - let instance = ty.to_string(); - if !visiting.insert(instance.clone()) { - return true; - } - let contains = cases.iter().any(|case| { - case.fields.iter().any(|field| { - Self::substitute_variant_type(declaration, arguments, &field.ty).is_none_or( - |field_ty| self.contains_resource_inner(&field_ty, visiting), - ) - }) - }); - visiting.remove(&instance); - contains + enum Frame { + Enter(Type), + Exit(String), + } + let mut frames = vec![Frame::Enter(ty.clone())]; + while let Some(frame) = frames.pop() { + match frame { + Frame::Exit(instance) => { + visiting.remove(&instance); + } + Frame::Enter(ty) => { + let Type::Named { name, arguments } = &ty else { + continue; + }; + let Some(declaration) = self.declaration(name) else { + continue; + }; + if matches!(declaration.kind, TypeDeclarationKind::Resource { .. }) { + return true; + } + let instance = ty.to_string(); + if !visiting.insert(instance.clone()) { + return true; + } + frames.push(Frame::Exit(instance)); + let fields: Box> = + match &declaration.kind { + TypeDeclarationKind::Record { fields } => Box::new(fields.iter()), + TypeDeclarationKind::Variant { cases } => { + Box::new(cases.iter().flat_map(|case| &case.fields)) + } + TypeDeclarationKind::Resource { .. } => unreachable!(), + }; + for field in fields.rev() { + let Some(field_ty) = + Self::substitute_variant_type(declaration, arguments, &field.ty) + else { + return true; + }; + frames.push(Frame::Enter(field_ty)); + } + } } } + false } fn lifecycle_effects( @@ -210,50 +337,62 @@ impl<'a> TypeTable<'a> { visiting: &mut HashSet, effects: &mut HashSet, ) { - let Type::Named { name, arguments } = ty else { - return; - }; - let Some(declaration) = self.declaration(name) else { - return; - }; - let instance = ty.to_string(); - if !visiting.insert(instance.clone()) { - return; + enum Frame { + Enter(Type), + Exit(String), } - match &declaration.kind { - TypeDeclarationKind::Resource { lifecycles } => { - if let Some(crate::ast::ResourceLifecycleDeclaration { - kind: ResourceLifecycleKind::Imported { import_key }, - .. - }) = lifecycles.first() - { - if let Some((_, import)) = imports.get(import_key.as_str()) { - effects.extend(import.effects.iter().cloned()); - } + let mut frames = vec![Frame::Enter(ty.clone())]; + while let Some(frame) = frames.pop() { + match frame { + Frame::Exit(instance) => { + visiting.remove(&instance); } - } - TypeDeclarationKind::Record { fields } => { - for field in fields { - if let Some(field_ty) = - Self::substitute_variant_type(declaration, arguments, &field.ty) - { - self.lifecycle_effects_inner(&field_ty, imports, visiting, effects); + Frame::Enter(ty) => { + let Type::Named { name, arguments } = &ty else { + continue; + }; + let Some(declaration) = self.declaration(name) else { + continue; + }; + let instance = ty.to_string(); + if !visiting.insert(instance.clone()) { + continue; } - } - } - TypeDeclarationKind::Variant { cases } => { - for case in cases { - for field in &case.fields { - if let Some(field_ty) = - Self::substitute_variant_type(declaration, arguments, &field.ty) - { - self.lifecycle_effects_inner(&field_ty, imports, visiting, effects); + frames.push(Frame::Exit(instance)); + match &declaration.kind { + TypeDeclarationKind::Resource { lifecycles } => { + if let Some(crate::ast::ResourceLifecycleDeclaration { + kind: ResourceLifecycleKind::Imported { import_key }, + .. + }) = lifecycles.first() + { + if let Some((_, import)) = imports.get(import_key.as_str()) { + effects.extend(import.effects.iter().cloned()); + } + } + } + TypeDeclarationKind::Record { fields } => { + for field in fields.iter().rev() { + if let Some(field_ty) = + Self::substitute_variant_type(declaration, arguments, &field.ty) + { + frames.push(Frame::Enter(field_ty)); + } + } + } + TypeDeclarationKind::Variant { cases } => { + for field in cases.iter().flat_map(|case| &case.fields).rev() { + if let Some(field_ty) = + Self::substitute_variant_type(declaration, arguments, &field.ty) + { + frames.push(Frame::Enter(field_ty)); + } + } } } } } } - visiting.remove(&instance); } } @@ -715,18 +854,29 @@ pub(crate) fn verify(program: &Program) -> Vec { )); } if !import.explicit_id || import.stable_id.is_empty() { - diagnostics.push( - error( + if import.native_rust { + diagnostics.push(error( program, - "SPX-I403", - format!( - "import `{}.{}` requires an explicit @id", - interface.name, import.name - ), + "SPX-B107", + "Native Rust Interop declaration set is unsupported: explicit persistent ID required", import.name_span, - ) - .with_help("the v1 import @id is also its target-neutral logical import key"), - ); + )); + } else { + diagnostics.push( + error( + program, + "SPX-I403", + format!( + "import `{}.{}` requires an explicit @id", + interface.name, import.name + ), + import.name_span, + ) + .with_help( + "the v1 import @id is also its target-neutral logical import key", + ), + ); + } } let import_identity_is_valid = !import.stable_id.contains('\0'); if !import_identity_is_valid { @@ -760,6 +910,7 @@ pub(crate) fn verify(program: &Program) -> Vec { } let types = TypeTable::new(program); + let mut native_rust_names = HashSet::new(); for interface in &program.interfaces { let permits = interface .permits @@ -767,6 +918,27 @@ pub(crate) fn verify(program: &Program) -> Vec { .map(String::as_str) .collect::>(); for import in &interface.imports { + if import.native_rust && !native_rust_names.insert(import.name.as_str()) { + diagnostics.push(error( + program, + "SPX-B107", + "Native Rust Interop declaration set is unsupported: symbol collision", + import.span, + )); + } + if import.native_rust + && program + .functions + .iter() + .any(|function| function.name == import.name) + { + diagnostics.push(error( + program, + "SPX-B107", + "Native Rust Interop declaration set is unsupported: symbol collision", + import.span, + )); + } for param in &import.params { check_declared_type( program, @@ -777,60 +949,111 @@ pub(crate) fn verify(program: &Program) -> Vec { &mut diagnostics, ); } - let valid_shape = import.params.len() == 1 - && import.params[0].mode == ParamMode::Own - && types.is_opaque_resource(&import.params[0].ty) - && import.consumes == import.params[0].name; + let valid_shape = if import.native_rust { + import.params.len() <= 8 + && import.consumes.is_empty() + && import.params.iter().all(|parameter| { + parameter.mode == ParamMode::Value + && matches!(parameter.ty, Type::I64 | Type::Bool) + }) + } else { + import.result == crate::ast::ImportResult::Unit + && import.params.len() == 1 + && import.params[0].mode == ParamMode::Own + && types.is_opaque_resource(&import.params[0].ty) + && import.consumes == import.params[0].name + }; if !valid_shape { diagnostics.push(error( program, - "SPX-I404", - format!( - "import `{}.{}` must take one owned resource parameter and consume it always", - interface.name, import.name - ), + if import.native_rust { "SPX-B107" } else { "SPX-I404" }, + if import.native_rust { + "Native Rust Interop declaration set is unsupported: scalar value signature required".to_owned() + } else { + format!( + "import `{}.{}` must take one owned resource parameter and consume it always", + interface.name, import.name + ) + }, import.span, )); } if let ImportFailure::Status { domain_id } = &import.failure { - if domain_id.is_empty() - || domain_id.len() > STATUS_DOMAIN_MAX_BYTES_V1 - || domain_id.contains('\0') + if (import.native_rust && !native_rust_status_domain(domain_id)) + || (!import.native_rust + && (domain_id.is_empty() + || domain_id.len() > STATUS_DOMAIN_MAX_BYTES_V1 + || domain_id.contains('\0'))) { diagnostics.push(error( program, - "SPX-I403", - format!( - "import `{}.{}` has an invalid failure domain; status v1 requires 1..={STATUS_DOMAIN_MAX_BYTES_V1} UTF-8 bytes and forbids NUL", - interface.name, import.name, - ), + if import.native_rust { "SPX-B107" } else { "SPX-I403" }, + if import.native_rust { + "Native Rust Interop declaration set is unsupported: status domain is invalid".to_owned() + } else { + format!( + "import `{}.{}` has an invalid failure domain", + interface.name, import.name + ) + }, import.span, )); } } let mut effects = HashSet::new(); + if import.native_rust + && import + .effects + .windows(2) + .any(|pair| pair[0].as_str() >= pair[1].as_str()) + { + diagnostics.push(error( + program, + "SPX-B107", + "Native Rust Interop declaration set is unsupported: effect or capability mismatch", + import.span, + )); + } for effect in &import.effects { if !effects.insert(effect.as_str()) { - diagnostics.push(error( - program, - "SPX-I403", - format!( - "import `{}.{}` declares duplicate effect `{effect}`", - interface.name, import.name - ), - import.span, - )); + diagnostics.push(if import.native_rust { + error( + program, + "SPX-B107", + "Native Rust Interop declaration set is unsupported: effect or capability mismatch", + import.span, + ) + } else { + error( + program, + "SPX-I403", + format!( + "import `{}.{}` declares duplicate effect `{effect}`", + interface.name, import.name + ), + import.span, + ) + }); } if !permits.contains(effect.as_str()) { - diagnostics.push(error( - program, - "SPX-I404", - format!( - "import `{}.{}` requires effect `{effect}` outside interface `{}` permits", - interface.name, import.name, interface.name - ), - import.span, - )); + diagnostics.push(if import.native_rust { + error( + program, + "SPX-B107", + "Native Rust Interop declaration set is unsupported: effect or capability mismatch", + import.span, + ) + } else { + error( + program, + "SPX-I404", + format!( + "import `{}.{}` requires effect `{effect}` outside interface `{}` permits", + interface.name, import.name, interface.name + ), + import.span, + ) + }); } } } @@ -847,7 +1070,8 @@ pub(crate) fn verify(program: &Program) -> Vec { let compatible = import_keys .get(import_key.as_str()) .is_some_and(|(_, import)| { - import.params.len() == 1 + !import.native_rust + && import.params.len() == 1 && import.params[0].mode == ParamMode::Own && import.params[0].ty == (Type::Named { @@ -1288,6 +1512,7 @@ pub(crate) fn verify(program: &Program) -> Vec { availability: Availability::Available, moved_places: HashMap::new(), definitely_partial: HashSet::new(), + native_unit_discard: false, }, ) .is_some() @@ -1316,7 +1541,7 @@ pub(crate) fn verify(program: &Program) -> Vec { ); } - if let Some(actual) = check_expr( + if let Some(actual) = check_expr_iterative( program, function, &function.body, @@ -1327,7 +1552,10 @@ pub(crate) fn verify(program: &Program) -> Vec { true, &mut diagnostics, ) { - if actual.ty != function.return_type { + if actual.native_unit { + reject_native_unit_value(program, &function.body, &actual, &mut diagnostics); + } + if !actual.native_unit && actual.ty != function.return_type { diagnostics.push(error( program, "SPX-T103", @@ -1473,60 +1701,132 @@ pub(crate) fn verify(program: &Program) -> Vec { .at_path(&program.path), ); } + let mut native_interop_failures = HashSet::new(); + diagnostics.retain(|diagnostic| { + diagnostic.code != "SPX-B107" || native_interop_failures.insert(diagnostic.message.clone()) + }); + if let Some(native_failure) = diagnostics + .iter() + .find(|diagnostic| diagnostic.code == "SPX-B107") + .cloned() + { + return vec![native_failure]; + } diagnostics } +fn native_rust_status_domain(value: &str) -> bool { + let bytes = value.as_bytes(); + (2..=128).contains(&bytes.len()) + && (bytes[0].is_ascii_lowercase() || bytes[0].is_ascii_digit()) + && (bytes[bytes.len() - 1].is_ascii_lowercase() || bytes[bytes.len() - 1].is_ascii_digit()) + && bytes.iter().all(|byte| { + byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'.' | b'-') + }) +} + fn record_layout_is_recursive( name: &str, types: &TypeTable<'_>, visiting: &mut HashSet, checked: &mut HashSet, ) -> bool { - if checked.contains(name) { - return false; + enum Frame<'a> { + Enter(&'a str), + Fields { + name: &'a str, + fields: &'a [FieldDeclaration], + parameters: HashSet<&'a str>, + index: usize, + }, } - if !visiting.insert(name.to_owned()) { - return true; - } - let recursive = types - .declaration(name) - .and_then(|declaration| match &declaration.kind { - TypeDeclarationKind::Record { fields } => Some(fields), - TypeDeclarationKind::Resource { .. } | TypeDeclarationKind::Variant { .. } => None, - }) - .is_some_and(|fields| { - let parameters = types - .declaration(name) - .map(|declaration| { - declaration - .type_parameters - .iter() - .map(|parameter| parameter.name.as_str()) - .collect::>() - }) - .unwrap_or_default(); - fields.iter().any(|field| { - let Type::Named { - name: field_type, - arguments, - } = &field.ty - else { - return false; + + let mut frames = vec![Frame::Enter(name)]; + let mut results = Vec::new(); + while let Some(frame) = frames.pop() { + match frame { + Frame::Enter(name) => { + if checked.contains(name) { + results.push(false); + continue; + } + if !visiting.insert(name.to_owned()) { + results.push(true); + continue; + } + let Some(declaration) = types.declaration(name) else { + visiting.remove(name); + checked.insert(name.to_owned()); + results.push(false); + continue; }; - if arguments.is_empty() && parameters.contains(field_type.as_str()) { - return false; + let TypeDeclarationKind::Record { fields } = &declaration.kind else { + visiting.remove(name); + checked.insert(name.to_owned()); + results.push(false); + continue; + }; + let parameters = declaration + .type_parameters + .iter() + .map(|parameter| parameter.name.as_str()) + .collect::>(); + frames.push(Frame::Fields { + name, + fields, + parameters, + index: 0, + }); + } + Frame::Fields { + name, + fields, + parameters, + mut index, + } => { + if results.pop().unwrap_or(false) { + visiting.remove(name); + results.push(true); + continue; } - matches!( - types.declaration(field_type).map(|item| &item.kind), - Some(TypeDeclarationKind::Record { .. }) - ) && record_layout_is_recursive(field_type, types, visiting, checked) - }) - }); - visiting.remove(name); - if !recursive { - checked.insert(name.to_owned()); + let mut child = None; + while let Some(field) = fields.get(index) { + index += 1; + let Type::Named { + name: field_type, + arguments, + } = &field.ty + else { + continue; + }; + if arguments.is_empty() && parameters.contains(field_type.as_str()) { + continue; + } + if matches!( + types.declaration(field_type).map(|item| &item.kind), + Some(TypeDeclarationKind::Record { .. }) + ) { + child = Some(field_type.as_str()); + break; + } + } + if let Some(child) = child { + frames.push(Frame::Fields { + name, + fields, + parameters, + index, + }); + frames.push(Frame::Enter(child)); + } else { + visiting.remove(name); + checked.insert(name.to_owned()); + results.push(false); + } + } + } } - recursive + results.pop().unwrap_or(false) } fn check_declared_type( @@ -1537,7 +1837,11 @@ fn check_declared_type( parameters: &HashSet<&str>, diagnostics: &mut Vec, ) { - if let Type::Named { name, arguments } = ty { + let mut pending = vec![ty]; + while let Some(ty) = pending.pop() { + let Type::Named { name, arguments } = ty else { + continue; + }; if parameters.contains(name.as_str()) { if !arguments.is_empty() { diagnostics.push(error( @@ -1547,7 +1851,7 @@ fn check_declared_type( span, )); } - return; + continue; } let Some(declaration) = types.declaration(name) else { let (code, message) = if parameters.is_empty() { @@ -1562,7 +1866,7 @@ fn check_declared_type( ) }; diagnostics.push(error(program, code, message, span)); - return; + continue; }; if arguments.len() != declaration.type_parameters.len() { diagnostics.push(error( @@ -1591,9 +1895,7 @@ fn check_declared_type( span, )); } - for argument in arguments { - check_declared_type(program, argument, span, types, parameters, diagnostics); - } + pending.extend(arguments.iter().rev()); } } @@ -1615,33 +1917,48 @@ fn substitute_function_type( arguments: &[Type], template: &Type, ) -> Option { - match template { - Type::I64 => Some(Type::I64), - Type::Bool => Some(Type::Bool), - Type::Named { - name, - arguments: nested, - } => { - if nested.is_empty() { - if let Some(index) = function - .type_parameters - .iter() - .position(|parameter| parameter.name == *name) - { - return arguments.get(index).cloned(); - } - } - Some(Type::Named { - name: name.clone(), - arguments: nested - .iter() - .map(|nested| substitute_function_type(function, arguments, nested)) - .collect::>>()?, - }) - } + enum Frame<'a> { + Enter(&'a Type), + Finish(&'a str, usize), } -} - + let mut frames = vec![Frame::Enter(template)]; + let mut resolved = Vec::new(); + while let Some(frame) = frames.pop() { + match frame { + Frame::Enter(template) => match template { + Type::I64 => resolved.push(Type::I64), + Type::Bool => resolved.push(Type::Bool), + Type::Named { + name, + arguments: nested, + } => { + if nested.is_empty() { + if let Some(index) = function + .type_parameters + .iter() + .position(|parameter| parameter.name == *name) + { + resolved.push(arguments.get(index)?.clone()); + continue; + } + } + frames.push(Frame::Finish(name, nested.len())); + frames.extend(nested.iter().rev().map(Frame::Enter)); + } + }, + Frame::Finish(name, count) => { + let split = resolved.len().checked_sub(count)?; + let arguments = resolved.drain(split..).collect(); + resolved.push(Type::Named { + name: name.to_owned(), + arguments, + }); + } + } + } + (resolved.len() == 1).then(|| resolved.pop().expect("type count checked above")) +} + fn scalar_function_substitutions(parameter_count: usize) -> Vec> { let count = 1_usize << parameter_count; (0..count) @@ -1660,39 +1977,40 @@ fn scalar_function_substitutions(parameter_count: usize) -> Vec> { } fn generic_function_expression_is_direct_scalar(expression: &Expr) -> bool { - match &expression.kind { - ExprKind::Int(_) | ExprKind::Bool(_) | ExprKind::Var(_) => true, - ExprKind::Call { args, .. } => args - .iter() - .all(generic_function_expression_is_direct_scalar), - ExprKind::Unary { value, .. } => generic_function_expression_is_direct_scalar(value), - ExprKind::Binary { left, right, .. } => { - generic_function_expression_is_direct_scalar(left) - && generic_function_expression_is_direct_scalar(right) - } - ExprKind::Block { statements, tail } => { - statements.iter().all(|statement| match statement { - crate::ast::Statement::Let { value, .. } => { - generic_function_expression_is_direct_scalar(value) - } - }) && generic_function_expression_is_direct_scalar(tail) - } - ExprKind::If { - condition, - then_branch, - else_branch, - } => { - generic_function_expression_is_direct_scalar(condition) - && generic_function_expression_is_direct_scalar(then_branch) - && generic_function_expression_is_direct_scalar(else_branch) + let mut pending = vec![expression]; + while let Some(expression) = pending.pop() { + match &expression.kind { + ExprKind::Int(_) | ExprKind::Bool(_) | ExprKind::Var(_) => {} + ExprKind::Call { args, .. } => pending.extend(args.iter().rev()), + ExprKind::Unary { value, .. } => pending.push(value), + ExprKind::Binary { left, right, .. } => { + pending.push(right); + pending.push(left); + } + ExprKind::Block { statements, tail } => { + pending.push(tail); + pending.extend(statements.iter().rev().map(|statement| match statement { + crate::ast::Statement::Let { value, .. } => value, + })); + } + ExprKind::If { + condition, + then_branch, + else_branch, + } => { + pending.push(else_branch); + pending.push(then_branch); + pending.push(condition); + } + ExprKind::ConstructRecord { .. } + | ExprKind::ConstructVariant { .. } + | ExprKind::Match { .. } + | ExprKind::Try { .. } + | ExprKind::UpdateRecord { .. } + | ExprKind::Project { .. } => return false, } - ExprKind::ConstructRecord { .. } - | ExprKind::ConstructVariant { .. } - | ExprKind::Match { .. } - | ExprKind::Try { .. } - | ExprKind::UpdateRecord { .. } - | ExprKind::Project { .. } => false, } + true } fn function_reaches( @@ -1701,17 +2019,19 @@ fn function_reaches( target: &str, visited: &mut HashSet, ) -> bool { - if current == target { - return true; - } - if !visited.insert(current.to_owned()) { - return false; + let mut pending = vec![current]; + while let Some(current) = pending.pop() { + if current == target { + return true; + } + if !visited.insert(current.to_owned()) { + continue; + } + if let Some(callees) = graph.get(current) { + pending.extend(callees.iter().rev().map(String::as_str)); + } } - graph.get(current).is_some_and(|callees| { - callees - .iter() - .any(|callee| function_reaches(graph, callee, target, visited)) - }) + false } fn function_reaches_any( @@ -1720,17 +2040,19 @@ fn function_reaches_any( targets: &HashSet<&str>, visited: &mut HashSet, ) -> bool { - if targets.contains(current) { - return true; - } - if !visited.insert(current.to_owned()) { - return false; + let mut pending = vec![current]; + while let Some(current) = pending.pop() { + if targets.contains(current) { + return true; + } + if !visited.insert(current.to_owned()) { + continue; + } + if let Some(callees) = graph.get(current) { + pending.extend(callees.iter().rev().map(String::as_str)); + } } - graph.get(current).is_some_and(|callees| { - callees - .iter() - .any(|callee| function_reaches_any(graph, callee, targets, visited)) - }) + false } fn validation_specialize_function(function: &Function, arguments: &[Type]) -> Option { @@ -1742,6 +2064,20 @@ fn validation_specialize_function(function: &Function, arguments: &[Type]) -> Op Some(specialized) } +fn validation_specialize_signature( + function: &Function, + arguments: &[Type], +) -> Option<(Vec, Type)> { + let mut params = Vec::with_capacity(function.params.len()); + for parameter in &function.params { + let mut specialized = parameter.clone(); + specialized.ty = substitute_function_type(function, arguments, ¶meter.ty)?; + params.push(specialized); + } + let return_type = substitute_function_type(function, arguments, &function.return_type)?; + Some((params, return_type)) +} + fn check_ownership_mode( program: &Program, function: &Function, @@ -1796,123 +2132,2728 @@ fn ordinary_option_argument(ty: &Type) -> Option<&Type> { if name != "Option" || arguments.len() != 1 { return None; } - Some(&arguments[0]) + Some(&arguments[0]) +} + +#[allow(clippy::too_many_arguments)] +fn check_record_pattern( + program: &Program, + pattern_type: &str, + fields: &[RecordMatchPatternField], + expected: &Type, + variables: &mut HashMap, + types: &TypeTable<'_>, + diagnostics: &mut Vec, + span: Span, +) { + enum Frame<'a, 't> { + Enter { + pattern_type: &'a str, + fields: &'a [RecordMatchPatternField], + expected: Type, + span: Span, + }, + Fields { + pattern_type: &'a str, + fields: &'a [RecordMatchPatternField], + expected: Type, + declared_fields: &'t [FieldDeclaration], + index: usize, + supplied: HashSet<&'a str>, + span: Span, + }, + } + + let mut frames = vec![Frame::Enter { + pattern_type, + fields, + expected: expected.clone(), + span, + }]; + while let Some(frame) = frames.pop() { + match frame { + Frame::Enter { + pattern_type, + fields, + expected, + span, + } => { + let compatible = matches!( + &expected, + Type::Named { name, .. } if name == pattern_type + ); + let declared_fields = types.record_fields(&expected); + if !compatible || declared_fields.is_none() || types.contains_resource(&expected) { + diagnostics.push(error( + program, + "SPX-M103", + format!( + "record pattern `{pattern_type}` is incompatible with `{expected}`" + ), + span, + )); + continue; + } + frames.push(Frame::Fields { + pattern_type, + fields, + expected, + declared_fields: declared_fields.expect("checked above"), + index: 0, + supplied: HashSet::new(), + span, + }); + } + Frame::Fields { + pattern_type, + fields, + expected, + declared_fields, + index, + mut supplied, + span, + } => { + let Some(field) = fields.get(index) else { + for declared in declared_fields { + if !supplied.contains(declared.name.as_str()) { + diagnostics.push(error( + program, + "SPX-M104", + format!( + "record pattern `{pattern_type}` is missing field `{}`", + declared.name + ), + span, + )); + } + } + continue; + }; + let declared = declared_fields + .iter() + .find(|candidate| candidate.name == field.name); + if !supplied.insert(field.name.as_str()) || declared.is_none() { + diagnostics.push(error( + program, + "SPX-M104", + format!( + "unknown or duplicate record pattern field `{}.{}`", + pattern_type, field.name + ), + field.span, + )); + frames.push(Frame::Fields { + pattern_type, + fields, + expected, + declared_fields, + index: index + 1, + supplied, + span, + }); + continue; + } + let declared = declared.expect("checked above"); + let field_ty = types + .record_field_type(&expected, declared) + .unwrap_or_else(|| declared.ty.clone()); + frames.push(Frame::Fields { + pattern_type, + fields, + expected, + declared_fields, + index: index + 1, + supplied, + span, + }); + match &field.pattern { + RecordMatchFieldPattern::Binding { name, span } => { + if !source_identifier(name) || variables.contains_key(name) { + diagnostics.push(error( + program, + "SPX-M104", + format!("invalid or duplicate record pattern binding `{name}`"), + *span, + )); + } else { + variables.insert( + name.clone(), + Binding { + ty: field_ty, + mode: ParamMode::Value, + availability: Availability::Available, + moved_places: HashMap::new(), + definitely_partial: HashSet::new(), + native_unit_discard: false, + }, + ); + } + } + RecordMatchFieldPattern::Wildcard { .. } => {} + RecordMatchFieldPattern::Record { + type_name, + fields, + span, + .. + } => frames.push(Frame::Enter { + pattern_type: type_name, + fields, + expected: field_ty, + span: *span, + }), + } + } + } + } +} + +struct VerifierScope { + bindings: HashMap, +} + +enum VerifierFrame<'a> { + Enter { + expression: &'a Expr, + scope: usize, + }, + ResumeUnary { + expression: &'a Expr, + operand: &'a Expr, + op: UnaryOp, + }, + ResumeBinaryLeft { + expression: &'a Expr, + op: BinaryOp, + right: &'a Expr, + scope: usize, + }, + ResumeBinaryRight { + expression: &'a Expr, + op: BinaryOp, + left: &'a Expr, + left_value: Option, + scope: usize, + evaluated_scope: usize, + baseline_names: Vec, + }, + ResumeIfCondition { + expression: &'a Expr, + then_branch: &'a Expr, + else_branch: &'a Expr, + scope: usize, + }, + ResumeIfThen { + expression: &'a Expr, + else_branch: &'a Expr, + scope: usize, + then_scope: usize, + baseline_names: Vec, + }, + ResumeIfElse { + expression: &'a Expr, + then_branch: &'a Expr, + else_branch: &'a Expr, + scope: usize, + else_scope: usize, + baseline_names: Vec, + then_value: Option, + then_bindings: HashMap, + }, + ResumeBlockStatement { + expression: &'a Expr, + statements: &'a [Statement], + tail: &'a Expr, + parent_scope: usize, + block_scope: usize, + index: usize, + outer_names: Vec, + }, + ResumeBlockTail { + parent_scope: usize, + block_scope: usize, + outer_names: Vec, + }, + ResumeCallArgument { + expression: &'a Expr, + name: &'a str, + args: &'a [Expr], + scope: usize, + index: usize, + target: VerifierCallTarget<'a>, + }, + ResumeTry { + expression: &'a Expr, + operand: &'a Expr, + scope: usize, + }, + ResumeProject { + expression: &'a Expr, + base: &'a Expr, + field: &'a str, + }, + ResumeRecordField { + expression: &'a Expr, + type_name: &'a str, + type_arguments: &'a [Type], + fields: &'a [crate::ast::FieldInitializer], + declared_fields: Option<&'a [FieldDeclaration]>, + scope: usize, + index: usize, + supplied: HashSet<&'a str>, + }, + PrepareRecordField { + expression: &'a Expr, + type_name: &'a str, + type_arguments: &'a [Type], + fields: &'a [crate::ast::FieldInitializer], + declared_fields: Option<&'a [FieldDeclaration]>, + scope: usize, + index: usize, + supplied: HashSet<&'a str>, + }, + ResumeVariantField { + expression: &'a Expr, + type_name: &'a str, + type_arguments: &'a [Type], + case_name: &'a str, + fields: &'a [crate::ast::FieldInitializer], + declaration: Option<&'a TypeDeclaration>, + case: Option<&'a VariantCaseDeclaration>, + scope: usize, + index: usize, + supplied: HashSet<&'a str>, + }, + PrepareVariantField { + expression: &'a Expr, + type_name: &'a str, + type_arguments: &'a [Type], + case_name: &'a str, + fields: &'a [crate::ast::FieldInitializer], + declaration: Option<&'a TypeDeclaration>, + case: Option<&'a VariantCaseDeclaration>, + scope: usize, + index: usize, + supplied: HashSet<&'a str>, + }, + ResumeUpdateBase { + expression: &'a Expr, + base: &'a Expr, + fields: &'a [crate::ast::FieldInitializer], + scope: usize, + }, + ResumeUpdateField { + expression: &'a Expr, + base_type: Type, + fields: &'a [crate::ast::FieldInitializer], + declared_fields: &'a [FieldDeclaration], + scope: usize, + index: usize, + supplied: HashSet<&'a str>, + }, + PrepareUpdateField { + expression: &'a Expr, + base_type: Type, + fields: &'a [crate::ast::FieldInitializer], + declared_fields: &'a [FieldDeclaration], + scope: usize, + index: usize, + supplied: HashSet<&'a str>, + }, + ResumeMatchScrutinee { + expression: &'a Expr, + scrutinee: &'a Expr, + arms: &'a [crate::ast::MatchArm], + scope: usize, + }, + ResumeRecordMatchArm { + arm: &'a crate::ast::MatchArm, + parent_scope: usize, + arm_scope: usize, + outer_names: Vec, + }, + PrepareVariantMatchArm(VariantMatchState<'a>), + ResumeVariantMatchArm { + state: VariantMatchState<'a>, + arm_scope: usize, + }, +} + +#[allow(dead_code)] +struct VariantMatchState<'a> { + expression: &'a Expr, + arms: &'a [crate::ast::MatchArm], + parent_scope: usize, + index: usize, + outer_names: Vec, + baseline: HashMap, + arm_states: Vec>, + covered: HashSet, + wildcard_seen: bool, + result: Option, + variant_name: Option, + variant_arguments: Vec, + declared_cases: Option<&'a [VariantCaseDeclaration]>, +} + +enum VerifierCallTarget<'a> { + Native(&'a ImportDeclaration), + Ordinary(Option>), +} + +enum VerifierFunctionSignature<'a> { + Borrowed(&'a Function), + Specialized { + params: Vec, + return_type: Type, + }, +} + +#[cfg(test)] +fn verifier_signature_owned_capacity(signature: &VerifierFunctionSignature<'_>) -> usize { + match signature { + VerifierFunctionSignature::Borrowed(_) => 0, + VerifierFunctionSignature::Specialized { + params, + return_type, + } => params + .capacity() + .saturating_mul(std::mem::size_of::()) + .saturating_add( + params + .iter() + .map(|param| { + param + .name + .capacity() + .saturating_add(ast_type_owned_capacity(¶m.ty)) + }) + .sum::(), + ) + .saturating_add(ast_type_owned_capacity(return_type)), + } +} + +#[cfg(test)] +fn variant_match_state_owned_capacity(state: &VariantMatchState<'_>) -> usize { + state + .outer_names + .capacity() + .saturating_mul(std::mem::size_of::()) + .saturating_add( + state + .outer_names + .iter() + .map(String::capacity) + .sum::(), + ) + .saturating_add( + state + .baseline + .capacity() + .saturating_mul(std::mem::size_of::<(String, Binding)>()), + ) + .saturating_add( + state + .baseline + .iter() + .map(|(name, binding)| name.capacity() + binding_owned_capacity(binding)) + .sum::(), + ) + .saturating_add( + state + .arm_states + .capacity() + .saturating_mul(std::mem::size_of::>()), + ) + .saturating_add( + state + .arm_states + .iter() + .map(|bindings| { + bindings + .capacity() + .saturating_mul(std::mem::size_of::<(String, Binding)>()) + .saturating_add( + bindings + .iter() + .map(|(name, binding)| { + name.capacity() + binding_owned_capacity(binding) + }) + .sum::(), + ) + }) + .sum::(), + ) + .saturating_add( + state + .covered + .capacity() + .saturating_mul(std::mem::size_of::()), + ) + .saturating_add(state.covered.iter().map(String::capacity).sum::()) + .saturating_add(state.variant_name.as_ref().map_or(0, String::capacity)) + .saturating_add( + state + .variant_arguments + .capacity() + .saturating_mul(std::mem::size_of::()), + ) + .saturating_add( + state + .variant_arguments + .iter() + .map(ast_type_owned_capacity) + .sum::(), + ) + .saturating_add( + state + .result + .as_ref() + .map_or(0, |value| ast_type_owned_capacity(&value.ty)), + ) +} + +#[cfg(test)] +fn diagnostics_owned_capacity(diagnostics: &Vec) -> usize { + diagnostics.capacity() * std::mem::size_of::() + + diagnostics + .iter() + .map(|diagnostic| { + diagnostic.message.capacity() + + diagnostic.path.as_ref().map_or(0, String::capacity) + + diagnostic.help.as_ref().map_or(0, String::capacity) + }) + .sum::() +} + +#[cfg(test)] +fn verifier_frame_owned_capacity(frame: &VerifierFrame<'_>) -> usize { + let strings = |values: &Vec| { + values + .capacity() + .saturating_mul(std::mem::size_of::()) + .saturating_add(values.iter().map(String::capacity).sum::()) + }; + match frame { + VerifierFrame::ResumeBinaryRight { baseline_names, .. } + | VerifierFrame::ResumeIfThen { baseline_names, .. } => strings(baseline_names), + VerifierFrame::ResumeIfElse { + baseline_names, + then_bindings, + .. + } => strings(baseline_names).saturating_add( + then_bindings + .capacity() + .saturating_mul(std::mem::size_of::<(String, Binding)>()) + .saturating_add( + then_bindings + .iter() + .map(|(name, binding)| name.capacity() + binding_owned_capacity(binding)) + .sum::(), + ), + ), + VerifierFrame::ResumeBlockStatement { outer_names, .. } + | VerifierFrame::ResumeBlockTail { outer_names, .. } + | VerifierFrame::ResumeRecordMatchArm { outer_names, .. } => strings(outer_names), + VerifierFrame::ResumeRecordField { supplied, .. } + | VerifierFrame::PrepareRecordField { supplied, .. } + | VerifierFrame::ResumeVariantField { supplied, .. } + | VerifierFrame::PrepareVariantField { supplied, .. } => supplied + .capacity() + .saturating_mul(std::mem::size_of::<&str>()), + VerifierFrame::ResumeUpdateField { + base_type, + supplied, + .. + } + | VerifierFrame::PrepareUpdateField { + base_type, + supplied, + .. + } => ast_type_owned_capacity(base_type).saturating_add( + supplied + .capacity() + .saturating_mul(std::mem::size_of::<&str>()), + ), + VerifierFrame::ResumeCallArgument { target, .. } => match target { + VerifierCallTarget::Native(_) => 0, + VerifierCallTarget::Ordinary(Some(signature)) => { + verifier_signature_owned_capacity(signature) + } + VerifierCallTarget::Ordinary(None) => 0, + }, + VerifierFrame::PrepareVariantMatchArm(state) + | VerifierFrame::ResumeVariantMatchArm { state, .. } => { + variant_match_state_owned_capacity(state) + } + _ => 0, + } +} + +impl VerifierFunctionSignature<'_> { + fn params(&self) -> &[Param] { + match self { + Self::Borrowed(function) => &function.params, + Self::Specialized { params, .. } => params, + } + } + + fn return_type(&self) -> &Type { + match self { + Self::Borrowed(function) => &function.return_type, + Self::Specialized { return_type, .. } => return_type, + } + } +} + +struct IterativeVerifier<'a, 'p> { + program: &'p Program, + current: &'p Function, + functions: &'p HashMap<&'p str, &'p Function>, + types: &'p TypeTable<'p>, + result_type: Option<&'p Type>, + allow_moves: bool, + diagnostics: &'a mut Vec, + scopes: Vec, + frames: Vec>, + values: Vec>, +} + +impl<'a, 'p> IterativeVerifier<'a, 'p> { + #[allow(clippy::too_many_arguments)] + fn new( + program: &'p Program, + current: &'p Function, + variables: HashMap, + functions: &'p HashMap<&'p str, &'p Function>, + types: &'p TypeTable<'p>, + result_type: Option<&'p Type>, + allow_moves: bool, + diagnostics: &'a mut Vec, + ) -> Self { + const { assert!(std::mem::size_of::>() == 320) }; + const { assert!(std::mem::size_of::>() == 312) }; + Self { + program, + current, + functions, + types, + result_type, + allow_moves, + diagnostics, + scopes: vec![VerifierScope { + bindings: variables, + }], + frames: Vec::new(), + values: Vec::new(), + } + } + + #[allow(clippy::collapsible_else_if)] + fn run(&mut self, expression: &'p Expr) -> Result, Diagnostic> { + self.frames.push(VerifierFrame::Enter { + expression, + scope: 0, + }); + while let Some(frame) = self.frames.pop() { + #[cfg(test)] + note_capacity_high_water( + self.frames.capacity() * std::mem::size_of::>() + + self.scopes.capacity() * std::mem::size_of::() + + self.values.capacity() * std::mem::size_of::>() + + self + .values + .iter() + .flatten() + .map(|value| ast_type_owned_capacity(&value.ty)) + .sum::() + + self.scopes.iter().map(scope_owned_capacity).sum::() + + self + .frames + .iter() + .map(verifier_frame_owned_capacity) + .sum::() + + verifier_frame_owned_capacity(&frame) + + diagnostics_owned_capacity(self.diagnostics), + ); + match frame { + VerifierFrame::Enter { expression, scope } => match &expression.kind { + ExprKind::Int(_) => self.values.push(Some(CheckedValue::value(Type::I64))), + ExprKind::Bool(_) => self.values.push(Some(CheckedValue::value(Type::Bool))), + ExprKind::Var(name) if name == "result" => { + let value = self.result_type.map(|ty| { + CheckedValue::returned(ty.clone(), self.types.contains_resource(ty)) + }); + if value.is_none() { + self.diagnostics.push(error( + self.program, + "SPX-T201", + "`result` is only available in postconditions", + expression.span, + )); + } + self.values.push(value); + } + ExprKind::Var(name) => { + let value = self.scopes[scope].bindings.get(name).map(|binding| { + match binding.availability { + Availability::Moved => self.diagnostics.push( + error( + self.program, + "SPX-O101", + format!("use of resource `{name}` after ownership was moved"), + expression.span, + ) + .with_help( + "borrow the resource if the callee does not need ownership", + ), + ), + Availability::MaybeMoved => self.diagnostics.push( + error( + self.program, + "SPX-O107", + format!("resource `{name}` may have been moved on another control-flow path"), + expression.span, + ) + .with_help("move the resource on every path or keep it borrowed"), + ), + Availability::Available => match overlapping_place_state(binding, &[]) { + Availability::Moved => self.diagnostics.push( + error(self.program, "SPX-O109", format!("use of partially moved place `{name}`"), expression.span) + .with_help("use an available sibling field or avoid moving this place earlier"), + ), + Availability::MaybeMoved => self.diagnostics.push( + error(self.program, "SPX-O110", format!("place `{name}` may have been moved on another control-flow path"), expression.span) + .with_help("move the field on every path or keep it borrowed"), + ), + Availability::Available => {} + }, + } + if binding.native_unit_discard { + self.diagnostics.push(error( + self.program, + "SPX-B107", + "Native Rust Interop declaration set is unsupported: scalar value signature required", + expression.span, + )); + } + CheckedValue { ty: binding.ty.clone(), mode: binding.mode, native_unit: binding.native_unit_discard } + }); + if value.is_none() { + self.diagnostics.push(error( + self.program, + "SPX-T202", + format!("unknown value `{name}` in `{}`", self.current.name), + expression.span, + )); + } + self.values.push(value); + } + ExprKind::Unary { op, value } => { + self.frames.push(VerifierFrame::ResumeUnary { + expression, + operand: value, + op: *op, + }); + self.frames.push(VerifierFrame::Enter { + expression: value, + scope, + }); + } + ExprKind::Binary { op, left, right } => { + self.frames.push(VerifierFrame::ResumeBinaryLeft { + expression, + op: *op, + right, + scope, + }); + self.frames.push(VerifierFrame::Enter { + expression: left, + scope, + }); + } + ExprKind::Call { + name, + type_arguments, + args, + } => { + let native = self + .program + .interfaces + .iter() + .flat_map(|interface| &interface.imports) + .find(|import| import.native_rust && import.name == *name); + let target = if let Some(import) = native { + if !type_arguments.is_empty() || args.len() != import.params.len() { + self.diagnostics.push(error( + self.program, + "SPX-B107", + "Native Rust Interop declaration set is unsupported: scalar value signature required", + expression.span, + )); + } + for effect in &import.effects { + if !self.current.effects.contains(effect) { + self.diagnostics.push(error( + self.program, + "SPX-B107", + "Native Rust Interop declaration set is unsupported: effect or capability mismatch", + expression.span, + )); + } + } + VerifierCallTarget::Native(import) + } else { + let target = self.functions.get(name.as_str()).copied(); + if target.is_none() { + self.diagnostics.push(error( + self.program, + "SPX-T203", + format!("unknown function `{name}`"), + expression.span, + )); + } + if target.is_some_and(|target| args.len() != target.params.len()) { + let target = target.expect("checked above"); + self.diagnostics.push(error( + self.program, + "SPX-T204", + format!( + "`{name}` expects {} arguments, received {}", + target.params.len(), + args.len() + ), + expression.span, + )); + } + let specialized = target.and_then(|target| { + if target.type_parameters.is_empty() { + if !type_arguments.is_empty() { + self.diagnostics.push(error( + self.program, + "SPX-T225", + format!("monomorphic function `{name}` does not accept type arguments"), + expression.span, + )); + return None; + } + return Some(VerifierFunctionSignature::Borrowed(target)); + } + if !self.current.type_parameters.is_empty() { + self.diagnostics.push(error( + self.program, + "SPX-T226", + format!("generic function `{}` cannot call generic function `{name}` in this slice", self.current.name), + expression.span, + )); + } + if type_arguments.len() != target.type_parameters.len() { + self.diagnostics.push(error( + self.program, + "SPX-T225", + format!("generic function `{name}` expects {} explicit type arguments, received {}", target.type_parameters.len(), type_arguments.len()), + expression.span, + )); + return None; + } + if type_arguments.iter().any(|argument| !direct_function_type_argument(argument)) { + self.diagnostics.push(error( + self.program, + "SPX-T225", + format!("generic function `{name}` accepts only direct `i64` or `bool` type arguments"), + expression.span, + )); + return None; + } + validation_specialize_signature(target, type_arguments).map( + |(params, return_type)| { + VerifierFunctionSignature::Specialized { + params, + return_type, + } + }, + ) + }); + VerifierCallTarget::Ordinary(specialized) + }; + if let Some(argument) = args.first() { + self.frames.push(VerifierFrame::ResumeCallArgument { + expression, + name, + args, + scope, + index: 0, + target, + }); + self.frames.push(VerifierFrame::Enter { + expression: argument, + scope, + }); + } else { + self.values.push(Some(match target { + VerifierCallTarget::Native(import) => { + let mut value = CheckedValue::value(match import.result { + ImportResult::Unit => Type::Named { + name: "\0native-rust-unit".to_owned(), + arguments: Vec::new(), + }, + ImportResult::I64 => Type::I64, + ImportResult::Bool => Type::Bool, + }); + value.native_unit = import.result == ImportResult::Unit; + value + } + VerifierCallTarget::Ordinary(Some(target)) => { + CheckedValue::returned( + target.return_type().clone(), + self.types.contains_resource(target.return_type()), + ) + } + VerifierCallTarget::Ordinary(None) => { + self.values.push(None); + continue; + } + })); + } + } + ExprKind::If { + condition, + then_branch, + else_branch, + } => { + self.frames.push(VerifierFrame::ResumeIfCondition { + expression, + then_branch, + else_branch, + scope, + }); + self.frames.push(VerifierFrame::Enter { + expression: condition, + scope, + }); + } + ExprKind::Block { statements, tail } => { + let outer_names = self.scopes[scope] + .bindings + .keys() + .cloned() + .collect::>(); + let block_scope = self.scopes.len(); + self.scopes.push(VerifierScope { + bindings: self.scopes[scope].bindings.clone(), + }); + if let Some(Statement::Let { + name, + name_span, + value, + .. + }) = statements.first() + { + if !source_identifier(name) { + self.diagnostics.push(error( + self.program, + "SPX-S109", + format!("`{name}` is reserved and cannot name a local binding"), + *name_span, + )); + } + self.frames.push(VerifierFrame::ResumeBlockStatement { + expression, + statements, + tail, + parent_scope: scope, + block_scope, + index: 0, + outer_names, + }); + self.frames.push(VerifierFrame::Enter { + expression: value, + scope: block_scope, + }); + } else { + self.frames.push(VerifierFrame::ResumeBlockTail { + parent_scope: scope, + block_scope, + outer_names, + }); + self.frames.push(VerifierFrame::Enter { + expression: tail, + scope: block_scope, + }); + } + } + ExprKind::Try { operand } => { + self.frames.push(VerifierFrame::ResumeTry { + expression, + operand, + scope, + }); + self.frames.push(VerifierFrame::Enter { + expression: operand, + scope, + }); + } + ExprKind::Project { base, field, .. } => { + if let Some(place) = + source_place(expression, &self.scopes[scope].bindings, self.types) + { + check_source_place_availability( + self.program, + &place, + &self.scopes[scope].bindings, + expression.span, + self.diagnostics, + ); + self.values.push(Some(CheckedValue { + ty: place.ty, + mode: place.mode, + native_unit: false, + })); + } else { + self.frames.push(VerifierFrame::ResumeProject { + expression, + base, + field, + }); + self.frames.push(VerifierFrame::Enter { + expression: base, + scope, + }); + } + } + ExprKind::ConstructRecord { + type_name, + type_arguments, + fields, + .. + } => { + let declaration = self.types.declaration(type_name); + let instance = Type::Named { + name: type_name.clone(), + arguments: type_arguments.clone(), + }; + check_declared_type( + self.program, + &instance, + expression.span, + self.types, + &HashSet::new(), + self.diagnostics, + ); + let declared_fields = + declaration.and_then(|declaration| match &declaration.kind { + TypeDeclarationKind::Record { fields } => Some(fields.as_slice()), + TypeDeclarationKind::Resource { .. } + | TypeDeclarationKind::Variant { .. } => None, + }); + if declared_fields.is_none() { + self.diagnostics.push(error( + self.program, + "SPX-T215", + format!("`{type_name}` is not a declared record type"), + expression.span, + )); + } + if !fields.is_empty() { + self.frames.push(VerifierFrame::PrepareRecordField { + expression, + type_name, + type_arguments, + fields, + declared_fields, + scope, + index: 0, + supplied: HashSet::new(), + }); + } else { + if let Some(declared_fields) = declared_fields { + for field in declared_fields { + self.diagnostics.push(error(self.program, "SPX-T213", format!("record `{type_name}` construction is missing field `{}`", field.name), expression.span)); + } + self.values.push(Some(CheckedValue::returned( + instance.clone(), + self.types.contains_resource(&instance), + ))); + } else { + self.values.push(None); + } + } + } + ExprKind::ConstructVariant { + type_name, + type_arguments, + case_name, + fields, + .. + } => { + let declaration = self.types.declaration(type_name); + let instance = Type::Named { + name: type_name.clone(), + arguments: type_arguments.clone(), + }; + check_declared_type( + self.program, + &instance, + expression.span, + self.types, + &HashSet::new(), + self.diagnostics, + ); + let cases = declaration.and_then(|declaration| match &declaration.kind { + TypeDeclarationKind::Variant { cases } => Some(cases.as_slice()), + TypeDeclarationKind::Resource { .. } + | TypeDeclarationKind::Record { .. } => None, + }); + let case = cases + .and_then(|cases| cases.iter().find(|case| case.name == *case_name)); + if cases.is_none() || case.is_none() { + self.diagnostics.push(error(self.program, "SPX-T215", format!("`{type_name}::{case_name}` is not a declared variant constructor"), expression.span)); + } + if !fields.is_empty() { + self.frames.push(VerifierFrame::PrepareVariantField { + expression, + type_name, + type_arguments, + case_name, + fields, + declaration, + case, + scope, + index: 0, + supplied: HashSet::new(), + }); + } else { + if let Some(case) = case { + for field in &case.fields { + self.diagnostics.push(error(self.program, "SPX-T213", format!("variant construction `{type_name}::{case_name}` is missing payload field `{}`", field.name), expression.span)); + } + self.values.push(Some(CheckedValue::value(instance))); + } else { + self.values.push(None); + } + } + } + ExprKind::UpdateRecord { base, fields } => { + self.frames.push(VerifierFrame::ResumeUpdateBase { + expression, + base, + fields, + scope, + }); + self.frames.push(VerifierFrame::Enter { + expression: base, + scope, + }); + } + ExprKind::Match { scrutinee, arms } => { + self.frames.push(VerifierFrame::ResumeMatchScrutinee { + expression, + scrutinee, + arms, + scope, + }); + self.frames.push(VerifierFrame::Enter { + expression: scrutinee, + scope, + }); + } + }, + VerifierFrame::ResumeUnary { + expression, + operand, + op, + } => { + let Some(actual) = self.values.pop().flatten() else { + self.values.push(None); + continue; + }; + let expected = match op { + UnaryOp::Neg => Type::I64, + UnaryOp::Not => Type::Bool, + }; + if !actual.native_unit && actual.ty != expected { + self.diagnostics.push(error( + self.program, + "SPX-T206", + format!("unary operator expects {expected}, received {}", actual.ty), + expression.span, + )); + } + reject_native_unit_value(self.program, operand, &actual, self.diagnostics); + self.values.push(Some(CheckedValue::value(expected))); + } + VerifierFrame::ResumeBinaryLeft { + expression, + op, + right, + scope, + } => { + let left_value = self.values.pop().unwrap_or(None); + let baseline_names = self.scopes[scope] + .bindings + .keys() + .cloned() + .collect::>(); + let evaluated_scope = if matches!(op, BinaryOp::And | BinaryOp::Or) { + let index = self.scopes.len(); + self.scopes.push(VerifierScope { + bindings: self.scopes[scope].bindings.clone(), + }); + index + } else { + scope + }; + let left = match &expression.kind { + ExprKind::Binary { left, .. } => left.as_ref(), + _ => unreachable!(), + }; + self.frames.push(VerifierFrame::ResumeBinaryRight { + expression, + op, + left, + left_value, + scope, + evaluated_scope, + baseline_names, + }); + self.frames.push(VerifierFrame::Enter { + expression: right, + scope: evaluated_scope, + }); + } + VerifierFrame::ResumeBinaryRight { + expression, + op, + left, + left_value, + scope, + evaluated_scope, + baseline_names, + } => { + let right_value = self.values.pop().unwrap_or(None); + if evaluated_scope != scope { + if evaluated_scope + 1 != self.scopes.len() { + return Err(Diagnostic::io( + "SPX-H006", + "lazy verifier scope is not the active child", + )); + } + let evaluated = self + .scopes + .pop() + .expect("active lazy scope index checked above") + .bindings; + join_conditional( + &mut self.scopes[scope].bindings, + &evaluated, + &baseline_names, + ); + } + if let Some(value) = &left_value { + reject_native_unit_value(self.program, left, value, self.diagnostics); + } + let right = match &expression.kind { + ExprKind::Binary { right, .. } => right.as_ref(), + _ => unreachable!(), + }; + if let Some(value) = &right_value { + reject_native_unit_value(self.program, right, value, self.diagnostics); + } + let native_unit = left_value.as_ref().is_some_and(|value| value.native_unit) + || right_value.as_ref().is_some_and(|value| value.native_unit); + let (expected, output) = match op { + BinaryOp::Add + | BinaryOp::Sub + | BinaryOp::Mul + | BinaryOp::Div + | BinaryOp::Rem => (Type::I64, Type::I64), + BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge => { + (Type::I64, Type::Bool) + } + BinaryOp::And | BinaryOp::Or => (Type::Bool, Type::Bool), + BinaryOp::Eq | BinaryOp::Ne => { + if !native_unit + && left_value.is_some() + && right_value.is_some() + && left_value.as_ref().map(|value| &value.ty) + != right_value.as_ref().map(|value| &value.ty) + { + self.diagnostics.push(error( + self.program, + "SPX-T207", + "equality operands must have the same type", + expression.span, + )); + } + self.values.push(Some(CheckedValue::value(Type::Bool))); + continue; + } + }; + if !native_unit + && (left_value + .as_ref() + .is_some_and(|value| value.ty != expected) + || right_value + .as_ref() + .is_some_and(|value| value.ty != expected)) + { + self.diagnostics.push(error( + self.program, + "SPX-T208", + format!("operator `{}` expects {expected} operands", op.text()), + expression.span, + )); + } + self.values.push(Some(CheckedValue::value(output))); + } + VerifierFrame::ResumeIfCondition { + expression, + then_branch, + else_branch, + scope, + } => { + let condition_value = self.values.pop().unwrap_or(None); + let condition = match &expression.kind { + ExprKind::If { condition, .. } => condition.as_ref(), + _ => unreachable!(), + }; + if let Some(value) = condition_value { + if value.native_unit { + reject_native_unit_value( + self.program, + condition, + &value, + self.diagnostics, + ); + } else if value.ty != Type::Bool { + self.diagnostics.push(error( + self.program, + "SPX-T210", + "`if` condition must be bool", + condition.span, + )); + } + } + let baseline_names = self.scopes[scope] + .bindings + .keys() + .cloned() + .collect::>(); + let then_scope = self.scopes.len(); + self.scopes.push(VerifierScope { + bindings: self.scopes[scope].bindings.clone(), + }); + self.frames.push(VerifierFrame::ResumeIfThen { + expression, + else_branch, + scope, + then_scope, + baseline_names, + }); + self.frames.push(VerifierFrame::Enter { + expression: then_branch, + scope: then_scope, + }); + } + VerifierFrame::ResumeIfThen { + expression, + else_branch, + scope, + then_scope, + baseline_names, + } => { + if then_scope + 1 != self.scopes.len() { + return Err(Diagnostic::io( + "SPX-H006", + "then verifier scope is not the active child", + )); + } + let then_value = self.values.pop().unwrap_or(None); + let then_bindings = self + .scopes + .pop() + .expect("active then scope index checked above") + .bindings; + let else_scope = self.scopes.len(); + self.scopes.push(VerifierScope { + bindings: self.scopes[scope].bindings.clone(), + }); + let then_branch = match &expression.kind { + ExprKind::If { then_branch, .. } => then_branch.as_ref(), + _ => unreachable!(), + }; + self.frames.push(VerifierFrame::ResumeIfElse { + expression, + then_branch, + else_branch, + scope, + else_scope, + baseline_names, + then_value, + then_bindings, + }); + self.frames.push(VerifierFrame::Enter { + expression: else_branch, + scope: else_scope, + }); + } + VerifierFrame::ResumeIfElse { + expression, + then_branch, + else_branch, + scope, + else_scope, + baseline_names, + then_value, + then_bindings, + } => { + if else_scope + 1 != self.scopes.len() { + return Err(Diagnostic::io( + "SPX-H006", + "else verifier scope is not the active child", + )); + } + let else_value = self.values.pop().unwrap_or(None); + let else_bindings = self + .scopes + .pop() + .expect("active else scope index checked above") + .bindings; + for name in &baseline_names { + if let Some(binding) = self.scopes[scope].bindings.get_mut(name) { + let then_state = then_bindings + .get(name) + .map_or(Availability::Available, |value| value.availability); + let else_state = else_bindings + .get(name) + .map_or(Availability::Available, |value| value.availability); + binding.availability = then_state.join(else_state); + if let (Some(then_binding), Some(else_binding)) = + (then_bindings.get(name), else_bindings.get(name)) + { + binding.moved_places = + join_moved_places(then_binding, else_binding); + binding.definitely_partial = + join_definitely_partial(then_binding, else_binding); + } + } + } + let output = match (then_value, else_value) { + (Some(then_value), Some(else_value)) => { + if then_value.native_unit || else_value.native_unit { + reject_native_unit_value( + self.program, + then_branch, + &then_value, + self.diagnostics, + ); + reject_native_unit_value( + self.program, + else_branch, + &else_value, + self.diagnostics, + ); + } else if then_value.ty != else_value.ty { + self.diagnostics.push(error( + self.program, + "SPX-T211", + format!( + "`if` branches return different types: {} and {}", + then_value.ty, else_value.ty + ), + expression.span, + )); + } + if self.types.contains_resource(&then_value.ty) + && then_value.mode != else_value.mode + { + self.diagnostics.push(error( + self.program, + "SPX-O106", + "`if` branches must produce the same resource ownership mode", + expression.span, + )); + } + Some(then_value) + } + _ => None, + }; + self.values.push(output); + } + VerifierFrame::ResumeBlockStatement { + expression, + statements, + tail, + parent_scope, + block_scope, + index, + outer_names, + } => { + let actual = self.values.pop().unwrap_or(None); + let Statement::Let { + name, + name_span, + value, + .. + } = &statements[index]; + if self.scopes[block_scope].bindings.contains_key(name) { + self.diagnostics.push(error( + self.program, + "SPX-T209", + format!("local binding `{name}` shadows an existing value"), + *name_span, + )); + } else if let Some(actual) = actual { + if self.types.contains_resource(&actual.ty) && actual.mode == ParamMode::Own + { + if self.allow_moves { + mark_value_sources_moved( + value, + &mut self.scopes[block_scope].bindings, + self.types, + ); + } else { + self.diagnostics.push(error( + self.program, + "SPX-O105", + "contract expression cannot transfer an owned resource into a local binding", + value.span, + )); + } + } + self.scopes[block_scope].bindings.insert( + name.clone(), + Binding { + ty: actual.ty, + mode: actual.mode, + availability: Availability::Available, + moved_places: HashMap::new(), + definitely_partial: HashSet::new(), + native_unit_discard: actual.native_unit, + }, + ); + } + let next = index + 1; + if let Some(Statement::Let { + name, + name_span, + value, + .. + }) = statements.get(next) + { + if !source_identifier(name) { + self.diagnostics.push(error( + self.program, + "SPX-S109", + format!("`{name}` is reserved and cannot name a local binding"), + *name_span, + )); + } + self.frames.push(VerifierFrame::ResumeBlockStatement { + expression, + statements, + tail, + parent_scope, + block_scope, + index: next, + outer_names, + }); + self.frames.push(VerifierFrame::Enter { + expression: value, + scope: block_scope, + }); + } else { + self.frames.push(VerifierFrame::ResumeBlockTail { + parent_scope, + block_scope, + outer_names, + }); + self.frames.push(VerifierFrame::Enter { + expression: tail, + scope: block_scope, + }); + } + } + VerifierFrame::ResumeBlockTail { + parent_scope, + block_scope, + outer_names, + } => { + if block_scope + 1 != self.scopes.len() { + return Err(Diagnostic::io( + "SPX-H006", + "block verifier scope is not the active child", + )); + } + let actual = self.values.pop().unwrap_or(None); + let block_bindings = self + .scopes + .pop() + .expect("active block scope index checked above") + .bindings; + merge_moved( + &mut self.scopes[parent_scope].bindings, + &block_bindings, + &outer_names, + ); + self.values.push(actual); + } + VerifierFrame::ResumeCallArgument { + expression, + name, + args, + scope, + index, + target, + } => { + let actual = self.values.pop().unwrap_or(None); + let argument = &args[index]; + match &target { + VerifierCallTarget::Native(import) => { + if let (Some(actual), Some(parameter)) = + (actual.as_ref(), import.params.get(index)) + { + reject_native_unit_value( + self.program, + argument, + actual, + self.diagnostics, + ); + if !actual.native_unit + && (actual.ty != parameter.ty + || actual.mode != ParamMode::Value) + { + self.diagnostics.push(error( + self.program, + "SPX-B107", + "Native Rust Interop declaration set is unsupported: scalar value signature required", + argument.span, + )); + } + } + } + VerifierCallTarget::Ordinary(specialized) => { + if let Some(param) = specialized + .as_ref() + .and_then(|target| target.params().get(index)) + { + if let Some(actual) = &actual { + reject_native_unit_value( + self.program, + argument, + actual, + self.diagnostics, + ); + } + if actual.as_ref().is_some_and(|actual| { + !actual.native_unit && actual.ty != param.ty + }) { + self.diagnostics.push(error( + self.program, + "SPX-T205", + format!( + "argument `{}` to `{name}` expects {}, received {}", + param.name, + param.ty, + actual.as_ref().expect("type checked above").ty + ), + argument.span, + )); + } + check_argument_ownership( + self.program, + self.current, + name, + argument, + param, + actual.as_ref(), + &mut self.scopes[scope].bindings, + self.types, + self.allow_moves, + self.diagnostics, + ); + } + } + } + let next = index + 1; + if let Some(argument) = args.get(next) { + self.frames.push(VerifierFrame::ResumeCallArgument { + expression, + name, + args, + scope, + index: next, + target, + }); + self.frames.push(VerifierFrame::Enter { + expression: argument, + scope, + }); + } else { + let output = match target { + VerifierCallTarget::Native(import) => { + let mut value = CheckedValue::value(match import.result { + ImportResult::Unit => Type::Named { + name: "\0native-rust-unit".to_owned(), + arguments: Vec::new(), + }, + ImportResult::I64 => Type::I64, + ImportResult::Bool => Type::Bool, + }); + value.native_unit = import.result == ImportResult::Unit; + Some(value) + } + VerifierCallTarget::Ordinary(Some(target)) => { + Some(CheckedValue::returned( + target.return_type().clone(), + self.types.contains_resource(target.return_type()), + )) + } + VerifierCallTarget::Ordinary(None) => None, + }; + self.values.push(output); + } + } + VerifierFrame::ResumeTry { + expression, + operand, + scope, + } => { + let Some(operand_value) = self.values.pop().flatten() else { + self.values.push(None); + continue; + }; + reject_native_unit_value( + self.program, + operand, + &operand_value, + self.diagnostics, + ); + if !self.allow_moves { + self.diagnostics.push(error( + self.program, + "SPX-T218", + "`?` is only valid in an executable function body", + expression.span, + )); + } + if self.scopes[scope] + .bindings + .values() + .any(|binding| self.types.contains_resource(&binding.ty)) + { + self.diagnostics.push(error( + self.program, + "SPX-T218", + "`?` with a live resource binding is not supported yet", + expression.span, + )); + } + if let Some((ok, error_ty)) = ordinary_result_arguments(&operand_value.ty) { + let Some((_, residual_error_ty)) = + ordinary_result_arguments(&self.current.return_type) + else { + self.diagnostics.push(error( + self.program, + "SPX-T218", + format!( + "function `{}` must return the ordinary compiler-owned Result to propagate a Result with `?`", + self.current.name + ), + expression.span, + )); + self.values.push(Some(CheckedValue::value(ok.clone()))); + continue; + }; + if error_ty != residual_error_ty { + self.diagnostics.push(error( + self.program, + "SPX-T219", + format!("`?` cannot propagate error type {error_ty} into Result error type {residual_error_ty}"), + expression.span, + )); + } + self.values.push(Some(CheckedValue::value(ok.clone()))); + continue; + } + if let Some(some) = ordinary_option_argument(&operand_value.ty) { + let outer = ordinary_option_argument(&self.current.return_type); + if outer.is_none() { + self.diagnostics.push(error( + self.program, + "SPX-T218", + format!("function `{}` must return the ordinary compiler-owned Option to propagate an Option with `?`", self.current.name), + expression.span, + )); + } else if !matches!(some, Type::I64 | Type::Bool) + || outer.is_some_and(|value| !matches!(value, Type::I64 | Type::Bool)) + { + self.diagnostics.push(error( + self.program, + "SPX-T218", + "Option `?` accepts only direct `i64` or `bool` source and enclosing payloads", + expression.span, + )); + } + self.values.push(Some(CheckedValue::value(some.clone()))); + continue; + } + self.diagnostics.push(error( + self.program, + "SPX-T218", + format!("`?` operand must be an ordinary compiler-owned Result or Option, received {}", operand_value.ty), + expression.span, + )); + self.values.push(None); + } + VerifierFrame::ResumeProject { + expression, + base, + field, + } => { + let Some(base_value) = self.values.pop().flatten() else { + self.values.push(None); + continue; + }; + reject_native_unit_value(self.program, base, &base_value, self.diagnostics); + let Some(fields) = self.types.record_fields(&base_value.ty) else { + self.diagnostics.push(error( + self.program, + "SPX-T214", + format!("cannot project field `{field}` from `{}`", base_value.ty), + expression.span, + )); + self.values.push(None); + continue; + }; + let Some(declared) = fields.iter().find(|candidate| candidate.name == field) + else { + self.diagnostics.push(error( + self.program, + "SPX-T214", + format!("record `{}` has no field `{field}`", base_value.ty), + expression.span, + )); + self.values.push(None); + continue; + }; + let projected = self + .types + .record_field_type(&base_value.ty, declared) + .unwrap_or_else(|| declared.ty.clone()); + let mode = if self.types.contains_resource(&projected) { + base_value.mode + } else { + ParamMode::Value + }; + self.values.push(Some(CheckedValue { + ty: projected, + mode, + native_unit: false, + })); + } + VerifierFrame::PrepareRecordField { + expression, + type_name, + type_arguments, + fields, + declared_fields, + scope, + index, + mut supplied, + } => { + let field = &fields[index]; + let declared = declared_fields.and_then(|declared| { + declared + .iter() + .find(|candidate| candidate.name == field.name) + }); + if !supplied.insert(field.name.as_str()) || declared.is_none() { + self.diagnostics.push(error( + self.program, + "SPX-T212", + format!( + "unknown or duplicate field `{}` in `{type_name}` construction", + field.name + ), + field.span, + )); + } + self.frames.push(VerifierFrame::ResumeRecordField { + expression, + type_name, + type_arguments, + fields, + declared_fields, + scope, + index, + supplied, + }); + self.frames.push(VerifierFrame::Enter { + expression: &field.value, + scope, + }); + } + VerifierFrame::ResumeRecordField { + expression, + type_name, + type_arguments, + fields, + declared_fields, + scope, + index, + supplied, + } => { + let actual = self.values.pop().unwrap_or(None); + let field = &fields[index]; + let declared = declared_fields.and_then(|declared| { + declared + .iter() + .find(|candidate| candidate.name == field.name) + }); + if let (Some(declared), Some(actual)) = (declared, actual) { + reject_native_unit_value( + self.program, + &field.value, + &actual, + self.diagnostics, + ); + let expected = self + .types + .declaration(type_name) + .and_then(|declaration| { + TypeTable::substitute_variant_type( + declaration, + type_arguments, + &declared.ty, + ) + }) + .unwrap_or_else(|| declared.ty.clone()); + if actual.ty != expected { + self.diagnostics.push(error( + self.program, + "SPX-T215", + format!( + "field `{}.{}` expects {}, received {}", + type_name, field.name, expected, actual.ty + ), + field.value.span, + )); + } + if self.types.contains_resource(&declared.ty) + && actual.mode == ParamMode::Own + { + if self.allow_moves { + mark_value_sources_moved( + &field.value, + &mut self.scopes[scope].bindings, + self.types, + ); + } else { + self.diagnostics.push(error( + self.program, + "SPX-O105", + "contract expression cannot transfer an owned record field", + field.value.span, + )); + } + } else if self.types.contains_resource(&declared.ty) + && matches!(actual.mode, ParamMode::Borrow | ParamMode::Shared) + { + self.diagnostics.push(error( + self.program, + "SPX-O108", + "cannot move an owned field through a borrowed or shared record", + field.value.span, + )); + } + } + let next = index + 1; + if fields.get(next).is_some() { + self.frames.push(VerifierFrame::PrepareRecordField { + expression, + type_name, + type_arguments, + fields, + declared_fields, + scope, + index: next, + supplied, + }); + } else { + if let Some(declared_fields) = declared_fields { + for field in declared_fields { + if !supplied.contains(field.name.as_str()) { + self.diagnostics.push(error(self.program, "SPX-T213", format!("record `{type_name}` construction is missing field `{}`", field.name), expression.span)); + } + } + let instance = Type::Named { + name: type_name.to_owned(), + arguments: type_arguments.to_vec(), + }; + self.values.push(Some(CheckedValue::returned( + instance.clone(), + self.types.contains_resource(&instance), + ))); + } else { + self.values.push(None); + } + } + } + VerifierFrame::PrepareVariantField { + expression, + type_name, + type_arguments, + case_name, + fields, + declaration, + case, + scope, + index, + mut supplied, + } => { + let field = &fields[index]; + let declared = case.and_then(|case| { + case.fields + .iter() + .find(|candidate| candidate.name == field.name) + }); + if !supplied.insert(field.name.as_str()) || declared.is_none() { + self.diagnostics.push(error(self.program, "SPX-T212", format!("unknown or duplicate payload field `{}` in `{type_name}::{case_name}` construction", field.name), field.span)); + } + self.frames.push(VerifierFrame::ResumeVariantField { + expression, + type_name, + type_arguments, + case_name, + fields, + declaration, + case, + scope, + index, + supplied, + }); + self.frames.push(VerifierFrame::Enter { + expression: &field.value, + scope, + }); + } + VerifierFrame::ResumeVariantField { + expression, + type_name, + type_arguments, + case_name, + fields, + declaration, + case, + scope, + index, + supplied, + } => { + let actual = self.values.pop().unwrap_or(None); + let field = &fields[index]; + let declared = case.and_then(|case| { + case.fields + .iter() + .find(|candidate| candidate.name == field.name) + }); + if let (Some(declaration), Some(declared), Some(actual)) = + (declaration, declared, actual) + { + reject_native_unit_value( + self.program, + &field.value, + &actual, + self.diagnostics, + ); + let expected = TypeTable::substitute_variant_type( + declaration, + type_arguments, + &declared.ty, + ) + .unwrap_or_else(|| declared.ty.clone()); + if actual.ty != expected { + self.diagnostics.push(error( + self.program, + "SPX-T215", + format!( + "payload `{}::{}.{}` expects {}, received {}", + type_name, case_name, field.name, expected, actual.ty + ), + field.value.span, + )); + } + } + let next = index + 1; + if fields.get(next).is_some() { + self.frames.push(VerifierFrame::PrepareVariantField { + expression, + type_name, + type_arguments, + case_name, + fields, + declaration, + case, + scope, + index: next, + supplied, + }); + } else { + if let Some(case) = case { + for field in &case.fields { + if !supplied.contains(field.name.as_str()) { + self.diagnostics.push(error(self.program, "SPX-T213", format!("variant construction `{type_name}::{case_name}` is missing payload field `{}`", field.name), expression.span)); + } + } + self.values.push(Some(CheckedValue::value(Type::Named { + name: type_name.to_owned(), + arguments: type_arguments.to_vec(), + }))); + } else { + self.values.push(None); + } + } + } + VerifierFrame::ResumeUpdateBase { + expression, + base, + fields, + scope, + } => { + let Some(base_value) = self.values.pop().flatten() else { + self.values.push(None); + continue; + }; + reject_native_unit_value(self.program, base, &base_value, self.diagnostics); + let Some(declared_fields) = self.types.record_fields(&base_value.ty) else { + self.diagnostics.push(error( + self.program, + "SPX-T215", + format!( + "record update requires a record base, received {}", + base_value.ty + ), + base.span, + )); + self.values.push(None); + continue; + }; + if self.types.contains_resource(&base_value.ty) { + match base_value.mode { + ParamMode::Own if self.allow_moves => mark_value_sources_moved( + base, + &mut self.scopes[scope].bindings, + self.types, + ), + ParamMode::Own => self.diagnostics.push(error( + self.program, + "SPX-O105", + "contract expression cannot transfer an owned record update base", + base.span, + )), + ParamMode::Borrow | ParamMode::Shared => self.diagnostics.push(error( + self.program, + "SPX-O108", + "cannot update an owned record through a borrowed or shared base", + base.span, + )), + ParamMode::Value => {} + } + } + if !fields.is_empty() { + self.frames.push(VerifierFrame::PrepareUpdateField { + expression, + base_type: base_value.ty, + fields, + declared_fields, + scope, + index: 0, + supplied: HashSet::new(), + }); + } else { + self.values.push(Some(CheckedValue::returned( + base_value.ty.clone(), + self.types.contains_resource(&base_value.ty), + ))); + } + } + VerifierFrame::PrepareUpdateField { + expression, + base_type, + fields, + declared_fields, + scope, + index, + mut supplied, + } => { + let field = &fields[index]; + let declared = declared_fields + .iter() + .find(|candidate| candidate.name == field.name); + if !supplied.insert(field.name.as_str()) || declared.is_none() { + self.diagnostics.push(error( + self.program, + "SPX-T212", + format!( + "unknown or duplicate field `{}` in `{}` update", + field.name, base_type + ), + field.span, + )); + } + self.frames.push(VerifierFrame::ResumeUpdateField { + expression, + base_type, + fields, + declared_fields, + scope, + index, + supplied, + }); + self.frames.push(VerifierFrame::Enter { + expression: &field.value, + scope, + }); + } + VerifierFrame::ResumeUpdateField { + expression, + base_type, + fields, + declared_fields, + scope, + index, + supplied, + } => { + let actual = self.values.pop().unwrap_or(None); + let field = &fields[index]; + let declared = declared_fields + .iter() + .find(|candidate| candidate.name == field.name); + if let (Some(declared), Some(actual)) = (declared, actual) { + reject_native_unit_value( + self.program, + &field.value, + &actual, + self.diagnostics, + ); + let expected = self + .types + .record_field_type(&base_type, declared) + .unwrap_or_else(|| declared.ty.clone()); + if actual.ty != expected { + self.diagnostics.push(error( + self.program, + "SPX-T215", + format!( + "field `{}.{}` expects {}, received {}", + base_type, field.name, expected, actual.ty + ), + field.value.span, + )); + } + if self.types.contains_resource(&expected) && actual.mode == ParamMode::Own + { + if self.allow_moves { + mark_value_sources_moved( + &field.value, + &mut self.scopes[scope].bindings, + self.types, + ); + } else { + self.diagnostics.push(error( + self.program, + "SPX-O105", + "contract expression cannot transfer an owned record replacement", + field.value.span, + )); + } + } else if self.types.contains_resource(&expected) + && matches!(actual.mode, ParamMode::Borrow | ParamMode::Shared) + { + self.diagnostics.push(error( + self.program, + "SPX-O108", + "cannot move an owned replacement through a borrowed or shared value", + field.value.span, + )); + } + } + let next = index + 1; + if fields.get(next).is_some() { + self.frames.push(VerifierFrame::PrepareUpdateField { + expression, + base_type, + fields, + declared_fields, + scope, + index: next, + supplied, + }); + } else { + self.values.push(Some(CheckedValue::returned( + base_type.clone(), + self.types.contains_resource(&base_type), + ))); + } + } + VerifierFrame::ResumeMatchScrutinee { + expression, + scrutinee, + arms, + scope, + } => { + let scrutinee_value = self.values.pop().unwrap_or(None); + if let Some(value) = &scrutinee_value { + reject_native_unit_value(self.program, scrutinee, value, self.diagnostics); + } + if scrutinee_value + .as_ref() + .is_some_and(|value| self.types.record_fields(&value.ty).is_some()) + { + let scrutinee_value = scrutinee_value.expect("record checked above"); + if self.types.contains_resource(&scrutinee_value.ty) + || scrutinee_value.mode != ParamMode::Value + { + self.diagnostics.push(error( + self.program, + "SPX-O111", + "plain record match requires a Copy scrutinee", + scrutinee.span, + )); + } + let Some((first, rest)) = arms.split_first() else { + self.diagnostics.push(error( + self.program, + "SPX-M101", + format!( + "non-exhaustive match; missing record pattern for `{}`", + scrutinee_value.ty + ), + expression.span, + )); + self.values.push(None); + continue; + }; + for arm in rest { + self.diagnostics.push(error( + self.program, + "SPX-M102", + "unreachable arm after an irrefutable record pattern", + arm.pattern.span(), + )); + } + let outer_names = self.scopes[scope] + .bindings + .keys() + .cloned() + .collect::>(); + let arm_scope = self.scopes.len(); + self.scopes.push(VerifierScope { + bindings: self.scopes[scope].bindings.clone(), + }); + match &first.pattern { + MatchPattern::Wildcard { .. } => {} + MatchPattern::Record { + type_name, + fields, + span, + .. + } => check_record_pattern( + self.program, + type_name, + fields, + &scrutinee_value.ty, + &mut self.scopes[arm_scope].bindings, + self.types, + self.diagnostics, + *span, + ), + MatchPattern::Variant { .. } => self.diagnostics.push(error( + self.program, + "SPX-M103", + "variant pattern is incompatible with a record scrutinee", + first.pattern.span(), + )), + } + self.frames.push(VerifierFrame::ResumeRecordMatchArm { + arm: first, + parent_scope: scope, + arm_scope, + outer_names, + }); + self.frames.push(VerifierFrame::Enter { + expression: &first.value, + scope: arm_scope, + }); + continue; + } + let variant_instance = + scrutinee_value.as_ref().and_then(|value| match &value.ty { + Type::Named { name, arguments } + if self.types.variant_cases(&value.ty).is_some() => + { + Some((name.clone(), arguments.clone())) + } + Type::I64 | Type::Bool | Type::Named { .. } => None, + }); + let variant_name = variant_instance.as_ref().map(|(name, _)| name.clone()); + let declared_cases = scrutinee_value + .as_ref() + .and_then(|value| self.types.variant_cases(&value.ty)); + if declared_cases.is_none() { + self.diagnostics.push(error( + self.program, + "SPX-M103", + format!( + "match scrutinee must be a Copy variant, received {}", + scrutinee_value.as_ref().map_or_else( + || "an invalid value".to_owned(), + |value| value.ty.to_string() + ) + ), + scrutinee.span, + )); + } + let outer_names = self.scopes[scope] + .bindings + .keys() + .cloned() + .collect::>(); + let state = VariantMatchState { + expression, + arms, + parent_scope: scope, + index: 0, + outer_names, + baseline: self.scopes[scope].bindings.clone(), + arm_states: Vec::new(), + covered: HashSet::new(), + wildcard_seen: false, + result: None, + variant_name, + variant_arguments: variant_instance + .map(|(_, arguments)| arguments) + .unwrap_or_default(), + declared_cases, + }; + self.frames + .push(VerifierFrame::PrepareVariantMatchArm(state)); + } + VerifierFrame::ResumeRecordMatchArm { + arm, + parent_scope, + arm_scope, + outer_names, + } => { + if arm_scope + 1 != self.scopes.len() { + return Err(Diagnostic::io( + "SPX-H006", + "record match arm scope is not the active child", + )); + } + let result = self.values.pop().unwrap_or(None); + if let Some(value) = &result { + reject_native_unit_value(self.program, &arm.value, value, self.diagnostics); + } + let arm_bindings = self + .scopes + .pop() + .expect("record arm scope is active") + .bindings; + merge_moved( + &mut self.scopes[parent_scope].bindings, + &arm_bindings, + &outer_names, + ); + if result.as_ref().is_some_and(|value| { + !matches!(value.ty, Type::I64 | Type::Bool) + || value.mode != ParamMode::Value + }) { + self.diagnostics.push(error( + self.program, + "SPX-T216", + "record match arm must return a Copy i64 or bool value", + arm.value.span, + )); + self.values.push(None); + } else { + self.values.push(result); + } + } + VerifierFrame::PrepareVariantMatchArm(mut state) => { + if state.index >= state.arms.len() { + if !state.wildcard_seen { + if let (Some(variant_name), Some(cases)) = + (&state.variant_name, state.declared_cases) + { + if let Some(missing) = cases + .iter() + .find(|case| !state.covered.contains(&case.name)) + { + let witness = if missing.fields.is_empty() { + format!("{variant_name}::{} {{}}", missing.name) + } else { + format!("{variant_name}::{} {{ .. }}", missing.name) + }; + self.diagnostics.push(error( + self.program, + "SPX-M101", + format!("non-exhaustive match; missing case `{witness}`"), + state.expression.span, + )); + } + } + } + if let Some((first, rest)) = state.arm_states.split_first() { + let mut joined = first.clone(); + for branch in rest { + for name in &state.outer_names { + if let (Some(joined_binding), Some(branch_binding)) = + (joined.get_mut(name), branch.get(name)) + { + joined_binding.availability = joined_binding + .availability + .join(branch_binding.availability); + joined_binding.moved_places = + join_moved_places(joined_binding, branch_binding); + joined_binding.definitely_partial = + join_definitely_partial(joined_binding, branch_binding); + } + } + } + merge_moved( + &mut self.scopes[state.parent_scope].bindings, + &joined, + &state.outer_names, + ); + } + self.values.push(state.result); + continue; + } + let arm = &state.arms[state.index]; + let arm_scope = self.scopes.len(); + self.scopes.push(VerifierScope { + bindings: state.baseline.clone(), + }); + match &arm.pattern { + MatchPattern::Wildcard { span } => { + if state.wildcard_seen + || state + .declared_cases + .is_some_and(|cases| state.covered.len() == cases.len()) + { + self.diagnostics.push(error( + self.program, + "SPX-M102", + "unreachable wildcard match arm", + *span, + )); + } + state.wildcard_seen = true; + } + MatchPattern::Variant { + type_name, + case_name, + fields, + span, + .. + } => { + let compatible = state.variant_name.as_deref() == Some(type_name); + let declared_case = compatible + .then_some(state.declared_cases) + .flatten() + .and_then(|cases| { + cases.iter().find(|case| case.name == *case_name) + }); + if declared_case.is_none() { + self.diagnostics.push(error( + self.program, + "SPX-M103", + format!("pattern `{type_name}::{case_name}` is incompatible with the match scrutinee"), + *span, + )); + } else if state.wildcard_seen + || !state.covered.insert(case_name.clone()) + { + self.diagnostics.push(error( + self.program, + "SPX-M102", + format!( + "unreachable duplicate case `{type_name}::{case_name}`" + ), + *span, + )); + } + let mut supplied = HashSet::new(); + let mut bindings = HashSet::new(); + for field in fields { + let declared_field = declared_case.and_then(|case| { + case.fields + .iter() + .find(|candidate| candidate.name == field.name) + }); + if !supplied.insert(field.name.as_str()) || declared_field.is_none() + { + self.diagnostics.push(error(self.program, "SPX-M104", format!("unknown or duplicate pattern field `{}` in `{type_name}::{case_name}`", field.name), field.span)); + } + if !source_identifier(&field.binding) + || !bindings.insert(field.binding.as_str()) + || self.scopes[arm_scope].bindings.contains_key(&field.binding) + { + self.diagnostics.push(error( + self.program, + "SPX-M104", + format!( + "invalid or duplicate pattern binding `{}`", + field.binding + ), + field.binding_span, + )); + continue; + } + if let Some(declared_field) = declared_field { + let binding_ty = state + .variant_name + .as_ref() + .and_then(|name| { + self.types.declaration(name).and_then(|declaration| { + TypeTable::substitute_variant_type( + declaration, + &state.variant_arguments, + &declared_field.ty, + ) + }) + }) + .unwrap_or_else(|| declared_field.ty.clone()); + self.scopes[arm_scope].bindings.insert( + field.binding.clone(), + Binding { + ty: binding_ty, + mode: ParamMode::Value, + availability: Availability::Available, + moved_places: HashMap::new(), + definitely_partial: HashSet::new(), + native_unit_discard: false, + }, + ); + } + } + if let Some(declared_case) = declared_case { + for field in &declared_case.fields { + if !supplied.contains(field.name.as_str()) { + self.diagnostics.push(error(self.program, "SPX-M104", format!("pattern `{type_name}::{case_name}` is missing payload field `{}`", field.name), *span)); + } + } + } + } + MatchPattern::Record { span, .. } => self.diagnostics.push(error( + self.program, + "SPX-M103", + "record pattern is incompatible with a variant scrutinee", + *span, + )), + } + self.frames + .push(VerifierFrame::ResumeVariantMatchArm { state, arm_scope }); + self.frames.push(VerifierFrame::Enter { + expression: &arm.value, + scope: arm_scope, + }); + } + VerifierFrame::ResumeVariantMatchArm { + mut state, + arm_scope, + } => { + if arm_scope + 1 != self.scopes.len() { + return Err(Diagnostic::io( + "SPX-H006", + "variant match arm scope is not the active child", + )); + } + let arm = &state.arms[state.index]; + let arm_value = self.values.pop().unwrap_or(None); + if let Some(value) = &arm_value { + reject_native_unit_value(self.program, &arm.value, value, self.diagnostics); + } + if let Some(arm_value) = arm_value { + if let Some(expected) = &state.result { + if expected.ty != arm_value.ty || expected.mode != arm_value.mode { + self.diagnostics.push(error( + self.program, + "SPX-T216", + format!( + "match arms return incompatible values: {} and {}", + expected.ty, arm_value.ty + ), + arm.value.span, + )); + } + } else { + state.result = Some(arm_value); + } + } + state.arm_states.push( + self.scopes + .pop() + .expect("variant arm scope is active") + .bindings, + ); + state.index += 1; + self.frames + .push(VerifierFrame::PrepareVariantMatchArm(state)); + } + } + } + if self.values.len() != 1 { + return Err(Diagnostic::io( + "SPX-H006", + "iterative verifier value stack did not settle", + )); + } + Ok(self.values.pop().expect("value count checked above")) + } } #[allow(clippy::too_many_arguments)] -fn check_record_pattern( +fn check_expr_iterative( program: &Program, - pattern_type: &str, - fields: &[RecordMatchPatternField], - expected: &Type, + current: &Function, + expr: &Expr, variables: &mut HashMap, + functions: &HashMap<&str, &Function>, types: &TypeTable<'_>, + result_type: Option<&Type>, + allow_moves: bool, diagnostics: &mut Vec, - span: Span, -) { - let Type::Named { - name: expected_name, - .. - } = expected - else { - diagnostics.push(error( - program, - "SPX-M103", - format!("record pattern `{pattern_type}` is incompatible with `{expected}`"), - span, - )); - return; - }; - let declared_fields = types.record_fields(expected); - if expected_name != pattern_type - || declared_fields.is_none() - || types.contains_resource(expected) - { - diagnostics.push(error( - program, - "SPX-M103", - format!("record pattern `{pattern_type}` is incompatible with `{expected}`"), - span, - )); - return; - } - let declared_fields = declared_fields.expect("checked above"); - let mut supplied = HashSet::new(); - for field in fields { - let declared = declared_fields - .iter() - .find(|candidate| candidate.name == field.name); - if !supplied.insert(field.name.as_str()) || declared.is_none() { - diagnostics.push(error( - program, - "SPX-M104", - format!( - "unknown or duplicate record pattern field `{}.{}`", - pattern_type, field.name - ), - field.span, - )); - continue; - } - let declared = declared.expect("checked above"); - let field_ty = types - .record_field_type(expected, declared) - .unwrap_or_else(|| declared.ty.clone()); - match &field.pattern { - RecordMatchFieldPattern::Binding { name, span } => { - if !source_identifier(name) || variables.contains_key(name) { - diagnostics.push(error( - program, - "SPX-M104", - format!("invalid or duplicate record pattern binding `{name}`"), - *span, - )); - } else { - variables.insert( - name.clone(), - Binding { - ty: field_ty, - mode: ParamMode::Value, - availability: Availability::Available, - moved_places: HashMap::new(), - definitely_partial: HashSet::new(), - }, - ); - } - } - RecordMatchFieldPattern::Wildcard { .. } => {} - RecordMatchFieldPattern::Record { - type_name, - fields, - span, - .. - } => check_record_pattern( - program, - type_name, - fields, - &field_ty, - variables, - types, - diagnostics, - *span, - ), - } - } - for declared in declared_fields { - if !supplied.contains(declared.name.as_str()) { - diagnostics.push(error( - program, - "SPX-M104", - format!( - "record pattern `{pattern_type}` is missing field `{}`", - declared.name - ), - span, - )); +) -> Option { + let initial = std::mem::take(variables); + let mut verifier = IterativeVerifier::new( + program, + current, + initial, + functions, + types, + result_type, + allow_moves, + diagnostics, + ); + let result = verifier.run(expr); + *variables = verifier + .scopes + .first_mut() + .map(|scope| std::mem::take(&mut scope.bindings)) + .unwrap_or_default(); + drop(verifier); + match result { + Ok(value) => value, + Err(diagnostic) => { + diagnostics.push(diagnostic.at_path(&program.path)); + None } } } +#[cfg(test)] #[allow(clippy::too_many_arguments)] fn check_expr( program: &Program, @@ -1989,9 +4930,18 @@ fn check_expr( Availability::Available => {} }, } + if binding.native_unit_discard { + diagnostics.push(error( + program, + "SPX-B107", + "Native Rust Interop declaration set is unsupported: scalar value signature required", + expr.span, + )); + } CheckedValue { ty: binding.ty.clone(), mode: binding.mode, + native_unit: binding.native_unit_discard, } }) .or_else(|| { @@ -2008,6 +4958,68 @@ fn check_expr( type_arguments, args, } => { + let native_import = program + .interfaces + .iter() + .flat_map(|interface| &interface.imports) + .find(|import| import.native_rust && import.name == *name); + if let Some(import) = native_import { + if !type_arguments.is_empty() || args.len() != import.params.len() { + diagnostics.push(error( + program, + "SPX-B107", + "Native Rust Interop declaration set is unsupported: scalar value signature required", + expr.span, + )); + } + for effect in &import.effects { + if !current.effects.contains(effect) { + diagnostics.push(error( + program, + "SPX-B107", + "Native Rust Interop declaration set is unsupported: effect or capability mismatch", + expr.span, + )); + } + } + for (index, argument) in args.iter().enumerate() { + let actual = check_expr( + program, + current, + argument, + variables, + functions, + types, + result_type, + allow_moves, + diagnostics, + ); + if let (Some(actual), Some(parameter)) = (actual, import.params.get(index)) { + reject_native_unit_value(program, argument, &actual, diagnostics); + if !actual.native_unit + && (actual.ty != parameter.ty || actual.mode != ParamMode::Value) + { + diagnostics.push(error( + program, + "SPX-B107", + "Native Rust Interop declaration set is unsupported: scalar value signature required", + argument.span, + )); + } + } + } + let native_unit = import.result == ImportResult::Unit; + let mut checked = CheckedValue::value(match import.result { + ImportResult::Unit => Type::Named { + name: "\0native-rust-unit".to_owned(), + arguments: Vec::new(), + }, + ImportResult::I64 => Type::I64, + ImportResult::Bool => Type::Bool, + }); + checked.native_unit = native_unit; + return Some(checked); + } let target = functions.get(name.as_str()).copied(); if target.is_none() { diagnostics.push(error( @@ -2098,7 +5110,13 @@ fn check_expr( else { continue; }; - if actual.as_ref().is_some_and(|actual| actual.ty != param.ty) { + if let Some(actual) = &actual { + reject_native_unit_value(program, arg, actual, diagnostics); + } + if actual + .as_ref() + .is_some_and(|actual| !actual.native_unit && actual.ty != param.ty) + { diagnostics.push(error( program, "SPX-T205", @@ -2132,10 +5150,19 @@ fn check_expr( }) } ExprKind::Unary { op, value } => { - let actual = check_expr( + // Peel maximal unary chains iteratively. The language admits an + // exact semantic depth of 512, which must not consume one verifier + // call frame per node on an ordinary caller stack. + let mut unary = vec![(*op, value.as_ref(), expr.span)]; + let mut leaf = value.as_ref(); + while let ExprKind::Unary { op, value } = &leaf.kind { + unary.push((*op, value.as_ref(), leaf.span)); + leaf = value; + } + let mut actual = check_expr( program, current, - value, + leaf, variables, functions, types, @@ -2143,19 +5170,23 @@ fn check_expr( allow_moves, diagnostics, )?; - let expected = match op { - UnaryOp::Neg => Type::I64, - UnaryOp::Not => Type::Bool, - }; - if actual.ty != expected { - diagnostics.push(error( - program, - "SPX-T206", - format!("unary operator expects {expected}, received {}", actual.ty), - expr.span, - )); + for (op, operand, span) in unary.into_iter().rev() { + let expected = match op { + UnaryOp::Neg => Type::I64, + UnaryOp::Not => Type::Bool, + }; + if !actual.native_unit && actual.ty != expected { + diagnostics.push(error( + program, + "SPX-T206", + format!("unary operator expects {expected}, received {}", actual.ty), + span, + )); + } + reject_native_unit_value(program, operand, &actual, diagnostics); + actual = CheckedValue::value(expected); } - Some(CheckedValue::value(expected)) + Some(actual) } ExprKind::Binary { op, left, right } => { let left_ty = check_expr( @@ -2198,6 +5229,14 @@ fn check_expr( diagnostics, ) }; + if let Some(value) = &left_ty { + reject_native_unit_value(program, left, value, diagnostics); + } + if let Some(value) = &right_ty { + reject_native_unit_value(program, right, value, diagnostics); + } + let native_unit_operand = left_ty.as_ref().is_some_and(|value| value.native_unit) + || right_ty.as_ref().is_some_and(|value| value.native_unit); let (expected, output) = match op { BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Rem => { (Type::I64, Type::I64) @@ -2207,7 +5246,8 @@ fn check_expr( } BinaryOp::And | BinaryOp::Or => (Type::Bool, Type::Bool), BinaryOp::Eq | BinaryOp::Ne => { - if left_ty.is_some() + if !native_unit_operand + && left_ty.is_some() && right_ty.is_some() && left_ty.as_ref().map(|value| &value.ty) != right_ty.as_ref().map(|value| &value.ty) @@ -2222,8 +5262,9 @@ fn check_expr( return Some(CheckedValue::value(Type::Bool)); } }; - if left_ty.as_ref().is_some_and(|value| value.ty != expected) - || right_ty.as_ref().is_some_and(|value| value.ty != expected) + if !native_unit_operand + && (left_ty.as_ref().is_some_and(|value| value.ty != expected) + || right_ty.as_ref().is_some_and(|value| value.ty != expected)) { diagnostics.push(error( program, @@ -2296,6 +5337,7 @@ fn check_expr( diagnostics, ); if let (Some(declared), Some(actual)) = (declared, actual) { + reject_native_unit_value(program, &field.value, &actual, diagnostics); let expected = declaration .and_then(|declaration| { TypeTable::substitute_variant_type( @@ -2424,6 +5466,7 @@ fn check_expr( if let (Some(declaration), Some(declared), Some(actual)) = (declaration, declared, actual) { + reject_native_unit_value(program, &field.value, &actual, diagnostics); let expected = TypeTable::substitute_variant_type( declaration, type_arguments, @@ -2472,6 +5515,9 @@ fn check_expr( allow_moves, diagnostics, ); + if let Some(value) = &scrutinee_value { + reject_native_unit_value(program, scrutinee, value, diagnostics); + } if scrutinee_value .as_ref() .is_some_and(|value| types.record_fields(&value.ty).is_some()) @@ -2546,6 +5592,9 @@ fn check_expr( allow_moves, diagnostics, ); + if let Some(value) = &result { + reject_native_unit_value(program, &first.value, value, diagnostics); + } merge_moved(variables, &arm_variables, &outer_names); if result.as_ref().is_some_and(|value| { !matches!(value.ty, Type::I64 | Type::Bool) @@ -2687,6 +5736,7 @@ fn check_expr( availability: Availability::Available, moved_places: HashMap::new(), definitely_partial: HashSet::new(), + native_unit_discard: false, }, ); } @@ -2725,6 +5775,9 @@ fn check_expr( allow_moves, diagnostics, ); + if let Some(value) = &arm_value { + reject_native_unit_value(program, &arm.value, value, diagnostics); + } if let Some(arm_value) = arm_value { if let Some(expected) = &result { if expected.ty != arm_value.ty || expected.mode != arm_value.mode { @@ -2785,7 +5838,7 @@ fn check_expr( result } ExprKind::Try { operand } => { - let operand = check_expr( + let operand_value = check_expr( program, current, operand, @@ -2796,7 +5849,8 @@ fn check_expr( allow_moves, diagnostics, ); - let operand = operand?; + let operand_value = operand_value?; + reject_native_unit_value(program, operand, &operand_value, diagnostics); if !allow_moves { diagnostics.push(error( program, @@ -2816,7 +5870,7 @@ fn check_expr( expr.span, )); } - if let Some((ok, error_ty)) = ordinary_result_arguments(&operand.ty) { + if let Some((ok, error_ty)) = ordinary_result_arguments(&operand_value.ty) { let Some((_, residual_error_ty)) = ordinary_result_arguments(¤t.return_type) else { @@ -2843,7 +5897,7 @@ fn check_expr( } return Some(CheckedValue::value(ok.clone())); } - if let Some(some) = ordinary_option_argument(&operand.ty) { + if let Some(some) = ordinary_option_argument(&operand_value.ty) { let outer = ordinary_option_argument(¤t.return_type); if outer.is_none() { diagnostics.push(error( @@ -2872,7 +5926,7 @@ fn check_expr( "SPX-T218", format!( "`?` operand must be an ordinary compiler-owned Result or Option, received {}", - operand.ty + operand_value.ty ), expr.span, )); @@ -2890,6 +5944,7 @@ fn check_expr( allow_moves, diagnostics, )?; + reject_native_unit_value(program, base, &base_value, diagnostics); let declared_fields = types.record_fields(&base_value.ty); if declared_fields.is_none() { diagnostics.push(error( @@ -2955,6 +6010,7 @@ fn check_expr( diagnostics, ); if let (Some(declared), Some(actual)) = (declared, actual) { + reject_native_unit_value(program, &field.value, &actual, diagnostics); let expected = types .record_field_type(&base_value.ty, declared) .unwrap_or_else(|| declared.ty.clone()); @@ -3010,6 +6066,7 @@ fn check_expr( return Some(CheckedValue { ty: place.ty, mode: place.mode, + native_unit: false, }); } let base_value = check_expr( @@ -3023,6 +6080,7 @@ fn check_expr( allow_moves, diagnostics, )?; + reject_native_unit_value(program, base, &base_value, diagnostics); let Some(fields) = types.record_fields(&base_value.ty) else { diagnostics.push(error( program, @@ -3052,6 +6110,7 @@ fn check_expr( Some(CheckedValue { ty: projected, mode, + native_unit: false, }) } ExprKind::Block { statements, tail } => { @@ -3115,6 +6174,7 @@ fn check_expr( availability: Availability::Available, moved_places: HashMap::new(), definitely_partial: HashSet::new(), + native_unit_discard: actual.native_unit, }, ); } @@ -3140,7 +6200,7 @@ fn check_expr( then_branch, else_branch, } => { - if check_expr( + if let Some(value) = check_expr( program, current, condition, @@ -3150,15 +6210,17 @@ fn check_expr( result_type, allow_moves, diagnostics, - ) - .is_some_and(|value| value.ty != Type::Bool) - { - diagnostics.push(error( - program, - "SPX-T210", - "`if` condition must be bool", - condition.span, - )); + ) { + if value.native_unit { + reject_native_unit_value(program, condition, &value, diagnostics); + } else if value.ty != Type::Bool { + diagnostics.push(error( + program, + "SPX-T210", + "`if` condition must be bool", + condition.span, + )); + } } let original_names = variables.keys().cloned().collect::>(); let mut then_variables = variables.clone(); @@ -3205,7 +6267,20 @@ fn check_expr( } match (then_value, else_value) { (Some(then_value), Some(else_value)) => { - if then_value.ty != else_value.ty { + if then_value.native_unit || else_value.native_unit { + reject_native_unit_value( + program, + then_branch, + &then_value, + diagnostics, + ); + reject_native_unit_value( + program, + else_branch, + &else_value, + diagnostics, + ); + } else if then_value.ty != else_value.ty { diagnostics.push(error( program, "SPX-T211", @@ -3314,63 +6389,118 @@ fn mark_value_sources_moved( variables: &mut HashMap, types: &TypeTable<'_>, ) { - match &expr.kind { - ExprKind::Var(name) => { - if let Some(binding) = variables.get_mut(name) { - if types.contains_resource(&binding.ty) - && binding.mode == ParamMode::Own - && binding.availability == Availability::Available - { - binding.availability = Availability::Moved; + enum Frame<'a> { + Enter(&'a Expr, usize), + AfterThen { + else_branch: &'a Expr, + parent: usize, + then_scope: usize, + names: Vec, + }, + AfterElse { + parent: usize, + else_scope: usize, + names: Vec, + then_variables: HashMap, + }, + } + let root = std::mem::take(variables); + let mut scopes = vec![root]; + let mut frames = vec![Frame::Enter(expr, 0)]; + while let Some(frame) = frames.pop() { + match frame { + Frame::Enter(expr, scope) => match &expr.kind { + ExprKind::Var(name) => { + if let Some(binding) = scopes[scope].get_mut(name) { + if types.contains_resource(&binding.ty) + && binding.mode == ParamMode::Own + && binding.availability == Availability::Available + { + binding.availability = Availability::Moved; + } + } } - } - } - ExprKind::Block { tail, .. } => mark_value_sources_moved(tail, variables, types), - ExprKind::Project { base, .. } => { - if let Some(place) = source_place(expr, variables, types) { - if let Some(binding) = variables.get_mut(&place.root) { - if binding.mode == ParamMode::Own { - binding - .moved_places - .insert(place.projections, Availability::Moved); + ExprKind::Block { tail, .. } => frames.push(Frame::Enter(tail, scope)), + ExprKind::Project { base, .. } => { + if let Some(place) = source_place(expr, &scopes[scope], types) { + if let Some(binding) = scopes[scope].get_mut(&place.root) { + if binding.mode == ParamMode::Own { + binding + .moved_places + .insert(place.projections, Availability::Moved); + } + } + } else { + frames.push(Frame::Enter(base, scope)); } } - } else { - mark_value_sources_moved(base, variables, types); + ExprKind::If { + then_branch, + else_branch, + .. + } => { + let names = scopes[scope].keys().cloned().collect::>(); + let then_scope = scopes.len(); + scopes.push(scopes[scope].clone()); + frames.push(Frame::AfterThen { + else_branch, + parent: scope, + then_scope, + names, + }); + frames.push(Frame::Enter(then_branch, then_scope)); + } + ExprKind::UpdateRecord { .. } | ExprKind::ConstructRecord { .. } => {} + _ => {} + }, + Frame::AfterThen { + else_branch, + parent, + then_scope, + names, + } => { + debug_assert_eq!(then_scope + 1, scopes.len()); + let then_variables = scopes.pop().expect("then move scope is active"); + let else_scope = scopes.len(); + scopes.push(scopes[parent].clone()); + frames.push(Frame::AfterElse { + parent, + else_scope, + names, + then_variables, + }); + frames.push(Frame::Enter(else_branch, else_scope)); } - } - ExprKind::If { - then_branch, - else_branch, - .. - } => { - let names = variables.keys().cloned().collect::>(); - let mut then_variables = variables.clone(); - let mut else_variables = variables.clone(); - mark_value_sources_moved(then_branch, &mut then_variables, types); - mark_value_sources_moved(else_branch, &mut else_variables, types); - for name in names { - if let Some(binding) = variables.get_mut(&name) { - let then_state = then_variables - .get(&name) - .map_or(Availability::Available, |value| value.availability); - let else_state = else_variables - .get(&name) - .map_or(Availability::Available, |value| value.availability); - binding.availability = then_state.join(else_state); - if let (Some(then_binding), Some(else_binding)) = - (then_variables.get(&name), else_variables.get(&name)) - { - binding.moved_places = join_moved_places(then_binding, else_binding); - binding.definitely_partial = - join_definitely_partial(then_binding, else_binding); + Frame::AfterElse { + parent, + else_scope, + names, + then_variables, + } => { + debug_assert_eq!(else_scope + 1, scopes.len()); + let else_variables = scopes.pop().expect("else move scope is active"); + for name in names { + if let Some(binding) = scopes[parent].get_mut(&name) { + let then_state = then_variables + .get(&name) + .map_or(Availability::Available, |value| value.availability); + let else_state = else_variables + .get(&name) + .map_or(Availability::Available, |value| value.availability); + binding.availability = then_state.join(else_state); + if let (Some(then_binding), Some(else_binding)) = + (then_variables.get(&name), else_variables.get(&name)) + { + binding.moved_places = join_moved_places(then_binding, else_binding); + binding.definitely_partial = + join_definitely_partial(then_binding, else_binding); + } } } } } - ExprKind::UpdateRecord { .. } | ExprKind::ConstructRecord { .. } => {} - _ => {} } + *variables = scopes.pop().expect("root move scope is retained"); } fn merge_moved( @@ -3420,32 +6550,35 @@ fn source_place( variables: &HashMap, types: &TypeTable<'_>, ) -> Option { - match &expr.kind { - ExprKind::Var(name) => { - let binding = variables.get(name)?; - Some(SourcePlace { - root: name.clone(), - root_span: expr.span, - projections: Vec::new(), - ty: binding.ty.clone(), - mode: binding.mode, - }) - } - ExprKind::Project { base, field, .. } => { - let mut place = source_place(base, variables, types)?; - let declared = types - .record_fields(&place.ty)? - .iter() - .find(|candidate| candidate.name == *field)?; - place.ty = types.record_field_type(&place.ty, declared)?; - if !types.contains_resource(&place.ty) { - place.mode = ParamMode::Value; - } - place.projections.push(field.clone()); - Some(place) + let mut current = expr; + let mut projected = Vec::new(); + while let ExprKind::Project { base, field, .. } = ¤t.kind { + projected.push(field.as_str()); + current = base; + } + let ExprKind::Var(name) = ¤t.kind else { + return None; + }; + let binding = variables.get(name)?; + let mut place = SourcePlace { + root: name.clone(), + root_span: current.span, + projections: Vec::with_capacity(projected.len()), + ty: binding.ty.clone(), + mode: binding.mode, + }; + for field in projected.into_iter().rev() { + let declared = types + .record_fields(&place.ty)? + .iter() + .find(|candidate| candidate.name == field)?; + place.ty = types.record_field_type(&place.ty, declared)?; + if !types.contains_resource(&place.ty) { + place.mode = ParamMode::Value; } - _ => None, + place.projections.push(field.to_owned()); } + Some(place) } fn check_source_place_availability( @@ -3622,7 +6755,7 @@ fn require_bool( } }); let mut contract_variables = variables.clone(); - if check_expr( + if let Some(value) = check_expr_iterative( program, function, contract, @@ -3632,15 +6765,17 @@ fn require_bool( result_type, false, diagnostics, - ) - .is_some_and(|value| value.ty != Type::Bool) - { - diagnostics.push(error( - program, - "SPX-C101", - format!("{kind} on `{}` must be bool", function.name), - contract.span, - )); + ) { + if value.native_unit { + reject_native_unit_value(program, contract, &value, diagnostics); + } else if value.ty != Type::Bool { + diagnostics.push(error( + program, + "SPX-C101", + format!("{kind} on `{}` must be bool", function.name), + contract.span, + )); + } } } @@ -3695,3 +6830,174 @@ fn source_identifier(value: &str) -> bool { | "result" ) } + +#[cfg(test)] +mod iterative_verifier_tests { + use super::*; + use std::path::Path; + + #[allow(clippy::type_complexity)] + fn diagnostics_key( + diagnostics: &[Diagnostic], + ) -> Vec<( + &'static str, + crate::diagnostic::Severity, + &str, + Option<&str>, + Option, + Option<&str>, + )> { + diagnostics + .iter() + .map(|diagnostic| { + ( + diagnostic.code, + diagnostic.severity, + diagnostic.message.as_str(), + diagnostic.path.as_deref(), + diagnostic.span, + diagnostic.help.as_deref(), + ) + }) + .collect() + } + + fn compare_scalar_body(source: &str) { + let program = crate::parse(source, Path::new("iterative-verifier.spx")).unwrap(); + let current = program + .functions + .iter() + .find(|function| function.name == "main") + .unwrap(); + let expression = match ¤t.body.kind { + ExprKind::Block { statements, tail } if statements.is_empty() => tail.as_ref(), + _ => ¤t.body, + }; + let functions = program + .functions + .iter() + .map(|function| (function.name.as_str(), function)) + .collect::>(); + let types = TypeTable::new(&program); + let mut oracle_scope = HashMap::new(); + for parameter in ¤t.params { + oracle_scope.insert( + parameter.name.clone(), + Binding { + ty: parameter.ty.clone(), + mode: parameter.mode, + availability: Availability::Available, + moved_places: HashMap::new(), + definitely_partial: HashSet::new(), + native_unit_discard: false, + }, + ); + } + let iterative_scope = oracle_scope.clone(); + let mut oracle_diagnostics = Vec::new(); + let oracle = check_expr( + &program, + current, + expression, + &mut oracle_scope, + &functions, + &types, + None, + true, + &mut oracle_diagnostics, + ); + let mut iterative_diagnostics = Vec::new(); + let mut iterative = IterativeVerifier::new( + &program, + current, + iterative_scope, + &functions, + &types, + None, + true, + &mut iterative_diagnostics, + ); + let actual = iterative.run(expression).unwrap(); + assert_eq!( + oracle + .as_ref() + .map(|value| (&value.ty, value.mode, value.native_unit)), + actual + .as_ref() + .map(|value| (&value.ty, value.mode, value.native_unit)) + ); + assert_eq!(oracle_scope.len(), iterative.scopes[0].bindings.len()); + for (name, expected) in oracle_scope { + let actual = &iterative.scopes[0].bindings[&name]; + assert_eq!(expected.ty, actual.ty); + assert_eq!(expected.mode, actual.mode); + assert_eq!(expected.availability, actual.availability); + assert_eq!(expected.moved_places, actual.moved_places); + assert_eq!(expected.definitely_partial, actual.definitely_partial); + assert_eq!(expected.native_unit_discard, actual.native_unit_discard); + } + drop(iterative); + assert_eq!( + diagnostics_key(&oracle_diagnostics), + diagnostics_key(&iterative_diagnostics) + ); + } + + #[test] + fn scalar_frame_machine_matches_recursive_oracle() { + compare_scalar_body("module t; fn main() -> i64 { -(1 + true) }"); + compare_scalar_body("module t; fn main() -> bool { missing_left == missing_right }"); + compare_scalar_body("module t; fn main(flag: bool) -> bool { flag && missing }"); + compare_scalar_body("module t; fn main(flag: bool) -> i64 { if flag { 1 } else { true } }"); + compare_scalar_body( + "module t; fn main(flag: bool) -> i64 { if missing_condition { missing_then } else { missing_else } }", + ); + compare_scalar_body( + "module t; fn main(flag: bool) -> i64 { let value = 1 + true; let value = missing; if flag { value } else { missing_tail } }", + ); + compare_scalar_body( + "module t; fn zero() -> i64 { 0 } fn main() -> i64 { zero(missing_a, missing_b) }", + ); + compare_scalar_body( + "module t; fn one(value: i64) -> i64 { value } fn main() -> i64 { one(true) + one(missing) }", + ); + compare_scalar_body( + "module t; fn identity(value: T) -> T { value } fn main() -> i64 { identity(1) + identity(true) }", + ); + compare_scalar_body( + "module t; @id(\"t.host\") interface Host permits { } { @id(\"t.host.ping\") import rust fn ping(value: i64) -> unit effects { } failure infallible; } fn main() -> i64 { let acknowledged = ping(1); let copied = acknowledged; 1 }", + ); + compare_scalar_body( + "module t; @id(\"t.buffer\") resource Buffer { @id(\"t.buffer.drop\") drop trivial; } fn inspect(value: borrow Buffer) -> i64 { 1 } fn consume(value: own Buffer) -> i64 { 1 } fn main(buffer: own Buffer) -> i64 { let first = consume(buffer) + missing; inspect(buffer) }", + ); + compare_scalar_body( + "module t; @id(\"t.buffer\") resource Buffer { @id(\"t.buffer.drop\") drop trivial; } fn inspect(value: borrow Buffer) -> i64 { 1 } fn consume(value: own Buffer) -> bool { true } fn main(buffer: own Buffer, left: bool, right: bool) -> i64 { let selected = left && (right && consume(buffer)); inspect(buffer) }", + ); + compare_scalar_body( + "module t; @id(\"t.pair\") record Pair { @id(\"t.pair.x\") x: i64, @id(\"t.pair.y\") y: i64, } fn main() -> Pair { Pair { missing: missing_rhs, x: true, x: missing_duplicate } }", + ); + compare_scalar_body( + "module t; @id(\"t.pair\") record Pair { @id(\"t.pair.x\") x: i64, @id(\"t.pair.y\") y: i64, } fn main() -> Pair { let pair = Pair { x: 1, y: 2 }; pair with { missing: missing_rhs, x: true, x: missing_duplicate } }", + ); + compare_scalar_body( + "module t; @id(\"t.choice\") variant Choice { @id(\"t.choice.none\") None, @id(\"t.choice.value\") Value { @id(\"t.choice.value.v\") value: i64, }, } fn main(choice: Choice) -> i64 { match choice { Choice::Value { value: item } => item, Choice::None {} => 0, } }", + ); + compare_scalar_body( + "module t; @id(\"t.choice\") variant Choice { @id(\"t.choice.none\") None, @id(\"t.choice.value\") Value { @id(\"t.choice.value.v\") value: i64, }, } fn main(choice: Choice) -> i64 { match choice { Choice::Value { missing: binding } => missing_arm, _ => true, } }", + ); + compare_scalar_body( + "module t; @id(\"t.choice\") variant Choice { @id(\"t.choice.value\") Value { @id(\"t.choice.value.v\") value: i64, }, } fn main() -> Choice { Choice::Value { missing: missing_rhs, value: true, value: missing_duplicate } }", + ); + compare_scalar_body( + "module t; @id(\"t.pair\") record Pair { @id(\"t.pair.x\") x: i64, } fn main(pair: Pair) -> i64 { match pair { Pair { x } => x, _ => missing_unreachable, } }", + ); + compare_scalar_body( + "module t; @id(\"t.inner\") record Inner { @id(\"t.inner.value\") value: i64, @id(\"t.inner.flag\") flag: bool, } @id(\"t.outer\") record Outer { @id(\"t.outer.inner\") inner: Inner, @id(\"t.outer.other\") other: i64, } fn main(input: Outer) -> i64 { match input { Outer { inner: Inner { value: item, missing: skipped, value: duplicate }, other: item } => missing_arm, } }", + ); + compare_scalar_body( + "module t; @id(\"t.pair\") record Pair { @id(\"t.pair.x\") x: i64, } fn main(pair: Pair) -> i64 { pair.x }", + ); + compare_scalar_body("module t; fn main() -> i64 { missing.field }"); + compare_scalar_body("module t; fn main() -> i64 { missing? }"); + } +} diff --git a/src/target_evidence.rs b/src/target_evidence.rs index f48f486..941b008 100644 --- a/src/target_evidence.rs +++ b/src/target_evidence.rs @@ -136,6 +136,8 @@ fn preview_with_hook( pub(crate) fn build_from_review( build: &review::ReviewBuild, ) -> Result> { + graph::reject_native_rust_imports(build.before_resolved()).map_err(|error| vec![error])?; + graph::reject_native_rust_imports(build.candidate_resolved()).map_err(|error| vec![error])?; let base_graph = bounded_graph(build.before_resolved(), build.base_revision())?; let base_graph_bytes = base_graph.len(); let base_graph_digest = domain_digest(GRAPH_DIGEST_DOMAIN, base_graph.as_bytes()); diff --git a/src/trace_path_certificate.rs b/src/trace_path_certificate.rs index c75b912..855dc25 100644 --- a/src/trace_path_certificate.rs +++ b/src/trace_path_certificate.rs @@ -289,6 +289,7 @@ pub fn build_trace_path_certificate( }, )?); let outcome = match function.return_type { + ResolvedType::Unit => TracePathOutcome::ScalarSuccess, ResolvedType::I64 | ResolvedType::Bool => { TracePathOutcome::ScalarSuccess } diff --git a/src/variant_layout.rs b/src/variant_layout.rs index 488c916..fd9719b 100644 --- a/src/variant_layout.rs +++ b/src/variant_layout.rs @@ -421,6 +421,11 @@ fn collect_expr_variant_types( collect_expr_variant_types(program, argument, instances)?; } } + ResolvedExprKind::NativeRustImportCall(call) => { + for argument in &call.args { + collect_expr_variant_types(program, argument, instances)?; + } + } ResolvedExprKind::Unary { value, .. } | ResolvedExprKind::Project { base: value, .. } => { collect_expr_variant_types(program, value, instances)?; } diff --git a/src/wasm.rs b/src/wasm.rs index 4b4a044..328c5b3 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -122,6 +122,7 @@ impl ByteOutput for crate::bounded_output::CappedVec { } pub fn emit_module(program: &Program) -> Result, Diagnostic> { + reject_native_rust_imports(program)?; let resolved = hir::resolve(program).map_err(|diagnostics| { diagnostics .into_iter() @@ -137,6 +138,17 @@ pub fn emit_module(program: &Program) -> Result, Diagnostic> { /// source first. This entry point exists for semantic consumers that already /// hold HIR and keeps all backend lowering independent of source-level names. pub fn emit_resolved_module(program: &ResolvedProgram) -> Result, Diagnostic> { + if program + .interfaces + .iter() + .flat_map(|interface| &interface.imports) + .any(|import| import.native_rust) + { + return Err(Diagnostic::io( + "SPX-W114", + "Native Rust imports are unavailable for WebAssembly targets", + )); + } hir::validate(program)?; let concrete_variants = VariantLayoutCache::build(program, VariantTarget::Wasm32)?; let has_authored_aggregate = program.types.iter().any(|declaration| { @@ -480,6 +492,7 @@ pub fn emit_resolved_module(program: &ResolvedProgram) -> Result, Diagno } pub fn build_web(program: &Program, output: &Path) -> Result<(), Diagnostic> { + reject_native_rust_imports(program)?; let resolved = hir::resolve(program).map_err(|diagnostics| { diagnostics .into_iter() @@ -548,6 +561,22 @@ pub fn build_web(program: &Program, output: &Path) -> Result<(), Diagnostic> { Ok(()) } +fn reject_native_rust_imports(program: &Program) -> Result<(), Diagnostic> { + if program + .interfaces + .iter() + .flat_map(|interface| &interface.imports) + .any(|import| import.native_rust) + { + Err(Diagnostic::io( + "SPX-W114", + "Native Rust imports are unavailable for WebAssembly targets", + )) + } else { + Ok(()) + } +} + fn collect_locals( expr: &ResolvedExpr, parameter_count: u32, @@ -559,6 +588,11 @@ fn collect_locals( collect_locals(arg, parameter_count, layout)?; } } + ResolvedExprKind::NativeRustImportCall(call) => { + for arg in &call.args { + collect_locals(arg, parameter_count, layout)?; + } + } ResolvedExprKind::Unary { value, .. } => { collect_locals(value, parameter_count, layout)?; } @@ -685,6 +719,12 @@ fn emit_expr( })?, ); } + ResolvedExprKind::NativeRustImportCall(_) => { + return Err(Diagnostic::io( + "SPX-W114", + "Native Rust imports are unavailable for WebAssembly targets", + )); + } ResolvedExprKind::Unary { op, value } => match op { UnaryOp::Neg => { emit_expr( @@ -896,6 +936,10 @@ fn call_import(output: &mut impl ByteOutput, index: u32) { fn wasm_type(ty: &ResolvedType) -> Result { match ty { + ResolvedType::Unit => Err(Diagnostic::io( + "SPX-W101", + "unit is not a WebAssembly value type", + )), ResolvedType::I64 => Ok(I64), ResolvedType::Bool | ResolvedType::Nominal { .. } => Ok(I32), ResolvedType::TypeParameter { .. } => Err(Diagnostic::io( diff --git a/src/wasm/aggregate.rs b/src/wasm/aggregate.rs index a68b357..b2198f2 100644 --- a/src/wasm/aggregate.rs +++ b/src/wasm/aggregate.rs @@ -247,6 +247,11 @@ impl FunctionPlan { self.collect_expr(program, variant_layouts, arg, parameter_count, frame)?; } } + ResolvedExprKind::NativeRustImportCall(call) => { + for arg in &call.args { + self.collect_expr(program, variant_layouts, arg, parameter_count, frame)?; + } + } ResolvedExprKind::Unary { value, .. } => { self.collect_expr(program, variant_layouts, value, parameter_count, frame)?; } @@ -464,6 +469,7 @@ fn expression_has_try(expression: &ResolvedExpr) -> bool { match &expression.kind { ResolvedExprKind::Try { .. } | ResolvedExprKind::TryOption { .. } => true, ResolvedExprKind::Call { args, .. } => args.iter().any(expression_has_try), + ResolvedExprKind::NativeRustImportCall(call) => call.args.iter().any(expression_has_try), ResolvedExprKind::Unary { value, .. } | ResolvedExprKind::Project { base: value, .. } => { expression_has_try(value) } @@ -1268,6 +1274,10 @@ impl Emitter<'_> { args, .. } => self.emit_call(expr, callee, instance.as_ref(), args), + ResolvedExprKind::NativeRustImportCall(_) => Err(Diagnostic::io( + "SPX-W114", + "Native Rust imports are unavailable for WebAssembly targets", + )), ResolvedExprKind::Unary { op, value } => self.emit_unary(expr, *op, value), ResolvedExprKind::Binary { op, left, right } => { self.emit_binary(expr, *op, left, right) diff --git a/src/workspace_graph.rs b/src/workspace_graph.rs index 06925a1..cc30b50 100644 --- a/src/workspace_graph.rs +++ b/src/workspace_graph.rs @@ -2426,6 +2426,17 @@ fn build_owned_inner( for source in &sources { let program = parse(&source.source, Path::new(&source.path)).map_err(|error| vec![error])?; + if program + .interfaces + .iter() + .flat_map(|interface| &interface.imports) + .any(|import| import.native_rust) + { + return Err(vec![graph_error( + "SPX-G218", + "Native Rust import declarations are outside the current semantic Graph schemas", + )]); + } let remaining = active_builder_limit().saturating_sub(canonical_bytes); let (canonical, overflowed) = crate::bounded_output::with_limit(remaining, || format::canonical(&program)); @@ -2583,6 +2594,13 @@ type ResolvedCore = ( Vec, ); +// Native Rust name resolution adds a private declaration-index map, but this +// Graph route rejects Native Rust imports before resolution. Preserve the +// frozen no-native workspace accounting bytes rather than charging an empty, +// backend-private map into existing Graph evidence. +const GRAPH_ACCOUNTED_RESOLVED_PROGRAM_BYTES: usize = std::mem::size_of::() + - std::mem::size_of::>(); + fn build_resolved_core( programs: &[Program], module_paths: &BTreeMap<&str, &str>, @@ -2596,7 +2614,7 @@ fn build_resolved_core( reserve_builder_structure( programs .len() - .checked_mul(std::mem::size_of::<(String, hir::ResolvedProgram)>()) + .checked_mul(std::mem::size_of::() + GRAPH_ACCOUNTED_RESOLVED_PROGRAM_BYTES) .ok_or_else(|| vec![limit_error("builder_bytes", active_builder_limit())])?, )?; let mut synthetic_modules = Vec::with_capacity(programs.len()); @@ -4608,6 +4626,11 @@ fn visit_resolved_calls( visit_resolved_calls(argument, visit); } } + hir::ResolvedExprKind::NativeRustImportCall(call) => { + for argument in &call.args { + visit_resolved_calls(argument, visit); + } + } hir::ResolvedExprKind::Unary { value, .. } => visit_resolved_calls(value, visit), hir::ResolvedExprKind::Binary { left, right, .. } => { visit_resolved_calls(left, visit); @@ -5006,6 +5029,19 @@ fn collect_resolved_expression_type_sites( )?; } } + hir::ResolvedExprKind::NativeRustImportCall(call) => { + for (index, argument) in call.args.iter().enumerate() { + collect_resolved_expression_type_sites( + owner, + argument, + &crate::bounded_output::budgeted_format(format_args!( + "{path}.native_rust_arg.{index}" + )), + imported, + out, + )?; + } + } hir::ResolvedExprKind::Unary { value, .. } => collect_resolved_expression_type_sites( owner, value, diff --git a/tests/component_runtime_ci_contract.rs b/tests/component_runtime_ci_contract.rs index ede7eb3..2737f87 100644 --- a/tests/component_runtime_ci_contract.rs +++ b/tests/component_runtime_ci_contract.rs @@ -41,11 +41,27 @@ fn standalone_runner_is_pinned_private_and_outside_the_root_workspace() { let root_manifest = read("Cargo.toml"); assert!(root_manifest.contains("license = \"Apache-2.0\"")); + let workspace = root_manifest + .split("[workspace]") + .nth(1) + .and_then(|source| source.split("[lib]").next()) + .expect("root workspace section"); + let members = workspace + .lines() + .filter_map(|line| { + let line = line.trim().trim_end_matches(','); + line.strip_prefix('"')?.strip_suffix('"') + }) + .collect::>(); assert_eq!( - root_manifest - .lines() - .find(|line| line.starts_with("members = ")), - Some("members = [\"crates/semaprax-native-host\", \"crates/semaprax-native-loader\"]") + members, + [ + "crates/semaprax-native-host", + "crates/semaprax-native-loader", + "crates/semaprax-native-rust-interop-platform", + "crates/semaprax-native-rust-interop-platform-sys", + "crates/semaprax-native-rust-interop-builder", + ] ); let toolchain = read("platform-tests/component-runtime/rust-toolchain.toml"); diff --git a/tests/economic_agent_v1.rs b/tests/economic_agent_v1.rs new file mode 100644 index 0000000..1fa9ee3 --- /dev/null +++ b/tests/economic_agent_v1.rs @@ -0,0 +1,648 @@ +use std::collections::BTreeMap; +use std::fs; +use std::process::Command; +use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use semaprax::agent_runtime::{ + Agent, AgentBoundaryProbe, AgentCancellation, AgentHost, AgentProviderAttempt, + AgentProviderDisposition, AgentProviderSink, AgentProviderUsage, AgentRun, AgentToolResultSink, +}; +use semaprax::economic_agent::{ + BitcoinPaymentAdapter, EconomicAdapterDisposition, EconomicAgent, EconomicAgentHost, + EconomicBoundaryProbe, EconomicBytesSink, EconomicDocumentSink, EconomicJournalLoad, + EconomicRail, EconomicRollingReservationUpdate, EconomicRunStatus, EvmPaymentAdapter, + PaymentApprover, PaymentJournal, SolanaPaymentAdapter, WalletCustody, X402InvoiceAdapter, +}; +use serde_json::json; +use sha2::{Digest, Sha256}; + +const EVIDENCE_DOMAIN: &[u8] = b"semaprax.economic-agent.evidence-digest.v1\0"; + +fn digest(domain: &[u8], bytes: &[u8]) -> String { + let mut digest = Sha256::new(); + digest.update(domain); + digest.update(bytes); + format!("sha256:{:x}", digest.finalize()) +} + +fn nonclaims() -> &'static str { + r#"["no_model_output_payment_authority","no_model_self_approval_or_policy_expansion","no_seed_private_key_credential_or_signing_material_input","no_secret_prompt_trace_evidence_log_or_diagnostic_exposure","no_builtin_network_http_dns_custody_or_chain_authority","no_mainnet_authority","no_wildcard_network_asset_recipient_origin_or_resource","no_token_contract_program_script_swap_bridge_or_unlimited_approval","no_raw_signing_or_signed_transaction_export","no_exactly_once_signing_broadcast_or_payment","no_automatic_uncertain_broadcast_retry","no_guaranteed_confirmation_finality_or_reorg_freedom","no_compromised_wallet_approver_adapter_provider_or_chain_recovery","no_power_loss_durability_without_host_journal_contract","no_cross_process_or_distributed_concurrency_guarantee","no_live_price_exchange_rate_fee_or_cost_accuracy","no_balance_allowance_or_simulation_truth_beyond_adapter","no_human_identity_intent_approval_provenance_or_nonrepudiation","no_signature_attestation_or_custody_provenance","no_tax_accounting_legal_regulatory_sanctions_or_compliance_correctness","no_privacy_data_residency_or_unlinkability_guarantee","no_x402_redirect_ssrf_private_network_or_server_honesty_guarantee_beyond_admitted_adapter_contract","no_automatic_refund_chargeback_replacement_or_fee_bumping","no_wallet_recovery_rotation_backup_or_inheritance","no_general_payment_sdk_or_production_readiness","no_language_graph_cleanup_backend_or_workspace_atomicity_semantics","no_current_agent_runtime_schema_api_or_kat_modification","no_completion_matrix_status_promotion"]"# +} + +fn limits() -> &'static str { + r#"{"max_policy_bytes":1048576,"max_intent_bytes":1048576,"max_invoice_bytes":1048576,"max_snapshot_bytes":1048576,"max_plan_bytes":1048576,"max_simulation_bytes":1048576,"max_approval_request_bytes":1048576,"max_approval_bytes":65536,"max_journal_bytes":8388608,"max_unsigned_transaction_bytes":1048576,"max_signed_transaction_bytes":2097152,"max_broadcast_receipt_bytes":1048576,"max_reconciliation_bytes":1048576,"max_trace_events":1024,"max_trace_bytes":8388608,"max_evidence_bytes":16777216,"max_builder_bytes":67108864,"max_json_depth":16,"max_identifier_bytes":128,"max_memo_bytes":1024,"max_recipients":128,"max_network_policies":16,"max_x402_origins":32,"max_utxos":100,"max_reconciliations":64,"max_elapsed_ms":600000,"max_amount_atomic":1000000000000000000,"max_fee_atomic":1000000000000000,"max_compute_units":200000,"max_confirmation_target":144,"max_concurrency":1,"max_unexpected_authority_calls":0}"# +} + +fn policy(recipient: &str, rail: &str, network: &str, asset: &str, x402: bool) -> String { + let origins = if x402 { + r#"[{"origin":"https://pay.example.com","methods":["POST"],"resources":["/v1/payments"],"settlement_rails":["evm"],"max_amount_atomic":1000000}]"# + } else { + "[]" + }; + format!( + "{{\"schema\":\"semaprax.economic-agent-policy.v1\",\"economic_agent_id\":\"fixture.economic\",\"wallet_id\":\"fixture.wallet\",\"network_policies\":[{{\"rail\":\"{rail}\",\"network\":\"{network}\",\"asset\":\"{asset}\",\"recipients\":[\"{recipient}\"],\"max_amount_atomic\":1000000,\"max_fee_atomic\":1000000,\"max_rolling_24h_atomic\":1000000}}],\"x402_origins\":{origins},\"limits\":{},\"nonclaims\":{}}}\n", + limits(), + nonclaims() + ) +} + +fn intent(rail: &str, payment: &str, key: &str) -> String { + format!( + "{{\"schema\":\"semaprax.economic-agent-payment-intent.v1\",\"intent_id\":\"fixture.intent.{rail}\",\"wallet_id\":\"fixture.wallet\",\"rail\":\"{rail}\",\"idempotency_key\":\"{key}\",\"created_at_ms\":1700000000000,\"expires_at_ms\":1700000300000,\"memo\":null,\"payment\":{payment}}}\n" + ) +} + +fn base58(bytes: &[u8]) -> String { + const ALPHABET: &[u8] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + let zeros = bytes.iter().take_while(|byte| **byte == 0).count(); + let mut digits = Vec::new(); + for byte in bytes.iter().skip(zeros) { + let mut carry = u32::from(*byte); + for digit in &mut digits { + let value = u32::from(*digit) * 256 + carry; + *digit = (value % 58) as u8; + carry = value / 58; + } + while carry != 0 { + digits.push((carry % 58) as u8); + carry /= 58; + } + } + std::iter::repeat_n('1', zeros) + .chain( + digits + .iter() + .rev() + .map(|digit| ALPHABET[usize::from(*digit)] as char), + ) + .collect() +} + +fn convert_bits(data: &[u8], from: u32, to: u32) -> Vec { + let (mut acc, mut bits) = (0u32, 0u32); + let mut output = Vec::new(); + for value in data { + acc = (acc << from) | u32::from(*value); + bits += from; + while bits >= to { + bits -= to; + output.push(((acc >> bits) & ((1 << to) - 1)) as u8); + } + } + if bits != 0 { + output.push(((acc << (to - bits)) & ((1 << to) - 1)) as u8); + } + output +} + +fn regtest(program: [u8; 20]) -> String { + const CHARSET: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + let mut data = vec![0]; + data.extend(convert_bits(&program, 8, 5)); + let mut values = vec![3, 3, 3, 3, 0, 2, 3, 18, 20]; + values.extend_from_slice(&data); + values.extend([0; 6]); + let mut polymod = 1u32; + for value in values { + let top = polymod >> 25; + polymod = ((polymod & 0x01ff_ffff) << 5) ^ u32::from(value); + for (index, generator) in [ + 0x3b6a_57b2, + 0x2650_8e6d, + 0x1ea1_19fa, + 0x3d42_33dd, + 0x2a14_62b3, + ] + .iter() + .enumerate() + { + if ((top >> index) & 1) != 0 { + polymod ^= generator; + } + } + } + polymod ^= 1; + let mut encoded = String::from("bcrt1"); + for value in data + .into_iter() + .chain((0..6).map(|index| ((polymod >> (5 * (5 - index))) & 31) as u8)) + { + encoded.push(CHARSET[usize::from(value)] as char); + } + encoded +} + +fn runtime_nonclaims() -> &'static str { + r#"["no_compiler_determinism_from_model_output","no_model_output_authority","no_provider_identity_provenance_or_quality_truth","no_secret_input_or_secret_leakage_guarantee_for_caller_supplied_content","no_credential_prompt_state_trace_or_diagnostic_exposure","no_ambient_network_filesystem_process_home_or_environment_authority","no_write_apply_mutation_or_target_execution_tool_authority","no_capability_minting_delegation_or_self_approval","no_human_approval_ui_or_policy","no_semantic_prompt_injection_proof","no_forced_cancellation_or_preemption","no_exactly_once_provider_billing_or_retry","no_durable_memory_persistence_recovery_or_resume","no_crash_reboot_or_power_loss_durability","no_distributed_or_parallel_execution","no_model_quality_accuracy_or_completion_guarantee","no_live_price_or_cost_accuracy_guarantee","no_reusable_authorization_token","no_signature_attestation_or_authenticated_provenance","no_wallet_payment_signing_asset_or_economic_authority","no_privacy_compliance_or_data_residency_guarantee","no_general_formal_proof","no_new_language_graph_cleanup_backend_or_runtime_semantics","no_current_schema_api_or_kat_modification"]"# +} + +fn runtime_profile() -> String { + format!( + "{{\"schema\":\"semaprax.agent-runtime-profile.v1\",\"agent_id\":\"fixture.agent\",\"models\":[{{\"provider_id\":\"fake.local\",\"model_id\":\"fake-basic\",\"locality\":\"local\",\"quality_tier\":\"basic\",\"tokenizer_id\":\"fake.bytes-v1\",\"max_context_tokens\":4096,\"input_usd_microunits_per_million_tokens\":0,\"output_usd_microunits_per_million_tokens\":0,\"capabilities\":[\"text\"]}}],\"tools\":[],\"policy\":{{\"allowed_provider_ids\":[\"fake.local\"],\"allowed_model_ids\":[\"fake-basic\"],\"required_locality\":\"local_only\",\"minimum_quality_tier\":\"basic\",\"required_model_capabilities\":[\"text\"],\"granted_capabilities\":[],\"allowed_tool_ids\":[]}},\"limits\":{{\"max_turns\":1,\"max_provider_attempts\":1,\"max_retries_per_turn\":0,\"max_concurrency\":1,\"max_elapsed_ms\":1000,\"max_provider_request_bytes\":65536,\"max_provider_response_bytes\":4096,\"max_stream_chunks\":4,\"max_total_provider_input_bytes\":131072,\"max_total_provider_output_bytes\":8192,\"max_reported_model_input_tokens\":131072,\"max_reported_model_output_tokens\":8192,\"max_usd_microunits\":0,\"max_tool_calls\":0,\"max_tool_arguments_bytes\":4096,\"max_tool_result_bytes\":4096,\"max_total_tool_bytes\":8192,\"max_retained_state_bytes\":16777216,\"max_trace_events\":64,\"max_trace_bytes\":16777216,\"max_evidence_bytes\":20971520,\"max_builder_bytes\":67108864}},\"nonclaims\":{}}}\n", + runtime_nonclaims() + ) +} + +#[derive(Clone)] +struct RuntimeProbe; +impl AgentBoundaryProbe for RuntimeProbe { + fn policy_epoch(&self) -> u64 { + 1 + } + fn elapsed_ms(&self) -> u64 { + 0 + } +} + +struct RuntimeHost { + message: String, +} +impl AgentHost for RuntimeHost { + fn policy_epoch(&self) -> u64 { + 1 + } + fn elapsed_ms(&self) -> u64 { + 0 + } + fn boundary_probe(&self) -> Box { + Box::new(RuntimeProbe) + } + fn tokenize(&mut self, _: &str, request: &str) -> Option { + Some(request.len() as u64) + } + fn attempt_provider( + &mut self, + _: &str, + _: &str, + request: &str, + _: u64, + sink: &mut AgentProviderSink, + ) -> AgentProviderAttempt { + let response = format!("{{\"schema\":\"semaprax.agent-runtime-action.v1\",\"kind\":\"final\",\"message\":{}}}\n", serde_json::to_string(&self.message).unwrap()); + assert!(sink.push(response.as_bytes())); + AgentProviderAttempt::new( + AgentProviderDisposition::Succeeded, + AgentProviderUsage::new(request.len() as u64, response.len() as u64, 0), + ) + } + fn invoke_tool(&mut self, _: &str, _: &str, _: &str, _: &mut AgentToolResultSink) -> bool { + panic!("no tool authority") + } +} + +fn sealed(message: String) -> AgentRun { + let task = format!("{{\"schema\":\"semaprax.agent-runtime-task.v1\",\"nonce\":\"{}\",\"objective\":\"Return the supplied canonical intent.\",\"context\":[]}}\n", "0".repeat(64)); + Agent::new( + &runtime_profile(), + RuntimeHost { message }, + AgentCancellation::new(), + ) + .unwrap() + .run(&task) + .unwrap() +} + +#[derive(Clone)] +struct Probe(Arc); +impl EconomicBoundaryProbe for Probe { + fn elapsed_ms(&self) -> u64 { + self.0.load(Ordering::Acquire) + } +} + +struct Host { + calls: Arc, + sequence: Arc>>, + elapsed: Arc, + expected_rail: EconomicRail, + expected_key: String, + journals: BTreeMap, +} + +impl Host { + fn new( + rail: EconomicRail, + key: &str, + ) -> (Self, Arc, Arc>>) { + let calls = Arc::new(AtomicUsize::new(0)); + let sequence = Arc::new(Mutex::new(Vec::new())); + ( + Self { + calls: calls.clone(), + sequence: sequence.clone(), + elapsed: Arc::new(AtomicU64::new(0)), + expected_rail: rail, + expected_key: key.into(), + journals: BTreeMap::new(), + }, + calls, + sequence, + ) + } + fn record(&self, name: &'static str) { + self.calls.fetch_add(1, Ordering::AcqRel); + self.sequence.lock().unwrap().push(name); + } + fn fail(&self, name: &'static str) -> EconomicAdapterDisposition { + self.record(name); + EconomicAdapterDisposition::DefinitelyNotStarted + } +} + +impl EconomicAgentHost for Host { + fn boundary_probe(&self) -> Box { + Box::new(Probe(self.elapsed.clone())) + } +} +impl PaymentJournal for Host { + fn load(&mut self, key: &str, sink: &mut EconomicDocumentSink) -> EconomicJournalLoad { + assert_eq!(key, self.expected_key); + self.record("load"); + match self.journals.get(key) { + Some(journal) => { + assert!(sink.push(journal.as_bytes())); + EconomicJournalLoad::Present + } + None => EconomicJournalLoad::Missing, + } + } + fn compare_and_swap( + &mut self, + key: &str, + _: u64, + journal: &str, + update: EconomicRollingReservationUpdate<'_>, + ) -> EconomicAdapterDisposition { + if let EconomicRollingReservationUpdate::Reserve(row) = update { + assert_eq!(row.rail(), self.expected_rail); + assert_eq!(row.wallet_id(), "fixture.wallet"); + } + self.record("cas"); + self.journals.insert(key.to_owned(), journal.to_owned()); + EconomicAdapterDisposition::Succeeded + } +} +impl X402InvoiceAdapter for Host { + fn fetch_invoice( + &mut self, + _: &str, + _: &str, + _: &str, + _: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition { + self.fail("invoice") + } +} +macro_rules! fail_rail { + ($trait:ident,$snap:ident,$sim:ident,$broadcast:ident,$reconcile:ident) => { + impl $trait for Host { + fn $snap( + &mut self, + _: &str, + _: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition { + self.fail(stringify!($snap)) + } + fn $sim( + &mut self, + _: &str, + _: &[u8], + _: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition { + self.fail(stringify!($sim)) + } + fn $broadcast( + &mut self, + _: &[u8], + _: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition { + self.fail(stringify!($broadcast)) + } + fn $reconcile( + &mut self, + _: &str, + _: &mut EconomicDocumentSink, + ) -> EconomicAdapterDisposition { + self.fail(stringify!($reconcile)) + } + } + }; +} +fail_rail!( + EvmPaymentAdapter, + evm_snapshot, + evm_simulate, + evm_broadcast, + evm_reconcile +); +fail_rail!( + SolanaPaymentAdapter, + solana_snapshot, + solana_simulate, + solana_broadcast, + solana_reconcile +); +fail_rail!( + BitcoinPaymentAdapter, + bitcoin_snapshot, + bitcoin_simulate, + bitcoin_broadcast, + bitcoin_reconcile +); +impl PaymentApprover for Host { + fn approve(&mut self, _: &str, _: &mut EconomicDocumentSink) -> EconomicAdapterDisposition { + self.fail("approve") + } +} +impl WalletCustody for Host { + fn sign( + &mut self, + _: &str, + _: EconomicRail, + _: &str, + _: &[u8], + _: &str, + _: &mut EconomicBytesSink, + ) -> EconomicAdapterDisposition { + self.fail("sign") + } +} + +#[test] +fn public_all_rails_and_x402_dispatch_are_replayable_and_authority_injected() { + let sol = base58(&[3; 32]); + let btc = regtest([9; 20]); + let rows = [ + ( + EconomicRail::Evm, + "evm", + "sepolia", + "native:eth", + "0x1111111111111111111111111111111111111111", + r#"{"kind":"evm","network":"sepolia","asset":"native:eth","recipient":"0x1111111111111111111111111111111111111111","amount_atomic":10,"max_fee_atomic":100000}"#, + "fixture.payment.evm", + false, + ), + ( + EconomicRail::Solana, + "solana", + "devnet", + "native:sol", + &sol, + &format!( + r#"{{"kind":"solana","network":"devnet","asset":"native:sol","recipient":"{sol}","amount_atomic":10,"max_fee_atomic":6000,"max_compute_units":200000,"max_priority_fee_atomic":1000}}"# + ), + "fixture.payment.solana", + false, + ), + ( + EconomicRail::Bitcoin, + "bitcoin", + "regtest", + "native:btc", + &btc, + &format!( + r#"{{"kind":"bitcoin","network":"regtest","asset":"native:btc","recipient":"{btc}","amount_atomic":10000,"max_fee_atomic":10000,"confirmation_target":1}}"# + ), + "fixture.payment.bitcoin", + false, + ), + ]; + for (rail, text, network, asset, recipient, payment, key, x402) in rows { + let source = sealed(intent(text, payment, key)); + let (host, calls, sequence) = Host::new(rail, key); + let mut agent = EconomicAgent::new( + &policy(recipient, text, network, asset, x402), + host, + AgentCancellation::new(), + ) + .unwrap(); + let run = agent.execute(&source).unwrap(); + assert_eq!(run.status(), EconomicRunStatus::AdapterFailed); + assert_eq!( + run.evidence_digest(), + digest(EVIDENCE_DOMAIN, run.evidence().as_bytes()) + ); + assert!(run.trace().ends_with('\n') && run.evidence().ends_with('\n')); + assert!(calls.load(Ordering::Acquire) >= 2); + assert_eq!( + &*sequence.lock().unwrap(), + &[ + "load", + "cas", + match rail { + EconomicRail::Evm => "evm_snapshot", + EconomicRail::Solana => "solana_snapshot", + EconomicRail::Bitcoin => "bitcoin_snapshot", + }, + "cas" + ] + ); + } + + let invoice_digest = digest( + b"semaprax.economic-agent.x402-invoice-digest.v1\0", + b"unavailable\n", + ); + let x402_payment = format!( + r#"{{"kind":"x402","origin":"https://pay.example.com","method":"POST","resource":"/v1/payments","invoice_digest":"{invoice_digest}","payee":"0x1111111111111111111111111111111111111111","settlement_rail":"evm","network":"sepolia","asset":"native:eth","amount_atomic":10,"max_fee_atomic":100000,"invoice_expires_at_ms":1700000299999,"invoice_nonce":"fixture.nonce"}}"# + ); + let key = "fixture.payment.x402"; + let source = sealed(intent("x402", &x402_payment, key)); + let (host, _, sequence) = Host::new(EconomicRail::Evm, key); + let mut agent = EconomicAgent::new( + &policy( + "0x1111111111111111111111111111111111111111", + "evm", + "sepolia", + "native:eth", + true, + ), + host, + AgentCancellation::new(), + ) + .unwrap(); + assert_eq!( + agent.execute(&source).unwrap().status(), + EconomicRunStatus::AdapterFailed + ); + assert_eq!( + &*sequence.lock().unwrap(), + &["load", "cas", "invoice", "cas"] + ); + + let evm_payment = r#"{"kind":"evm","network":"sepolia","asset":"native:eth","recipient":"0x1111111111111111111111111111111111111111","amount_atomic":10,"max_fee_atomic":100000}"#; + let key = "fixture.payment.reconcile"; + let source = sealed(intent("evm", evm_payment, key)); + let (host, _, sequence) = Host::new(EconomicRail::Evm, key); + let mut agent = EconomicAgent::new( + &policy( + "0x1111111111111111111111111111111111111111", + "evm", + "sepolia", + "native:eth", + false, + ), + host, + AgentCancellation::new(), + ) + .unwrap(); + assert_eq!( + agent.execute(&source).unwrap().status(), + EconomicRunStatus::AdapterFailed + ); + sequence.lock().unwrap().clear(); + assert_eq!( + agent.reconcile(key, &source).unwrap().status(), + EconomicRunStatus::AdapterFailed + ); + let calls = sequence.lock().unwrap().clone(); + assert_eq!(calls, ["load"]); + assert!(!calls.contains(&"sign") && !calls.contains(&"evm_broadcast")); + let substituted = sealed(intent( + "evm", + &evm_payment.replace("\"amount_atomic\":10", "\"amount_atomic\":11"), + key, + )); + sequence.lock().unwrap().clear(); + let error = match agent.reconcile(key, &substituted) { + Ok(_) => panic!("substituted AgentRun admitted"), + Err(error) => error, + }; + assert_eq!(error[0].code, "SPX-G215"); + assert_eq!(&*sequence.lock().unwrap(), &["load"]); +} + +#[test] +fn public_pre_effect_cancellation_caps_and_source_binding_fail_closed() { + let payment = r#"{"kind":"evm","network":"sepolia","asset":"native:eth","recipient":"0x1111111111111111111111111111111111111111","amount_atomic":10,"max_fee_atomic":100000}"#; + let message = intent("evm", payment, "fixture.payment.evm"); + let source = sealed(message); + let cancellation = AgentCancellation::new(); + cancellation.cancel(); + let (host, calls, _) = Host::new(EconomicRail::Evm, "fixture.payment.evm"); + let mut agent = EconomicAgent::new( + &policy( + "0x1111111111111111111111111111111111111111", + "evm", + "sepolia", + "native:eth", + false, + ), + host, + cancellation, + ) + .unwrap(); + let error = match agent.execute(&source) { + Ok(_) => panic!("cancelled execution produced evidence"), + Err(error) => error, + }; + assert_eq!(error[0].code, "SPX-I228"); + assert_eq!(calls.load(Ordering::Acquire), 0); + + let (host, calls, _) = Host::new(EconomicRail::Evm, "fixture.payment.evm"); + let error = EconomicAgent::new("{}\n", host, AgentCancellation::new()) + .err() + .expect("malformed policy admitted"); + assert_eq!(error[0].code, "SPX-G210"); + assert_eq!(calls.load(Ordering::Acquire), 0); +} + +#[test] +fn public_surface_traits_and_closed_status_domains_are_exhaustive() { + let dispositions = [ + EconomicAdapterDisposition::Succeeded, + EconomicAdapterDisposition::DefinitelyNotStarted, + EconomicAdapterDisposition::FailedUncertain, + EconomicAdapterDisposition::PolicyRejected, + ]; + assert_eq!(dispositions.len(), 4); + let loads = [ + EconomicJournalLoad::Missing, + EconomicJournalLoad::Present, + EconomicJournalLoad::DefinitelyNotStarted, + EconomicJournalLoad::FailedUncertain, + ]; + assert_eq!(loads.len(), 4); + let rails = [ + EconomicRail::Evm, + EconomicRail::Solana, + EconomicRail::Bitcoin, + ]; + assert_eq!(rails.len(), 3); + let statuses = [ + EconomicRunStatus::Confirmed, + EconomicRunStatus::Pending, + EconomicRunStatus::Reorged, + EconomicRunStatus::Dropped, + EconomicRunStatus::Rejected, + EconomicRunStatus::Cancelled, + EconomicRunStatus::DeadlineExceeded, + EconomicRunStatus::BudgetExhausted, + EconomicRunStatus::JournalFailed, + EconomicRunStatus::AdapterFailed, + EconomicRunStatus::ApprovalFailed, + EconomicRunStatus::CustodyFailed, + EconomicRunStatus::BroadcastUnknown, + EconomicRunStatus::ReconciliationFailed, + ]; + assert_eq!(statuses.len(), 14); + assert_eq!(json!({"surface":"opaque"})["surface"], "opaque"); +} + +#[test] +fn external_consumer_surface_is_opaque_and_has_no_cli_or_ambient_authority() { + let root = std::env::temp_dir().join(format!( + "semaprax-economic-surface-{}-{}", + std::process::id(), + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + fs::create_dir_all(root.join("src")).unwrap(); + let manifest_root = env!("CARGO_MANIFEST_DIR").replace('\\', "\\\\"); + fs::write(root.join("Cargo.toml"), format!("[package]\nname=\"economic-surface-lock\"\nversion=\"0.0.0\"\nedition=\"2021\"\n[workspace]\n[dependencies]\nsemaprax={{path=\"{manifest_root}\",default-features=false}}\n")).unwrap(); + fs::write(root.join("src/main.rs"), r#"use semaprax::economic_agent::{EconomicAgent,EconomicRun,EconomicDocumentSink,EconomicBytesSink,EconomicRollingReservation,parse_policy,replay_bundle,Policy,Intent}; +fn clone() {} fn debug() {} +fn reject() { clone::>(); debug::>(); } +fn main() { clone::(); debug::(); let _=EconomicDocumentSink::new(); let _=EconomicBytesSink::new(); let _=EconomicRollingReservation{wallet_id:String::new()}; let _=parse_policy; let _=replay_bundle; let _=std::mem::size_of::(); let _=std::mem::size_of::(); } +"#).unwrap(); + let checked = Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into())) + .args(["check", "--offline", "--manifest-path"]) + .arg(root.join("Cargo.toml")) + .env("CARGO_TARGET_DIR", root.join("target")) + .output() + .unwrap(); + assert!(!checked.status.success()); + let stderr = String::from_utf8_lossy(&checked.stderr); + for name in ["parse_policy", "replay_bundle", "Clone", "Debug", "private"] { + assert!(stderr.contains(name), "missing `{name}` in:\n{stderr}"); + } + let cli = Command::new(env!("CARGO_BIN_EXE_semaprax")) + .output() + .unwrap(); + let cli_text = format!( + "{}{}", + String::from_utf8_lossy(&cli.stdout), + String::from_utf8_lossy(&cli.stderr) + ); + assert!(!cli_text.contains("economic-agent")); + let source = include_str!("../src/economic_agent.rs"); + for forbidden in [ + "std::net::TcpStream", + "std::net::UdpSocket", + "reqwest::", + "std::fs::", + "std::process::Command", + "std::env::var(", + ] { + assert!( + !source[..source.find("#[cfg(test)]").unwrap()].contains(forbidden), + "ambient authority `{forbidden}`" + ); + } + fs::remove_dir_all(root).unwrap(); +} diff --git a/tests/fixtures/native_rust_hir_capacity.spx b/tests/fixtures/native_rust_hir_capacity.spx new file mode 100644 index 0000000..6894810 --- /dev/null +++ b/tests/fixtures/native_rust_hir_capacity.spx @@ -0,0 +1,217 @@ +module test.native_rust_hir_capacity; + +permit { host.echo } + +@id("token.type") +resource Token { + @id("token.drop") + drop trivial; +} + +@id("pair.type") +record Pair { + @id("pair.first") + first: Token, + @id("pair.second") + second: Token, +} + +@id("inner.type") +record Inner { + @id("inner.left") + left: Token, + @id("inner.right") + right: Token, +} + +@id("outer.type") +record Outer { + @id("outer.inner") + inner: Inner, + @id("outer.tail") + tail: Token, +} + +@id("choice.type") +variant Choice { + @id("choice.a") + A { @id("choice.a.value") value: i64, }, + @id("choice.b") + B { @id("choice.b.value") value: i64, }, + @id("choice.empty") + Empty, +} + +@id("host.echo.interface") +interface HostEcho permits { host.echo } { + @id("host.echo") + import rust fn host_echo(value: i64) -> i64 + effects { host.echo } + failure status "host.echo.v1"; +} + +@id("token.identity") +fn identity(value: own Token) -> Token { value } + +@id("token.consume") +fn consume(value: own Token) -> i64 { 1 } + +@id("token.take-two") +fn take_two(first: own Token, second: own Token) -> i64 { 2 } + +@id("pair.construct") +fn construct_pair(first: own Token, second: own Token) -> Pair { + Pair { second: second, first: identity(first) } +} + +@id("pair.update") +fn update_pair(pair: own Pair, first: own Token, second: own Token) -> Pair { + pair with { second: identity(second), first: first } +} + +@id("outer.construct") +fn construct_outer(left: own Token, right: own Token, tail: own Token) -> Outer { + Outer { + inner: Inner { right: right, left: left }, + tail: identity(tail), + } +} + +@id("token.call") +fn call_owned(first: own Token, second: own Token) -> i64 { + take_two(identity(first), second) +} + +@id("choice.choose") +fn choose_choice(choice: Choice) -> i64 { + match choice { + Choice::A { value } => value, + Choice::B { value } => value + 1, + Choice::Empty {} => 0, + } +} + +@id("choice.nested") +fn nested_choice( + choice: Choice, + first: own Token, + second: own Token, + third: own Token, + fourth: own Token, + fifth: own Token, + sixth: own Token, + seventh: own Token, + eighth: own Token +) -> i64 { + let value = match choice { + Choice::A { value: outer_a } => match choice { + Choice::A { value: middle_aa } => match choice { + Choice::A { value: leaf_aaa } => leaf_aaa, + Choice::B { value: leaf_aab } => leaf_aab + 1, + Choice::Empty {} => 0, + }, + Choice::B { value: middle_ab } => match choice { + Choice::A { value: leaf_aba } => leaf_aba, + Choice::B { value: leaf_abb } => leaf_abb + 1, + Choice::Empty {} => 0, + }, + Choice::Empty {} => outer_a, + }, + Choice::B { value: outer_b } => match choice { + Choice::A { value: middle_ba } => match choice { + Choice::A { value: leaf_baa } => leaf_baa, + Choice::B { value: leaf_bab } => leaf_bab + 1, + Choice::Empty {} => 0, + }, + Choice::B { value: middle_bb } => match choice { + Choice::A { value: leaf_bba } => leaf_bba, + Choice::B { value: leaf_bbb } => leaf_bbb + 1, + Choice::Empty {} => 0, + }, + Choice::Empty {} => outer_b, + }, + Choice::Empty {} => 0, + }; + value +} + +@id("scalar.wide") +fn wide_scalar(a: i64, b: i64, c: i64, d: i64, e: i64, f: i64, g: i64, h: i64) -> i64 { + a + b + c + d + e + f + g + h +} + +@id("scalar.nested-wide") +fn nested_wide(value: i64) -> i64 { + wide_scalar( + wide_scalar( + wide_scalar( + wide_scalar(value, 2, 3, 4, 5, 6, 7, 8), + 2, 3, 4, 5, 6, 7, 8 + ), + 2, 3, 4, 5, 6, 7, 8 + ), + 2, 3, 4, 5, 6, 7, 8 + ) +} + +@id("generic.identity") +fn generic_identity(value: T) -> T { value } + +@id("generic.calls") +fn generic_calls(value: i64, flag: bool) -> i64 { + let a = generic_identity(value); + let b = generic_identity(flag); + let c = generic_identity(a + 1); + let d = generic_identity(b && true); + let e = generic_identity(c + 1); + let f = generic_identity(d || false); + if f { e } else { 0 } +} + +@id("flow.branch") +fn branch_owned(condition: bool, first: own Token, second: own Token) -> i64 { + if condition { consume(first) } else { consume(second) } +} + +@id("flow.many") +fn many_bindings( + first: own Token, + second: own Token, + third: own Token, + fourth: own Token, + fifth: own Token, + sixth: own Token, + seventh: own Token, + eighth: own Token +) -> i64 { + let a = identity(first); + let b = identity(second); + let c = identity(third); + let d = identity(fourth); + let e = identity(fifth); + let f = identity(sixth); + let g = identity(seventh); + let h = identity(eighth); + consume(a) + consume(b) + consume(c) + consume(d) + + consume(e) + consume(f) + consume(g) + consume(h) +} + +@id("option.use") +fn option_use(value: Option) -> Option { + let checked = value?; + Option::Some { value: checked > 0 } +} + +@id("result.use") +fn result_use(value: Result) -> Result { + let checked = value?; + Result::Ok { value: checked > 0 } +} + +@id("host.use") +fn host_use(value: i64) -> i64 uses { host.echo } { + host_echo(value) +} + +@id("app.main") +fn main() -> i64 { 0 } diff --git a/tests/formatter_iterative.rs b/tests/formatter_iterative.rs new file mode 100644 index 0000000..554ef86 --- /dev/null +++ b/tests/formatter_iterative.rs @@ -0,0 +1,64 @@ +use std::path::Path; + +use semaprax::{format, parse}; + +const SOURCE: &str = r#" +module test.formatter_separators; + +@id("test.empty") +fn empty(x: i64) -> i64 { match x {} } + +@id("test.one") +fn one(x: i64) -> i64 { match x { _ => 1, } } + +@id("test.many") +fn many(x: i64) -> i64 { match x { _ => 1, _ => 2, } } + +@id("test.lets") +fn lets() -> i64 { + let nested = { let first = 1; let second = 2; first + second }; + nested +} +"#; + +const CANONICAL: &str = r#"module test.formatter_separators; + +@id("test.empty") +fn empty(x: i64) -> i64 +{ + match x { } +} + +@id("test.one") +fn one(x: i64) -> i64 +{ + match x { _ => 1, } +} + +@id("test.many") +fn many(x: i64) -> i64 +{ + match x { _ => 1, _ => 2, } +} + +@id("test.lets") +fn lets() -> i64 +{ + let nested = { let first = 1; let second = 2; first + second }; + nested +} +"#; + +#[test] +fn iterative_formatter_preserves_exact_match_and_block_separator_bytes() { + let program = parse(SOURCE, Path::new("formatter-separators.spx")).unwrap(); + let canonical = format::canonical(&program); + assert_eq!(canonical, CANONICAL); + assert!(canonical.contains("match x { }")); + assert!(canonical.contains("match x { _ => 1, }")); + assert!(canonical.contains("match x { _ => 1, _ => 2, }")); + assert!(canonical.contains("{ let first = 1; let second = 2; first + second }")); + + let reparsed = parse(&canonical, Path::new("formatter-separators-canonical.spx")).unwrap(); + assert_eq!(format::canonical(&reparsed), CANONICAL); +} diff --git a/tests/native_rust_interop_ci_contract.rs b/tests/native_rust_interop_ci_contract.rs new file mode 100644 index 0000000..927d4e7 --- /dev/null +++ b/tests/native_rust_interop_ci_contract.rs @@ -0,0 +1,546 @@ +use std::fs; +use std::path::Path; +use std::process::Command; + +fn read(relative: &str) -> String { + fs::read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join(relative)) + .unwrap_or_else(|error| panic!("read {relative}: {error}")) +} + +fn assert_contains_all(label: &str, source: &str, required: &[&str]) { + for value in required { + assert!(source.contains(value), "{label} is missing `{value}`"); + } +} + +#[test] +fn private_native_rust_interop_crates_are_unpublished_and_quarantined() { + let root_manifest = read("Cargo.toml"); + assert_contains_all( + "workspace membership", + &root_manifest, + &[ + "crates/semaprax-native-rust-interop-builder", + "crates/semaprax-native-rust-interop-platform-sys", + "crates/semaprax-native-rust-interop-platform", + "default-members = [\".\"]", + ], + ); + + for manifest in [ + "crates/semaprax-native-rust-interop-builder/Cargo.toml", + "crates/semaprax-native-rust-interop-platform/Cargo.toml", + "crates/semaprax-native-rust-interop-platform-sys/Cargo.toml", + ] { + let source = read(manifest); + assert!( + source.contains("publish = false"), + "{manifest} became publishable" + ); + for forbidden in ["libloading", "dlopen", "dlsym", "LoadLibrary"] { + assert!( + !source.contains(forbidden), + "{manifest} admitted dynamic loading through `{forbidden}`" + ); + } + } + + let root_lib = read("src/lib.rs"); + let root_main = read("src/main.rs"); + assert!(!root_lib.contains("native_rust_interop")); + assert!(!root_main.contains("native-rust-interop")); + + let builder = read("crates/semaprax-native-rust-interop-builder/src/lib.rs"); + let platform = read("crates/semaprax-native-rust-interop-platform/src/lib.rs"); + let sys = read("crates/semaprax-native-rust-interop-platform-sys/src/lib.rs"); + assert!(builder.contains("#![forbid(unsafe_code)]")); + assert!(platform.contains("#![forbid(unsafe_code)]")); + assert!(sys.contains("#![deny(unsafe_op_in_unsafe_fn)]")); + for forbidden in [ + "LoadLibraryA(", + "LoadLibraryW(", + "GetProcAddress(", + "FreeLibrary(", + "dlopen(", + "dlsym(", + "dlclose(", + ] { + assert!( + !platform.contains(forbidden) && !sys.contains(forbidden), + "private platform source admitted dynamic loading through `{forbidden}`" + ); + } +} + +#[test] +fn private_native_rust_interop_nonclaims_are_the_frozen_ordered_set() { + let implementation = read("crates/semaprax-native-rust-interop-builder/src/implementation.rs"); + let nonclaims = implementation + .split("const NONCLAIMS: &[&str] = &[") + .nth(1) + .and_then(|tail| tail.split("];\n").next()) + .expect("private nonclaim constant"); + let actual = nonclaims + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .map(|line| { + line.strip_prefix('"') + .and_then(|line| line.strip_suffix("\",")) + .expect("canonical nonclaim literal") + }) + .collect::>(); + assert_eq!( + actual, + [ + "no_resource_owned_borrow_shared_or_aggregate_abi", + "no_pointer_reference_slice_string_trait_object_or_generic_abi", + "no_cross_boundary_allocator_or_deallocator", + "no_wasm_component_or_canonical_abi_detour", + "no_dynamic_loading_symbol_lookup_unload_or_hot_reload", + "no_public_execution_or_spx_b104_change_in_private_ab", + "no_callable_v2_v3_proof_bundle_or_loader_wire_change", + "no_graph_schema_api_kat_or_semantic_projection_change", + "no_agent_runtime_economic_workspace_or_patch_wire_change", + "no_untrusted_native_code_sandbox_or_memory_safety", + "no_same_uid_process_signal_or_task_port_isolation", + "no_same_uid_active_filesystem_mutation_or_namespace_race_isolation", + "no_same_user_process_handle_or_thread_resume_isolation", + "no_abi_compatibility_outside_exact_descriptor_target_toolchain", + "no_cross_target_cross_toolchain_or_cross_build_bundle_reuse", + "no_panic_or_unwind_across_ffi", + "no_abort_oom_stack_overflow_signal_seh_or_process_crash_recovery", + "no_power_loss_durability_or_crash_atomicity", + "no_async_reentrant_parallel_cross_thread_or_send_sync_bridge", + "no_host_capability_provenance_or_os_authority", + "no_ambient_effect_capability_or_callback_discovery", + "no_host_error_text_panic_payload_secret_or_pointer_evidence", + "no_exactly_once_external_effect", + "no_exception_cpp_rust_unwind_translation", + "no_dynamic_library_code_signing_supply_chain_or_linker_provenance", + "no_dynamic_dependency_identity_or_filesystem_race_isolation", + "no_c_cpp_objective_c_swift_kotlin_jni_or_other_ecosystem_binding", + "no_stable_rust_abi_claim_beyond_generated_c_abi_wrapper", + "no_public_cli_package_registry_or_build_script_network", + "no_general_interop_or_production_readiness", + "no_completion_matrix_status_promotion", + ] + ); +} + +#[test] +fn public_semaprax_package_excludes_private_interop_crate_sources() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let output = Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into())) + .current_dir(root) + .args([ + "package", + "--locked", + "--allow-dirty", + "-p", + "semaprax", + "--list", + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "cargo package --list failed without exposing output bytes" + ); + let inventory = String::from_utf8(output.stdout).unwrap(); + assert!(!inventory.contains("crates/semaprax-native-rust-interop")); + assert!(!inventory.contains("src/native_rust_interop.rs")); +} + +#[test] +fn private_builder_uses_held_platform_authority_for_every_physical_step() { + let implementation = read("crates/semaprax-native-rust-interop-builder/src/implementation.rs"); + assert_contains_all( + "named private replay and hostile evidence", + &implementation, + &[ + "fn private_a_is_canonical_and_pure()", + "fn source_descriptor_and_generated_views_reconstruct_from_authenticated_facts()", + "fn descriptor_and_generated_source_replay_reject_every_bound_family()", + "fn exact_replayers_reject_every_generated_and_descriptor_byte_substitution()", + "fn six_output_artifact_known_answer_vectors_are_frozen()", + "fn cumulative_builder_limit_is_exact_and_cannot_be_widened()", + "fn private_b_builds_exact_static_inventory_without_clobber()", + "fn build_race_hooks_reject_each_pre_effect_mutation_and_preserve_foreign_bytes()", + "fn linked_bridge_round_trips_rust_to_semaprax_to_rust_and_closes_failures()", + "fn bool_and_infallible_import_abi_is_exact_at_o0_and_o2()", + ], + ); + let physical_tail = implementation + .split("fn platform_publication_error") + .nth(1) + .expect("physical production implementation"); + let physical = physical_tail + .split("#[cfg(test)]\nmod tests") + .next() + .expect("active production implementation prefix"); + + assert_contains_all( + "held platform authority", + &implementation, + &[ + "platform::create_directory_new_prepared", + "platform::write_file_new_prepared", + "platform::compile_c_tool_prepared", + "platform::compile_rust_tool_prepared", + "platform::link_tool_prepared", + "platform::execute_tool_prepared", + "platform::compare_exact", + "platform::inventory_exact_prepared", + "platform::publish_directory_new_prepared", + "platform::discard_owned_stage_prepared", + ], + ); + for forbidden in [ + "std::process::Command", + "std::fs::hard_link", + "std::fs::rename", + "std::fs::OpenOptions", + ] { + assert!( + !physical.contains(forbidden), + "private B bypassed held platform authority through `{forbidden}`" + ); + } + assert!( + physical.contains("for (optimization, invocation) in [(0_u8, c_o0), (2_u8, c_o2)]"), + "private B does not execute both O0 and O2 evidence builds" + ); + assert_contains_all( + "complete O0/O2 static-link and runtime evidence", + physical, + &[ + "module_O0.o", + "module_O2.o", + "__semaprax_native_rust_link_O0", + "__semaprax_native_rust_link_O2", + "platform::link_tool_prepared", + "platform::execute_tool_prepared", + ], + ); + assert_eq!( + implementation + .matches("fn discard_run_stage(") + .count(), + 1, + "private B must define one narrow exact-inventory settlement helper" + ); + assert_contains_all( + "unconditional one-attempt run-stage settlement", + &implementation, + &[ + "let mut run_files = prepare_run_discard_inventory()?;", + "let build = (|| {", + "let cleanup = discard_run_stage(&parent_authority, &run_stage, &run_files);", + "let mut facts = match (build, cleanup)", + "discard_run_stage(&parent_authority, &stage, &publish_files)", + "if publication.is_err()", + ], + ); + + let sys = read("crates/semaprax-native-rust-interop-platform-sys/src/lib.rs"); + assert!(sys.contains("\"-O0\"")); + assert!(sys.contains("\"-O2\"")); + let windows = sys + .split("#[cfg(windows)]") + .nth(1) + .expect("Windows quarantine implementation"); + assert!( + !windows.contains("unsupported!("), + "Windows physical authority is still an unsupported stub" + ); + let windows_run = windows + .split("fn run_argv(") + .nth(1) + .and_then(|tail| tail.split("pub fn rustc_version(").next()) + .expect("bounded Windows run_argv implementation"); + for forbidden in [ + "_authority_markers", + "_authority_types", + "Command::new", + ".spawn()", + ] { + assert!( + !windows_run.contains(forbidden), + "Windows run authority is still a marker or std::process baseline: `{forbidden}`" + ); + } + let ordered_calls = [ + "InitializeProcThreadAttributeList(", + "UpdateProcThreadAttribute(", + "CreateJobObjectW(", + "SetInformationJobObject(", + "CreateProcessW(", + "QueryFullProcessImageNameW(", + "AssignProcessToJobObject(", + "ResumeThread(", + ]; + let mut prior = None; + for call in ordered_calls { + let offset = windows_run + .find(call) + .unwrap_or_else(|| panic!("Windows run authority does not call `{call}`")); + if let Some(previous) = prior { + assert!( + offset > previous, + "Windows suspended-process authority calls `{call}` out of order" + ); + } + prior = Some(offset); + } + assert_contains_all( + "Windows contained error settlement", + windows_run, + &[ + "PROC_THREAD_ATTRIBUTE_HANDLE_LIST", + "CREATE_SUSPENDED", + "EXTENDED_STARTUPINFO_PRESENT", + "TerminateJobObject(", + "WaitForSingleObject(", + "must_terminate_unassigned(process_handle.raw());", + "must_settle_job(job.raw(), process_handle.raw(), true);", + "DeleteProcThreadAttributeList(", + ], + ); + assert!( + !windows_run.contains("let _ = settle_job(") + && !windows_run.contains("let _ = terminate_unassigned("), + "Windows run authority ignores failed process-settlement proof" + ); + assert_contains_all( + "fail-stop process settlement", + &sys, + &[ + "fn must_settle_failed_group(", + "if settle_failed_group(pid, pipe, leader_reaped).is_err()", + "fn must_terminate_unassigned(", + "if terminate_unassigned(process).is_err()", + "fn must_settle_job(", + "if settle_job(job, process, terminate).is_err()", + "std::process::abort();", + ], + ); + assert_contains_all( + "Windows handle settlement RAII", + windows, + &[ + "struct CheckedHandle(Option);", + "impl Drop for CheckedHandle", + "if unsafe { CloseHandle(handle) } == 0", + ], + ); + let ambient_settlement_variable = + ["SEMAPRAX_NATIVE_RUST", "_INTEROP_TEST_SETTLEMENT_FAILURE"].concat(); + assert!( + !sys.contains(&ambient_settlement_variable), + "production sys retains ambient settlement-failure authority" + ); + assert_contains_all( + "cfg(test)-local process settlement evidence", + &sys, + &[ + "#[cfg(test)]\nstatic TEST_SETTLEMENT_FAILURES", + "linux_runner_boundaries_settle_or_fail_stop_without_later_action", + "helper_linux_parent_write_close", + "helper_linux_waitpid", + "windows_runner_failures_use_only_explicit_test_state", + "execute_harness_with_argument", + "helper_windows_query_job_fail_stop", + "later action ran after fail-stop", + "later action ran after destroy uncertainty", + ], + ); + let windows_filesystem = windows + .split("fn open_directory(") + .nth(1) + .and_then(|tail| tail.split("fn run_argv(").next()) + .expect("bounded Windows filesystem authority implementation"); + for forbidden in [ + "_authority_markers", + "_authority_types", + "std::fs::OpenOptions", + "std::fs::create_dir", + "std::fs::hard_link", + "std::fs::read_dir", + "std::fs::rename", + "std::fs::remove_file", + "std::fs::remove_dir", + ".path.join(", + ] { + assert!( + !windows_filesystem.contains(forbidden), + "Windows filesystem authority still uses a stored-path or marker baseline: `{forbidden}`" + ); + } + assert_contains_all( + "Windows root-handle-relative filesystem authority", + windows_filesystem, + &[ + "NtCreateFile(", + "NtSetInformationFile(", + "OBJECT_ATTRIBUTES", + "RootDirectory", + "FILE_OPEN_REPARSE_POINT", + "FileIdBothDirectoryInfo", + "SetFileInformationByHandle(", + "FileLinkInformationEx", + "FileRenameInfoEx", + "FileDispositionInfoEx", + ], + ); + assert_contains_all( + "Windows 128-bit held identity", + windows, + &[ + "GetFileInformationByHandleEx(", + "FileIdInfo", + "file_id: [u8; 16]", + ], + ); + assert!( + windows_filesystem.matches("relative_file(").count() == 4, + "Windows Nt RootDirectory helper definition/caller topology drifted" + ); + assert!( + windows_filesystem.matches("NtSetInformationFile(").count() == 1 + && windows_filesystem + .matches("SetFileInformationByHandle(") + .count() + == 2 + && windows_filesystem.matches("disposition_delete(").count() >= 3, + "Windows link, publish, and delete callers are not all held-handle operations" + ); + let link_helper = windows_filesystem + .split("pub fn link_or_copy_new_prepared") + .nth(1) + .and_then(|tail| tail.split("pub fn inventory_exact_prepared").next()) + .expect("bounded Windows prepared NT link operation"); + assert_contains_all( + "Windows held-handle link authority", + link_helper, + &["NtSetInformationFile(", "FileLinkInformationEx"], + ); + assert!( + !link_helper.contains("SetFileInformationByHandle("), + "Windows native FileLinkInformationEx was sent through the Win32 information API" + ); + let windows_evidence = + read("crates/semaprax-native-rust-interop-platform/tests/windows_authority.rs"); + assert_contains_all( + "Windows physical authority evidence", + &windows_evidence, + &[ + "windows_junctions_and_same_path_directory_substitution_are_rejected", + "windows_create_inventory_publish_and_exact_discard_are_no_clobber", + "windows_discard_stops_on_inventory_and_stage_identity_drift", + "windows_held_executable_uses_held_identity_and_empty_environment", + "windows_run_argv_handles_zero_and_small_stdout_at_normal_eof", + "windows_names_are_exact_ascii_non_dos_and_casefold_no_clobber", + "windows_descendant_held_stdout_is_quiesced_without_output_overflow", + "windows_silent_timeout_is_bounded_and_reaps_the_leader", + "windows_output_overflow_kills_and_reaps_the_process_tree_with_a_bounded_wait", + "windows_external_consumer_cannot_extract_handles_or_reach_sys_quarantine", + ], + ); +} + +#[test] +fn private_platform_cleanup_surface_is_exact_inventory_only() { + let facade = read("crates/semaprax-native-rust-interop-platform/src/lib.rs"); + let sys = read("crates/semaprax-native-rust-interop-platform-sys/src/lib.rs"); + assert_contains_all( + "safe exact-inventory cleanup facade", + &facade, + &[ + "pub fn discard_owned_stage_prepared(", + "parent: &HeldDirectory", + "stage: &HeldDirectory", + "stage_name: &PreparedStageName", + "inventory: &PreparedDiscardInventory", + ], + ); + assert_contains_all( + "system exact-inventory cleanup quarantine", + &sys, + &[ + "pub fn discard_owned_stage_prepared(", + "parent: &Directory", + "stage: &Directory", + "stage_name: &PreparedRelativeNameArena", + "names: &PreparedDiscardNames", + "files: &[Option<&RegularFile>; N]", + ], + ); + let production_sys = sys + .split("#[cfg(test)]\nmod tests") + .next() + .expect("production sys prefix"); + for (label, source) in [ + ("safe facade", facade.as_str()), + ("system quarantine", production_sys), + ] { + for forbidden in [ + "remove_dir_all", + "pub fn remove", + "pub fn delete", + "pub fn discard_path", + "pub fn discard_directory", + ] { + assert!( + !source.contains(forbidden), + "{label} exposed generic or recursive deletion through `{forbidden}`" + ); + } + } +} + +#[test] +fn hosted_workflow_names_all_private_interop_evidence_boundaries() { + let workflow = read(".github/workflows/ci.yml"); + for required in [ + "Require private Native Rust Interop language, HIR, Graph, and Wasm preservation evidence", + "cargo test --locked -p semaprax --test native_rust_interop_v1 -- --nocapture", + "cargo test --locked -p semaprax --test native_rust_interop_ci_contract -- --nocapture", + "Require private Native Rust Interop A+B replay, static-link, runtime, and hostile evidence", + "cargo test --locked -p semaprax-native-rust-interop -- --nocapture", + "Require private Native Rust Interop platform authority evidence", + "cargo test --locked -p semaprax-native-rust-interop-platform --all-targets -- --nocapture", + "Require private Native Rust Interop ASan + UBSan round trip (Linux)", + "SEMAPRAX_REQUIRE_NATIVE_RUST_INTEROP_SANITIZERS: \"1\"", + "implementation::tests::linked_bridge_round_trips_rust_to_semaprax_to_rust_and_closes_failures -- --exact --nocapture", + ] { + assert!(workflow.contains(required), "workflow is missing `{required}`"); + } + assert_eq!( + workflow + .matches("cargo test --locked -p semaprax-native-rust-interop -- --nocapture") + .count(), + 1 + ); + let sanitizer_step = workflow + .split("- name: Require private Native Rust Interop ASan + UBSan round trip (Linux)") + .nth(1) + .and_then(|tail| tail.split(" - ").next()) + .expect("private sanitizer workflow step"); + assert!( + !sanitizer_step.contains("CLANG:"), + "sanitizer workflow must not bypass authenticated tool discovery with a bare CLANG path" + ); + + let implementation = read("crates/semaprax-native-rust-interop-builder/src/implementation.rs"); + for required in [ + "SEMAPRAX_REQUIRE_NATIVE_RUST_INTEROP_SANITIZERS", + "-fsanitize=address,undefined", + "-fno-sanitize-recover=all", + ] { + assert!( + implementation.contains(required), + "sanitizer gate is not enforced by production/test build code: `{required}`" + ); + } +} diff --git a/tests/native_rust_interop_v1.rs b/tests/native_rust_interop_v1.rs new file mode 100644 index 0000000..1a892d5 --- /dev/null +++ b/tests/native_rust_interop_v1.rs @@ -0,0 +1,256 @@ +use std::path::Path; + +use semaprax::format; +use semaprax::hir::{ + self, DeclarationId, ResolvedExprKind, ResolvedImportFailure, ResolvedImportResultKind, + ResolvedType, +}; +use semaprax::{graph, parse, wasm}; + +const SOURCE: &str = r#"module test.native_rust; + +@id("rust.host") +interface RustHost + permits { } +{ + @id("rust.host.combine") + import rust fn combine(left: i64, selected: bool) -> i64 + effects { } + failure status "rust.test"; + @id("rust.host.invert") + import rust fn invert(value: bool) -> bool + effects { } + failure infallible; + @id("rust.host.ping") + import rust fn ping(value: i64) -> unit + effects { } + failure status "1x"; +} + +@id("test.call_combine") +fn call_combine(value: i64, selected: bool) -> i64 +{ + combine(value, selected) +} + +@id("test.call_invert") +fn call_invert(value: bool) -> bool +{ + invert(value) +} + +@id("test.ping_once") +fn ping_once(value: i64) -> i64 +{ + let acknowledged = ping(value); + 1 +} + +@id("test.main") +fn main() -> i64 +{ + call_combine(41, true) +} +"#; + +#[test] +fn native_rust_import_syntax_format_and_hir_are_exact_and_deterministic() { + let parsed = parse(SOURCE, Path::new("native-rust.spx")).unwrap(); + assert_eq!(format::canonical(&parsed), SOURCE); + let first = hir::resolve(&parsed).unwrap(); + let second = hir::resolve(&parse(SOURCE, Path::new("other.spx")).unwrap()).unwrap(); + assert_eq!(first, second); + assert_eq!(first.interfaces.len(), 1); + let imports = &first.interfaces[0].imports; + assert_eq!(imports.len(), 3); + assert!(imports.iter().all(|import| import.native_rust)); + assert_eq!(imports[0].id.as_str(), "rust.host.combine"); + assert_eq!(imports[0].parameters.len(), 2); + assert_eq!(imports[0].parameters[0].ty, ResolvedType::I64); + assert_eq!(imports[0].parameters[1].ty, ResolvedType::Bool); + assert_eq!(imports[0].result.kind, ResolvedImportResultKind::I64); + assert_eq!( + imports[0].failure, + ResolvedImportFailure::Status { + domain_id: "rust.test".to_owned(), + normalization: "semaprax.status.v1", + } + ); + assert_eq!(imports[1].result.kind, ResolvedImportResultKind::Bool); + assert_eq!(imports[1].failure, ResolvedImportFailure::Infallible); + assert_eq!(imports[2].result.kind, ResolvedImportResultKind::Unit); + + let wrapper = first + .functions + .iter() + .find(|function| function.name == "call_combine") + .unwrap(); + let ResolvedExprKind::Block { tail, .. } = &wrapper.body.kind else { + panic!("wrapper body") + }; + let ResolvedExprKind::NativeRustImportCall(call) = &tail.kind else { + panic!("native Rust call") + }; + assert_eq!(call.import.as_str(), "rust.host.combine"); + assert_eq!(call.args.len(), 2); + assert_eq!(call.result, ResolvedImportResultKind::I64); + assert_eq!(tail.ty, ResolvedType::I64); + assert_eq!(call.expression, tail.id); + hir::validate(&first).unwrap(); + + let endpoint = SOURCE.replace("failure status \"1x\"", "failure status \"x1\""); + hir::resolve(&parse(&endpoint, Path::new("endpoint.spx")).unwrap()).unwrap(); +} + +#[test] +fn native_rust_import_declaration_set_is_closed_with_exact_diagnostics() { + let cases = [ + (SOURCE.replace(" @id(\"rust.host.combine\")\n", ""), "SPX-B107", "explicit persistent ID required"), + (SOURCE.replace("left: i64", "left: borrow i64"), "SPX-B107", "scalar value signature required"), + (SOURCE.replace("left: i64, selected: bool", "a:i64,b:i64,c:i64,d:i64,e:i64,f:i64,g:i64,h:i64,i:i64"), "SPX-B107", "scalar value signature required"), + (SOURCE.replace("failure status \"rust.test\"", "failure status \"Rust/Test\""), "SPX-B107", "status domain is invalid"), + (SOURCE.replace("import rust fn invert", "import rust fn combine"), "SPX-B107", "symbol collision"), + (SOURCE.replace("permits { }", "permits { allowed.effect }").replace("effects { }\n failure status \"rust.test\"", "effects { outside.effect }\n failure status \"rust.test\""), "SPX-B107", "effect or capability mismatch"), + (SOURCE.replace("effects { }\n failure status \"rust.test\"", "effects { repeated.effect, repeated.effect }\n failure status \"rust.test\""), "SPX-B107", "effect or capability mismatch"), + (SOURCE.replace("fn call_combine", "fn combine"), "SPX-B107", "symbol collision"), + ]; + for (index, (source, code, message)) in cases.into_iter().enumerate() { + let program = parse(&source, Path::new(&format!("hostile-{index}.spx"))).unwrap(); + let diagnostics = hir::resolve(&program).unwrap_err(); + assert_eq!(diagnostics.len(), 1, "case {index}: {diagnostics:?}"); + assert_eq!(diagnostics[0].code, code, "case {index}"); + assert!( + diagnostics[0].message.contains(message), + "case {index}: {}", + diagnostics[0].message + ); + } + + let omitted_failure = SOURCE.replace("\n failure infallible", ""); + let diagnostic = parse(&omitted_failure, Path::new("omitted-failure.spx")).unwrap_err(); + assert_eq!(diagnostic.code, "SPX-P106"); + assert_eq!(diagnostic.message, "expected keyword `failure`"); + + let ordinary_scalar = SOURCE.replace("import rust fn combine", "import fn combine"); + let diagnostic = parse(&ordinary_scalar, Path::new("ordinary-scalar.spx")).unwrap_err(); + assert_eq!(diagnostic.code, "SPX-P106"); + assert_eq!(diagnostic.message, "expected admitted import result type"); + + let forged_unit_value = SOURCE.replace( + " let acknowledged = ping(value);\n 1", + " ping(value) + 1", + ); + let diagnostics = + hir::resolve(&parse(&forged_unit_value, Path::new("forged-unit-value.spx")).unwrap()) + .unwrap_err(); + assert_eq!(diagnostics.len(), 1, "{diagnostics:?}"); + assert_eq!(diagnostics[0].code, "SPX-B107"); + assert_eq!( + diagnostics[0].message, + "Native Rust Interop declaration set is unsupported: scalar value signature required" + ); +} + +#[test] +fn native_rust_imports_are_explicitly_excluded_from_graph_and_wasm_without_fallback() { + let program = parse(SOURCE, Path::new("native-rust.spx")).unwrap(); + let graph_error = graph::to_json(&program).unwrap_err(); + assert_eq!(graph_error.len(), 1); + assert_eq!(graph_error[0].code, "SPX-G218"); + assert_eq!( + graph_error[0].message, + "Native Rust import declarations are outside the current semantic Graph schemas" + ); + let wasm_error = wasm::emit_module(&program).unwrap_err(); + assert_eq!(wasm_error.code, "SPX-W114"); + assert_eq!( + wasm_error.message, + "Native Rust imports are unavailable for WebAssembly targets" + ); + + let context_error = graph::context_json(&program, "test.call_combine", 1).unwrap_err(); + assert_eq!(context_error.len(), 1); + assert_eq!(context_error[0].code, "SPX-G218"); + assert_eq!( + context_error[0].message, + "Native Rust import declarations are outside the current semantic Graph schemas" + ); + let resolved = hir::resolve(&program).unwrap(); + let resolved_wasm_error = wasm::emit_resolved_module(&resolved).unwrap_err(); + assert_eq!(resolved_wasm_error.code, "SPX-W114"); + assert_eq!( + resolved_wasm_error.message, + "Native Rust imports are unavailable for WebAssembly targets" + ); +} + +#[test] +fn forged_native_rust_call_hir_is_rejected_by_target_result_and_effect() { + fn native_call_mut( + resolved: &mut hir::ResolvedProgram, + ) -> &mut hir::ResolvedNativeRustImportCall { + let wrapper = resolved + .functions + .iter_mut() + .find(|function| function.name == "call_combine") + .unwrap(); + let ResolvedExprKind::Block { tail, .. } = &mut wrapper.body.kind else { + panic!("wrapper body") + }; + let ResolvedExprKind::NativeRustImportCall(call) = &mut tail.kind else { + panic!("native Rust call") + }; + call + } + + let program = parse(SOURCE, Path::new("native-rust.spx")).unwrap(); + let resolved = hir::resolve(&program).unwrap(); + + let mut unknown_target = resolved.clone(); + native_call_mut(&mut unknown_target).import = DeclarationId::new("forged.target".to_owned()); + let diagnostic = hir::validate(&unknown_target).unwrap_err(); + assert_eq!(diagnostic.code, "SPX-H006"); + assert_eq!( + diagnostic.message, + "native Rust import call has an unknown target" + ); + + let mut wrong_result = resolved.clone(); + native_call_mut(&mut wrong_result).result = ResolvedImportResultKind::Bool; + let diagnostic = hir::validate(&wrong_result).unwrap_err(); + assert_eq!(diagnostic.code, "SPX-H006"); + assert_eq!( + diagnostic.message, + "native Rust import call disagrees with its declaration" + ); + + let mut undeclared_effect = resolved; + undeclared_effect.permits.push("forged.effect".to_owned()); + undeclared_effect.interfaces[0] + .permits + .push("forged.effect".to_owned()); + undeclared_effect.interfaces[0].imports[0] + .effects + .push("forged.effect".to_owned()); + undeclared_effect.interfaces[0].imports[0] + .required_authority + .push("forged.effect".to_owned()); + let diagnostic = hir::validate(&undeclared_effect).unwrap_err(); + assert_eq!(diagnostic.code, "SPX-H006"); + assert_eq!( + diagnostic.message, + "native Rust import call requires an undeclared effect" + ); + + let mut forged_failure = hir::resolve(&program).unwrap(); + forged_failure.interfaces[0].imports[0].failure = ResolvedImportFailure::Status { + domain_id: "rust.test".to_owned(), + normalization: "forged.status", + }; + let diagnostic = hir::validate(&forged_failure).unwrap_err(); + assert_eq!(diagnostic.code, "SPX-H006"); + assert_eq!( + diagnostic.message, + "import `rust.host.combine` has an invalid status contract" + ); +} diff --git a/tests/semantic_impact_v1.rs b/tests/semantic_impact_v1.rs index 3cd148a..4c5a850 100644 --- a/tests/semantic_impact_v1.rs +++ b/tests/semantic_impact_v1.rs @@ -61,6 +61,10 @@ fn first_call<'a>(expression: &'a ResolvedExpr, template: &str) -> Option<&'a Re ResolvedExprKind::Call { args, .. } => args .iter() .find_map(|argument| first_call(argument, template)), + ResolvedExprKind::NativeRustImportCall(call) => call + .args + .iter() + .find_map(|argument| first_call(argument, template)), ResolvedExprKind::Unary { value, .. } | ResolvedExprKind::Project { base: value, .. } | ResolvedExprKind::Try { operand: value, .. } diff --git a/tests/semantic_patch_v2.rs b/tests/semantic_patch_v2.rs index 58c99c7..ec23179 100644 --- a/tests/semantic_patch_v2.rs +++ b/tests/semantic_patch_v2.rs @@ -39,6 +39,10 @@ fn first_call<'a>(expression: &'a ResolvedExpr, template: &str) -> Option<&'a Re args.iter() .find_map(|argument| first_call(argument, template)) } + ResolvedExprKind::NativeRustImportCall(call) => call + .args + .iter() + .find_map(|argument| first_call(argument, template)), ResolvedExprKind::Unary { value, .. } | ResolvedExprKind::Try { operand: value, .. } | ResolvedExprKind::TryOption { operand: value, .. } => first_call(value, template), diff --git a/tests/semantic_review_v1.rs b/tests/semantic_review_v1.rs index 911440a..866497a 100644 --- a/tests/semantic_review_v1.rs +++ b/tests/semantic_review_v1.rs @@ -62,6 +62,10 @@ fn first_call<'a>(expression: &'a ResolvedExpr, template: &str) -> Option<&'a Re ResolvedExprKind::Call { args, .. } => args .iter() .find_map(|argument| first_call(argument, template)), + ResolvedExprKind::NativeRustImportCall(call) => call + .args + .iter() + .find_map(|argument| first_call(argument, template)), ResolvedExprKind::Unary { value, .. } | ResolvedExprKind::Project { base: value, .. } | ResolvedExprKind::Try { operand: value, .. } diff --git a/tests/verifier_parity.rs b/tests/verifier_parity.rs index 034f9b6..8d3bafd 100644 --- a/tests/verifier_parity.rs +++ b/tests/verifier_parity.rs @@ -70,3 +70,145 @@ fn main() -> i64 { 42 } r#"{"code":"SPX-S103","severity":"warning","message":"function `main` has an automatic identity that changes when renamed","path":"fixtures/verifier-warnings.spx","location":{"line":3,"column":4,"start":35,"end":39},"help":"add @id(\"your.namespace.symbol\") before the declaration"}"# ); } + +fn ordered_errors(program: &semaprax::ast::Program) -> Vec { + verify::verify(program) + .into_iter() + .filter(|diagnostic| diagnostic.severity.is_error()) + .map(|diagnostic| format!("{}: {}", diagnostic.code, diagnostic.message)) + .collect() +} + +#[test] +fn iterative_recovery_preserves_child_parent_and_fallback_order() { + let source = r#" +module test.verifier_recovery; + +@id("test.zero") +fn zero() -> i64 { 0 } + +@id("test.recover") +fn recover() -> i64 { + let call = zero(missing_a, missing_b); + let unary = -missing_unary; + let projected = missing_base.field; + missing_tail +} + +@id("test.fallback") +fn fallback(value: Result) -> i64 { + let unwrapped = value?; + missing_after_fallback +} + +@id("test.branch") +fn branch() -> i64 { + if 1 { missing_then } else { missing_else } +} + +@id("app.main") +fn main() -> i64 { 0 } +"#; + let program = parse(source, Path::new("fixtures/verifier-recovery.spx")).unwrap(); + let expected = vec![ + "SPX-T204: `zero` expects 0 arguments, received 2", + "SPX-T202: unknown value `missing_a` in `recover`", + "SPX-T202: unknown value `missing_b` in `recover`", + "SPX-T202: unknown value `missing_unary` in `recover`", + "SPX-T202: unknown value `missing_base` in `recover`", + "SPX-T202: unknown value `missing_tail` in `recover`", + "SPX-T218: function `fallback` must return the ordinary compiler-owned Result to propagate a Result with `?`", + "SPX-T202: unknown value `missing_after_fallback` in `fallback`", + "SPX-T210: `if` condition must be bool", + "SPX-T202: unknown value `missing_then` in `branch`", + "SPX-T202: unknown value `missing_else` in `branch`", + ]; + + assert_eq!(ordered_errors(&program), expected); + let analysis = hir::analyze(&program); + assert!(analysis.resolved.is_none()); + assert_eq!( + analysis + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity.is_error()) + .map(|diagnostic| format!("{}: {}", diagnostic.code, diagnostic.message)) + .collect::>(), + expected + ); +} + +#[test] +fn iterative_ownership_mutations_commit_only_at_the_frozen_scope_boundaries() { + let source = r#" +module test.verifier_ownership_recovery; + +@id("buffer.type") +resource Buffer { + @id("buffer.type.drop") + drop trivial; +} + +@id("buffer.inspect") +fn inspect(buffer: borrow Buffer) -> i64 { 1 } + +@id("buffer.consume") +fn consume(buffer: own Buffer) -> i64 { 1 } + +@id("test.zero") +fn zero() -> i64 { 0 } + +@id("test.commit_after_none") +fn commit_after_none(buffer: own Buffer) -> i64 { + let consumed = consume(buffer) + missing_rhs; + inspect(buffer) +} + +@id("test.unmatched_argument") +fn unmatched_argument(buffer: own Buffer) -> i64 { + let ignored = zero(buffer); + inspect(buffer) +} + +@id("test.rejected_shadow") +fn rejected_shadow(buffer: own Buffer) -> i64 { + let buffer = buffer; + inspect(buffer) +} + +@id("test.branch_join") +fn branch_join(flag: bool, buffer: own Buffer) -> i64 { + let maybe = if flag { consume(buffer) } else { missing_else }; + inspect(buffer) +} + +@id("app.main") +fn main() -> i64 { 0 } +"#; + let program = parse( + source, + Path::new("fixtures/verifier-ownership-recovery.spx"), + ) + .unwrap(); + let expected = vec![ + "SPX-T202: unknown value `missing_rhs` in `commit_after_none`", + "SPX-O101: use of resource `buffer` after ownership was moved", + "SPX-T204: `zero` expects 0 arguments, received 1", + "SPX-T209: local binding `buffer` shadows an existing value", + "SPX-T202: unknown value `missing_else` in `branch_join`", + "SPX-O107: resource `buffer` may have been moved on another control-flow path", + ]; + + assert_eq!(ordered_errors(&program), expected); + let analysis = hir::analyze(&program); + assert!(analysis.resolved.is_none()); + assert_eq!( + analysis + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity.is_error()) + .map(|diagnostic| format!("{}: {}", diagnostic.code, diagnostic.message)) + .collect::>(), + expected + ); +}