diff --git a/.github/workflows/required-wasmtime.yml b/.github/workflows/required-wasmtime.yml index fc08e67d..1b297f14 100644 --- a/.github/workflows/required-wasmtime.yml +++ b/.github/workflows/required-wasmtime.yml @@ -2,6 +2,7 @@ name: Required Wasmtime execution on: push: + branches: [master] pull_request: jobs: @@ -11,6 +12,24 @@ jobs: PSRS_REQUIRE_WASMTIME: "1" steps: - uses: actions/checkout@v4 + - name: Read the locked standard-library revision + id: stdlib-lock + run: | + revision=$(python3 -c 'import json; print(json.load(open("stdlib.lock.json"))["revision"])') + echo "revision=$revision" >> "$GITHUB_OUTPUT" + - name: Checkout the locked standard library + uses: actions/checkout@v4 + with: + repository: biuld/psrs-stdlib + ref: ${{ steps.stdlib-lock.outputs.revision }} + path: psrs-stdlib-checkout + ssh-key: ${{ secrets.PSRS_STDLIB_DEPLOY_KEY }} + - name: Place the package where the lock expects it + run: | + dest="$GITHUB_WORKSPACE/../psrs-stdlib" + rm -rf "$dest" + mv "$GITHUB_WORKSPACE/psrs-stdlib-checkout" "$dest" + test -f "$dest/manifest.json" - uses: dtolnay/rust-toolchain@stable - name: Install pinned Wasmtime run: | diff --git a/.gitignore b/.gitignore index 1fc17ab8..7653088b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,20 @@ .DS_Store node_modules/ dist/ + +# Raw diagnosis snapshots are local, machine-specific and regenerable. +diagnose.json + +# Generated stdlib diagnosis, oracle and runtime reports stay local. +/docs/implementation/stdlib/**/diagnosis.json +/docs/implementation/stdlib/**/next-blocker.json +/docs/implementation/stdlib/**/run.json +/docs/implementation/stdlib/**/validation.json +/docs/implementation/stdlib/**/observations.json +/docs/implementation/stdlib/**/*-run.json +/docs/implementation/stdlib/**/diagnosis-checkpoints.json +/docs/implementation/stdlib/**/import-summary.json +/docs/implementation/stdlib/**/summary.json + +__pycache__/ +*.egg-info/ diff --git a/AGENTS.md b/AGENTS.md index 1a398249..cbe8b726 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,8 +19,45 @@ uncommitted work. - For a new user-facing feature, maintain the relevant feature and design documents under `docs/`. Add a decision record only for a major, durable decision. Keep documentation proportional to the change. +- For compiler fixes, follow the [compiler iteration SOP](docs/workflow/compiler-iteration-sop.md) + to capture a comparable baseline, locate the owning stage contract, and verify + the change at the right layer. - Review the local diff and run the validation relevant to the files changed. +### Standard-library package + +- Keep official library sources and case data in the independent `psrs-stdlib` + repository. The compiler consumes `stdlib.lock.json`; use `PSRS_STDLIB_ROOT` + only for an explicit development package. +- Follow [the stdlib source-fidelity contract](docs/workflow/stdlib-vendoring.md) + when importing or changing official library sources. +- Pin upstream package versions and commits. Preserve official pure functions, + signatures, exports, classes, instances, and modules. Repair compiler defects + in the compiler rather than editing valid official source to compile. +- Official vendored `.purs` files retain their original length, including files + above 500 lines. The 500-line limit still applies to maintained compiler and + tooling source; do not split or rewrite official library modules to meet it. +- Differences require a concrete Wasm/WASI or DEC-16 representation reason, + an explicit implementation boundary, and focused behavior evidence. A compiler + limitation, reduced API, or convenient rewrite is not a target justification. +- Never replace an unimplemented foreign value with recursion, a fabricated + result, or another successful-looking placeholder. Keep its source contract + and report missing target support explicitly. +- Compile acceptance, source fidelity, and runtime correctness are separate + claims. Importing every module with an unused `main` does not prove that the + APIs execute, that every declaration survives backend lowering, or that FFI + behavior agrees with its contract. + +- Prefer small checked runtime/storage primitives and ordinary target library + wrappers. Keep stdlib algorithms, traversal order, and callbacks in + `psrs-stdlib`; missing JS FFI alone does not justify a whole-function intrinsic. + Validate primitive values at their checked use types before ABI erasure; + copying a polymorphic array argument cannot preserve an in-place write. +- Use the library-owned Node [conformance commands](docs/workflow/stdlib-conformance.md) + in `psrs-stdlib/tools/` for source and runtime comparisons. Maintain tool code + and case engines in that repository; keep compiler locks and Rust tests here. + The tool consumes executable and package paths; it must not depend on compiler-internal representations. + ### Commit granularity - Group a commit by topic, not by file type. Code, tests, and the design or @@ -104,6 +141,10 @@ as project fields would create a second source of truth. - New syntax and new diagnostics land with official-suite evidence, not only a local test. The issue states whether that is a `purs` differential case or a scoreboard number. +- Before a broad compiler fix, record a small or filtered compile-diagnosis + baseline and inspect the first blocker, diagnostic origin, and last completed + stage. Use the SOP's failure-group counts to guide investigation, while + choosing roadmap work and semantic ownership by the rules in this section. - Run the issue's `Validation` block. It is the issue-specific superset of the workspace validation below. @@ -239,6 +280,11 @@ cargo test --workspace cargo clippy --workspace --all-targets -- -D warnings ``` +An explicit user-defined validation scope takes precedence over these +defaults. An issue's `Validation` block adds its required checks to the +applicable defaults. Report omitted commands and their scope; do not present +unrun checks as passing. + The workspace default member is the CLI so `cargo run -- ...` works from the repository root. Always use `cargo test --workspace` to include library tests. diff --git a/Cargo.lock b/Cargo.lock index 87b59ba4..f3831254 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,30 @@ version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + [[package]] name = "cc" version = "1.4.7" @@ -67,6 +91,46 @@ dependencies = [ "stacker", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dlmalloc" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad5208a115eaba24916f7456929832e310a81518c641f93fee4f89aa93aa3675" +dependencies = [ + "cfg-if", + "libc", + "windows-sys", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -79,12 +143,28 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "foldhash" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -112,12 +192,32 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "id-arena" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "im-rc" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1955a75fa080c677d3972822ec4bad316169ab1cfc6c257a942c2265dbe5fe" +dependencies = [ + "bitmaps", + "rand_core", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", +] + [[package]] name = "indexmap" version = "2.14.2" @@ -148,6 +248,12 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "log" version = "0.4.34" @@ -175,6 +281,16 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -208,6 +324,8 @@ version = "0.1.0" dependencies = [ "psrs-core", "psrs-hir", + "psrs-linker", + "psrs-runtime", "psrs-span", "psrs-thir", "wasm-encoder", @@ -224,9 +342,13 @@ dependencies = [ "psrs-ast", "psrs-driver", "psrs-hir", + "psrs-linker", "psrs-resolve", "psrs-span", "psrs-syntax", + "serde", + "serde_json", + "wat", ] [[package]] @@ -268,6 +390,10 @@ dependencies = [ "psrs-syntax", "psrs-thir", "psrs-typecheck", + "serde", + "serde_json", + "wasm-encoder", + "wasmparser", ] [[package]] @@ -289,6 +415,21 @@ dependencies = [ "psrs-syntax", ] +[[package]] +name = "psrs-linker" +version = "0.1.0" +dependencies = [ + "psrs-runtime", + "sha2", + "wasm-compose", + "wasm-encoder", + "wasmparser", + "wasmprinter", + "wat", + "wit-component", + "wit-parser", +] + [[package]] name = "psrs-resolve" version = "0.1.0" @@ -298,9 +439,24 @@ dependencies = [ "psrs-span", ] +[[package]] +name = "psrs-runtime" +version = "0.1.0" +dependencies = [ + "dlmalloc", + "libm", + "ryu-js", + "wasm-encoder", + "wasmparser", + "wasmprinter", +] + [[package]] name = "psrs-span" version = "0.1.0" +dependencies = [ + "stacker", +] [[package]] name = "psrs-syntax" @@ -342,6 +498,33 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "ryu-js" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd29631678d6fb0903b69223673e122c32e9ae559d0960a38d574695ebc0ea15" + [[package]] name = "semver" version = "1.0.28" @@ -355,6 +538,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", + "serde_derive", ] [[package]] @@ -390,12 +574,52 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + +[[package]] +name = "smallvec" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" + [[package]] name = "stacker" version = "0.1.25" @@ -440,24 +664,63 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + [[package]] name = "unicode-ident" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wasm-compose" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd23d12cc95c451c1306db5bc63075fbebb612bb70c53b4237b1ce5bc178343" +dependencies = [ + "anyhow", + "heck", + "im-rc", + "indexmap", + "log", + "petgraph", + "serde", + "serde_derive", + "serde_yaml", + "smallvec", + "wasm-encoder", + "wasmparser", + "wat", +] + [[package]] name = "wasm-encoder" version = "0.245.1" @@ -504,6 +767,28 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wast" +version = "245.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cf1149285569120b8ce39db8b465e8a2b55c34cbb586bd977e43e2bc7300bf" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width", + "wasm-encoder", +] + +[[package]] +name = "wat" +version = "1.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd48d1679b6858988cb96b154dda0ec5bbb09275b71db46057be37332d5477be" +dependencies = [ + "wast", +] + [[package]] name = "winapi-util" version = "0.1.11" diff --git a/Cargo.toml b/Cargo.toml index 1d1e355c..9c8165d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,8 @@ members = [ "crates/psrs-desugar", "crates/psrs-core", "crates/psrs-backend", + "crates/psrs-linker", + "crates/psrs-runtime", "crates/psrs-driver", "crates/psrs-syntax", "crates/psrs-cli", @@ -35,8 +37,19 @@ psrs-kind = { path = "crates/psrs-kind" } psrs-desugar = { path = "crates/psrs-desugar" } psrs-core = { path = "crates/psrs-core" } psrs-backend = { path = "crates/psrs-backend" } +psrs-linker = { path = "crates/psrs-linker" } +psrs-runtime = { path = "crates/psrs-runtime" } psrs-driver = { path = "crates/psrs-driver" } psrs-syntax = { path = "crates/psrs-syntax" } wasm-encoder = "=0.245.1" wasmparser = "=0.245.1" wasmprinter = "=0.245.1" +stacker = "0.1.22" + +[profile.target-runtime] +inherits = "release" +panic = "abort" +opt-level = "s" +lto = true +codegen-units = 1 +strip = true diff --git a/README.md b/README.md index 11c624ee..d052528b 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,13 @@ cargo run -- check-program-kinds examples/basic.purs cargo run -- dump mir examples/basic.purs ``` +The local default expects `../psrs-stdlib` at the revision/content recorded in +`stdlib.lock.json`. Set `PSRS_STDLIB_ROOT` for an explicit development package. +See [package and conformance setup](docs/workflow/stdlib-conformance.md). + `build ...` resolves and links every listed module with the standard -library from `stdlib/lib` and writes the artifact. `wat ...` renders -the text form. `dump ` prints an intermediate IR for +library from the locked `psrs-stdlib` package and writes the artifact. +`wat ...` renders the text form. `dump ` prints an intermediate IR for debugging. A selected `main :: Int` returns its value as the process exit code. A selected `main :: Effect Unit` runs that action once and returns 0; a trap still propagates. @@ -46,17 +50,17 @@ what remains in each layer. | Gate | Measured | Scope | | --- | --- | --- | | L0/L1 lexing, layout, parsing | 904/908 | non-FFI `layout`, `passing`, `failing`, `warning` files; the four differences are recorded DEC-16 intentional differences | -| L2 resolution | 72/72 failing, 276/413 passing | official `errorCode`s; 53 `passing` files stop on a missing module and 80 at P3 | -| L3 kinds | 36/48 failing | official kind `errorCode`s | -| L4 types | 35/50 failing | official `errorCode`s | -| L5 classes | 53/79 failing | official `errorCode`s | -| L6/M7 runtime | 125/413 passing | all 125 exit 0; 63 have no selected `main`; 53 stop on a missing module | +| L2 resolution | 72/72 failing, 403/413 passing | official `errorCode`s; 6 passing files stop at P3 and 4 at P0; no harness blockers | +| L3 kinds | 39/48 failing | official kind `errorCode`s | +| L4 types | 39/50 failing | official `errorCode`s | +| L5 classes | 58/81 failing | official `errorCode`s | +| L6/M7 runtime | 210/413 passing | all 210 exit 0; 203 do not agree, including 47 with no selected `main` | | M8 warnings, optimization | not measured | no scoreboard exists | Run the scoreboards yourself: ```sh -PSRS_ORACLE=annotations \ +PSRS_ORACLE=annotations PSRS_REQUIRE_WASMTIME=1 \ cargo test -p psrs-driver --test suite -- --ignored --nocapture ``` @@ -76,7 +80,7 @@ The compiler currently: functional dependencies**, then passes dictionaries through to Wasm — a method reached through a superclass constraint returns the right value under `wasmtime`, as does a call through a constrained function argument; -- loads the PureScript standard library from [`stdlib/lib`](stdlib/lib) on disk, +- loads the PureScript standard library from the locked `psrs-stdlib` package, exposing console, clock, random, process arguments and environment, filesystem, and sockets over WASI. @@ -114,8 +118,12 @@ Verified working subsets, each with source tests and Wasmtime execution: scalar, string, list, flags, handle, and variant shapes ([canonical ABI](docs/design/backend/wasm/canonical-abi-and-wit.md)), and the synthesized aggregate fixtures validate but do not yet execute; -- **the standard library** — `stdlib/lib` holds 12 modules, and 244 corpus - programs import a module it does not provide. +- **the standard library** — the independent `psrs-stdlib` package holds the + pinned official core libraries (215 source modules). A module the compiler owns, + such as `Safe.Coerce`, + resolves through its primitive interface rather than the vendored file, which + stays faithful to upstream; `Unsafe.Coerce.unsafeCoerce` has no interface yet + and is a recorded gap. ## Workspace @@ -133,7 +141,7 @@ Verified working subsets, each with source tests and Wasmtime execution: | `psrs-desugar` | Operator desugaring while preserving HIR. | | `psrs-core` | Typed Core and its HIR lowering. | | `psrs-backend` | CC IR, MIR/CFG, structured Wasm encoding, WIT/ABI lowering, validation, and WAT. | -| `psrs-driver` | Wires the compiler passes together and loads `stdlib/lib`. | +| `psrs-driver` | Wires the compiler passes together and loads the locked `psrs-stdlib` package. | | `psrs-cli` | Source inspection, `build`, `wat`, and `dump` commands. | The architecture defines twelve major passes across six long-lived IR families; diff --git a/crates/psrs-ast/src/equations.rs b/crates/psrs-ast/src/equations.rs index dab05324..894e4740 100644 --- a/crates/psrs-ast/src/equations.rs +++ b/crates/psrs-ast/src/equations.rs @@ -135,7 +135,7 @@ fn argument_record(arguments: &[Binder], span: TextRange) -> Expr { }) .collect::>(); Expr { - kind: ExprKind::Record(fields), + kind: ExprKind::MatchProduct(fields), span, } } diff --git a/crates/psrs-ast/src/expr/chain.rs b/crates/psrs-ast/src/expr/chain.rs new file mode 100644 index 00000000..4d1b72d1 --- /dev/null +++ b/crates/psrs-ast/src/expr/chain.rs @@ -0,0 +1,64 @@ +use crate::{LowerError, Operator, lower_expr, lower_name}; +use psrs_cst::{self as cst, ExprKind as CstExprKind}; +use psrs_span::TextRange; + +use super::{Expr, ExprKind}; + +pub(crate) fn lower_operator_chain( + operator: cst::CstName, + left: cst::Expr, + right: cst::Expr, + span: TextRange, +) -> Result { + let mut operands = Vec::new(); + let mut operators = Vec::new(); + collect_operator_chain(left, &mut operands, &mut operators)?; + operators.push(Operator { + name: lower_name(operator.clone()), + span: operator.span, + }); + collect_operator_chain(right, &mut operands, &mut operators)?; + Ok(Expr { + kind: ExprKind::OperatorChain { + operands, + operators, + }, + span, + }) +} + +fn collect_operator_chain( + expression: cst::Expr, + operands: &mut Vec, + operators: &mut Vec, +) -> Result<(), LowerError> { + psrs_span::with_sufficient_stack(|| { + collect_operator_chain_inner(expression, operands, operators) + }) +} + +fn collect_operator_chain_inner( + expression: cst::Expr, + operands: &mut Vec, + operators: &mut Vec, +) -> Result<(), LowerError> { + let span = expression.span; + match expression.kind { + CstExprKind::Operator { + operator, + left, + right, + } => { + collect_operator_chain(*left, operands, operators)?; + operators.push(Operator { + name: lower_name(operator.clone()), + span: operator.span, + }); + collect_operator_chain(*right, operands, operators) + } + kind => { + operands.push(lower_expr(cst::Expr { kind, span })?); + Ok(()) + } + } +} diff --git a/crates/psrs-ast/src/expr/guards.rs b/crates/psrs-ast/src/expr/guards.rs index 686d2b19..cd1eef2a 100644 --- a/crates/psrs-ast/src/expr/guards.rs +++ b/crates/psrs-ast/src/expr/guards.rs @@ -152,6 +152,58 @@ pub(crate) fn prepend_guards(mut expression: Expr, guards: Vec, span: Tex } } +/// An immediate `_` in an `if` condition, then branch, or else branch is a +/// function parameter, in that written order. Nested expressions keep their +/// own underscores. +pub(crate) fn lower_if( + condition: cst::Expr, + then_branch: cst::Expr, + else_branch: cst::Expr, + span: TextRange, +) -> Result { + let mut binders = Vec::new(); + let condition = lower_immediate_anonymous(condition, &mut binders)?; + let then_branch = lower_immediate_anonymous(then_branch, &mut binders)?; + let else_branch = lower_immediate_anonymous(else_branch, &mut binders)?; + let mut expression = Expr { + kind: ExprKind::If { + condition: Box::new(condition), + then_branch: Box::new(then_branch), + else_branch: Box::new(else_branch), + }, + span, + }; + for binder in binders.into_iter().rev() { + expression = Expr { + kind: ExprKind::Lambda { + binder, + body: Box::new(expression), + }, + span, + }; + } + Ok(expression) +} + +fn lower_immediate_anonymous( + expression: cst::Expr, + binders: &mut Vec, +) -> Result { + if matches!(&expression.kind, cst::ExprKind::Name(name) if name.text == "_") { + let span = expression.span; + let name = format!("$psrs_if_argument_{}", span.start); + binders.push(Binder { + name: name.clone(), + span, + }); + return Ok(Expr { + kind: ExprKind::Name(Name { text: name, span }), + span, + }); + } + lower_expr(expression) +} + pub(crate) fn lower_case_scrutinees( scrutinees: Vec, span: TextRange, @@ -193,7 +245,7 @@ pub(crate) fn lower_case_scrutinees( lowered.pop().expect("one case scrutinee").1 } else { Expr { - kind: ExprKind::Record( + kind: ExprKind::MatchProduct( lowered .into_iter() .map(|(index, value)| (tuple_label(index), value)) diff --git a/crates/psrs-ast/src/expr/mod.rs b/crates/psrs-ast/src/expr/mod.rs index 7811a9f7..d22e0268 100644 --- a/crates/psrs-ast/src/expr/mod.rs +++ b/crates/psrs-ast/src/expr/mod.rs @@ -1,13 +1,18 @@ use crate::{LowerError, Name, Type}; -use psrs_cst::{self as cst, RecordField, RecordUpdateField}; +use psrs_cst as cst; use psrs_span::TextRange; use std::collections::HashSet; +mod chain; mod guards; +mod records; +pub(super) use chain::lower_operator_chain; pub use guards::{Guard, GuardedExpr}; pub(super) use guards::{ - lower_case_patterns, lower_case_scrutinees, lower_guard, lower_guarded_rhs, prepend_guards, + lower_case_patterns, lower_case_scrutinees, lower_guard, lower_guarded_rhs, lower_if, + prepend_guards, }; +pub(super) use records::{lower_record, lower_record_update}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct Binder { @@ -38,9 +43,13 @@ pub enum ExprKind { Char(char), Array(Vec), Record(Vec<(String, Expr)>), + /// An ordered product of independent match scrutinees. Unlike a source + /// record literal, its fields retain polymorphism until pattern checking. + /// P5 checks the fields and converts the product to an ordinary typed record. + MatchProduct(Vec<(String, Expr)>), RecordUpdate { expression: Box, - fields: Vec<(String, Expr)>, + fields: Vec, }, FieldAccess { expression: Box, @@ -103,6 +112,22 @@ pub enum ExprKind { Guarded(Vec), } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RecordUpdateField { + pub label: String, + pub value: RecordUpdateValue, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RecordUpdateValue { + Expression(Expr), + /// A source path update, whose base is the enclosing record's field. + Nested { + fields: Vec, + span: TextRange, + }, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct CaseBranch { pub pattern: Pattern, @@ -159,36 +184,48 @@ pub enum PatternKind { }, } -pub(super) fn lower_record( - fields: Vec, - tail: Option>, +pub(super) fn lower_field_access( + expression: psrs_cst::Expr, + field: psrs_cst::CstName, span: TextRange, -) -> Result { - if tail.is_some() { - return Err(LowerError::new( - span, - "open record rows are not supported yet", - )); - } - Ok(ExprKind::Record( - fields - .into_iter() - .map(|field| Ok((field.label.text, super::lower_expr(field.value)?))) - .collect::, LowerError>>()?, - )) +) -> Result { + Ok(Expr { + kind: ExprKind::FieldAccess { + expression: Box::new(super::lower_expr(expression)?), + field: field.text, + }, + span, + }) } -pub(super) fn lower_record_update( - expression: psrs_cst::Expr, - fields: Vec, -) -> Result { - Ok(ExprKind::RecordUpdate { - expression: Box::new(super::lower_expr(expression)?), - fields: fields - .into_iter() - .map(|field| Ok((field.label.text, super::lower_expr(field.value)?))) - .collect::, LowerError>>()?, - }) +pub(super) fn lower_record_accessor( + marker_span: TextRange, + fields: Vec, + span: TextRange, +) -> Expr { + let binder = Binder { + name: format!("$psrs_record_accessor_{}", marker_span.start), + span: marker_span, + }; + let mut body = Expr { + kind: ExprKind::Name(crate::Name { + text: binder.name.clone(), + span: marker_span, + }), + span: marker_span, + }; + for field in fields { + body = Expr { + kind: ExprKind::FieldAccess { + expression: Box::new(body), + field: field.field.text, + }, + span: TextRange::new(marker_span.start, field.field.span.end), + }; + } + let mut accessor = super::lower_lambda(binder, body); + accessor.span = span; + accessor } pub(super) fn lower_pattern_lambda( diff --git a/crates/psrs-ast/src/expr/records.rs b/crates/psrs-ast/src/expr/records.rs new file mode 100644 index 00000000..1b927638 --- /dev/null +++ b/crates/psrs-ast/src/expr/records.rs @@ -0,0 +1,103 @@ +use super::{Binder, Expr, ExprKind, RecordUpdateValue}; +use crate::{LowerError, Name, lower_expr}; +use psrs_cst::{self as cst, RecordField, RecordUpdateField}; +use psrs_span::TextRange; + +/// Only immediate anonymous arguments belong to this constructor. Nested +/// literals introduce their own lambdas; nested update paths share this one. +fn lower_argument(expression: cst::Expr, binders: &mut Vec) -> Result { + if matches!(&expression.kind, cst::ExprKind::Name(name) if name.text == "_") { + let span = expression.span; + let name = format!("$psrs_record_argument_{}", span.start); + binders.push(Binder { + name: name.clone(), + span, + }); + Ok(Expr { + kind: ExprKind::Name(Name { text: name, span }), + span, + }) + } else { + lower_expr(expression) + } +} + +fn wrap_arguments(kind: ExprKind, binders: Vec, span: TextRange) -> Expr { + let mut expression = Expr { kind, span }; + for binder in binders.into_iter().rev() { + expression = Expr { + kind: ExprKind::Lambda { + binder, + body: Box::new(expression), + }, + span, + }; + } + expression +} + +pub(crate) fn lower_record( + fields: Vec, + tail: Option>, + span: TextRange, +) -> Result { + if tail.is_some() { + return Err(LowerError::new( + span, + "open record rows are not supported yet", + )); + } + let mut binders = Vec::new(); + let fields = fields + .into_iter() + .map(|field| Ok((field.label.text, lower_argument(field.value, &mut binders)?))) + .collect::, LowerError>>()?; + Ok(wrap_arguments(ExprKind::Record(fields), binders, span)) +} + +pub(crate) fn lower_record_update( + expression: cst::Expr, + fields: Vec, + span: TextRange, +) -> Result { + let mut binders = Vec::new(); + let expression = lower_argument(expression, &mut binders)?; + let fields = lower_update_fields(fields, &mut binders)?; + Ok(wrap_arguments( + ExprKind::RecordUpdate { + expression: Box::new(expression), + fields, + }, + binders, + span, + )) +} + +fn lower_update_fields( + fields: Vec, + binders: &mut Vec, +) -> Result, LowerError> { + fields + .into_iter() + .map(|field| { + // The parser marks path updates with the label span in place of + // an equals token. Explicit `field = expression` is a new scope. + let value = if field.equals_span == field.label.span { + let span = field.value.span; + let cst::ExprKind::RecordUpdate { fields, .. } = field.value.kind else { + return Err(LowerError::new(span, "invalid nested record update")); + }; + RecordUpdateValue::Nested { + fields: lower_update_fields(fields, binders)?, + span, + } + } else { + RecordUpdateValue::Expression(lower_argument(field.value, binders)?) + }; + Ok(super::RecordUpdateField { + label: field.label.text, + value, + }) + }) + .collect() +} diff --git a/crates/psrs-ast/src/lib.rs b/crates/psrs-ast/src/lib.rs index ac1c73ea..3417c7bb 100644 --- a/crates/psrs-ast/src/lib.rs +++ b/crates/psrs-ast/src/lib.rs @@ -18,7 +18,7 @@ mod type_decl; pub use export::{ExportList, ExportRef, TypeMembers}; pub use expr::{ Binder, CaseBranch, Declaration, Expr, ExprKind, Guard, GuardedExpr, Pattern, PatternKind, - RecordPatternMode, + RecordPatternMode, RecordUpdateField, RecordUpdateValue, }; pub use fixity::{Associativity, FixityDeclaration, FixityNamespace, Operator, SectionSide}; pub use import::{Import, ImportList, ImportRef}; @@ -48,14 +48,13 @@ pub struct Module { pub span: TextRange, } -/// A `foreign import` with a WIT binding: a value provided by a WIT interface -/// rather than defined in source. +/// A source-declared foreign value, implemented by the target rather than source. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ForeignImport { pub name: Name, pub annotation: Type, - /// The WIT binding, `#`. - pub binding: String, + /// An explicit target binding, or an ordinary library foreign declaration. + pub binding: Option, pub span: TextRange, } @@ -97,19 +96,13 @@ impl LowerError { } fn lower_foreign_import(declaration: cst::ForeignDeclaration) -> Result { - let Some(binding) = declaration.binding else { - return Err(LowerError::new( - declaration.span, - "a foreign import requires a `\"#\"` WIT binding", - )); - }; Ok(ForeignImport { name: Name { text: declaration.name.text, span: declaration.name.span, }, annotation: lower_type(declaration.type_expr)?, - binding: binding.text, + binding: declaration.binding.map(|binding| binding.text), span: declaration.span, }) } @@ -191,6 +184,10 @@ pub(crate) fn check_argument_names(parameters: &[cst::Pattern]) -> Option Result { + psrs_span::with_sufficient_stack(|| lower_expr_inner(expression)) +} + +fn lower_expr_inner(expression: cst::Expr) -> Result { let span = expression.span; let cst_kind = match expression.kind { CstExprKind::Let { @@ -212,16 +209,19 @@ pub(crate) fn lower_expr(expression: cst::Expr) -> Result { .map(lower_expr) .collect::, _>>()?, ), - CstExprKind::Record { fields, tail, .. } => expr::lower_record(fields, tail, span)?, + CstExprKind::Record { fields, tail, .. } => return expr::lower_record(fields, tail, span), CstExprKind::RecordUpdate { expression, fields, .. - } => expr::lower_record_update(*expression, fields)?, + } => return expr::lower_record_update(*expression, fields, span), CstExprKind::FieldAccess { expression, field, .. - } => ExprKind::FieldAccess { - expression: Box::new(lower_expr(*expression)?), - field: field.text, - }, + } => return expr::lower_field_access(*expression, field, span), + CstExprKind::RecordAccessor { + marker_span, + fields, + } => { + return Ok(expr::lower_record_accessor(marker_span, fields, span)); + } CstExprKind::Application(function, argument) => ExprKind::Application( Box::new(lower_expr(*function)?), Box::new(lower_expr(*argument)?), @@ -230,7 +230,7 @@ pub(crate) fn lower_expr(expression: cst::Expr) -> Result { operator, left, right, - } => return lower_operator_chain(operator, *left, *right, span), + } => return expr::lower_operator_chain(operator, *left, *right, span), CstExprKind::OperatorSection { operator, operand, @@ -273,11 +273,9 @@ pub(crate) fn lower_expr(expression: cst::Expr) -> Result { then_branch, else_branch, .. - } => ExprKind::If { - condition: Box::new(lower_expr(*condition)?), - then_branch: Box::new(lower_expr(*then_branch)?), - else_branch: Box::new(lower_expr(*else_branch)?), - }, + } => { + return expr::lower_if(*condition, *then_branch, *else_branch, span); + } CstExprKind::Case { scrutinees, alternatives, @@ -428,55 +426,6 @@ fn lower_lambda(binder: Binder, body: Expr) -> Expr { } } -fn lower_operator_chain( - operator: cst::CstName, - left: cst::Expr, - right: cst::Expr, - span: TextRange, -) -> Result { - let mut operands = Vec::new(); - let mut operators = Vec::new(); - collect_operator_chain(left, &mut operands, &mut operators)?; - operators.push(Operator { - name: lower_name(operator.clone()), - span: operator.span, - }); - collect_operator_chain(right, &mut operands, &mut operators)?; - Ok(Expr { - kind: ExprKind::OperatorChain { - operands, - operators, - }, - span, - }) -} - -fn collect_operator_chain( - expression: cst::Expr, - operands: &mut Vec, - operators: &mut Vec, -) -> Result<(), LowerError> { - let span = expression.span; - match expression.kind { - CstExprKind::Operator { - operator, - left, - right, - } => { - collect_operator_chain(*left, operands, operators)?; - operators.push(Operator { - name: lower_name(operator.clone()), - span: operator.span, - }); - collect_operator_chain(*right, operands, operators) - } - kind => { - operands.push(lower_expr(cst::Expr { kind, span })?); - Ok(()) - } - } -} - /// Tuple component labels. A tuple is the closed record `{ _1, _2, ... }`. pub(crate) fn tuple_label(index: usize) -> String { format!("_{}", index + 1) diff --git a/crates/psrs-ast/src/local.rs b/crates/psrs-ast/src/local.rs index da9a62f0..fe8930dd 100644 --- a/crates/psrs-ast/src/local.rs +++ b/crates/psrs-ast/src/local.rs @@ -19,7 +19,12 @@ pub(crate) fn lower_local_declarations( return Err(error); } let pattern_span = pattern.span; - let scrutinee = Box::new(crate::lower_expr(pattern.value.clone())?); + let mut scrutinee = crate::lower_expr(pattern.value.clone())?; + if let Some(block) = &pattern.where_block { + let span = TextRange::new(block.span.start, scrutinee.span.end); + scrutinee = lower_local_declarations(block.declarations.clone(), scrutinee, span)?; + } + let scrutinee = Box::new(scrutinee); let pattern = crate::expr::lower_pattern(pattern.pattern.clone())?; let remaining = lower_local_declarations(declarations[1..].to_vec(), body, span)?; let branch_span = TextRange::new(pattern_span.start, remaining.span.end); diff --git a/crates/psrs-ast/src/tests.rs b/crates/psrs-ast/src/tests.rs deleted file mode 100644 index a745509c..00000000 --- a/crates/psrs-ast/src/tests.rs +++ /dev/null @@ -1,487 +0,0 @@ -use super::*; -use psrs_cst::{ - CstName, Declaration as CstDeclaration, Module as CstModule, Pattern, PatternKind, - TypeVarBinder, ValueDeclaration, ValueRhs, -}; -use psrs_span::TextRange; - -fn name(text: &str, start: u32) -> CstName { - CstName { - text: text.into(), - span: TextRange::new(start, start + text.len() as u32), - } -} - -fn pattern_var(text: &str, start: u32) -> Pattern { - Pattern { - kind: PatternKind::Var(name(text, start)), - span: TextRange::new(start, start + text.len() as u32), - } -} - -fn type_var_binder(text: &str, start: u32) -> TypeVarBinder { - TypeVarBinder { - name: name(text, start), - kind: None, - span: TextRange::new(start, start + text.len() as u32), - } -} - -fn cst_module(declaration: CstDeclaration) -> CstModule { - CstModule { - module_keyword_span: TextRange::new(0, 6), - name: name("Main", 7), - exports: None, - where_keyword_span: TextRange::new(12, 17), - imports: Vec::new(), - declarations: vec![declaration], - span: TextRange::new(0, 40), - } -} - -fn value_declaration( - name_text: &str, - name_start: u32, - parameters: Vec, - equals_span: TextRange, - value: cst::Expr, - span_end: u32, - annotation: Option, -) -> CstDeclaration { - CstDeclaration::Value(ValueDeclaration { - name: name(name_text, name_start), - parameters, - rhs: ValueRhs::Plain { equals_span, value }, - where_block: None, - span: TextRange::new(name_start, span_end), - annotation, - }) -} - -#[test] -fn function_parameters_become_nested_lambdas_and_names_stay_unresolved() { - let value = cst::Expr { - kind: CstExprKind::Operator { - operator: name("+", 33), - left: Box::new(cst::Expr { - kind: CstExprKind::Name(name("x", 31)), - span: TextRange::new(31, 32), - }), - right: Box::new(cst::Expr { - kind: CstExprKind::Name(name("y", 35)), - span: TextRange::new(35, 36), - }), - }, - span: TextRange::new(31, 36), - }; - let declaration = value_declaration( - "add", - 19, - vec![pattern_var("x", 23), pattern_var("y", 25)], - TextRange::new(27, 28), - value, - 36, - None, - ); - let module = lower_module(cst_module(declaration)).unwrap(); - let ExprKind::Lambda { binder, body } = &module.declarations[0].value.kind else { - panic!("expected the first normalized lambda"); - }; - assert_eq!(binder.name, "x"); - let ExprKind::Lambda { binder, body } = &body.kind else { - panic!("expected the second normalized lambda"); - }; - assert_eq!(binder.name, "y"); - let ExprKind::OperatorChain { - operands, - operators, - } = &body.kind - else { - panic!("expected the source operator chain to remain unresolved"); - }; - assert_eq!(operators.len(), 1); - assert_eq!(operators[0].name.text, "+"); - assert!(matches!(&operands[0].kind, ExprKind::Name(name) if name.text == "x")); - assert!(matches!(&operands[1].kind, ExprKind::Name(name) if name.text == "y")); -} - -#[test] -fn parentheses_are_removed_without_losing_the_expression_range() { - let declaration = value_declaration( - "main", - 18, - Vec::new(), - TextRange::new(23, 24), - cst::Expr { - kind: CstExprKind::Parens { - open_paren_span: TextRange::new(25, 26), - expression: Box::new(cst::Expr { - kind: CstExprKind::Integer("42".into()), - span: TextRange::new(26, 28), - }), - close_paren_span: TextRange::new(28, 29), - }, - span: TextRange::new(25, 29), - }, - 29, - None, - ); - let module = lower_module(cst_module(declaration)).unwrap(); - let value = &module.declarations[0].value; - assert!(matches!(value.kind, ExprKind::Integer(ref value) if value == "42")); - assert_eq!(value.span, TextRange::new(25, 29)); -} - -#[test] -fn lowers_forall_types_and_removes_parentheses() { - let annotation = cst::TypeExpr { - kind: cst::TypeExprKind::Forall { - forall_span: TextRange::new(0, 6), - variables: vec![type_var_binder("a", 7)], - dot_span: TextRange::new(8, 9), - body: Box::new(cst::TypeExpr { - kind: cst::TypeExprKind::Parens { - open_paren_span: TextRange::new(10, 11), - expression: Box::new(cst::TypeExpr { - kind: cst::TypeExprKind::Name(name("a", 11)), - span: TextRange::new(11, 12), - }), - close_paren_span: TextRange::new(12, 13), - }, - span: TextRange::new(10, 13), - }), - }, - span: TextRange::new(0, 13), - }; - let declaration = value_declaration( - "id", - 19, - vec![pattern_var("x", 23)], - TextRange::new(25, 26), - cst::Expr { - kind: CstExprKind::Name(name("x", 27)), - span: TextRange::new(27, 28), - }, - 28, - Some(annotation), - ); - let module = lower_module(cst_module(declaration)).unwrap(); - let annotation = module.declarations[0].annotation.as_ref().unwrap(); - let TypeKind::Forall { variables, body } = &annotation.kind else { - panic!("expected a lowered forall"); - }; - assert_eq!(variables.len(), 1); - assert_eq!(variables[0].name.text, "a"); - assert!(variables[0].kind.is_none()); - assert!(matches!(&body.kind, TypeKind::Name(name) if name.text == "a")); - assert_eq!(body.span, TextRange::new(10, 13)); -} - -fn cst_expr(kind: CstExprKind, start: u32, end: u32) -> cst::Expr { - cst::Expr { - kind, - span: TextRange::new(start, end), - } -} - -fn do_expr(statements: Vec, start: u32, end: u32) -> cst::Expr { - cst::Expr { - kind: CstExprKind::Do { - do_keyword_span: TextRange::new(start, start + 2), - layout_start_span: TextRange::empty(start + 2), - statements, - layout_end_span: TextRange::empty(end), - in_keyword_span: None, - result: None, - }, - span: TextRange::new(start, end), - } -} - -fn lower_do_blocks(statements: Vec) -> Result> { - let value = do_expr(statements, 30, 90); - let declaration = value_declaration( - "main", - 18, - Vec::new(), - TextRange::new(24, 25), - value, - 90, - None, - ); - Ok(lower_module(cst_module(declaration))? - .declarations - .remove(0) - .value) -} - -#[test] -fn lowers_a_do_bind_to_bind_and_a_continuation_lambda() { - let statements = vec![ - cst::DoStatement::Bind { - pattern: pattern_var("x", 40), - left_arrow_span: TextRange::new(42, 44), - value: cst_expr(CstExprKind::Name(name("effect", 45)), 45, 51), - }, - cst::DoStatement::Discard(cst_expr(CstExprKind::Name(name("rest", 55)), 55, 59)), - ]; - let value = lower_do_blocks(statements).unwrap(); - let ExprKind::Application(partial, continuation) = &value.kind else { - panic!("expected a bind application, got {:?}", value.kind); - }; - let ExprKind::Application(function, argument) = &partial.kind else { - panic!("expected the bind function to be applied to the effect"); - }; - assert!(matches!(&function.kind, ExprKind::Name(name) if name.text == "bind")); - assert!(matches!(&argument.kind, ExprKind::Name(name) if name.text == "effect")); - let ExprKind::Lambda { binder, body } = &continuation.kind else { - panic!("expected a continuation lambda"); - }; - assert_eq!(binder.name, "x"); - assert!(matches!(&body.kind, ExprKind::Name(name) if name.text == "rest")); - assert_eq!(value.span, TextRange::new(30, 90)); -} - -#[test] -fn lowers_a_do_discard_to_discard_and_a_wildcard_lambda() { - let statements = vec![ - cst::DoStatement::Discard(cst_expr(CstExprKind::Name(name("effect", 40)), 40, 46)), - cst::DoStatement::Discard(cst_expr(CstExprKind::Name(name("rest", 48)), 48, 52)), - ]; - let value = lower_do_blocks(statements).unwrap(); - let ExprKind::Application(partial, continuation) = &value.kind else { - panic!("expected a discard application"); - }; - let ExprKind::Application(function, _) = &partial.kind else { - panic!("expected the discard function to be applied to the effect"); - }; - assert!(matches!(&function.kind, ExprKind::Name(name) if name.text == "discard")); - let ExprKind::Lambda { binder, .. } = &continuation.kind else { - panic!("expected a wildcard continuation lambda"); - }; - assert!(binder.name.starts_with("__psrs_do_wildcard_")); -} - -#[test] -fn a_final_do_value_is_the_result_without_pure() { - let statements = vec![cst::DoStatement::Discard(cst_expr( - CstExprKind::Name(name("result", 40)), - 40, - 46, - ))]; - let value = lower_do_blocks(statements).unwrap(); - assert!(matches!(&value.kind, ExprKind::Name(name) if name.text == "result")); - assert_eq!(value.span, TextRange::new(30, 90)); -} - -#[test] -fn lowers_a_non_variable_do_binder_through_a_case() { - let constructor = Pattern { - kind: PatternKind::Constructor { - name: name("Unit", 40), - arguments: Vec::new(), - }, - span: TextRange::new(40, 44), - }; - let statements = vec![ - cst::DoStatement::Bind { - pattern: constructor, - left_arrow_span: TextRange::new(45, 47), - value: cst_expr(CstExprKind::Name(name("effect", 48)), 48, 54), - }, - cst::DoStatement::Discard(cst_expr(CstExprKind::Name(name("rest", 56)), 56, 60)), - ]; - let value = lower_do_blocks(statements).unwrap(); - let ExprKind::Application(_, continuation) = &value.kind else { - panic!("expected a bind application"); - }; - let ExprKind::Lambda { binder, body } = &continuation.kind else { - panic!("expected a continuation lambda"); - }; - assert!(binder.name.starts_with("$psrs_pattern_")); - let ExprKind::Case { - scrutinee, - branches, - } = &body.kind - else { - panic!("expected a case for the non-variable binder"); - }; - assert!(matches!(&scrutinee.kind, ExprKind::Name(name) if name.text == binder.name)); - assert_eq!(branches.len(), 1); - assert!(matches!( - &branches[0].pattern.kind, - crate::PatternKind::Constructor { name, arguments } - if name.text == "Unit" && arguments.is_empty() - )); -} - -#[test] -fn lowers_a_do_let_to_a_let_around_the_rest() { - let binding = value_declaration( - "y", - 40, - Vec::new(), - TextRange::new(42, 43), - cst_expr(CstExprKind::Integer("1".into()), 44, 45), - 45, - None, - ); - let statements = vec![ - cst::DoStatement::Let { - let_keyword_span: TextRange::new(32, 35), - declarations: vec![binding], - layout_start_span: TextRange::empty(36), - layout_end_span: TextRange::empty(45), - }, - cst::DoStatement::Discard(cst_expr(CstExprKind::Name(name("rest", 48)), 48, 52)), - ]; - let value = lower_do_blocks(statements).unwrap(); - let ExprKind::Let { declarations, body } = &value.kind else { - panic!("expected a let around the rest"); - }; - assert_eq!(declarations.len(), 1); - assert_eq!(declarations[0].name.text, "y"); - assert!(matches!(&body.kind, ExprKind::Name(name) if name.text == "rest")); -} - -#[test] -fn rejects_an_empty_do_block() { - let error = lower_do_blocks(Vec::new()).unwrap_err(); - assert_eq!(error[0].message, "an empty `do` block is not allowed"); - assert!(error[0].code.is_none()); -} - -#[test] -fn rejects_a_do_block_ending_in_a_bind() { - let statements = vec![cst::DoStatement::Bind { - pattern: pattern_var("x", 40), - left_arrow_span: TextRange::new(42, 44), - value: cst_expr(CstExprKind::Name(name("effect", 45)), 45, 51), - }]; - let error = lower_do_blocks(statements).unwrap_err(); - assert_eq!(error[0].code, Some("InvalidDoBind")); -} - -fn declaration_block( - declarations: Vec, - start: u32, - end: u32, -) -> cst::DeclarationBlock { - cst::DeclarationBlock { - where_keyword_span: TextRange::new(start, start + 5), - layout_start_span: TextRange::empty(start + 5), - declarations, - layout_end_span: TextRange::empty(end), - span: TextRange::new(start, end), - } -} - -#[test] -fn lowers_a_value_where_block_to_a_let_inside_the_parameters() { - let binding = value_declaration( - "y", - 40, - Vec::new(), - TextRange::new(42, 43), - cst_expr(CstExprKind::Integer("1".into()), 44, 45), - 45, - None, - ); - let declaration = CstDeclaration::Value(ValueDeclaration { - name: name("f", 30), - parameters: vec![pattern_var("x", 32)], - rhs: ValueRhs::Plain { - equals_span: TextRange::new(34, 35), - value: cst_expr(CstExprKind::Name(name("y", 36)), 36, 37), - }, - where_block: Some(declaration_block(vec![binding], 38, 50)), - span: TextRange::new(30, 50), - annotation: None, - }); - let module = lower_module(cst_module(declaration)).unwrap(); - let ExprKind::Lambda { binder, body } = &module.declarations[0].value.kind else { - panic!("expected the parameter lambda"); - }; - assert_eq!(binder.name, "x"); - let ExprKind::Let { declarations, body } = &body.kind else { - panic!("expected the where declarations as a let inside the parameter"); - }; - assert_eq!(declarations.len(), 1); - assert_eq!(declarations[0].name.text, "y"); - assert!(matches!(&body.kind, ExprKind::Name(name) if name.text == "y")); -} - -#[test] -fn lowers_a_case_where_block_to_a_let_in_the_branch() { - let binding = value_declaration( - "y", - 40, - Vec::new(), - TextRange::new(42, 43), - cst_expr(CstExprKind::Name(name("x", 44)), 44, 45), - 45, - None, - ); - let alternative = cst::CaseAlternative { - patterns: vec![pattern_var("x", 33)], - rhs: cst::CaseRhs::Plain { - arrow_span: TextRange::new(35, 37), - value: cst_expr(CstExprKind::Name(name("y", 38)), 38, 39), - where_block: Some(declaration_block(vec![binding], 40, 50)), - }, - span: TextRange::new(33, 50), - }; - let case = cst_expr( - CstExprKind::Case { - case_keyword_span: TextRange::new(20, 24), - scrutinees: vec![cst_expr(CstExprKind::Name(name("value", 25)), 25, 30)], - of_keyword_span: TextRange::new(31, 33), - layout_start_span: TextRange::empty(33), - alternatives: vec![alternative], - layout_end_span: TextRange::empty(50), - }, - 20, - 50, - ); - let declaration = value_declaration( - "main", - 18, - Vec::new(), - TextRange::new(24, 25), - case, - 50, - None, - ); - let module = lower_module(cst_module(declaration)).unwrap(); - let ExprKind::Case { branches, .. } = &module.declarations[0].value.kind else { - panic!("expected a case expression"); - }; - assert_eq!(branches.len(), 1); - let ExprKind::Let { declarations, body } = &branches[0].value.kind else { - panic!("expected the where declarations as a let around the branch body"); - }; - assert_eq!(declarations.len(), 1); - assert_eq!(declarations[0].name.text, "y"); - assert!(matches!(&body.kind, ExprKind::Name(name) if name.text == "y")); -} - -#[test] -fn rejects_a_do_block_ending_in_a_let() { - let binding = value_declaration( - "y", - 40, - Vec::new(), - TextRange::new(42, 43), - cst_expr(CstExprKind::Integer("1".into()), 44, 45), - 45, - None, - ); - let statements = vec![cst::DoStatement::Let { - let_keyword_span: TextRange::new(32, 35), - declarations: vec![binding], - layout_start_span: TextRange::empty(36), - layout_end_span: TextRange::empty(45), - }]; - let error = lower_do_blocks(statements).unwrap_err(); - assert_eq!(error[0].code, Some("InvalidDoLet")); -} diff --git a/crates/psrs-ast/src/tests/do_blocks.rs b/crates/psrs-ast/src/tests/do_blocks.rs new file mode 100644 index 00000000..13ace09e --- /dev/null +++ b/crates/psrs-ast/src/tests/do_blocks.rs @@ -0,0 +1,200 @@ +use super::*; + +fn do_expr(statements: Vec, start: u32, end: u32) -> cst::Expr { + cst::Expr { + kind: CstExprKind::Do { + do_keyword_span: TextRange::new(start, start + 2), + layout_start_span: TextRange::empty(start + 2), + statements, + layout_end_span: TextRange::empty(end), + in_keyword_span: None, + result: None, + }, + span: TextRange::new(start, end), + } +} + +fn lower_do_blocks(statements: Vec) -> Result> { + let value = do_expr(statements, 30, 90); + let declaration = value_declaration( + "main", + 18, + Vec::new(), + TextRange::new(24, 25), + value, + 90, + None, + ); + Ok(lower_module(cst_module(declaration))? + .declarations + .remove(0) + .value) +} + +#[test] +fn lowers_a_do_bind_to_bind_and_a_continuation_lambda() { + let statements = vec![ + cst::DoStatement::Bind { + pattern: pattern_var("x", 40), + left_arrow_span: TextRange::new(42, 44), + value: cst_expr(CstExprKind::Name(name("effect", 45)), 45, 51), + }, + cst::DoStatement::Discard(cst_expr(CstExprKind::Name(name("rest", 55)), 55, 59)), + ]; + let value = lower_do_blocks(statements).unwrap(); + let ExprKind::Application(partial, continuation) = &value.kind else { + panic!("expected a bind application, got {:?}", value.kind); + }; + let ExprKind::Application(function, argument) = &partial.kind else { + panic!("expected the bind function to be applied to the effect"); + }; + assert!(matches!(&function.kind, ExprKind::Name(name) if name.text == "bind")); + assert!(matches!(&argument.kind, ExprKind::Name(name) if name.text == "effect")); + let ExprKind::Lambda { binder, body } = &continuation.kind else { + panic!("expected a continuation lambda"); + }; + assert_eq!(binder.name, "x"); + assert!(matches!(&body.kind, ExprKind::Name(name) if name.text == "rest")); + assert_eq!(value.span, TextRange::new(30, 90)); +} + +#[test] +fn lowers_a_do_discard_to_discard_and_a_wildcard_lambda() { + let statements = vec![ + cst::DoStatement::Discard(cst_expr(CstExprKind::Name(name("effect", 40)), 40, 46)), + cst::DoStatement::Discard(cst_expr(CstExprKind::Name(name("rest", 48)), 48, 52)), + ]; + let value = lower_do_blocks(statements).unwrap(); + let ExprKind::Application(partial, continuation) = &value.kind else { + panic!("expected a discard application"); + }; + let ExprKind::Application(function, _) = &partial.kind else { + panic!("expected the discard function to be applied to the effect"); + }; + assert!(matches!(&function.kind, ExprKind::Name(name) if name.text == "discard")); + let ExprKind::Lambda { binder, .. } = &continuation.kind else { + panic!("expected a wildcard continuation lambda"); + }; + assert!(binder.name.starts_with("__psrs_do_wildcard_")); +} + +#[test] +fn a_final_do_value_is_the_result_without_pure() { + let statements = vec![cst::DoStatement::Discard(cst_expr( + CstExprKind::Name(name("result", 40)), + 40, + 46, + ))]; + let value = lower_do_blocks(statements).unwrap(); + assert!(matches!(&value.kind, ExprKind::Name(name) if name.text == "result")); + assert_eq!(value.span, TextRange::new(30, 90)); +} + +#[test] +fn lowers_a_non_variable_do_binder_through_a_case() { + let constructor = Pattern { + kind: PatternKind::Constructor { + name: name("Unit", 40), + arguments: Vec::new(), + }, + span: TextRange::new(40, 44), + }; + let statements = vec![ + cst::DoStatement::Bind { + pattern: constructor, + left_arrow_span: TextRange::new(45, 47), + value: cst_expr(CstExprKind::Name(name("effect", 48)), 48, 54), + }, + cst::DoStatement::Discard(cst_expr(CstExprKind::Name(name("rest", 56)), 56, 60)), + ]; + let value = lower_do_blocks(statements).unwrap(); + let ExprKind::Application(_, continuation) = &value.kind else { + panic!("expected a bind application"); + }; + let ExprKind::Lambda { binder, body } = &continuation.kind else { + panic!("expected a continuation lambda"); + }; + assert!(binder.name.starts_with("$psrs_pattern_")); + let ExprKind::Case { + scrutinee, + branches, + } = &body.kind + else { + panic!("expected a case for the non-variable binder"); + }; + assert!(matches!(&scrutinee.kind, ExprKind::Name(name) if name.text == binder.name)); + assert_eq!(branches.len(), 1); + assert!(matches!( + &branches[0].pattern.kind, + crate::PatternKind::Constructor { name, arguments } + if name.text == "Unit" && arguments.is_empty() + )); +} + +#[test] +fn lowers_a_do_let_to_a_let_around_the_rest() { + let binding = value_declaration( + "y", + 40, + Vec::new(), + TextRange::new(42, 43), + cst_expr(CstExprKind::Integer("1".into()), 44, 45), + 45, + None, + ); + let statements = vec![ + cst::DoStatement::Let { + let_keyword_span: TextRange::new(32, 35), + declarations: vec![binding], + layout_start_span: TextRange::empty(36), + layout_end_span: TextRange::empty(45), + }, + cst::DoStatement::Discard(cst_expr(CstExprKind::Name(name("rest", 48)), 48, 52)), + ]; + let value = lower_do_blocks(statements).unwrap(); + let ExprKind::Let { declarations, body } = &value.kind else { + panic!("expected a let around the rest"); + }; + assert_eq!(declarations.len(), 1); + assert_eq!(declarations[0].name.text, "y"); + assert!(matches!(&body.kind, ExprKind::Name(name) if name.text == "rest")); +} + +#[test] +fn rejects_an_empty_do_block() { + let error = lower_do_blocks(Vec::new()).unwrap_err(); + assert_eq!(error[0].message, "an empty `do` block is not allowed"); + assert!(error[0].code.is_none()); +} + +#[test] +fn rejects_a_do_block_ending_in_a_bind() { + let statements = vec![cst::DoStatement::Bind { + pattern: pattern_var("x", 40), + left_arrow_span: TextRange::new(42, 44), + value: cst_expr(CstExprKind::Name(name("effect", 45)), 45, 51), + }]; + let error = lower_do_blocks(statements).unwrap_err(); + assert_eq!(error[0].code, Some("InvalidDoBind")); +} + +#[test] +fn rejects_a_do_block_ending_in_a_let() { + let binding = value_declaration( + "y", + 40, + Vec::new(), + TextRange::new(42, 43), + cst_expr(CstExprKind::Integer("1".into()), 44, 45), + 45, + None, + ); + let statements = vec![cst::DoStatement::Let { + let_keyword_span: TextRange::new(32, 35), + declarations: vec![binding], + layout_start_span: TextRange::empty(36), + layout_end_span: TextRange::empty(45), + }]; + let error = lower_do_blocks(statements).unwrap_err(); + assert_eq!(error[0].code, Some("InvalidDoLet")); +} diff --git a/crates/psrs-ast/src/tests/mod.rs b/crates/psrs-ast/src/tests/mod.rs new file mode 100644 index 00000000..a698a2b3 --- /dev/null +++ b/crates/psrs-ast/src/tests/mod.rs @@ -0,0 +1,225 @@ +use super::*; +use psrs_cst::{ + CstName, Declaration as CstDeclaration, Module as CstModule, Pattern, PatternKind, + TypeVarBinder, ValueDeclaration, ValueRhs, +}; +use psrs_span::TextRange; +mod do_blocks; +mod where_blocks; + +fn name(text: &str, start: u32) -> CstName { + CstName { + text: text.into(), + span: TextRange::new(start, start + text.len() as u32), + } +} + +fn pattern_var(text: &str, start: u32) -> Pattern { + Pattern { + kind: PatternKind::Var(name(text, start)), + span: TextRange::new(start, start + text.len() as u32), + } +} + +fn type_var_binder(text: &str, start: u32) -> TypeVarBinder { + TypeVarBinder { + name: name(text, start), + kind: None, + span: TextRange::new(start, start + text.len() as u32), + } +} + +fn cst_module(declaration: CstDeclaration) -> CstModule { + CstModule { + module_keyword_span: TextRange::new(0, 6), + name: name("Main", 7), + exports: None, + where_keyword_span: TextRange::new(12, 17), + imports: Vec::new(), + declarations: vec![declaration], + span: TextRange::new(0, 40), + } +} + +fn value_declaration( + name_text: &str, + name_start: u32, + parameters: Vec, + equals_span: TextRange, + value: cst::Expr, + span_end: u32, + annotation: Option, +) -> CstDeclaration { + CstDeclaration::Value(ValueDeclaration { + name: name(name_text, name_start), + parameters, + rhs: ValueRhs::Plain { equals_span, value }, + where_block: None, + span: TextRange::new(name_start, span_end), + annotation, + }) +} + +#[test] +fn function_parameters_become_nested_lambdas_and_names_stay_unresolved() { + let value = cst::Expr { + kind: CstExprKind::Operator { + operator: name("+", 33), + left: Box::new(cst::Expr { + kind: CstExprKind::Name(name("x", 31)), + span: TextRange::new(31, 32), + }), + right: Box::new(cst::Expr { + kind: CstExprKind::Name(name("y", 35)), + span: TextRange::new(35, 36), + }), + }, + span: TextRange::new(31, 36), + }; + let declaration = value_declaration( + "add", + 19, + vec![pattern_var("x", 23), pattern_var("y", 25)], + TextRange::new(27, 28), + value, + 36, + None, + ); + let module = lower_module(cst_module(declaration)).unwrap(); + let ExprKind::Lambda { binder, body } = &module.declarations[0].value.kind else { + panic!("expected the first normalized lambda"); + }; + assert_eq!(binder.name, "x"); + let ExprKind::Lambda { binder, body } = &body.kind else { + panic!("expected the second normalized lambda"); + }; + assert_eq!(binder.name, "y"); + let ExprKind::OperatorChain { + operands, + operators, + } = &body.kind + else { + panic!("expected the source operator chain to remain unresolved"); + }; + assert_eq!(operators.len(), 1); + assert_eq!(operators[0].name.text, "+"); + assert!(matches!(&operands[0].kind, ExprKind::Name(name) if name.text == "x")); + assert!(matches!(&operands[1].kind, ExprKind::Name(name) if name.text == "y")); +} + +#[test] +fn parentheses_are_removed_without_losing_the_expression_range() { + let declaration = value_declaration( + "main", + 18, + Vec::new(), + TextRange::new(23, 24), + cst::Expr { + kind: CstExprKind::Parens { + open_paren_span: TextRange::new(25, 26), + expression: Box::new(cst::Expr { + kind: CstExprKind::Integer("42".into()), + span: TextRange::new(26, 28), + }), + close_paren_span: TextRange::new(28, 29), + }, + span: TextRange::new(25, 29), + }, + 29, + None, + ); + let module = lower_module(cst_module(declaration)).unwrap(); + let value = &module.declarations[0].value; + assert!(matches!(value.kind, ExprKind::Integer(ref value) if value == "42")); + assert_eq!(value.span, TextRange::new(25, 29)); +} + +#[test] +fn anonymous_record_accessor_is_lowered_to_a_lambda() { + let underscore_span = TextRange::new(25, 26); + let field_span = TextRange::new(27, 32); + let expression = cst::Expr { + kind: CstExprKind::RecordAccessor { + marker_span: underscore_span, + fields: vec![cst::RecordAccessorField { + dot_span: TextRange::new(26, 27), + field: name("value", field_span.start), + }], + }, + span: TextRange::new(25, 32), + }; + let declaration = value_declaration( + "project", + 18, + Vec::new(), + TextRange::new(23, 24), + expression, + 32, + None, + ); + let module = lower_module(cst_module(declaration)).unwrap(); + let ExprKind::Lambda { binder, body } = &module.declarations[0].value.kind else { + panic!("an anonymous record accessor should become a lambda"); + }; + assert_eq!(module.declarations[0].value.span, TextRange::new(25, 32)); + let ExprKind::FieldAccess { expression, field } = &body.kind else { + panic!("the accessor lambda should contain a field access"); + }; + assert_eq!(field, "value"); + assert!(matches!( + &expression.kind, + ExprKind::Name(name) if name.text == binder.name + )); +} + +#[test] +fn lowers_forall_types_and_removes_parentheses() { + let annotation = cst::TypeExpr { + kind: cst::TypeExprKind::Forall { + forall_span: TextRange::new(0, 6), + variables: vec![type_var_binder("a", 7)], + dot_span: TextRange::new(8, 9), + body: Box::new(cst::TypeExpr { + kind: cst::TypeExprKind::Parens { + open_paren_span: TextRange::new(10, 11), + expression: Box::new(cst::TypeExpr { + kind: cst::TypeExprKind::Name(name("a", 11)), + span: TextRange::new(11, 12), + }), + close_paren_span: TextRange::new(12, 13), + }, + span: TextRange::new(10, 13), + }), + }, + span: TextRange::new(0, 13), + }; + let declaration = value_declaration( + "id", + 19, + vec![pattern_var("x", 23)], + TextRange::new(25, 26), + cst::Expr { + kind: CstExprKind::Name(name("x", 27)), + span: TextRange::new(27, 28), + }, + 28, + Some(annotation), + ); + let module = lower_module(cst_module(declaration)).unwrap(); + let annotation = module.declarations[0].annotation.as_ref().unwrap(); + let TypeKind::Forall { variables, body } = &annotation.kind else { + panic!("expected a lowered forall"); + }; + assert_eq!(variables.len(), 1); + assert_eq!(variables[0].name.text, "a"); + assert!(variables[0].kind.is_none()); + assert!(matches!(&body.kind, TypeKind::Name(name) if name.text == "a")); + assert_eq!(body.span, TextRange::new(10, 13)); +} + +fn cst_expr(kind: CstExprKind, start: u32, end: u32) -> cst::Expr { + cst::Expr { + kind, + span: TextRange::new(start, end), + } +} diff --git a/crates/psrs-ast/src/tests/where_blocks.rs b/crates/psrs-ast/src/tests/where_blocks.rs new file mode 100644 index 00000000..54771ed9 --- /dev/null +++ b/crates/psrs-ast/src/tests/where_blocks.rs @@ -0,0 +1,104 @@ +use super::*; + +fn declaration_block( + declarations: Vec, + start: u32, + end: u32, +) -> cst::DeclarationBlock { + cst::DeclarationBlock { + where_keyword_span: TextRange::new(start, start + 5), + layout_start_span: TextRange::empty(start + 5), + declarations, + layout_end_span: TextRange::empty(end), + span: TextRange::new(start, end), + } +} + +#[test] +fn lowers_a_value_where_block_to_a_let_inside_the_parameters() { + let binding = value_declaration( + "y", + 40, + Vec::new(), + TextRange::new(42, 43), + cst_expr(CstExprKind::Integer("1".into()), 44, 45), + 45, + None, + ); + let declaration = CstDeclaration::Value(ValueDeclaration { + name: name("f", 30), + parameters: vec![pattern_var("x", 32)], + rhs: ValueRhs::Plain { + equals_span: TextRange::new(34, 35), + value: cst_expr(CstExprKind::Name(name("y", 36)), 36, 37), + }, + where_block: Some(declaration_block(vec![binding], 38, 50)), + span: TextRange::new(30, 50), + annotation: None, + }); + let module = lower_module(cst_module(declaration)).unwrap(); + let ExprKind::Lambda { binder, body } = &module.declarations[0].value.kind else { + panic!("expected the parameter lambda"); + }; + assert_eq!(binder.name, "x"); + let ExprKind::Let { declarations, body } = &body.kind else { + panic!("expected the where declarations as a let inside the parameter"); + }; + assert_eq!(declarations.len(), 1); + assert_eq!(declarations[0].name.text, "y"); + assert!(matches!(&body.kind, ExprKind::Name(name) if name.text == "y")); +} + +#[test] +fn lowers_a_case_where_block_to_a_let_in_the_branch() { + let binding = value_declaration( + "y", + 40, + Vec::new(), + TextRange::new(42, 43), + cst_expr(CstExprKind::Name(name("x", 44)), 44, 45), + 45, + None, + ); + let alternative = cst::CaseAlternative { + patterns: vec![pattern_var("x", 33)], + rhs: cst::CaseRhs::Plain { + arrow_span: TextRange::new(35, 37), + value: cst_expr(CstExprKind::Name(name("y", 38)), 38, 39), + where_block: Some(declaration_block(vec![binding], 40, 50)), + }, + span: TextRange::new(33, 50), + }; + let case = cst_expr( + CstExprKind::Case { + case_keyword_span: TextRange::new(20, 24), + scrutinees: vec![cst_expr(CstExprKind::Name(name("value", 25)), 25, 30)], + of_keyword_span: TextRange::new(31, 33), + layout_start_span: TextRange::empty(33), + alternatives: vec![alternative], + layout_end_span: TextRange::empty(50), + }, + 20, + 50, + ); + let declaration = value_declaration( + "main", + 18, + Vec::new(), + TextRange::new(24, 25), + case, + 50, + None, + ); + let module = lower_module(cst_module(declaration)).unwrap(); + let ExprKind::Case { branches, .. } = &module.declarations[0].value.kind else { + panic!("expected a case expression"); + }; + assert_eq!(branches.len(), 1); + let ExprKind::Let { declarations, body } = &branches[0].value.kind else { + panic!("expected the where declarations as a let around the branch body"); + }; + assert_eq!(declarations.len(), 1); + assert_eq!(declarations[0].name.text, "y"); + assert!(matches!(&body.kind, ExprKind::Name(name) if name.text == "y")); +} diff --git a/crates/psrs-ast/src/type_decl.rs b/crates/psrs-ast/src/type_decl.rs index e4028d5d..e97fb526 100644 --- a/crates/psrs-ast/src/type_decl.rs +++ b/crates/psrs-ast/src/type_decl.rs @@ -336,7 +336,8 @@ fn collect_parameters( if let cst::TypeExprKind::Application(function, arguments) = &expression.kind { collect_parameters(function, out)?; for argument in arguments { - match &strip_parens(argument).kind { + let argument = strip_parens(argument); + match &argument.kind { cst::TypeExprKind::Name(name) if is_type_variable(&name.text) => { out.push(TypeParameter { name: lower_name(name.clone()), @@ -360,6 +361,23 @@ fn collect_parameters( }); } } + // In a class head, `(a :: k)` is a kinded class parameter. + // The CST parser represents this parenthesized form as a + // one-field row because the same token sequence also spells a + // row type. At this boundary the class-head context makes the + // binder interpretation explicit. + cst::TypeExprKind::Row { fields, tail, .. } + if tail.is_none() + && fields.len() == 1 + && is_type_variable(&fields[0].label.text) => + { + let field = &fields[0]; + out.push(TypeParameter { + name: lower_name(field.label.clone()), + kind: Some(lower_type(field.type_expr.clone())?), + span: field.label.span, + }); + } _ => {} } } diff --git a/crates/psrs-backend/Cargo.toml b/crates/psrs-backend/Cargo.toml index 503c3bc2..6ea7c20c 100644 --- a/crates/psrs-backend/Cargo.toml +++ b/crates/psrs-backend/Cargo.toml @@ -8,6 +8,8 @@ license.workspace = true psrs-core.workspace = true psrs-hir.workspace = true psrs-span.workspace = true +psrs-linker.workspace = true +psrs-runtime = { workspace = true, default-features = false, features = ["catalog", "gc-storage"] } wasm-encoder.workspace = true wasmparser.workspace = true wasmprinter.workspace = true diff --git a/crates/psrs-backend/src/abi/canonical/control.rs b/crates/psrs-backend/src/abi/canonical/control.rs new file mode 100644 index 00000000..032d846a --- /dev/null +++ b/crates/psrs-backend/src/abi/canonical/control.rs @@ -0,0 +1,91 @@ +//! Control callbacks cannot store call-scoped canonical values. +use super::{CanonicalType, Ownership}; + +impl CanonicalType { + /// A borrowed handle, or any aggregate that contains one. + /// + /// Call-scoped `borrow` cannot be stored in a software continuation: the + /// host releases it when the canonical call returns. Owned handles and + /// GC-copied strings/lists are not this predicate; they have a different + /// lifetime owner. + pub(crate) fn contains_borrowed_handle(&self) -> bool { + match self { + Self::Handle { + ownership: Ownership::Borrow, + .. + } => true, + Self::List(inner) | Self::Option(inner) => inner.contains_borrowed_handle(), + Self::FixedList { element, .. } => element.contains_borrowed_handle(), + Self::Record(fields) => fields + .iter() + .any(|field| field.ty.contains_borrowed_handle()), + Self::Variant(cases) => cases.iter().any(|case| { + case.payload + .as_deref() + .is_some_and(Self::contains_borrowed_handle) + }), + Self::Result { ok, err } => { + ok.as_deref().is_some_and(Self::contains_borrowed_handle) + || err.as_deref().is_some_and(Self::contains_borrowed_handle) + } + _ => false, + } + } +} + +/// Control callbacks may not close over a borrowed canonical handle. +pub(crate) fn reject_borrowed_control_capture<'a>( + types: impl IntoIterator, +) -> Result<(), &'static str> { + if types + .into_iter() + .any(CanonicalType::contains_borrowed_handle) + { + Err("control callback cannot capture a borrowed host resource handle") + } else { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::super::ResourceId; + use super::*; + use psrs_hir::{ModuleId, SymbolId}; + + fn handle(borrowed: bool) -> CanonicalType { + CanonicalType::Handle { + resource: ResourceId { + interface: "fixture:io/files@0.1.0".into(), + name: "file".into(), + }, + ownership: if borrowed { + Ownership::Borrow + } else { + Ownership::Own { + drop: SymbolId::new(ModuleId(0), 1), + } + }, + } + } + + #[test] + fn borrowed_handles_are_unsupported_control_captures() { + let borrowed = handle(true); + let owned = handle(false); + assert!(borrowed.contains_borrowed_handle()); + assert!(CanonicalType::Option(Box::new(borrowed.clone())).contains_borrowed_handle()); + assert!(CanonicalType::List(Box::new(borrowed.clone())).contains_borrowed_handle()); + assert!( + CanonicalType::Record(vec![super::super::CanonicalField { + name: "file".into(), + ty: borrowed.clone(), + }]) + .contains_borrowed_handle() + ); + assert!(reject_borrowed_control_capture([&borrowed]).is_err()); + assert!(!owned.contains_borrowed_handle()); + assert!(!CanonicalType::String.contains_borrowed_handle()); + assert!(reject_borrowed_control_capture([&owned, &CanonicalType::String]).is_ok()); + } +} diff --git a/crates/psrs-backend/src/abi/canonical/mod.rs b/crates/psrs-backend/src/abi/canonical/mod.rs index 8fd7ba4b..7268b3f2 100644 --- a/crates/psrs-backend/src/abi/canonical/mod.rs +++ b/crates/psrs-backend/src/abi/canonical/mod.rs @@ -10,6 +10,7 @@ //! conformance, and MIR lowering all read a [`CanonicalType`] directly. //! `dead_code` is not allowed; every operation has a caller. +mod control; mod flatten; mod leaves; mod memory; @@ -19,6 +20,7 @@ mod resolve; #[cfg(test)] mod tests; +pub(crate) use control::reject_borrowed_control_capture; pub(crate) use flatten::flatten; pub(crate) use leaves::{FlatLeaf, flat_leaves_of, handle_at_flat_index}; #[allow(unused_imports)] diff --git a/crates/psrs-backend/src/abi/canonical/tests.rs b/crates/psrs-backend/src/abi/canonical/tests.rs index 5dc5ce0f..d3c4d126 100644 --- a/crates/psrs-backend/src/abi/canonical/tests.rs +++ b/crates/psrs-backend/src/abi/canonical/tests.rs @@ -70,7 +70,7 @@ fn assert_matches_oracle(resolve: &Resolve, function: &Function) { #[test] fn flatten_matches_wasm_signature_for_every_wasi_function() { let mut resolve = Resolve::default(); - crate::component::load_vendored_wasi(&mut resolve).expect("vendored WASI should load"); + crate::abi::load_wit(&mut resolve).expect("vendored WASI should load"); let mut checked = 0; for (_, interface) in resolve.interfaces.iter() { diff --git a/crates/psrs-backend/src/abi/definitions.rs b/crates/psrs-backend/src/abi/definitions.rs new file mode 100644 index 00000000..356382e4 --- /dev/null +++ b/crates/psrs-backend/src/abi/definitions.rs @@ -0,0 +1,57 @@ +//! Pinned WIT loading and permitted-interface derivation. + +use std::collections::HashSet; +use wit_parser::Resolve; + +/// Pushes the runtime catalog's pinned WIT into `resolve` in dependency order. +pub(crate) fn load_wit(resolve: &mut Resolve) -> Result<(), String> { + for source in psrs_runtime::WASI_WIT { + resolve + .push_str(source.path, source.contents) + .map_err(|error| format!("invalid vendored WIT `{}`: {error}", source.path))?; + } + let app = psrs_runtime::APP_WIT; + resolve + .push_str(app.path, app.contents) + .map_err(|error| format!("invalid application WIT: {error}"))?; + Ok(()) +} + +/// The canonical ids the default world permits. A resolve that omits the +/// application world (an isolated ABI fixture) admits every interface it +/// defines; production resolves always include the world. +pub(super) fn supported_interfaces(resolve: &Resolve) -> HashSet { + let world = resolve + .packages + .iter() + .find(|(_, package)| { + package.name.namespace == psrs_runtime::DEFAULT_WORLD.package_namespace + && package.name.name == psrs_runtime::DEFAULT_WORLD.package_name + }) + .and_then(|(_, package)| { + package + .worlds + .get(psrs_runtime::DEFAULT_WORLD.world) + .copied() + }); + let mut supported = HashSet::new(); + match world { + Some(world) => { + for item in resolve.worlds[world].imports.values() { + if let wit_parser::WorldItem::Interface { id, .. } = item + && let Some(canonical) = resolve.id_of(*id) + { + supported.insert(canonical); + } + } + } + None => { + for (id, _) in resolve.interfaces.iter() { + if let Some(canonical) = resolve.id_of(id) { + supported.insert(canonical); + } + } + } + } + supported +} diff --git a/crates/psrs-backend/src/abi/mod.rs b/crates/psrs-backend/src/abi/mod.rs index d4cc20bc..bf0e0233 100644 --- a/crates/psrs-backend/src/abi/mod.rs +++ b/crates/psrs-backend/src/abi/mod.rs @@ -6,26 +6,34 @@ use crate::TargetCapabilities; use crate::types::ValueType; use psrs_hir::{ModuleId, SymbolId}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; use wit_parser::Resolve; use wit_parser::abi::AbiVariant; pub(crate) mod canonical; +mod definitions; mod handles; pub(crate) mod layout; pub(crate) mod link; +mod reserved; #[cfg(test)] mod tests; mod validation; +pub(crate) use reserved::*; + use canonical::{ CanonicalType, FnAbi, Ownership, function_abi, function_abi_from_types, resolve as resolve_canonical, }; +pub(crate) use definitions::load_wit; +use definitions::supported_interfaces; pub use handles::{HandleMode, HandleResource}; #[cfg(test)] pub(crate) use link::intern_source_type; -use validation::{unsupported_shape, wasi_interface_enabled}; +use validation::unsupported_shape; +pub(crate) use validation::wasi_interface_enabled; /// Maps a resolved WIT core value type to the backend's value type. fn value_type(ty: wit_parser::abi::WasmType) -> Result { @@ -55,17 +63,16 @@ pub const SCRATCH_SIZE: u32 = 16; /// The first linear-memory offset after the scratch region. pub const SCRATCH_END: u32 = PRINT_SCRATCH as u32 + SCRATCH_SIZE; -/// The start of the allocator's heap-state segment: two pointer-width words -/// holding the free-list head and the bump break. It follows the scratch region. +/// Former generated-allocator state address. The runtime provider keeps no +/// state here, so the range is empty and the planner skips it. pub const HEAP_STATE: u32 = SCRATCH_END; -/// The byte size of the heap-state segment: a free-list head word and a bump -/// break word, each one wasm32 pointer word. -pub const HEAP_STATE_SIZE: u32 = 2 * WORD_SIZE; +/// The generated heap-state segment is not reserved. +pub const HEAP_STATE_SIZE: u32 = 0; /// The first address the canonical allocator may hand out. It is aligned to the -/// block granularity so every block header stays aligned. -pub const HEAP_START: u32 = (HEAP_STATE + HEAP_STATE_SIZE).next_multiple_of(MIN_BLOCK); +/// block granularity and begins immediately after the scratch region. +pub const HEAP_START: u32 = SCRATCH_END.next_multiple_of(MIN_BLOCK); /// A wasm32 address word. pub const WORD_SIZE: u32 = 4; @@ -77,34 +84,6 @@ pub const HEADER_SIZE: u32 = 8; /// (`i64`/`f64`), so block headers and payloads stay aligned. pub const MIN_BLOCK: u32 = 8; -/// Reserved MIR symbol for the allocator synthesized after ABI memory layout -/// is known. Calls to this symbol become calls to the local `cabi_realloc` -/// function during Wasm lowering; it is never emitted as a core import. -pub(crate) const REALLOC_SYMBOL: SymbolId = SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 1); - -/// Reserved symbols for the string boundary helpers at the canonical ABI. -/// P10 synthesizes them as ordinary local Wasm functions; like -/// `REALLOC_SYMBOL` they are never emitted as core imports. -pub(crate) const STRING_TO_BYTES_SYMBOL: SymbolId = - SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 2); -pub(crate) const BYTES_TO_STRING_SYMBOL: SymbolId = - SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 3); - -/// Reserved MIR symbol for the synthesized `validate_step` helper. Like the -/// other boundary symbols it is never a core import. -pub(crate) const VALIDATE_STEP_SYMBOL: SymbolId = SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 4); - -/// Intrinsic symbols the MIR lowering reserves for the canonical ABI and the -/// string boundary. Any other intrinsic-symbol allocator (for example the -/// aggregate conversion helpers, which allocate downward from `u32::MAX`) must -/// skip these. -pub(crate) const RESERVED_ABI_SYMBOLS: [SymbolId; 4] = [ - REALLOC_SYMBOL, - STRING_TO_BYTES_SYMBOL, - BYTES_TO_STRING_SYMBOL, - VALIDATE_STEP_SYMBOL, -]; - /// WASI interfaces and functions the backend itself references. The standard /// library names its own imports in source. pub mod names { @@ -163,10 +142,12 @@ impl WasiImport { /// Resolves WASI imports against the vendored WIT, interning each distinct /// `(module, name)` to a stable [`SymbolId`]. pub struct WasiRegistry { - resolve: Resolve, + resolve: Arc, imports: Vec, keys: HashMap<(String, String), usize>, target: TargetCapabilities, + /// Canonical ids of the interfaces the default world permits. + supported: HashSet, } impl WasiRegistry { @@ -175,11 +156,20 @@ impl WasiRegistry { const SYMBOL_BASE: u32 = 1 << 20; pub(crate) fn from_resolve(resolve: Resolve, target: TargetCapabilities) -> Self { + Self::from_shared_resolve(Arc::new(resolve), target) + } + + /// Builds a registry over the linker's resolved-world context. The same + /// parsed definitions are shared, so interface identities are not + /// regenerated. + pub(crate) fn from_shared_resolve(resolve: Arc, target: TargetCapabilities) -> Self { + let supported = supported_interfaces(&resolve); Self { resolve, imports: Vec::new(), keys: HashMap::new(), target, + supported, } } @@ -187,10 +177,11 @@ impl WasiRegistry { Self::load_with_capabilities(TargetCapabilities::default()) } - /// Loads the vendored WIT with the service families enabled by `target`. + /// Loads the pinned WIT from the runtime catalog with the service families + /// enabled by `target`. pub fn load_with_capabilities(target: TargetCapabilities) -> Result { let mut resolve = Resolve::default(); - crate::component::load_vendored_wasi(&mut resolve)?; + load_wit(&mut resolve)?; Ok(Self::from_resolve(resolve, target)) } @@ -279,7 +270,7 @@ impl WasiRegistry { }) }) .or_else(|| { - (!crate::component::component_interface_supported(&module)).then(|| { + (!self.supported.contains(&module)).then(|| { format!( "WASI interface `{module}` is not in the current component capability profile" ) @@ -356,6 +347,12 @@ impl WasiRegistry { &self.imports } + /// A shared handle to the parsed definitions this registry resolves + /// against. Isolated lowering fixtures wrap it in a permissive context. + pub(crate) fn shared_resolve(&self) -> Arc { + Arc::clone(&self.resolve) + } + /// The core import module and field for an interned import symbol. This is /// the only place the WIT interface and function names are resolved. pub fn symbol_name(&self, symbol: SymbolId) -> Option<(&str, &str)> { diff --git a/crates/psrs-backend/src/abi/reserved.rs b/crates/psrs-backend/src/abi/reserved.rs new file mode 100644 index 00000000..be4f0594 --- /dev/null +++ b/crates/psrs-backend/src/abi/reserved.rs @@ -0,0 +1,82 @@ +//! Symbols reserved from the high end of the intrinsic-symbol space. +//! +//! Allocators that count downward from `u32::MAX` must skip this set. The +//! numeric-runtime symbols are ordinary MIR imports of artifact exports. The +//! allocator symbol is a runtime operation lowered through a local forwarder. +//! String-boundary symbols are synthesized locally and are never core imports. + +use psrs_hir::{ModuleId, SymbolId}; + +/// Reserved MIR symbol for canonical `cabi_realloc`. +/// +/// Linking selects the allocator runtime unit. Wasm lowering imports that +/// export and emits a local forwarder; codecs and cleanup call the forwarder. +pub(crate) const REALLOC_SYMBOL: SymbolId = SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 1); + +/// Reserved symbols for the string boundary helpers at the canonical ABI. +/// P10 synthesizes them as ordinary local Wasm functions; like +/// `REALLOC_SYMBOL` they are never emitted as core imports. +pub(crate) const STRING_TO_BYTES_SYMBOL: SymbolId = + SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 2); +pub(crate) const BYTES_TO_STRING_SYMBOL: SymbolId = + SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 3); + +/// Reserved MIR symbol for the synthesized `validate_step` helper. Like the +/// other boundary symbols it is never a core import. +pub(crate) const VALIDATE_STEP_SYMBOL: SymbolId = SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 4); + +pub(crate) const NUMBER_TO_STRING_SYMBOL: SymbolId = + SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 5); +pub(crate) const NUMBER_FROM_DECIMAL_SYMBOL: SymbolId = + SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 6); +pub(crate) const NUMBER_ACOS_SYMBOL: SymbolId = SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 7); +pub(crate) const NUMBER_ASIN_SYMBOL: SymbolId = SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 8); +pub(crate) const NUMBER_ATAN_SYMBOL: SymbolId = SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 9); +pub(crate) const NUMBER_ATAN2_SYMBOL: SymbolId = SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 10); +pub(crate) const NUMBER_SIN_SYMBOL: SymbolId = SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 11); +pub(crate) const NUMBER_COS_SYMBOL: SymbolId = SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 12); +pub(crate) const NUMBER_TAN_SYMBOL: SymbolId = SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 13); +pub(crate) const NUMBER_EXP_SYMBOL: SymbolId = SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 14); +pub(crate) const NUMBER_LOG_SYMBOL: SymbolId = SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 15); +pub(crate) const NUMBER_POW_SYMBOL: SymbolId = SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 16); +pub(crate) const NUMBER_MIN_SYMBOL: SymbolId = SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 17); +pub(crate) const NUMBER_MAX_SYMBOL: SymbolId = SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 18); +pub(crate) const NUMBER_SIGN_SYMBOL: SymbolId = SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 19); +pub(crate) const NUMBER_REMAINDER_SYMBOL: SymbolId = + SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 20); +pub(crate) const NUMBER_IS_NAN_SYMBOL: SymbolId = + SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 21); +pub(crate) const NUMBER_NAN_SYMBOL: SymbolId = SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 22); +pub(crate) const NUMBER_INFINITY_SYMBOL: SymbolId = + SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 23); + +/// Reserved symbol for the application-owned constant heap getter. +pub(crate) const HEAP_BASE_SYMBOL: SymbolId = SymbolId::new(ModuleId::INTRINSICS, u32::MAX - 24); + +/// Every symbol this allocator must not reuse. +pub(crate) const RESERVED_ABI_SYMBOLS: [SymbolId; 24] = [ + REALLOC_SYMBOL, + HEAP_BASE_SYMBOL, + STRING_TO_BYTES_SYMBOL, + BYTES_TO_STRING_SYMBOL, + VALIDATE_STEP_SYMBOL, + NUMBER_TO_STRING_SYMBOL, + NUMBER_FROM_DECIMAL_SYMBOL, + NUMBER_ACOS_SYMBOL, + NUMBER_ASIN_SYMBOL, + NUMBER_ATAN_SYMBOL, + NUMBER_ATAN2_SYMBOL, + NUMBER_SIN_SYMBOL, + NUMBER_COS_SYMBOL, + NUMBER_TAN_SYMBOL, + NUMBER_EXP_SYMBOL, + NUMBER_LOG_SYMBOL, + NUMBER_POW_SYMBOL, + NUMBER_MIN_SYMBOL, + NUMBER_MAX_SYMBOL, + NUMBER_SIGN_SYMBOL, + NUMBER_REMAINDER_SYMBOL, + NUMBER_IS_NAN_SYMBOL, + NUMBER_NAN_SYMBOL, + NUMBER_INFINITY_SYMBOL, +]; diff --git a/crates/psrs-backend/src/abi/tests/borrowed_results.rs b/crates/psrs-backend/src/abi/tests/borrowed_results.rs new file mode 100644 index 00000000..2f800c2b --- /dev/null +++ b/crates/psrs-backend/src/abi/tests/borrowed_results.rs @@ -0,0 +1,121 @@ +//! WIT resolution owns recursive borrowed-result rejection. The compiler must +//! not erase that rule into a successful-looking source value. Parameters keep +//! their borrowing metadata; owned results retain the explicit-drop contract. +use super::super::{ + WasiRegistry, + canonical::{CanonicalType as T, Ownership}, +}; + +fn source(result: &str, parameter: bool, owned: bool) -> String { + let ownership = if owned { "own" } else { "borrow" }; + let signature = if parameter { + format!("use-value: func(value: {result});") + } else { + format!("get: func() -> {result};") + }; + format!( + "package wasi:io@0.2.12; interface streams {{ resource file; record payload {{ value: {ownership} }} variant envelope {{ absent, value({ownership}) }} {signature} }}" + ) +} + +#[test] +fn wit_resolution_rejects_borrowed_results_at_every_supported_nesting_form() { + for payload in [ + "borrow", + "payload", + "envelope", + "tuple", + "list", + ] { + for result in [ + payload.to_owned(), + format!("option<{payload}>"), + format!("result<{payload}, u32>"), + format!("result"), + format!("option>"), + ] { + let mut resolve = wit_parser::Resolve::default(); + let error = resolve + .push_str("borrow-result.wit", &source(&result, false, false)) + .expect_err( + "borrowed result must fail WIT resolution before canonical/source erasure", + ); + let message = format!("{error:#}"); + assert!( + message.contains("function returns a type which contains a `borrow`"), + "{result}: unexpected rejection owner: {message}" + ); + } + } +} + +fn registry(ty: &str, parameter: bool, owned: bool) -> WasiRegistry { + let mut resolve = wit_parser::Resolve::default(); + resolve + .push_str("resource-boundary.wit", &source(ty, parameter, owned)) + .expect("positive WIT declaration must resolve"); + WasiRegistry::from_resolve(resolve, crate::TargetCapabilities::default()) +} + +fn contains_borrow(ty: &T) -> bool { + match ty { + T::Handle { + ownership: Ownership::Borrow, + .. + } => true, + T::Record(fields) => fields.iter().any(|field| contains_borrow(&field.ty)), + T::Variant(cases) => cases + .iter() + .any(|case| case.payload.as_deref().is_some_and(contains_borrow)), + T::Option(payload) => contains_borrow(payload), + T::Result { ok, err } => { + ok.as_deref().is_some_and(contains_borrow) + || err.as_deref().is_some_and(contains_borrow) + } + _ => false, + } +} + +#[test] +fn borrowed_parameters_preserve_their_scoped_canonical_metadata() { + for ty in [ + "borrow", + "payload", + "envelope", + "option", + "result", + "tuple", + ] { + let mut registry = registry(ty, true, false); + let import = registry.import("wasi:io/streams", "use-value").unwrap(); + assert_eq!(import.unsupported, None, "{ty}"); + assert_eq!(import.params.len(), 1); + assert!( + contains_borrow(&import.params[0]), + "borrow metadata disappeared inside {ty}" + ); + } +} + +#[test] +fn owned_aggregate_results_remain_supported_with_explicit_drops() { + for ty in [ + "own", + "payload", + "envelope", + "option", + "result", + "tuple", + ] { + let mut registry = registry(ty, false, true); + let import = registry.import("wasi:io/streams", "get").unwrap(); + assert_eq!(import.unsupported, None, "{ty}"); + assert!(!contains_borrow( + import.canonical_result.as_ref().expect("owned result") + )); + let drop = registry + .import("wasi:io/streams", "[resource-drop]file") + .unwrap(); + assert_eq!(drop.unsupported, None); + } +} diff --git a/crates/psrs-backend/src/abi/tests/mod.rs b/crates/psrs-backend/src/abi/tests/mod.rs index 0affdbce..a340165b 100644 --- a/crates/psrs-backend/src/abi/tests/mod.rs +++ b/crates/psrs-backend/src/abi/tests/mod.rs @@ -5,6 +5,7 @@ use psrs_core::{Type as CoreType, TypeConstructor as CoreTypeConstructor, TypeId use wit_parser::Type as WitType; mod aggregates; +mod borrowed_results; mod capability_gates; mod enums; mod indirect; diff --git a/crates/psrs-backend/src/abi/validation.rs b/crates/psrs-backend/src/abi/validation.rs index 50e09d2f..e34a4954 100644 --- a/crates/psrs-backend/src/abi/validation.rs +++ b/crates/psrs-backend/src/abi/validation.rs @@ -136,22 +136,6 @@ pub(super) fn enum_cases( ) } -pub(super) fn wasi_interface_enabled(target: TargetCapabilities, module: &str) -> bool { - let package_path = module - .split_once('/') - .map_or(module, |(package, _)| package); - let package = package_path - .split_once('@') - .map_or(package_path, |(package, _)| package); - match package { - "wasi:cli" => target.wasi_cli, - "wasi:io" => target.wasi_io, - "wasi:clocks" => target.wasi_clocks, - "wasi:random" => target.wasi_random, - "wasi:filesystem" => target.wasi_filesystem, - "wasi:sockets" => target.wasi_sockets, - "wasi:http" => target.wasi_http, - "wasi:tls" => target.wasi_tls, - _ => false, - } +pub(crate) fn wasi_interface_enabled(target: TargetCapabilities, module: &str) -> bool { + target.wasi_interface_enabled(module) } diff --git a/crates/psrs-backend/src/bindings/mod.rs b/crates/psrs-backend/src/bindings/mod.rs index 23002ac9..81477c1d 100644 --- a/crates/psrs-backend/src/bindings/mod.rs +++ b/crates/psrs-backend/src/bindings/mod.rs @@ -6,6 +6,18 @@ use psrs_core::{Module as CoreModule, TypeId as CoreTypeId}; use psrs_hir::{ExternalKind, ModuleId, SymbolId}; use std::collections::{HashMap, HashSet}; +mod primitives; +mod runtime; +pub(crate) use primitives::lower as lower_primitives; +pub(crate) use runtime::call::canonical_signature as runtime_cc_signature; +pub use runtime::metadata::RuntimeBinding; +pub(crate) use runtime::metadata::{ + from_core as runtime_metadata_from_core, validate_core as validate_runtime_bindings, +}; +pub(crate) use runtime::target::{ + plan_import as plan_runtime_import, verify_import as verify_runtime_import, +}; + /// The complete input consumed by P9. Platform binding metadata is kept beside /// CC rather than embedded in the CC module itself. #[derive(Clone, Debug, PartialEq, Eq)] @@ -18,6 +30,7 @@ pub struct BackendInput { #[derive(Clone, Debug, PartialEq, Eq, Default)] pub struct ExternalBindings { pub imports: Vec, + pub runtime: Vec, } impl ExternalBindings { @@ -57,7 +70,10 @@ impl ExternalBindings { }) }) .collect(); - Self { imports } + Self { + imports, + runtime: runtime_metadata_from_core(module), + } } /// Resolves and validates every source WIT binding against the vendored WIT @@ -118,6 +134,39 @@ impl ExternalBindings { /// cannot accidentally make a target binding disappear by supplying a /// partial table. pub(crate) fn validate_core(&self, module: &CoreModule) -> Result<(), Vec> { + validate_runtime_bindings(&self.runtime, module)?; + runtime::validate(module)?; + let unsupported = module + .externals + .iter() + .filter_map(|external| { + let ExternalKind::Library { module: owner } = &external.kind else { + return None; + }; + let mut error = BackendError::new( + "P8 library linking", + external + .signature + .as_ref() + .map_or(module.span, |ty| ty.span), + format!( + "foreign value `{owner}.{}` has no target implementation", + external.name + ), + ); + if let Some(checked) = module + .external_types + .iter() + .find(|checked| checked.symbol == external.symbol) + { + error = error.with_module(checked.source_module); + } + Some(error) + }) + .collect::>(); + if !unsupported.is_empty() { + return Err(unsupported); + } let expected = module .externals .iter() @@ -166,9 +215,9 @@ impl ExternalBindings { } } - /// Checks that P9 receives the same abstract signature that CC used when - /// type-checking calls. WIT names remain in this table, while CC sees only - /// the resulting target-neutral signature. + /// Checks that the binding inventory covers CC externals exactly once. + /// Source conformance is checked against Core before CC publication; + /// runtime operands must agree with the canonical CC call signature. pub(crate) fn validate_cc(&self, module: &cc::Module) -> Result<(), Vec> { let mut cc_externals = HashMap::new(); let mut errors = Vec::new(); @@ -199,6 +248,28 @@ impl ExternalBindings { )); } } + for binding in &self.runtime { + if !seen.insert(binding.symbol) { + errors.push( + BackendError::invalid_ir( + "P9 external binding validation", + binding.span, + "runtime external binding is duplicated or conflicts with a WIT binding", + ) + .with_module(binding.source_module), + ); + } + if !cc_externals.contains_key(&binding.symbol) { + errors.push( + BackendError::invalid_ir( + "P9 external binding validation", + binding.span, + "runtime external binding is absent from CC", + ) + .with_module(binding.source_module), + ); + } + } for external in cc_externals.values() { if !seen.contains(&external.symbol) { errors.push(BackendError::new( @@ -208,11 +279,13 @@ impl ExternalBindings { )); } } - if errors.is_empty() { - Ok(()) - } else { - Err(errors) + if !errors.is_empty() { + return Err(errors); } + for binding in &self.runtime { + runtime::call::checked(binding, module)?; + } + Ok(()) } } diff --git a/crates/psrs-backend/src/bindings/primitives/mod.rs b/crates/psrs-backend/src/bindings/primitives/mod.rs new file mode 100644 index 00000000..9af5a7f3 --- /dev/null +++ b/crates/psrs-backend/src/bindings/primitives/mod.rs @@ -0,0 +1,205 @@ +//! Discharges explicit primitive bindings into verified ordinary Core functions. + +use crate::BackendError; +use psrs_core::{Binder, Declaration, Expr, ExprKind, Module, arrow_parts, scheme_parts}; +use psrs_hir::{ExternalKind, Intrinsic, IntrinsicCategory, LocalId}; + +#[cfg(test)] +mod tests; + +pub(crate) fn lower(module: &mut Module, source: Option<&Module>) -> Result<(), Vec> { + let bindings = module + .externals + .iter() + .filter_map(|external| { + let ExternalKind::Primitive(intrinsic) = external.kind else { + return None; + }; + Some((external.clone(), intrinsic)) + }) + .collect::>(); + module + .verify_with_source(source.unwrap_or(module)) + .map_err(|errors| { + errors + .into_iter() + .map(|error| { + BackendError::invalid_ir("P8 primitive linking", error.span, error.message) + .with_module(error.module) + }) + .collect::>() + })?; + // Work on a complete candidate. Failed validation must not publish a + // partially discharged external table or generated declaration. + let mut candidate = module.clone(); + // Each generated function is closed over only its typed parameters and + // the registry operation. Validate these independent units with the full + // type/representation facts, without rechecking every source body for + // each binding. The common CC entry validates the complete output module. + let mut probe = module.clone(); + probe.declarations.clear(); + probe.entry = None; + // Compiler-interface globals and explicit foreign primitive bindings share + // the same checked value elaboration. A primitive used as an alias or a + // partial application still needs expansion when there are no externals. + let operations = Intrinsic::ALL + .iter() + .map(|intrinsic| (intrinsic.symbol(), *intrinsic)) + .chain( + bindings + .iter() + .map(|(external, intrinsic)| (external.symbol, *intrinsic)), + ) + .collect(); + for (external, intrinsic) in bindings { + let span = external + .signature + .as_ref() + .map_or(module.span, |ty| ty.span); + let Some(checked) = module + .external_types + .iter() + .find(|ty| ty.symbol == external.symbol) + else { + return Err(vec![BackendError::new( + "P8 primitive linking", + span, + "primitive binding has no checked source signature", + )]); + }; + let error = |message: String| { + vec![ + BackendError::new("P8 primitive linking", span, message) + .with_module(checked.source_module), + ] + }; + if !matches!( + intrinsic.descriptor().category, + IntrinsicCategory::Unary + | IntrinsicCategory::BinaryScalar + | IntrinsicCategory::ArrayLength + | IntrinsicCategory::ArrayIndex + | IntrinsicCategory::ArrayUpdate + | IntrinsicCategory::ArrayAppend + | IntrinsicCategory::ArrayFill + | IntrinsicCategory::ArrayWrite + | IntrinsicCategory::StringToBytes + | IntrinsicCategory::BytesToString + ) && intrinsic != Intrinsic::UnsafeCoerce + && !matches!(intrinsic, Intrinsic::NumberNaN | Intrinsic::NumberInfinity) + { + return Err(error(format!( + "primitive binding `{}` has no foreign-function implementation yet", + intrinsic.descriptor().name + ))); + } + let Some((quantified, body_type)) = scheme_parts(&candidate.types, checked.ty) else { + return Err(error( + "primitive binding has an invalid checked scheme".into(), + )); + }; + let mut result = body_type; + let mut parameters = Vec::new(); + let mut arrows = Vec::new(); + while let Some((parameter, tail)) = arrow_parts(&candidate.types, result) { + arrows.push(result); + parameters.push(Binder { + id: LocalId(parameters.len() as u32), + name: format!("primitive_argument_{}", parameters.len()), + ty: parameter, + span, + }); + result = tail; + } + if parameters.len() != intrinsic.descriptor().arity as usize { + return Err(error(format!( + "primitive binding `{}` does not match its declared function type", + intrinsic.descriptor().name + ))); + } + let mut value = psrs_core::primitive::primitive_value( + intrinsic, + parameters + .iter() + .map(|binder| Expr { + kind: ExprKind::Local(binder.id), + ty: binder.ty, + span, + }) + .collect(), + result, + span, + ); + for (binder, ty) in parameters.into_iter().zip(arrows).rev() { + value = Expr { + kind: ExprKind::Lambda { + binder, + body: Box::new(value), + }, + ty, + span, + }; + } + let declaration = Declaration { + symbol: external.symbol, + name: external.name, + name_span: span, + quantified, + ty: body_type, + value, + span, + }; + probe.declarations.push(declaration.clone()); + candidate.declarations.push(declaration); + candidate + .externals + .retain(|value| value.symbol != external.symbol); + candidate + .external_types + .retain(|value| value.symbol != external.symbol); + probe + .externals + .retain(|value| value.symbol != external.symbol); + probe + .external_types + .retain(|value| value.symbol != external.symbol); + // Core's existing intrinsic rules check operand and result identity, + // including unused bindings. No source-type or arity heuristic is used. + if let Err(errors) = probe.verify_with_source(source.unwrap_or(&probe)) { + return Err(error(format!( + "primitive binding `{}` violates its checked type contract: {}", + intrinsic.descriptor().name, + errors + .iter() + .map(|error| error.message) + .collect::>() + .join("; ") + ))); + } + probe.declarations.clear(); + } + psrs_core::primitive::expand_primitive_globals(&mut candidate, &operations).map_err( + |errors| { + errors + .into_iter() + .map(|error| { + BackendError::invalid_ir("P8 primitive linking", error.span, error.message) + .with_module(error.module) + }) + .collect::>() + }, + )?; + candidate + .verify_with_source(source.unwrap_or(&candidate)) + .map_err(|errors| { + errors + .into_iter() + .map(|error| { + BackendError::invalid_ir("P8 primitive linking", error.span, error.message) + .with_module(error.module) + }) + .collect::>() + })?; + *module = candidate; + Ok(()) +} diff --git a/crates/psrs-backend/src/bindings/primitives/tests.rs b/crates/psrs-backend/src/bindings/primitives/tests.rs new file mode 100644 index 00000000..e9ee0fd9 --- /dev/null +++ b/crates/psrs-backend/src/bindings/primitives/tests.rs @@ -0,0 +1,85 @@ +use super::*; +use psrs_core::{ExternalType, Type, TypeConstructor, TypeId}; +use psrs_hir::{ExternalSymbol, Intrinsic, ModuleId, SymbolId}; +use psrs_span::TextRange; + +fn fixture(types: &[TypeId]) -> Module { + let span = TextRange::new(0, 1); + let externals = types + .iter() + .enumerate() + .map(|(index, _)| ExternalSymbol { + source_module: None, + symbol: SymbolId::new(ModuleId::INTRINSICS, 200 + index as u32), + name: format!("convert{index}"), + kind: ExternalKind::Primitive(Intrinsic::IntToNumber), + signature: None, + }) + .collect::>(); + let external_types = externals + .iter() + .zip(types) + .map(|(external, ty)| ExternalType { + symbol: external.symbol, + source_module: ModuleId(7), + ty: *ty, + }) + .collect(); + Module { + id: ModuleId(7), + name: "Bindings".into(), + externals, + external_types, + types: vec![ + Type::Constructor(TypeConstructor::Int), + Type::Constructor(TypeConstructor::Number), + Type::Constructor(TypeConstructor::Function), + Type::Application(TypeId(2), TypeId(0)), + Type::Application(TypeId(3), TypeId(1)), + Type::Application(TypeId(3), TypeId(0)), + ], + newtype_ids: Vec::new(), + opaque_ids: Vec::new(), + callable_types: Vec::new(), + constructors: Vec::new(), + declarations: Vec::new(), + type_names: Vec::new(), + entry: None, + span, + } +} + +#[test] +fn primitive_linking_does_not_publish_an_earlier_binding_when_a_later_one_fails() { + let mut module = fixture(&[TypeId(4), TypeId(5)]); + let before = module.clone(); + let errors = lower(&mut module, None).expect_err("second source signature is wrong"); + assert_eq!(module, before); + assert!(errors.iter().all(|error| error.module == Some(ModuleId(7)))); +} + +#[test] +fn primitive_linking_rejects_cyclic_checked_input_before_traversing_arrows() { + let mut module = fixture(&[TypeId(4)]); + module.types[4] = Type::Application(TypeId(3), TypeId(4)); + let before = module.clone(); + let errors = lower(&mut module, None).expect_err("cyclic source scheme is invalid IR"); + assert_eq!(module, before); + assert!( + errors + .iter() + .any(|error| error.kind == crate::BackendErrorKind::InvalidCompilerIr) + ); +} + +#[test] +fn primitive_linking_transfers_checked_identity_to_a_verified_function() { + let mut module = fixture(&[TypeId(4)]); + let symbol = module.externals[0].symbol; + lower(&mut module, None).unwrap(); + assert!(module.externals.is_empty()); + assert!(module.external_types.is_empty()); + assert_eq!(module.declarations[0].symbol, symbol); + assert_eq!(module.declarations[0].ty, TypeId(4)); + module.verify().unwrap(); +} diff --git a/crates/psrs-backend/src/bindings/runtime/call/mod.rs b/crates/psrs-backend/src/bindings/runtime/call/mod.rs new file mode 100644 index 00000000..e0e5258d --- /dev/null +++ b/crates/psrs-backend/src/bindings/runtime/call/mod.rs @@ -0,0 +1,91 @@ +//! Ordinary CC call signatures for the runtime storage ABI. +use crate::{BackendError, RuntimeBinding, cc}; +use psrs_runtime::{StorageOperation, StorageValue}; + +#[cfg(test)] +mod tests; + +fn error(binding: &RuntimeBinding, message: &str) -> Vec { + vec![ + BackendError::invalid_ir("P8 runtime ABI", binding.span, message) + .with_module(binding.source_module), + ] +} + +pub(crate) fn checked( + binding: &RuntimeBinding, + module: &cc::Module, +) -> Result<(), Vec> { + let signature = module + .externals + .iter() + .find(|external| external.symbol == binding.symbol) + .and_then(|external| external.signature.as_ref()) + .ok_or_else(|| error(binding, "runtime external has no CC signature"))?; + let canonical = canonical_signature(binding, signature, &module.representations)?; + if &canonical != signature { + return Err(error( + binding, + "runtime external disagrees with its canonical CC signature", + )); + } + Ok(()) +} + +/// Use the common CC payload protocol at the external call boundary. Source +/// arrays already store erased elements: their identity is never converted. +pub(crate) fn canonical_signature( + binding: &RuntimeBinding, + signature: &cc::Signature, + table: &cc::RepresentationTable, +) -> Result> { + if binding.module != psrs_runtime::STORAGE_MODULE { + return Err(error(binding, "unknown runtime storage provider")); + } + let operation = StorageOperation::from_export(&binding.function) + .ok_or_else(|| error(binding, "unknown runtime storage export"))?; + let contract = operation.source_contract(); + if signature.parameters.len() != contract.parameters.len() { + return Err(error( + binding, + "runtime CC signature has the wrong parameter count", + )); + } + let shape = |expected, actual| -> Result<_, Vec> { + match expected { + StorageValue::Int | StorageValue::Unit if actual == cc::ValueShape::Integer => { + Ok(actual) + } + StorageValue::Element => Ok(cc::payload::erased_shape()), + StorageValue::Any => Ok(actual), + StorageValue::Array => { + if let cc::ValueShape::Reference(cc::Reference { + nullable: false, + heap: cc::RefShape::Repr(id), + }) = actual + && matches!(table.representation(id), Some(cc::Representation::Array { element }) if *element == cc::payload::erased_shape()) + { + return Ok(actual); + } + Err(error( + binding, + "runtime array requires canonical erased-element storage", + )) + } + _ => Err(error( + binding, + "runtime CC operand shape disagrees with its contract", + )), + } + }; + Ok(cc::Signature { + parameters: contract + .parameters + .iter() + .copied() + .zip(&signature.parameters) + .map(|(expected, actual)| shape(expected, *actual)) + .collect::>()?, + result: shape(contract.payload, signature.result)?, + }) +} diff --git a/crates/psrs-backend/src/bindings/runtime/call/tests.rs b/crates/psrs-backend/src/bindings/runtime/call/tests.rs new file mode 100644 index 00000000..ff839891 --- /dev/null +++ b/crates/psrs-backend/src/bindings/runtime/call/tests.rs @@ -0,0 +1,76 @@ +use super::*; + +#[test] +fn canonical_runtime_call_rejects_malformed_cc_signatures() { + let binding = RuntimeBinding { + symbol: psrs_hir::SymbolId::new(psrs_hir::ModuleId(7), 1), + source_module: psrs_hir::ModuleId(7), + module: psrs_runtime::STORAGE_MODULE.into(), + function: "array_read".into(), + type_id: None, + span: psrs_span::TextRange::new(0, 1), + }; + let mut table = cc::RepresentationTable::default(); + let array = table.reserve(); + table.set( + array, + cc::Representation::Array { + element: cc::payload::erased_shape(), + }, + ); + let mut module = cc::Module { + name: "Probe".into(), + externals: vec![cc::External { + symbol: binding.symbol, + signature: Some(cc::Signature { + parameters: vec![ + cc::ValueShape::Reference(cc::Reference { + nullable: false, + heap: cc::RefShape::Repr(array), + }), + cc::ValueShape::Integer, + ], + result: cc::payload::erased_shape(), + }), + projection: None, + }], + representations: table, + functions: Vec::new(), + entry: None, + span: binding.span, + }; + checked(&binding, &module).unwrap(); + for change in 0..5 { + let mut wrong = module.clone(); + match change { + 0 => wrong.externals[0].signature = None, + 1 => { + wrong.externals[0] + .signature + .as_mut() + .unwrap() + .parameters + .pop(); + } + 2 => wrong.externals[0].signature.as_mut().unwrap().result = cc::ValueShape::Integer, + 3 => { + wrong.externals[0].signature.as_mut().unwrap().parameters[0] = + cc::payload::erased_shape() + } + _ => wrong.representations.set( + array, + cc::Representation::Array { + element: cc::ValueShape::Integer, + }, + ), + } + let errors = checked(&binding, &wrong).expect_err("malformed runtime CC signature"); + assert!( + errors + .iter() + .all(|error| error.module == Some(binding.source_module)) + ); + } + module.externals.clear(); + assert!(checked(&binding, &module).is_err()); +} diff --git a/crates/psrs-backend/src/bindings/runtime/metadata.rs b/crates/psrs-backend/src/bindings/runtime/metadata.rs new file mode 100644 index 00000000..e0d1589b --- /dev/null +++ b/crates/psrs-backend/src/bindings/runtime/metadata.rs @@ -0,0 +1,147 @@ +//! Lossless source runtime binding identity beside target-neutral CC. +use crate::BackendError; +use psrs_core::{Module as CoreModule, TypeId as CoreTypeId}; +use psrs_hir::{ExternalKind, ModuleId, SymbolId}; +use std::collections::{HashMap, HashSet}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RuntimeBinding { + pub symbol: SymbolId, + pub source_module: ModuleId, + pub module: String, + pub function: String, + /// Identity of the checked source scheme in the owning Core arena. + pub type_id: Option, + pub span: psrs_span::TextRange, +} + +pub(crate) fn from_core(module: &CoreModule) -> Vec { + module + .externals + .iter() + .filter_map(|external| { + let ExternalKind::Runtime { + module: provider, + function, + } = &external.kind + else { + return None; + }; + let checked = module + .external_types + .iter() + .find(|checked| checked.symbol == external.symbol); + Some(RuntimeBinding { + symbol: external.symbol, + source_module: checked.map_or(module.id, |checked| checked.source_module), + module: provider.clone(), + function: function.clone(), + type_id: checked.map(|checked| checked.ty), + span: external + .signature + .as_ref() + .map_or(module.span, |signature| signature.span), + }) + }) + .collect() +} + +pub(crate) fn validate_core( + bindings: &[RuntimeBinding], + module: &CoreModule, +) -> Result<(), Vec> { + let expected = from_core(module) + .into_iter() + .map(|binding| (binding.symbol, binding)) + .collect::>(); + let mut seen = HashSet::new(); + let mut errors = Vec::new(); + for binding in bindings { + let message = if !seen.insert(binding.symbol) { + Some("runtime binding is duplicated") + } else { + match expected.get(&binding.symbol) { + None => Some("runtime binding is not a source runtime import"), + Some(expected) if binding != expected => { + Some("runtime binding disagrees with its checked source identity") + } + Some(_) => None, + } + }; + if let Some(message) = message { + errors.push( + BackendError::invalid_ir("P8 external binding validation", binding.span, message) + .with_module(binding.source_module), + ); + } + } + for (symbol, binding) in expected { + if !seen.contains(&symbol) { + errors.push( + BackendError::invalid_ir( + "P8 external binding validation", + binding.span, + "source runtime import has no external binding", + ) + .with_module(binding.source_module), + ); + } + } + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn fixture() -> CoreModule { + let mut module = crate::bindings::tests::module(Vec::new()); + module.externals.push(psrs_hir::ExternalSymbol { + source_module: None, + symbol: SymbolId::new(module.id, 7), + name: "read".into(), + kind: ExternalKind::Runtime { + module: psrs_runtime::STORAGE_MODULE.into(), + function: "array_read".into(), + }, + signature: None, + }); + module.types.push(psrs_core::Type::Constructor( + psrs_core::TypeConstructor::Int, + )); + module.external_types.push(psrs_core::ExternalType { + symbol: module.externals[0].symbol, + source_module: ModuleId(42), + ty: CoreTypeId(0), + }); + module + } + + #[test] + fn runtime_metadata_preserves_identity_and_rejects_substitution() { + let module = fixture(); + let bindings = from_core(&module); + validate_core(&bindings, &module).unwrap(); + assert_eq!(bindings[0].source_module, ModuleId(42)); + assert_eq!(bindings[0].type_id, Some(CoreTypeId(0))); + for change in 0..4 { + let mut wrong = bindings.clone(); + match change { + 0 => wrong[0].function = "array_write".into(), + 1 => wrong[0].module = "another-provider".into(), + 2 => wrong[0].type_id = Some(CoreTypeId(1)), + _ => wrong[0].source_module = ModuleId(999), + } + assert!(validate_core(&wrong, &module).is_err()); + } + assert!(validate_core(&[], &module).is_err()); + assert!(validate_core(&[bindings[0].clone(), bindings[0].clone()], &module).is_err()); + let mut wrong = bindings; + wrong[0].symbol = SymbolId::new(module.id, 8); + assert!(validate_core(&wrong, &module).is_err()); + } +} diff --git a/crates/psrs-backend/src/bindings/runtime/mod.rs b/crates/psrs-backend/src/bindings/runtime/mod.rs new file mode 100644 index 00000000..c305382b --- /dev/null +++ b/crates/psrs-backend/src/bindings/runtime/mod.rs @@ -0,0 +1,52 @@ +//! Checked source contracts for ordinary runtime provider bindings. +//! Export lookup and physical operations belong to psrs-runtime, not HIR. + +use crate::BackendError; +use psrs_core::Module; +use psrs_hir::ExternalKind; +use psrs_runtime::StorageOperation; + +pub(super) mod call; +pub(super) mod metadata; +mod source; +pub(super) mod target; + +/// Validate all declarations, including unused ones, before lowering/erasure. +/// The immutable checked Core arena owns every signature identity used here. +/// +/// Source operands are ordinary values; no sequencing protocol is inferred. +pub(crate) fn validate(module: &Module) -> Result<(), Vec> { + for external in &module.externals { + let ExternalKind::Runtime { + module: provider, + function, + } = &external.kind + else { + continue; + }; + let checked = module + .external_types + .iter() + .find(|ty| ty.symbol == external.symbol); + let source_module = checked.map_or(module.id, |ty| ty.source_module); + let span = external + .signature + .as_ref() + .map_or(module.span, |ty| ty.span); + let error = |message| { + vec![BackendError::new("P8 runtime linking", span, message).with_module(source_module)] + }; + if provider != psrs_runtime::STORAGE_MODULE { + return Err(error(format!("unknown runtime provider `{provider}`"))); + } + let operation = StorageOperation::from_export(function).ok_or_else(|| { + error(format!( + "runtime provider `{provider}` has no export `{function}`" + )) + })?; + let checked = checked + .ok_or_else(|| error("runtime binding has no checked source signature".into()))?; + source::validate(module, checked.ty, operation).map_err(error)?; + } + Ok(()) +} diff --git a/crates/psrs-backend/src/bindings/runtime/source.rs b/crates/psrs-backend/src/bindings/runtime/source.rs new file mode 100644 index 00000000..e0f6d212 --- /dev/null +++ b/crates/psrs-backend/src/bindings/runtime/source.rs @@ -0,0 +1,52 @@ +//! Source type agreement checked before ABI erasure, including unused imports. +use psrs_core::{Module, Type, TypeConstructor, TypeId}; +use psrs_runtime::{StorageOperation, StorageValue}; + +pub(super) fn validate( + module: &Module, + ty: TypeId, + operation: StorageOperation, +) -> Result<(), String> { + let (parameters, result) = psrs_core::call_parts(&module.types, ty).map_err(str::to_owned)?; + let contract = operation.source_contract(); + if parameters.len() != contract.parameters.len() { + return Err("runtime storage signature has the wrong parameter count".into()); + } + let mut element = None; + for (ty, expected) in parameters + .into_iter() + .zip(contract.parameters) + .chain(std::iter::once((result, &contract.payload))) + { + let candidate = match expected { + StorageValue::Int | StorageValue::Unit => { + let constructor = if *expected == StorageValue::Int { + TypeConstructor::Int + } else { + TypeConstructor::Unit + }; + if module.types.get(ty.0 as usize) != Some(&Type::Constructor(constructor)) { + return Err(format!("runtime storage signature expects {expected:?}")); + } + continue; + } + StorageValue::Array => { + let Some((TypeConstructor::Array, arguments)) = module.applied_constructor(ty) + else { + return Err("runtime storage signature expects Array".into()); + }; + let [element] = arguments.as_slice() else { + return Err("runtime storage Array must have one element type".into()); + }; + *element + } + StorageValue::Element => ty, + StorageValue::Any => continue, + }; + if element.is_some_and(|previous| !module.types_equivalent(previous, candidate)) { + return Err("runtime storage signature has inconsistent element types".into()); + } + element = Some(candidate); + } + Ok(()) +} diff --git a/crates/psrs-backend/src/bindings/runtime/target.rs b/crates/psrs-backend/src/bindings/runtime/target.rs new file mode 100644 index 00000000..9730d1a3 --- /dev/null +++ b/crates/psrs-backend/src/bindings/runtime/target.rs @@ -0,0 +1,134 @@ +//! Storage-provider ABI adaptation at the external-binding boundary. +//! MIR consumers receive physical signatures and a general return convention. +use crate::mir::{Import, layout::PlannedLayout}; +use crate::types::{CompositeType, FieldType, HeapType, RecGroup, RefType, StorageType, ValueType}; +use crate::{BackendError, RuntimeBinding, cc}; +use psrs_runtime::StorageType as RawType; + +fn matches_raw_type(raw: RawType, value: ValueType, types: &[RecGroup]) -> bool { + match (raw, value) { + (RawType::I32, ValueType::I32) => true, + ( + RawType::Value, + ValueType::Ref(RefType { + nullable: true, + heap: HeapType::Eq, + }), + ) => true, + ( + RawType::Array, + ValueType::Ref(RefType { + nullable: false, + heap: HeapType::Index(id), + }), + ) => { + let definition = types.iter().flat_map(|group| &group.0).nth(id.0 as usize); + matches!( + definition.map(|definition| &definition.composite), + Some(CompositeType::Array(FieldType { + mutable: true, + storage: StorageType::Ref(RefType { + nullable: true, + heap: HeapType::Eq + }), + })) + ) + } + _ => false, + } +} + +pub(crate) fn verify_import(import: &Import, types: &[RecGroup]) -> Result<(), String> { + let Some(provider) = &import.runtime else { + return Ok(()); + }; + if crate::target_intrinsics::generated::name(import.symbol).is_some() + || crate::target_runtime::for_symbol(import.symbol).is_some() + { + return Err("MIR source runtime import cannot replace a reserved target binding".into()); + } + if provider.module != psrs_runtime::STORAGE_MODULE { + return Err("MIR runtime import has an unknown provider".into()); + } + let operation = psrs_runtime::StorageOperation::from_export(&provider.function) + .ok_or("MIR runtime import has an unknown export")?; + let raw = operation.projection().map_err(str::to_owned)?; + if raw.abi().parameters.len() != import.parameters.len() + || !raw + .abi() + .parameters + .iter() + .copied() + .zip(import.parameters.iter().copied()) + .all(|(raw, value)| matches_raw_type(raw, value, types)) + || match (raw.abi().result, import.result) { + (Some(raw), Some(value)) => !matches_raw_type(raw, value, types), + (None, None) => false, + _ => true, + } + { + return Err("MIR runtime import signature disagrees with its provider contract".into()); + } + Ok(()) +} + +pub(crate) fn error(binding: &RuntimeBinding, message: &str) -> Vec { + vec![ + BackendError::invalid_ir("P9 runtime ABI", binding.span, message) + .with_module(binding.source_module), + ] +} + +/// Plan raw types using the canonical CC signature and the application's arena. +pub(crate) fn plan_import( + binding: &RuntimeBinding, + logical: &cc::Module, + layout: &PlannedLayout, +) -> Result> { + super::call::checked(binding, logical)?; + let signature = logical + .externals + .iter() + .find(|external| external.symbol == binding.symbol) + .and_then(|external| external.signature.as_ref()) + .ok_or_else(|| error(binding, "runtime external has no CC signature"))?; + let operation = psrs_runtime::StorageOperation::from_export(&binding.function) + .ok_or_else(|| error(binding, "unknown runtime storage export"))?; + let raw = operation + .projection() + .map_err(|message| error(binding, message))?; + let physical = |raw, shape| -> Result<_, Vec> { + Ok(match raw { + RawType::I32 => ValueType::I32, + RawType::Value => ValueType::Ref(RefType { + nullable: true, + heap: HeapType::Eq, + }), + RawType::Array => layout + .value_type(&shape) + .map_err(|_| error(binding, "runtime array has no planned layout"))?, + }) + }; + let import = Import { + symbol: binding.symbol, + runtime: Some(crate::mir::RuntimeImport { + module: binding.module.clone(), + function: binding.function.clone(), + }), + parameters: raw + .abi() + .parameters + .iter() + .copied() + .zip(&signature.parameters) + .map(|(raw, shape)| physical(raw, *shape)) + .collect::>()?, + result: raw + .abi() + .result + .map(|raw| physical(raw, signature.result)) + .transpose()?, + }; + verify_import(&import, layout.types()).map_err(|message| error(binding, &message))?; + Ok(import) +} diff --git a/crates/psrs-backend/src/bindings/tests.rs b/crates/psrs-backend/src/bindings/tests.rs index 7e11a40d..2e75b6f1 100644 --- a/crates/psrs-backend/src/bindings/tests.rs +++ b/crates/psrs-backend/src/bindings/tests.rs @@ -13,6 +13,7 @@ fn span() -> TextRange { fn wit_external(symbol: SymbolId) -> ExternalSymbol { ExternalSymbol { + source_module: None, symbol, name: "log".into(), kind: ExternalKind::Wit { @@ -34,7 +35,7 @@ fn binding(symbol: SymbolId) -> ExternalBinding { } } -fn module(externals: Vec) -> CoreModule { +pub(super) fn module(externals: Vec) -> CoreModule { CoreModule { type_names: Vec::new(), id: ModuleId(1), @@ -60,6 +61,7 @@ fn errors_contain(errors: &[BackendError], fragment: &str) -> bool { fn accepts_a_complete_wit_projection() { let external = symbol(1); let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![binding(external)], }; assert!( @@ -83,6 +85,7 @@ fn rejects_a_missing_wit_binding() { fn rejects_a_duplicate_binding() { let external = symbol(1); let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![binding(external), binding(external)], }; let errors = bindings @@ -95,6 +98,7 @@ fn rejects_a_duplicate_binding() { fn rejects_a_binding_for_a_non_source_import() { let external = symbol(1); let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![binding(external)], }; let errors = bindings diff --git a/crates/psrs-backend/src/boundary/mod.rs b/crates/psrs-backend/src/boundary/mod.rs new file mode 100644 index 00000000..c9f9bb95 --- /dev/null +++ b/crates/psrs-backend/src/boundary/mod.rs @@ -0,0 +1,284 @@ +//! The Core-to-CC boundary side table. +//! +//! Checked instantiation evidence and the representation policies of source +//! constructors travel to P8 through this table, beside CC, the same way +//! [`crate::ExternalBindings`] carries the WIT boundary. P8 consumes it: it +//! does not read the Core type arena for a relation, key a semantic decision on +//! a HIR identity, or derive a producer policy by searching signatures. + +use crate::cc::{RefShape, Reference, Signature, SignatureId, ValueShape}; +use psrs_core::{Instantiation, Module as CoreModule, TypeConstructor, TypeId}; +use psrs_hir::TypeVariableId; +use std::collections::{HashMap, HashSet}; + +mod newtypes; +#[cfg(test)] +mod tests; + +/// A representation owner's policy for the runtime form of a source +/// constructor. It records the constructor's fixed calling-convention +/// parameters; the payload is the value's declared result. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum RepresentationPolicy { + /// The fixed parameters are the constructor's checked instantiation + /// arguments (a function arrow's domain). + InstantiationArguments, + /// Complete source and canonical callable fields of a transparent newtype. + /// Storage uses the declared field, without specializing its free variables. + Newtype(newtypes::NewtypeCallable), +} + +/// The registry of representation owners, keyed by source constructor. +/// +/// `Function` stores its values under the arrow domain from the checked +/// instantiation; a representation owner registers additional constructors, +/// such as the trusted `Effect`. A constructor with no entry has no callable +/// representation contract, so transport reports it rather than guessing one +/// from the declaration arity or a signature search. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub(crate) struct RepresentationRegistry { + policies: HashMap, +} + +impl RepresentationRegistry { + /// The checked newtype field owns its erased callable representation. + /// Preserve full domains/results without inventing or specializing nodes. + pub(crate) fn register_newtypes(&mut self, module: &CoreModule) { + for id in &module.newtype_ids { + if self.policies.contains_key(&TypeConstructor::User(*id)) { + continue; + } + if let Some(template) = newtypes::callable(module, *id) { + self.register( + TypeConstructor::User(*id), + RepresentationPolicy::Newtype(template), + ); + } + } + } + /// The built-in owners. `Function` is representation-directed by its own + /// checked instantiation arguments. + pub(crate) fn new() -> Self { + let mut registry = Self::default(); + registry.register( + TypeConstructor::Function, + RepresentationPolicy::InstantiationArguments, + ); + registry + } + + /// Registers one constructor's representation owner. + pub(crate) fn register(&mut self, constructor: TypeConstructor, policy: RepresentationPolicy) { + self.policies.insert(constructor, policy); + } + + /// The fixed protocol parameters for a constructor application, or `None` + /// when no owner registered the constructor. + pub(crate) fn protocol_parameters( + &self, + constructor: TypeConstructor, + arguments: &[TypeId], + ) -> Option> { + Some(match self.policies.get(&constructor)? { + RepresentationPolicy::InstantiationArguments => arguments.to_vec(), + RepresentationPolicy::Newtype(template) => template.parameters.clone(), + }) + } + + /// A transparent newtype's full declared storage result. Ordinary function + /// policies obtain their result from the use type. + fn protocol_result(&self, constructor: TypeConstructor) -> Option { + match self.policies.get(&constructor)? { + RepresentationPolicy::Newtype(template) => Some(template.result), + _ => None, + } + } +} + +/// Checked evidence and producer policies at the Core-to-CC boundary. +pub(crate) struct BoundaryEvidence<'a> { + /// The immutable Core program that owns the checked relation. + source: &'a CoreModule, + /// The module under lowering. A relation whose type ids still exist in the + /// source uses the source; a type allocated only by representation + /// lowering uses the physical module. + physical: &'a CoreModule, + registry: RepresentationRegistry, + /// The payload-erased protocol signature of every callable constructor, + /// keyed by the concrete callable's signature. Built by P8 layout from the + /// registered owners and the module's callable signatures. + protocols: HashMap, + function_slot: Option, +} + +impl<'a> BoundaryEvidence<'a> { + pub(crate) fn new( + source: &'a CoreModule, + physical: &'a CoreModule, + registry: RepresentationRegistry, + protocols: HashMap, + function_slot: Option, + ) -> Self { + Self { + source, + physical, + registry, + protocols, + function_slot, + } + } + + /// A boundary with no relation beyond the module itself, for lowering + /// fixtures that exercise no abstract constructor protocol. + #[cfg(test)] + pub(crate) fn empty(physical: &'a CoreModule) -> Self { + Self::new( + physical, + physical, + RepresentationRegistry::new(), + HashMap::new(), + None, + ) + } + + fn relation(&self, scheme: TypeId, instance: TypeId) -> &'a CoreModule { + if (scheme.0 as usize) < self.source.types.len() + && (instance.0 as usize) < self.source.types.len() + { + self.source + } else { + self.physical + } + } + + /// Checks a declaration use through the checking-owned relation, retaining + /// its solved constructor bindings. P8 reads the result; it does not + /// reimplement the relation. + pub(crate) fn instantiation_at( + &self, + scheme: TypeId, + instance: TypeId, + ) -> Option> { + let relation = self.relation(scheme, instance); + let quantified = scheme_quantifiers(relation, scheme); + relation.checked_instantiation(scheme, &quantified, instance) + } + + /// Checks a declaration use whose quantifiers are already known. + pub(crate) fn checked_instantiation( + &self, + scheme: TypeId, + quantified: &[TypeVariableId], + instance: TypeId, + ) -> Option> { + self.relation(scheme, instance) + .checked_instantiation(scheme, quantified, instance) + } + + /// The fixed protocol parameters for a constructor application. + pub(crate) fn protocol_parameters( + &self, + constructor: TypeConstructor, + arguments: &[TypeId], + ) -> Option> { + self.registry.protocol_parameters(constructor, arguments) + } + + pub(crate) fn protocol_result(&self, constructor: TypeConstructor) -> Option { + self.registry.protocol_result(constructor) + } + + /// Bare polymorphic function slots use one erased argument at a time. + /// Multi-argument functions are curried on entry and flattened on recovery. + pub(crate) fn function_slot_signature(&self) -> Option { + self.function_slot + } + + /// The payload-erased protocol signature a callable constructor stores its + /// values under, keyed by the concrete callable signature. + pub(crate) fn protocol_signature(&self, concrete: SignatureId) -> Option { + self.protocols.get(&concrete).copied() + } +} + +/// Leading quantifiers of a scheme. Pre-seeding these keeps their constructor +/// bindings when instantiation opens the same quantifiers. +fn scheme_quantifiers(module: &CoreModule, mut ty: TypeId) -> Vec { + let mut variables = Vec::new(); + let mut seen = HashSet::new(); + while let Some((bound, body)) = psrs_core::forall_parts(&module.types, ty) { + if !seen.insert(ty) { + break; + } + variables.extend(bound.iter().copied()); + ty = body; + } + variables +} + +/// Builds the payload-erased protocol signature for every distinct callable +/// signature. The protocol keeps the concrete calling convention but erases the +/// payload, so a producer and its generic consumer can be related by the +/// constructor policy rather than by a cast onto the consumer's signature. +pub(crate) fn payload_erased_protocols( + representations: &mut crate::cc::RepresentationTable, + function_types: &HashMap, + span: psrs_span::TextRange, +) -> Result, Vec> { + let mut protocols = HashMap::new(); + let mut interned = HashMap::::new(); + let mut ids = function_types.values().copied().collect::>(); + ids.sort_by_key(|id| id.0); + ids.dedup(); + for concrete in ids { + if protocols.contains_key(&concrete) { + continue; + } + let signature = representations + .signature(concrete) + .cloned() + .ok_or_else(|| { + vec![crate::BackendError::invalid_ir( + "P8 callable protocol", + span, + "callable owner has no registered signature", + )] + })?; + let result = ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Erased, + }); + let protocol = Signature { + parameters: signature.parameters, + result, + }; + let protocol_id = if let Some(existing) = interned.get(&protocol) { + *existing + } else { + let new = representations.add_signature(protocol.clone()); + interned.insert(protocol, new); + new + }; + protocols.insert(concrete, protocol_id); + } + Ok(protocols) +} + +/// The representation owner for a function hidden by a bare type variable. +/// A unary protocol keeps the slot stable when instantiation changes arity. +pub(crate) fn function_slot_protocol(table: &mut crate::cc::RepresentationTable) -> SignatureId { + let erased = ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Erased, + }); + let signature = Signature { + parameters: vec![erased], + result: erased, + }; + table + .signatures + .iter() + .position(|existing| *existing == signature) + .map(|index| SignatureId(index as u32)) + .unwrap_or_else(|| table.add_signature(signature)) +} diff --git a/crates/psrs-backend/src/boundary/newtypes/mod.rs b/crates/psrs-backend/src/boundary/newtypes/mod.rs new file mode 100644 index 00000000..497facf5 --- /dev/null +++ b/crates/psrs-backend/src/boundary/newtypes/mod.rs @@ -0,0 +1,63 @@ +//! Complete callable storage templates of checked transparent newtypes. +//! +//! A newtype's canonical field keeps its declared erased representation. +//! Constructor arguments do not specialize that physical storage convention. +use psrs_core::{Module, TypeConstructor, TypeId}; +use psrs_hir::{TypeId as HirTypeId, TypeVariableId}; +use std::collections::HashSet; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct NewtypeCallable { + /// The outer declaration's complete field, including nested applications. + pub source_field: TypeId, + pub source_variables: Vec, + /// The declaration that owns the final canonical callable field. + pub storage_owner: HirTypeId, + pub storage_field: TypeId, + pub parameters: Vec, + /// Preserve structural results and returned closures as actual type ids. + pub result: TypeId, +} + +pub(super) fn callable(module: &Module, id: HirTypeId) -> Option { + let outer = constructor(module, id)?; + let source_field = outer.field_types[0]; + let mut storage_owner = id; + let mut storage_field = source_field; + let mut visited = HashSet::from([id]); + loop { + if crate::cc::is_callable_type(module, storage_field) { + let (parameters, result) = + crate::cc::checked_function_arrow_parameters(module, storage_field).ok()?; + return Some(NewtypeCallable { + source_field, + source_variables: outer.parameters.clone(), + storage_owner, + storage_field, + parameters, + result, + }); + } + let (_, body) = psrs_core::scheme_parts(&module.types, storage_field)?; + let (TypeConstructor::User(inner), _) = module.applied_constructor(body)? else { + return None; + }; + if !module.newtype_ids.contains(&inner) || !visited.insert(inner) { + return None; + } + storage_owner = inner; + storage_field = constructor(module, inner)?.field_types[0]; + } +} + +fn constructor(module: &Module, id: HirTypeId) -> Option<&psrs_core::ConstructorInfo> { + let mut constructors = module.constructors.iter().filter(|ctor| ctor.type_id == id); + let constructor = constructors.next()?; + (constructors.next().is_none() + && constructor.field_count == 1 + && constructor.field_types.len() == 1) + .then_some(constructor) +} + +#[cfg(test)] +mod tests; diff --git a/crates/psrs-backend/src/boundary/newtypes/tests.rs b/crates/psrs-backend/src/boundary/newtypes/tests.rs new file mode 100644 index 00000000..07a34467 --- /dev/null +++ b/crates/psrs-backend/src/boundary/newtypes/tests.rs @@ -0,0 +1,133 @@ +use super::*; +use psrs_core::{ConstructorInfo, Type}; +use psrs_hir::{ModuleId, SymbolId}; + +const OWNER: HirTypeId = HirTypeId::new(ModuleId(0), 0); + +fn module(types: Vec, field: TypeId, parameters: Vec) -> Module { + Module { + id: ModuleId(0), + name: "CallableTemplate".into(), + externals: Vec::new(), + external_types: Vec::new(), + types, + newtype_ids: vec![OWNER], + opaque_ids: Vec::new(), + callable_types: Vec::new(), + constructors: vec![ConstructorInfo { + symbol: SymbolId::new(ModuleId(0), 0), + name: "Action".into(), + type_id: OWNER, + tag: 0, + field_count: 1, + field_types: vec![field], + parameters, + }], + declarations: Vec::new(), + type_names: Vec::new(), + entry: None, + span: psrs_span::TextRange::new(0, 1), + } +} + +#[test] +fn nested_newtype_keeps_application_evidence_and_canonical_storage_owner() { + let inner = HirTypeId::new(ModuleId(0), 1); + let r = TypeVariableId(0); + let a = TypeVariableId(1); + let outer_a = TypeVariableId(2); + let mut module = module( + vec![ + Type::Variable(r), + Type::Variable(a), + Type::Constructor(TypeConstructor::Function), + Type::Application(TypeId(2), TypeId(0)), + Type::Application(TypeId(3), TypeId(1)), + Type::Constructor(TypeConstructor::User(inner)), + Type::Constructor(TypeConstructor::Int), + Type::Application(TypeId(5), TypeId(6)), + Type::Variable(outer_a), + Type::Application(TypeId(7), TypeId(8)), + ], + TypeId(9), + vec![outer_a], + ); + module.newtype_ids.push(inner); + module.constructors.push(ConstructorInfo { + symbol: SymbolId::new(ModuleId(0), 1), + name: "Inner".into(), + type_id: inner, + tag: 0, + field_count: 1, + field_types: vec![TypeId(4)], + parameters: vec![r, a], + }); + let template = callable(&module, OWNER).unwrap(); + assert_eq!(template.source_field, TypeId(9)); + assert_eq!(template.source_variables, [outer_a]); + assert_eq!(template.storage_owner, inner); + assert_eq!(template.storage_field, TypeId(4)); + assert_eq!( + template.parameters, + [TypeId(0)], + "canonical storage is not specialized to Int" + ); + assert_eq!(template.result, TypeId(1)); +} + +#[test] +fn nullary_closure_returns_a_function_without_absorbing_its_parameters() { + let module = module( + vec![ + Type::Constructor(TypeConstructor::Int), + Type::Constructor(TypeConstructor::Function), + Type::Application(TypeId(1), TypeId(0)), + Type::Application(TypeId(2), TypeId(0)), + Type::Closure { + parameters: vec![], + result: TypeId(3), + }, + ], + TypeId(4), + vec![], + ); + let template = callable(&module, OWNER).unwrap(); + assert!(template.parameters.is_empty()); + assert_eq!(template.result, TypeId(3)); +} + +#[test] +fn cyclic_and_dangling_call_boundaries_do_not_become_valid_empty_protocols() { + for (types, field) in [ + ( + vec![ + Type::Constructor(TypeConstructor::Function), + Type::Constructor(TypeConstructor::Int), + Type::Application(TypeId(0), TypeId(1)), + Type::Application(TypeId(2), TypeId(3)), + ], + TypeId(3), + ), + ( + vec![ + Type::Constructor(TypeConstructor::Int), + Type::Closure { + parameters: vec![TypeId(99)], + result: TypeId(0), + }, + ], + TypeId(1), + ), + ( + vec![Type::ForAll { + variables: vec![TypeVariableId(0)], + body: TypeId(0), + }], + TypeId(0), + ), + ] { + let module = module(types, field, vec![]); + assert!(crate::cc::checked_function_arrow_parameters(&module, field).is_err()); + assert!(callable(&module, OWNER).is_none()); + } +} diff --git a/crates/psrs-backend/src/boundary/tests.rs b/crates/psrs-backend/src/boundary/tests.rs new file mode 100644 index 00000000..aac0a817 --- /dev/null +++ b/crates/psrs-backend/src/boundary/tests.rs @@ -0,0 +1,14 @@ +use super::*; +use crate::cc::RepresentationTable; +use psrs_span::TextRange; + +#[test] +fn callable_protocol_rejects_missing_owner_signature() { + let errors = payload_erased_protocols( + &mut RepresentationTable::default(), + &HashMap::from([(TypeId(0), SignatureId(99))]), + TextRange::default(), + ) + .unwrap_err(); + assert!(errors[0].message.contains("no registered signature")); +} diff --git a/crates/psrs-backend/src/capability.rs b/crates/psrs-backend/src/capability.rs index 735f45f7..c9d1b28c 100644 --- a/crates/psrs-backend/src/capability.rs +++ b/crates/psrs-backend/src/capability.rs @@ -52,9 +52,9 @@ pub struct TargetCapabilities { impl TargetCapabilities { /// The stable target used by `compile`. /// - /// GC and typed function references are required by the current runtime - /// representation. Other Tier 1 Wasmtime proposals remain opt-in until - /// a lowering and an execution test actually use them. + /// GC, typed function references and tail calls are required by the current + /// runtime representation and stack-safe callable execution. Other proposals + /// remain opt-in until a lowering and an execution test actually use them. pub const fn wasmtime_wasi_0_2() -> Self { Self { mutable_globals: true, @@ -67,7 +67,7 @@ impl TargetCapabilities { gc: true, simd: false, relaxed_simd: false, - tail_call: false, + tail_call: true, multi_memory: false, memory64: false, exceptions: false, @@ -92,8 +92,29 @@ impl TargetCapabilities { } } + /// Whether the selected profile permits a WASI interface family. + pub fn wasi_interface_enabled(self, module: &str) -> bool { + let package_path = module + .split_once('/') + .map_or(module, |(package, _)| package); + let package = package_path + .split_once('@') + .map_or(package_path, |(package, _)| package); + match package { + "wasi:cli" => self.wasi_cli, + "wasi:io" => self.wasi_io, + "wasi:clocks" => self.wasi_clocks, + "wasi:random" => self.wasi_random, + "wasi:filesystem" => self.wasi_filesystem, + "wasi:sockets" => self.wasi_sockets, + "wasi:http" => self.wasi_http, + "wasi:tls" => self.wasi_tls, + _ => false, + } + } + /// Converts the profile to the validator feature set. - pub(crate) fn wasm_features(self) -> wasmparser::WasmFeatures { + pub fn wasm_features(self) -> wasmparser::WasmFeatures { use wasmparser::WasmFeatures; let mut features = WasmFeatures::MVP; @@ -154,7 +175,7 @@ mod tests { gc: true, simd: false, relaxed_simd: false, - tail_call: false, + tail_call: true, multi_memory: false, memory64: false, exceptions: false, diff --git a/crates/psrs-backend/src/cc/branch_facts.rs b/crates/psrs-backend/src/cc/branch_facts.rs new file mode 100644 index 00000000..ac3ffd8a --- /dev/null +++ b/crates/psrs-backend/src/cc/branch_facts.rs @@ -0,0 +1,68 @@ +#![allow(dead_code)] +//! Immutable variant tag facts on structured CC paths. +use super::{AssignmentKind as A, ReprId, ValueId}; +use std::collections::HashMap; + +pub(crate) type Facts = HashMap<(ReprId, ValueId), i32>; +pub(crate) type Definitions<'a> = HashMap; + +pub(crate) fn tag_source( + value: ValueId, + definitions: &Definitions<'_>, +) -> Option<(ReprId, ValueId)> { + let origin = |value| { + super::value_identity::reference_origin(value, |value| definitions.get(&value).copied()) + }; + match definitions.get(&origin(value)).copied()? { + A::VariantTag { + representation, + value, + .. + } => Some((*representation, origin(*value))), + _ => None, + } +} + +pub(crate) fn selected( + representation: ReprId, + value: ValueId, + facts: &Facts, + definitions: &Definitions<'_>, +) -> Option { + let value = + super::value_identity::reference_origin(value, |value| definitions.get(&value).copied()); + match definitions.get(&value).copied() { + Some(A::VariantNew { + representation: actual, + case, + .. + }) if *actual == representation => i32::try_from(*case).ok(), + _ => facts.get(&(representation, value)).copied(), + } +} + +/// Equality established by a selected Boolean branch, including swapped +/// operands. Unknown inequalities do not become fabricated scalar facts. +pub(crate) fn equality_selection( + condition: ValueId, + truth: bool, + definitions: &Definitions<'_>, + aliases: &HashMap, +) -> Option<(ValueId, i32)> { + let origin = |value| super::value_identity::path_origin(value, definitions, aliases); + let A::Primitive { op, left, right } = definitions.get(&origin(condition)).copied()? else { + return None; + }; + if !matches!( + (op, truth), + (super::BinaryOp::IntEq, true) | (super::BinaryOp::IntNe, false) + ) { + return None; + } + for (value, constant) in [(*left, *right), (*right, *left)] { + if let Some(A::Constant(constant)) = definitions.get(&origin(constant)).copied() { + return Some((origin(value), *constant)); + } + } + None +} diff --git a/crates/psrs-backend/src/cc/case/coverage/engine.rs b/crates/psrs-backend/src/cc/case/coverage/engine.rs index 2c528092..6652b8f1 100644 --- a/crates/psrs-backend/src/cc/case/coverage/engine.rs +++ b/crates/psrs-backend/src/cc/case/coverage/engine.rs @@ -69,6 +69,29 @@ fn useful_inner( return useful_array_any(module, matrix, query, tail, *ty, element_type, active); } if let Some(shapes) = signature(module, *ty) { + // An incomplete signature uses the default matrix. Expanding every + // constructor here grows unconstrained recursive product columns + // forever, even when the matrix is empty. Only a complete set of + // observed heads needs field-by-field specialization. + let missing = shapes + .iter() + .filter(|shape| { + !matrix + .iter() + .filter_map(|row| row.first()) + .any(|pattern| head_of(pattern).as_ref() == Some(&shape.head)) + }) + .collect::>(); + if !missing.is_empty() { + let witness = useful_with_active(module, &default_matrix(matrix), tail, active)?; + for shape in missing { + if let Some(head) = inhabited_shape(module, shape, &mut Default::default()) { + return Some(std::iter::once(head).chain(witness).collect()); + } + } + // Missing uninhabited constructors leave observed fields to + // check; their partial patterns can still expose a witness. + } for shape in shapes { let specialized = specialize(matrix, &shape); let mut specialized_query = expanded_any(&shape, matrix, query); @@ -117,6 +140,48 @@ fn useful_inner( }) } +/// Constructs a finite inhabitant independently of the pattern matrix. The +/// active path is keyed by type, so recursive products cannot grow a sequence +/// of distinct query states while looking for a base constructor. +fn inhabited_type( + module: &Module, + ty: TypeId, + active: &mut std::collections::HashSet, +) -> Option { + let ty = crate::cc::layout::unquantified_type(module, ty); + if !active.insert(ty) { + return None; + } + let result = if array_element(module, ty).is_some() { + Some(SurfacePattern::Array { + elements: Vec::new(), + ty, + span: TextRange::default(), + }) + } else if let Some(shapes) = signature(module, ty) { + shapes + .iter() + .find_map(|shape| inhabited_shape(module, shape, active)) + } else { + Some(SurfacePattern::Any { ty }) + }; + active.remove(&ty); + result +} + +fn inhabited_shape( + module: &Module, + shape: &Shape, + active: &mut std::collections::HashSet, +) -> Option { + let fields = shape + .fields + .iter() + .map(|(_, ty)| inhabited_type(module, *ty, active)) + .collect::>>()?; + Some(reconstruct(shape, fields)) +} + fn useful_array_any( module: &Module, matrix: &Matrix, @@ -175,6 +240,14 @@ fn useful_array_any( fn signature(module: &Module, ty: TypeId) -> Option> { let ty = crate::cc::layout::unquantified_type(module, ty); if let Some(type_id) = user_type_id(module, ty) { + // An uninhabited user type has no constructors, but unchecked coercions + // can still produce its values (the reference `Free` over `Val` builds + // `Return` values this way). Coverage must not treat it as a complete + // empty signature, so its shape is left unknown and the wildcard matrix + // handles it. + if crate::cc::layout::is_uninhabited_data_type(module, type_id) { + return None; + } return Some( module .constructors @@ -418,65 +491,3 @@ fn reconstruct(shape: &Shape, arguments: Vec) -> SurfacePattern }, } } - -pub(super) fn render(module: &Module, pattern: &SurfacePattern) -> String { - match pattern { - SurfacePattern::Any { .. } | SurfacePattern::Var { .. } => "_".to_owned(), - SurfacePattern::Literal { value, .. } => render_literal(value), - SurfacePattern::Array { elements, .. } => format!( - "[{}]", - elements - .iter() - .map(|element| render(module, element)) - .collect::>() - .join(", ") - ), - SurfacePattern::Constructor { - symbol, arguments, .. - } => { - let name = module - .constructors - .iter() - .find(|constructor| constructor.symbol == *symbol) - .map_or_else( - || format!("Constructor{}", symbol.index), - |constructor| constructor.name.clone(), - ); - if arguments.is_empty() { - name - } else { - let arguments = arguments - .iter() - .map(|argument| match argument { - SurfacePattern::Any { .. } => "_".to_owned(), - SurfacePattern::Constructor { arguments, .. } if arguments.is_empty() => { - render(module, argument) - } - nested => format!("({})", render(module, nested)), - }) - .collect::>() - .join(" "); - format!("{name} {arguments}") - } - } - SurfacePattern::Record { fields, .. } => format!( - "{{ {} }}", - fields - .iter() - .map(|(label, value)| format!("{label}: {}", render(module, value))) - .collect::>() - .join(", ") - ), - SurfacePattern::Named { pattern, .. } => render(module, pattern), - } -} - -fn render_literal(literal: &Literal) -> String { - match literal { - Literal::Integer(value) => value.to_string(), - Literal::Number(value) => value.clone(), - Literal::String(value) => format!("{value:?}"), - Literal::Char(value) => format!("{value:?}"), - Literal::Boolean(value) => value.to_string(), - } -} diff --git a/crates/psrs-backend/src/cc/case/coverage/mod.rs b/crates/psrs-backend/src/cc/case/coverage/mod.rs index 96b27599..7514dede 100644 --- a/crates/psrs-backend/src/cc/case/coverage/mod.rs +++ b/crates/psrs-backend/src/cc/case/coverage/mod.rs @@ -24,7 +24,9 @@ impl CoverageReport { } mod engine; -use engine::{render, useful}; +mod render; +use engine::useful; +use render::render; pub(super) fn analyze( module: &Module, @@ -44,7 +46,12 @@ pub(super) fn analyze( } let matrix = branches .iter() - .filter(|branch| branch.coverage == CaseBranchCoverage::Source) + .filter(|branch| { + matches!( + branch.coverage, + CaseBranchCoverage::Source | CaseBranchCoverage::PartialFallback + ) + }) .map(|branch| vec![coverage_pattern(&branch.pattern)]) .collect::>>(); let query = vec![SurfacePattern::Any { ty: scrutinee_type }]; @@ -54,7 +61,10 @@ pub(super) fn analyze( let mut prior = Vec::new(); let mut redundant_branches = Vec::new(); for (index, branch) in branches.iter().enumerate() { - if branch.coverage == CaseBranchCoverage::Generated { + if matches!( + branch.coverage, + CaseBranchCoverage::Generated | CaseBranchCoverage::PartialFallback + ) { continue; } let query = vec![coverage_pattern(&branch.pattern)]; diff --git a/crates/psrs-backend/src/cc/case/coverage/render.rs b/crates/psrs-backend/src/cc/case/coverage/render.rs new file mode 100644 index 00000000..2a47ab8b --- /dev/null +++ b/crates/psrs-backend/src/cc/case/coverage/render.rs @@ -0,0 +1,64 @@ +use super::super::decision::SurfacePattern; +use psrs_core::{Literal, Module}; + +pub(super) fn render(module: &Module, pattern: &SurfacePattern) -> String { + match pattern { + SurfacePattern::Any { .. } | SurfacePattern::Var { .. } => "_".to_owned(), + SurfacePattern::Literal { value, .. } => render_literal(value), + SurfacePattern::Array { elements, .. } => format!( + "[{}]", + elements + .iter() + .map(|element| render(module, element)) + .collect::>() + .join(", ") + ), + SurfacePattern::Constructor { + symbol, arguments, .. + } => { + let name = module + .constructors + .iter() + .find(|constructor| constructor.symbol == *symbol) + .map_or_else( + || format!("Constructor{}", symbol.index), + |constructor| constructor.name.clone(), + ); + if arguments.is_empty() { + name + } else { + let arguments = arguments + .iter() + .map(|argument| match argument { + SurfacePattern::Any { .. } => "_".to_owned(), + SurfacePattern::Constructor { arguments, .. } if arguments.is_empty() => { + render(module, argument) + } + nested => format!("({})", render(module, nested)), + }) + .collect::>() + .join(" "); + format!("{name} {arguments}") + } + } + SurfacePattern::Record { fields, .. } => format!( + "{{ {} }}", + fields + .iter() + .map(|(label, value)| format!("{label}: {}", render(module, value))) + .collect::>() + .join(", ") + ), + SurfacePattern::Named { pattern, .. } => render(module, pattern), + } +} + +fn render_literal(literal: &Literal) -> String { + match literal { + Literal::Integer(value) => value.to_string(), + Literal::Number(value) => value.clone(), + Literal::String(value) => format!("{value:?}"), + Literal::Char(value) => format!("{value:?}"), + Literal::Boolean(value) => value.to_string(), + } +} diff --git a/crates/psrs-backend/src/cc/case/coverage/tests/mod.rs b/crates/psrs-backend/src/cc/case/coverage/tests/mod.rs index c02202ad..dc0de84e 100644 --- a/crates/psrs-backend/src/cc/case/coverage/tests/mod.rs +++ b/crates/psrs-backend/src/cc/case/coverage/tests/mod.rs @@ -4,6 +4,7 @@ use psrs_hir::{LocalId, ModuleId, SymbolId, TypeId as HirTypeId}; use psrs_span::TextRange; use std::rc::Rc; +mod recursive_products; mod scalar_array; fn symbol(index: u32) -> SymbolId { @@ -295,7 +296,7 @@ fn recursive_adt_analysis_finds_a_finite_uncovered_witness() { }, )); let report = analyze(&module, TypeId(0), &[cons_nil]); - assert_eq!(report.witness.as_deref(), Some("Cons (Cons Nil)")); + assert_eq!(report.witness.as_deref(), Some("Nil")); } #[derive(Clone, Debug)] @@ -411,3 +412,40 @@ fn recursive_coverage_agrees_with_a_bounded_first_match_oracle() { ); } } + +#[test] +fn a_field_of_an_uninhabited_type_does_not_make_its_constructor_redundant() { + // `data View a b = Return a | Bind b Int` checked at `b` uninhabited, like + // the reference `Free` instantiated at `Val`. `Val` has no constructors, but + // unchecked coercions still produce `Return` values, so coverage must treat + // the uninhabited field as an unknown shape rather than a complete empty + // signature and must not call `Return` redundant. + let module = module( + vec![ + Type::Constructor(TypeConstructor::User(hir_type_id(0))), + Type::Constructor(TypeConstructor::User(hir_type_id(1))), + Type::Constructor(TypeConstructor::Int), + ], + vec![ + constructor(0, "Return", 0, vec![TypeId(1)]), + constructor(1, "Bind", 0, vec![TypeId(1), TypeId(2)]), + ], + ); + let return_branch = branch(pat( + 0, + PatternKind::Constructor { + symbol: symbol(0), + arguments: vec![binder(1)], + }, + )); + let bind_branch = branch(pat( + 0, + PatternKind::Constructor { + symbol: symbol(1), + arguments: vec![binder(1), binder(2)], + }, + )); + let report = analyze(&module, TypeId(0), &[return_branch, bind_branch]); + assert!(report.exhaustive, "{report:?}"); + assert!(report.redundant_branches.is_empty(), "{report:?}"); +} diff --git a/crates/psrs-backend/src/cc/case/coverage/tests/recursive_products.rs b/crates/psrs-backend/src/cc/case/coverage/tests/recursive_products.rs new file mode 100644 index 00000000..d5a849ed --- /dev/null +++ b/crates/psrs-backend/src/cc/case/coverage/tests/recursive_products.rs @@ -0,0 +1,87 @@ +use super::*; + +#[test] +fn recursive_products_terminate_in_either_constructor_order() { + for node_first in [true, false] { + let mut constructors = vec![ + constructor(0, "Node", 0, vec![TypeId(1), TypeId(0), TypeId(0)]), + constructor(1, "Leaf", 0, Vec::new()), + ]; + if !node_first { + constructors.reverse(); + } + let module = module( + vec![ + Type::Constructor(TypeConstructor::User(hir_type_id(0))), + Type::Constructor(TypeConstructor::Int), + ], + constructors, + ); + let node = || { + pat( + 0, + PatternKind::Constructor { + symbol: symbol(0), + arguments: vec![ + pat(1, PatternKind::Wildcard), + pat(0, PatternKind::Wildcard), + pat(0, PatternKind::Wildcard), + ], + }, + ) + }; + let incomplete = analyze(&module, TypeId(0), &[branch(node())]); + assert!(!incomplete.exhaustive, "{incomplete:?}"); + assert_eq!(incomplete.witness.as_deref(), Some("Leaf")); + assert!(incomplete.redundant_branches.is_empty()); + let complete = analyze( + &module, + TypeId(0), + &[branch(node()), branch(nullary(1, 0)), branch(node())], + ); + assert!(complete.exhaustive, "{complete:?}"); + assert_eq!(complete.redundant_branches, vec![2]); + } +} + +#[test] +fn an_uninhabited_recursive_product_does_not_invent_a_witness() { + let module = module( + vec![Type::Constructor(TypeConstructor::User(hir_type_id(0)))], + vec![constructor(0, "Loop", 0, vec![TypeId(0), TypeId(0)])], + ); + let report = analyze(&module, TypeId(0), &[]); + assert!(report.exhaustive, "{report:?}"); + assert!(report.witness.is_none()); +} + +#[test] +fn missing_uninhabited_heads_do_not_hide_observed_field_gaps() { + let module = module( + vec![ + Type::Constructor(TypeConstructor::User(hir_type_id(0))), + Type::Constructor(TypeConstructor::User(hir_type_id(1))), + Type::Constructor(TypeConstructor::Boolean), + ], + vec![ + constructor(0, "Impossible", 0, vec![TypeId(1)]), + constructor(1, "Present", 0, vec![TypeId(2)]), + constructor(2, "Loop", 1, vec![TypeId(1)]), + ], + ); + let present_true = pat( + 0, + PatternKind::Constructor { + symbol: symbol(1), + arguments: vec![pat( + 2, + PatternKind::Literal { + value: psrs_core::Literal::Boolean(true), + }, + )], + }, + ); + let report = analyze(&module, TypeId(0), &[branch(present_true)]); + assert!(!report.exhaustive, "{report:?}"); + assert_eq!(report.witness.as_deref(), Some("Present (false)")); +} diff --git a/crates/psrs-backend/src/cc/case/decision/realize/mod.rs b/crates/psrs-backend/src/cc/case/decision/realize/mod.rs index 4bfb265c..bd68aae7 100644 --- a/crates/psrs-backend/src/cc/case/decision/realize/mod.rs +++ b/crates/psrs-backend/src/cc/case/decision/realize/mod.rs @@ -201,7 +201,7 @@ impl FunctionLowerer<'_> { )); }; let stored_shape = *stored_shape; - let declared_type = array_element_type(self.module, *source_type) + let _declared_type = array_element_type(self.module, *source_type) .ok_or_else(|| case_error(*span, "array pattern type is not an array"))?; let index_value = self.fresh(ValueShape::Integer); assignments.push(Assignment { @@ -224,13 +224,7 @@ impl FunctionLowerer<'_> { span: *span, }); let target_shape = self.value_shape(*target_type, *span)?; - let conversion = self.typed_conversion( - declared_type, - *target_type, - stored_shape, - target_shape, - *span, - )?; + let conversion = self.recover_payload(target_shape, *span)?; let value = self.emit_conversion( stored_value, stored_shape, @@ -307,6 +301,16 @@ impl FunctionLowerer<'_> { tag: *tag, field: *field, template_type: *declared_type, + parameters: self + .module + .constructors + .iter() + .find(|constructor| constructor.symbol == *symbol) + .ok_or_else(|| { + case_error(*span, "variant field has no source constructor owner") + })? + .parameters + .clone(), target_type: *target_type, target_shape, stored_shape, diff --git a/crates/psrs-backend/src/cc/case/decision/realize/tests/mod.rs b/crates/psrs-backend/src/cc/case/decision/realize/tests/mod.rs index 06a58810..ff57cb83 100644 --- a/crates/psrs-backend/src/cc/case/decision/realize/tests/mod.rs +++ b/crates/psrs-backend/src/cc/case/decision/realize/tests/mod.rs @@ -1,4 +1,5 @@ use super::*; +use crate::boundary::BoundaryEvidence; use crate::cc::lower::{FunctionLowerer, GeneratedSymbolAllocator}; use crate::cc::{ Assignment, AssignmentKind, Function, Module as CcModule, RepresentationTable, ValueDecl, @@ -126,6 +127,7 @@ fn compiled_root_switch_resolves_the_realizer_root_slot() { let function_types = HashMap::new(); let function_wrappers = HashMap::new(); let generated_symbols = Rc::new(RefCell::new(GeneratedSymbolAllocator::new(&module))); + let boundary = BoundaryEvidence::empty(&module); let mut lowerer = FunctionLowerer { next_value: 1, values: vec![ValueDecl { @@ -136,11 +138,18 @@ fn compiled_root_switch_resolves_the_realizer_root_slot() { signatures: &signatures, representations: &representations, module: &module, + boundary: &boundary, enum_types: &enum_types, aggregate_types: &aggregate_types, newtype_ids: &newtype_ids, + nominal_types: &std::collections::HashMap::new(), boxed_integer_type: None, boxed_number_type: None, + control_layout: None, + control_functions: std::rc::Rc::new(std::cell::RefCell::new(None)), + generated_signatures: std::rc::Rc::new(std::cell::RefCell::new( + std::collections::HashMap::new(), + )), array_types: &array_types, record_types: &record_types, constructor_tags: &constructor_tags, diff --git a/crates/psrs-backend/src/cc/case/decision/realize/tests/nested_tests.rs b/crates/psrs-backend/src/cc/case/decision/realize/tests/nested_tests.rs index 244f3d7b..5a1ac855 100644 --- a/crates/psrs-backend/src/cc/case/decision/realize/tests/nested_tests.rs +++ b/crates/psrs-backend/src/cc/case/decision/realize/tests/nested_tests.rs @@ -1,3 +1,4 @@ +use crate::boundary::BoundaryEvidence; use crate::cc::lower::{FunctionLowerer, GeneratedSymbolAllocator}; use crate::cc::{ Assignment, AssignmentKind, Function, RefShape, Reference, Representation, RepresentationTable, @@ -175,6 +176,7 @@ fn nested_sum_patterns_project_once_per_selected_constructor_and_trap_missing_ta nullable: false, heap: RefShape::Repr(representation), }); + let boundary = BoundaryEvidence::empty(&module); let mut lowerer = FunctionLowerer { next_value: 1, values: vec![ValueDecl { @@ -185,11 +187,18 @@ fn nested_sum_patterns_project_once_per_selected_constructor_and_trap_missing_ta signatures: &signatures, representations: &representations, module: &module, + boundary: &boundary, enum_types: &enum_types, aggregate_types: &aggregate_types, newtype_ids: &newtype_ids, + nominal_types: &std::collections::HashMap::new(), boxed_integer_type: None, boxed_number_type: None, + control_layout: None, + control_functions: std::rc::Rc::new(std::cell::RefCell::new(None)), + generated_signatures: std::rc::Rc::new(std::cell::RefCell::new( + std::collections::HashMap::new(), + )), array_types: &array_types, record_types: &record_types, constructor_tags: &constructor_tags, diff --git a/crates/psrs-backend/src/cc/case/decision/realize/tests/newtype_tests.rs b/crates/psrs-backend/src/cc/case/decision/realize/tests/newtype_tests.rs index 1cb615b9..9ffa9411 100644 --- a/crates/psrs-backend/src/cc/case/decision/realize/tests/newtype_tests.rs +++ b/crates/psrs-backend/src/cc/case/decision/realize/tests/newtype_tests.rs @@ -1,3 +1,4 @@ +use crate::boundary::BoundaryEvidence; use crate::cc::lower::{FunctionLowerer, GeneratedSymbolAllocator}; use crate::cc::{AssignmentKind, Function, RepresentationTable, ValueDecl, ValueShape}; use psrs_core::{ @@ -115,6 +116,7 @@ fn newtype_constructor_erases_before_nested_enum_dispatch() { let function_types = HashMap::new(); let function_wrappers = HashMap::new(); let generated_symbols = Rc::new(RefCell::new(GeneratedSymbolAllocator::new(&module))); + let boundary = BoundaryEvidence::empty(&module); let mut lowerer = FunctionLowerer { next_value: 1, values: vec![ValueDecl { @@ -125,11 +127,18 @@ fn newtype_constructor_erases_before_nested_enum_dispatch() { signatures: &signatures, representations: &representations, module: &module, + boundary: &boundary, enum_types: &enum_types, aggregate_types: &aggregate_types, newtype_ids: &newtype_ids, + nominal_types: &std::collections::HashMap::new(), boxed_integer_type: None, boxed_number_type: None, + control_layout: None, + control_functions: std::rc::Rc::new(std::cell::RefCell::new(None)), + generated_signatures: std::rc::Rc::new(std::cell::RefCell::new( + std::collections::HashMap::new(), + )), array_types: &array_types, record_types: &record_types, constructor_tags: &constructor_tags, diff --git a/crates/psrs-backend/src/cc/case/decision/realize/tests/parameterized_tests/support.rs b/crates/psrs-backend/src/cc/case/decision/realize/tests/parameterized_tests/support.rs index 3bcaf406..6da39b4e 100644 --- a/crates/psrs-backend/src/cc/case/decision/realize/tests/parameterized_tests/support.rs +++ b/crates/psrs-backend/src/cc/case/decision/realize/tests/parameterized_tests/support.rs @@ -1,3 +1,4 @@ +use crate::boundary::BoundaryEvidence; use crate::cc::lower::{FunctionLowerer, GeneratedSymbolAllocator}; use crate::cc::{ Function, RefShape, Reference, ReprId, RepresentationTable, ValueDecl, ValueShape, @@ -43,6 +44,7 @@ pub(super) fn lower_and_verify( let function_types = HashMap::new(); let function_wrappers = HashMap::new(); let generated_symbols = Rc::new(RefCell::new(GeneratedSymbolAllocator::new(module))); + let boundary = BoundaryEvidence::empty(module); let mut lowerer = FunctionLowerer { next_value: 1, values: vec![ValueDecl { @@ -53,11 +55,18 @@ pub(super) fn lower_and_verify( signatures: &signatures, representations: context.representations, module, + boundary: &boundary, enum_types: context.enum_types, aggregate_types: context.aggregate_types, newtype_ids: &newtype_ids, + nominal_types: &std::collections::HashMap::new(), boxed_integer_type: None, boxed_number_type: None, + control_layout: None, + control_functions: std::rc::Rc::new(std::cell::RefCell::new(None)), + generated_signatures: std::rc::Rc::new(std::cell::RefCell::new( + std::collections::HashMap::new(), + )), array_types: context.array_types, record_types: context.record_types, constructor_tags: &constructor_tags, diff --git a/crates/psrs-backend/src/cc/case/decision/realize/tests/product_tests.rs b/crates/psrs-backend/src/cc/case/decision/realize/tests/product_tests.rs index 5636f8a8..7d402055 100644 --- a/crates/psrs-backend/src/cc/case/decision/realize/tests/product_tests.rs +++ b/crates/psrs-backend/src/cc/case/decision/realize/tests/product_tests.rs @@ -1,3 +1,4 @@ +use crate::boundary::BoundaryEvidence; use crate::cc::lower::{FunctionLowerer, GeneratedSymbolAllocator}; use crate::cc::{ Function, RefShape, Reference, Representation, RepresentationTable, ValueDecl, ValueShape, @@ -111,6 +112,7 @@ fn single_constructor_product_dispatch_projects_and_binds_first_row_once() { nullable: false, heap: RefShape::Repr(representation), }); + let boundary = BoundaryEvidence::empty(&module); let mut lowerer = FunctionLowerer { next_value: 1, values: vec![ValueDecl { @@ -121,11 +123,18 @@ fn single_constructor_product_dispatch_projects_and_binds_first_row_once() { signatures: &signatures, representations: &representations, module: &module, + boundary: &boundary, enum_types: &enum_types, aggregate_types: &aggregate_types, newtype_ids: &newtype_ids, + nominal_types: &std::collections::HashMap::new(), boxed_integer_type: None, boxed_number_type: None, + control_layout: None, + control_functions: std::rc::Rc::new(std::cell::RefCell::new(None)), + generated_signatures: std::rc::Rc::new(std::cell::RefCell::new( + std::collections::HashMap::new(), + )), array_types: &array_types, record_types: &record_types, constructor_tags: &constructor_tags, diff --git a/crates/psrs-backend/src/cc/case/decision/realize/tests/record_tests.rs b/crates/psrs-backend/src/cc/case/decision/realize/tests/record_tests.rs index 1970b478..a80bd98a 100644 --- a/crates/psrs-backend/src/cc/case/decision/realize/tests/record_tests.rs +++ b/crates/psrs-backend/src/cc/case/decision/realize/tests/record_tests.rs @@ -1,3 +1,4 @@ +use crate::boundary::BoundaryEvidence; use crate::cc::lower::{FunctionLowerer, GeneratedSymbolAllocator}; use crate::cc::{ AssignmentKind, Function, RefShape, Reference, Representation, RepresentationTable, ValueDecl, @@ -135,6 +136,7 @@ fn nested_record_patterns_share_one_product_projection_and_keep_source_spans() { nullable: false, heap: RefShape::Repr(record_repr), }); + let boundary = BoundaryEvidence::empty(&module); let mut lowerer = FunctionLowerer { next_value: 1, values: vec![ValueDecl { @@ -145,11 +147,18 @@ fn nested_record_patterns_share_one_product_projection_and_keep_source_spans() { signatures: &signatures, representations: &representations, module: &module, + boundary: &boundary, enum_types: &enum_types, aggregate_types: &aggregate_types, newtype_ids: &newtype_ids, + nominal_types: &std::collections::HashMap::new(), boxed_integer_type: None, boxed_number_type: None, + control_layout: None, + control_functions: std::rc::Rc::new(std::cell::RefCell::new(None)), + generated_signatures: std::rc::Rc::new(std::cell::RefCell::new( + std::collections::HashMap::new(), + )), array_types: &array_types, record_types: &record_types, constructor_tags: &constructor_tags, diff --git a/crates/psrs-backend/src/cc/control/contracts.rs b/crates/psrs-backend/src/cc/control/contracts.rs new file mode 100644 index 00000000..9bc7e9c9 --- /dev/null +++ b/crates/psrs-backend/src/cc/control/contracts.rs @@ -0,0 +1,110 @@ +//! Immutable source contracts checked by Core and quoted before representation +//! erasure. No Core arena indices remain in the published CC evidence. + +use crate::cc::{ReprId, SignatureId}; +use psrs_hir::{Intrinsic, TypeId, TypeVariableId}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct ControlContractId(pub u32); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct SourceTypeIdentityId(pub u32); + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TypeConstructorIdentity { + Function, + Record, + Row, + Array, + Int, + Number, + Boolean, + String, + Char, + Unit, + Type, + Constraint, + Symbol, + User(TypeId), +} + +/// Lexical binders and stable free-variable identities are retained, including +/// nested quantifiers and open rows. This is evidence, not a second type solver. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum TypeIdentity { + Variable(TypeVariableId), + Constructor(TypeConstructorIdentity), + Application(Box, Box), + ForAll { + variables: Vec, + body: Box, + }, + Closure { + parameters: Vec, + result: Box, + }, + RowEmpty, + RowExtend { + label: String, + ty: Box, + tail: Box, + }, + TypeLevelString(String), + TypeLevelInt(i64), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ControlDomain { + pub request: TypeIdentity, + pub answer: TypeIdentity, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(in crate::cc) enum ControlImplementation { + Frame { + representation: ReprId, + case: u32, + }, + Call { + signature: SignatureId, + role: crate::cc::ControlHelperRole, + }, +} + +/// A source use can only be published from the control owner's checked proof. +/// Its fields are immutable to callers constructing or inspecting CC. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ControlContract { + pub(in crate::cc) implementation: ControlImplementation, + pub(in crate::cc) result_identity: SourceTypeIdentityId, + pub(in crate::cc) argument_identities: Vec, + pub(in crate::cc) operation: Intrinsic, + pub(in crate::cc) result: TypeIdentity, + pub(in crate::cc) arguments: Vec, + pub(in crate::cc) source: Option, + pub(in crate::cc) target: Option, +} + +impl ControlContract { + pub fn result_identity(&self) -> SourceTypeIdentityId { + self.result_identity + } + pub fn argument_identities(&self) -> &[SourceTypeIdentityId] { + &self.argument_identities + } + pub fn operation(&self) -> Intrinsic { + self.operation + } + pub fn result(&self) -> &TypeIdentity { + &self.result + } + pub fn arguments(&self) -> &[TypeIdentity] { + &self.arguments + } + pub fn source(&self) -> Option<&ControlDomain> { + self.source.as_ref() + } + pub fn target(&self) -> Option<&ControlDomain> { + self.target.as_ref() + } +} diff --git a/crates/psrs-backend/src/cc/control/helpers.rs b/crates/psrs-backend/src/cc/control/helpers.rs new file mode 100644 index 00000000..43850464 --- /dev/null +++ b/crates/psrs-backend/src/cc/control/helpers.rs @@ -0,0 +1,126 @@ +#![allow(dead_code)] +//! Stable semantic identities of predeclared control-machine helpers. +use crate::cc::SignatureId; +use psrs_hir::SymbolId; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct ControlHelperId(pub u32); + +pub use crate::control::schema::ControlHelperRole; + +/// A checked call identifies a particular helper, even when two helpers have +/// the same physical signature. Only P8 planning publishes these bindings. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ControlHelperContract { + pub(in crate::cc) role: ControlHelperRole, + pub(in crate::cc) function: SymbolId, + pub(in crate::cc) signature: SignatureId, +} + +impl ControlHelperContract { + pub fn role(&self) -> ControlHelperRole { + self.role + } + pub fn function(&self) -> SymbolId { + self.function + } + pub fn signature(&self) -> SignatureId { + self.signature + } +} + +impl crate::cc::RepresentationTable { + pub(crate) fn control_sequence(&self) -> Option { + Some(self.control_schema()?.sequence) + } + + pub(crate) fn verify_control_call( + &self, + function: SymbolId, + contract: Option, + ) -> Result<(), &'static str> { + match contract { + Some(id) => { + let helper = self + .control_helpers() + .get(id.0 as usize) + .ok_or("control call has no predeclared helper contract")?; + if helper.function != function { + return Err( + "control call target disagrees with its predeclared semantic helper", + ); + } + } + None if self + .control_helpers() + .iter() + .any(|helper| helper.function == function) => + { + return Err("control helper call is missing its semantic contract"); + } + None => {} + } + Ok(()) + } +} + +/// The raw resumption code target is planned separately from direct-call helpers. +/// Its callable protocol excludes the hidden capture receiver parameter. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ControlResumptionContract { + pub(in crate::cc) function: SymbolId, + pub(in crate::cc) signature: SignatureId, +} + +impl ControlResumptionContract { + pub fn function(&self) -> SymbolId { + self.function + } + pub fn signature(&self) -> SignatureId { + self.signature + } +} + +#[derive(Clone, Copy)] +pub(crate) struct ControlCheckpointSchema { + pub computation: crate::cc::ReprId, + pub context: crate::cc::ReprId, +} + +impl crate::cc::RepresentationTable { + pub(crate) fn control_checkpoint_schema(&self) -> Option { + let schema = self.control_schema()?; + Some(ControlCheckpointSchema { + computation: schema.computation, + context: schema.context, + }) + } +} + +#[derive(Clone, Copy)] +pub(crate) struct ControlReplaySchema { + pub sequence: crate::cc::ReprId, + pub boundary: crate::cc::ReprId, + pub status: crate::cc::ReprId, + pub result: crate::cc::ReprId, +} + +impl crate::cc::RepresentationTable { + pub(crate) fn control_replay_schema(&self) -> Option { + let schema = self.control_schema()?; + if !matches!( + self.representation(schema.status), + Some(crate::cc::Representation::Array { + element: crate::cc::ValueShape::Integer + }) + ) { + return None; + } + Some(ControlReplaySchema { + sequence: schema.sequence, + boundary: schema.extent, + status: schema.status, + result: schema.replay_result, + }) + } +} diff --git a/crates/psrs-backend/src/cc/control/mod.rs b/crates/psrs-backend/src/cc/control/mod.rs new file mode 100644 index 00000000..6952700c --- /dev/null +++ b/crates/psrs-backend/src/cc/control/mod.rs @@ -0,0 +1,18 @@ +//! Checked source contracts and semantic identities for the control machine. + +mod contracts; +mod helpers; +mod plan; +mod schema; + +pub(in crate::cc) use contracts::ControlImplementation; +pub use contracts::{ + ControlContract, ControlContractId, ControlDomain, SourceTypeIdentityId, + TypeConstructorIdentity, TypeIdentity, +}; +pub use helpers::{ + ControlHelperContract, ControlHelperId, ControlHelperRole, ControlResumptionContract, +}; +pub(in crate::cc) use plan::ControlPlanning; +pub use plan::FrozenControlPlan; +pub(crate) use schema::ControlSchema; diff --git a/crates/psrs-backend/src/cc/control/plan/mod.rs b/crates/psrs-backend/src/cc/control/plan/mod.rs new file mode 100644 index 00000000..1b1127a3 --- /dev/null +++ b/crates/psrs-backend/src/cc/control/plan/mod.rs @@ -0,0 +1,145 @@ +//! Mutable source planning has one transition to an immutable machine contract. +use super::schema::ControlSchema; +use super::{ControlContract, ControlHelperContract, ControlHelperRole, ControlResumptionContract}; +use crate::cc::{ReprId, RepresentationTable}; +use std::collections::HashMap; + +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub(in crate::cc) struct ControlPlanning { + contracts: Vec, + identities: Vec, + frozen: Option, +} + +/// P8's published machine metadata. Instructions and layouts remain ordinary +/// mutable IR; consumers cannot modify or replace the semantic bindings. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FrozenControlPlan { + contracts: Box<[ControlContract]>, + identities: Box<[super::super::TypeIdentity]>, + helpers: Box<[ControlHelperContract]>, + roles: HashMap, + resumption: ControlResumptionContract, + schema: ControlSchema, +} + +impl FrozenControlPlan { + pub fn source_type_identities(&self) -> &[super::super::TypeIdentity] { + &self.identities + } + pub fn contracts(&self) -> &[ControlContract] { + &self.contracts + } + pub fn helpers(&self) -> &[ControlHelperContract] { + &self.helpers + } + pub fn helper(&self, role: ControlHelperRole) -> Option<&ControlHelperContract> { + self.roles + .get(&role) + .and_then(|index| self.helpers.get(*index)) + } + pub fn resumption(&self) -> &ControlResumptionContract { + &self.resumption + } + pub fn result_pair(&self) -> ReprId { + self.schema.result_pair + } + pub(in crate::cc) fn schema(&self) -> ControlSchema { + self.schema + } +} + +impl RepresentationTable { + pub fn source_type_identities(&self) -> &[super::super::TypeIdentity] { + self.control_plan().map_or( + &self.control.identities, + FrozenControlPlan::source_type_identities, + ) + } + + pub(in crate::cc) fn plan_control_identity( + &mut self, + identity: super::super::TypeIdentity, + ) -> super::super::SourceTypeIdentityId { + assert!( + self.control.frozen.is_none(), + "source type identities cannot change after control planning freezes" + ); + let id = super::super::SourceTypeIdentityId(self.control.identities.len() as u32); + self.control.identities.push(identity); + id + } + pub fn control_plan(&self) -> Option<&FrozenControlPlan> { + self.control.frozen.as_ref() + } + pub fn control_helpers(&self) -> &[ControlHelperContract] { + self.control_plan().map_or(&[], FrozenControlPlan::helpers) + } + pub fn control_contracts(&self) -> &[ControlContract] { + self.control_plan() + .map_or(&self.control.contracts, FrozenControlPlan::contracts) + } + pub fn control_resumption(&self) -> Option<&ControlResumptionContract> { + self.control_plan().map(FrozenControlPlan::resumption) + } + pub fn control_result_pair(&self) -> Option { + self.control_plan().map(FrozenControlPlan::result_pair) + } + + pub(in crate::cc) fn plan_control_contract(&mut self, contract: ControlContract) { + assert!( + self.control.frozen.is_none(), + "source contracts cannot change after control planning freezes" + ); + if !self.control.contracts.contains(&contract) { + self.control.contracts.push(contract); + } + } + + pub(in crate::cc) fn freeze_control_plan( + &mut self, + helpers: Vec, + resumption: ControlResumptionContract, + schema: ControlSchema, + ) -> Result<(), &'static str> { + if self.control.frozen.is_some() { + return Err("control machine plan is already frozen"); + } + let mut roles = HashMap::new(); + let mut symbols = std::collections::HashSet::from([resumption.function()]); + for (index, helper) in helpers.iter().enumerate() { + if !symbols.insert(helper.function()) { + return Err("control machine planning repeats a callable identity"); + } + if roles.insert(helper.role(), index).is_some() { + return Err("control machine planning repeats a helper role"); + } + } + if ControlHelperRole::ALL + .iter() + .any(|role| !roles.contains_key(role)) + { + return Err("control machine planning omits a helper role"); + } + let contracts = std::mem::take(&mut self.control.contracts).into_boxed_slice(); + let identities = std::mem::take(&mut self.control.identities).into_boxed_slice(); + self.control.frozen = Some(FrozenControlPlan { + contracts, + identities, + helpers: helpers.into_boxed_slice(), + roles, + resumption, + schema, + }); + if let Err(error) = self.verify_control_schema() { + let candidate = self.control.frozen.take().expect("candidate plan exists"); + self.control.contracts = candidate.contracts.into_vec(); + self.control.identities = candidate.identities.into_vec(); + return Err(error); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/psrs-backend/src/cc/control/plan/tests.rs b/crates/psrs-backend/src/cc/control/plan/tests.rs new file mode 100644 index 00000000..63b3f5f2 --- /dev/null +++ b/crates/psrs-backend/src/cc/control/plan/tests.rs @@ -0,0 +1,173 @@ +use super::*; +use crate::cc::{Representation, Signature, ValueShape}; + +fn fixture() -> ( + RepresentationTable, + Vec, + ControlResumptionContract, + ControlSchema, +) { + let mut table = RepresentationTable::default(); + let payload = crate::cc::payload::erased_shape(); + let continuation = table.add_signature(Signature { + parameters: vec![payload], + result: payload, + }); + let nominal = crate::cc::layout::control::reserve(&mut table); + let layout = crate::cc::layout::control::append(&mut table, continuation, &nominal); + let schema = ControlSchema { + computation: layout.computation, + sequence: layout.sequence, + context: layout.context, + extent: layout.boundary, + status: layout.status, + owner: layout.owner, + callback: layout.callback, + result_pair: layout.pair, + replay_result: layout.replay_result, + }; + let helpers = ControlHelperRole::ALL + .into_iter() + .enumerate() + .map(|(index, role)| { + let signature = match role { + ControlHelperRole::Executor => layout.executor, + ControlHelperRole::Run => layout.run, + ControlHelperRole::CloneSequence => layout.clone_sequence, + ControlHelperRole::RebuildSequence => layout.rebuild_sequence, + ControlHelperRole::Start => layout.start, + ControlHelperRole::Check | ControlHelperRole::Close => layout.boundary_check, + ControlHelperRole::Once => layout.once, + ControlHelperRole::Cancel => layout.cancel, + }; + ControlHelperContract { + role, + signature, + function: psrs_hir::SymbolId::new(psrs_hir::ModuleId(0), index as u32), + } + }) + .collect(); + let raw = ControlResumptionContract { + function: psrs_hir::SymbolId::new( + psrs_hir::ModuleId(0), + ControlHelperRole::ALL.len() as u32, + ), + signature: continuation, + }; + (table, helpers, raw, schema) +} + +#[test] +fn freezing_is_single_assignment_and_preserves_the_published_plan() { + let (mut table, helpers, raw, schema) = fixture(); + table + .freeze_control_plan(helpers.clone(), raw.clone(), schema) + .unwrap(); + let frozen = table.control_plan().unwrap().clone(); + assert_eq!( + frozen.helper(ControlHelperRole::Executor), + Some(&helpers[0]) + ); + let error = table.freeze_control_plan(helpers, raw, schema).unwrap_err(); + assert_eq!(error, "control machine plan is already frozen"); + assert_eq!(table.control_plan(), Some(&frozen)); +} + +#[test] +fn planning_failure_does_not_publish_a_partial_plan_and_can_be_retried() { + let (mut table, helpers, raw, schema) = fixture(); + let identity = crate::cc::TypeIdentity::Constructor(crate::cc::TypeConstructorIdentity::Int); + let id = table.plan_control_identity(identity.clone()); + let original = table.representation(schema.status).unwrap().clone(); + table.set( + schema.status, + Representation::Array { + element: ValueShape::Boolean, + }, + ); + assert!( + table + .freeze_control_plan(helpers.clone(), raw.clone(), schema) + .is_err() + ); + assert!(table.control_plan().is_none()); + assert_eq!( + table.source_type_identities().get(id.0 as usize), + Some(&identity) + ); + table.set(schema.status, original); + table.freeze_control_plan(helpers, raw, schema).unwrap(); + assert_eq!( + table + .control_plan() + .unwrap() + .source_type_identities() + .get(id.0 as usize), + Some(&identity) + ); +} + +#[test] +fn helper_roles_and_symbols_must_be_complete_and_unique_before_publication() { + for mutation in 0..3 { + let (mut table, mut helpers, raw, schema) = fixture(); + match mutation { + 0 => { + helpers.pop(); + } + 1 => helpers[1].role = helpers[0].role, + 2 => helpers[1].function = raw.function(), + _ => unreachable!(), + } + assert!(table.freeze_control_plan(helpers, raw, schema).is_err()); + assert!(table.control_plan().is_none()); + } +} + +#[test] +fn same_arity_layout_mutations_cannot_change_machine_storage_roles() { + for mutation in 0..4 { + let (mut table, helpers, raw, schema) = fixture(); + table.freeze_control_plan(helpers, raw, schema).unwrap(); + match mutation { + 0 => { + let Representation::Variant { mut cases } = + table.representation(schema.sequence).unwrap().clone() + else { + unreachable!() + }; + cases[1].fields.swap(0, 1); + table.set(schema.sequence, Representation::Variant { cases }); + } + 1 => table.set( + schema.status, + Representation::Array { + element: ValueShape::Boolean, + }, + ), + 2 => { + let Representation::Product { mut fields } = + table.representation(schema.replay_result).unwrap().clone() + else { + unreachable!() + }; + fields.swap(0, 1); + table.set(schema.replay_result, Representation::Product { fields }); + } + 3 => { + let signature = table + .control_plan() + .unwrap() + .helper(ControlHelperRole::Executor) + .unwrap() + .signature(); + table.signatures[signature.0 as usize].parameters.swap(2, 3); + } + _ => unreachable!(), + } + assert!( + table.verify_control_schema().is_err(), + "mutation {mutation} escaped schema validation" + ); + } +} diff --git a/crates/psrs-backend/src/cc/control/schema.rs b/crates/psrs-backend/src/cc/control/schema.rs new file mode 100644 index 00000000..8eadb49d --- /dev/null +++ b/crates/psrs-backend/src/cc/control/schema.rs @@ -0,0 +1,198 @@ +//! The frozen placement of the named machine schema in one representation table. +//! +//! Planning fixes every machine carrier and private cell once, after the shared +//! [`crate::control::schema`] names and actually emitted helper signatures +//! agree. Consumers read named placements from here instead of recovering a +//! role from a helper parameter offset or a variant field position. The value +//! is immutable evidence: a later pass may read it but not replace it. + +use super::{ControlHelperContract, ControlHelperRole}; +use crate::cc::{RefShape, Reference, ReprId, Representation, SignatureId, ValueShape}; +use crate::control::schema::FieldKind; +use crate::control::schema::{ComputationCase, ContextCase, ExtentCase, OwnerCase, SequenceCase}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ControlSchema { + /// The shared `Control f a` frame. + pub computation: ReprId, + /// The persistent sequencing spine. + pub sequence: ReprId, + /// The dispatch context. + pub context: ReprId, + /// A lifetime extent (the boundary carrier). + pub extent: ReprId, + /// The one-slot mutable status cell. + pub status: ReprId, + /// The one-shot owner object. + pub owner: ReprId, + /// The one-slot callback cell. + pub callback: ReprId, + /// The closed-run result pair. + pub result_pair: ReprId, + /// The replay result product. + pub replay_result: ReprId, +} + +impl ControlSchema { + /// Resolve one logical storage role through this module's frozen placement. + pub(crate) fn shape(self, kind: FieldKind, continuation: SignatureId) -> ValueShape { + use FieldKind::*; + let heap = match kind { + Payload => return crate::cc::payload::erased_shape(), + Callable => RefShape::Closure(continuation), + Computation => RefShape::Repr(self.computation), + Sequence => RefShape::Repr(self.sequence), + Context => RefShape::Repr(self.context), + Extent => RefShape::Repr(self.extent), + Status => RefShape::Repr(self.status), + Owner => RefShape::Repr(self.owner), + Callback => RefShape::Repr(self.callback), + ResultPair => RefShape::Repr(self.result_pair), + ReplayResult => RefShape::Repr(self.replay_result), + }; + ValueShape::Reference(Reference { + nullable: false, + heap, + }) + } +} + +impl crate::cc::RepresentationTable { + /// The frozen machine placement, when this module uses control. + pub(crate) fn control_schema(&self) -> Option { + self.control_plan().map(|plan| plan.schema()) + } + + /// The single checked contract for a semantic helper role. + pub(crate) fn control_helper(&self, role: ControlHelperRole) -> Option<&ControlHelperContract> { + self.control_plan()?.helper(role) + } + + /// Validate the complete logical storage contract against physical CC shapes. + pub(crate) fn verify_control_schema(&self) -> Result<(), &'static str> { + let Some(schema) = self.control_schema() else { + return if self.control_helpers().is_empty() { + Ok(()) + } else { + Err("control helpers have no placed machine schema") + }; + }; + let continuation = self + .control_resumption() + .ok_or("control placement has no raw continuation signature")? + .signature(); + let shape = |kind| schema.shape(kind, continuation); + let mut identities = std::collections::HashSet::new(); + for repr in [ + schema.computation, + schema.sequence, + schema.context, + schema.extent, + schema.status, + schema.owner, + schema.callback, + schema.result_pair, + schema.replay_result, + ] { + if !identities.insert(repr) { + return Err("control placement aliases distinct storage roles"); + } + } + for (repr, expected) in [ + ( + schema.computation, + ComputationCase::ALL + .iter() + .map(|case| (case.tag(), case.fields())) + .collect::>(), + ), + ( + schema.sequence, + SequenceCase::ALL + .iter() + .map(|case| (case.tag(), case.fields())) + .collect(), + ), + ( + schema.context, + ContextCase::ALL + .iter() + .map(|case| (case.tag(), case.fields())) + .collect(), + ), + ( + schema.extent, + ExtentCase::ALL + .iter() + .map(|case| (case.tag(), case.fields())) + .collect(), + ), + ( + schema.owner, + OwnerCase::ALL + .iter() + .map(|case| (case.tag(), case.fields())) + .collect(), + ), + ] { + let Some(Representation::Variant { cases }) = self.representation(repr) else { + return Err("control placement names a carrier that is not a variant"); + }; + if cases.len() != expected.len() { + return Err("control carrier does not match the named schema case count"); + } + for (actual, (tag, fields)) in cases.iter().zip(expected) { + let expected = fields.iter().map(|kind| shape(*kind)).collect::>(); + if actual.tag != tag || actual.fields != expected { + return Err("control carrier fields disagree with their logical storage roles"); + } + } + } + for (repr, fields) in [ + (schema.result_pair, vec![shape(FieldKind::Payload); 2]), + ( + schema.replay_result, + vec![shape(FieldKind::Sequence), shape(FieldKind::Extent)], + ), + ] { + if self.representation(repr) != Some(&Representation::Product { fields }) { + return Err("control product fields disagree with their logical storage roles"); + } + } + for (repr, element) in [ + (schema.status, ValueShape::Integer), + (schema.callback, shape(FieldKind::Payload)), + ] { + if self.representation(repr) != Some(&Representation::Array { element }) { + return Err("control private cell has the wrong element role"); + } + } + if self.control_result_pair() != Some(schema.result_pair) { + return Err("control result pair disagrees with its placed role"); + } + for helper in self.control_helpers() { + let signature = self + .signature(helper.signature()) + .ok_or("control helper has no planned signature")?; + let parameters = helper + .role() + .parameters() + .iter() + .map(|kind| shape(*kind)) + .collect::>(); + if signature.parameters != parameters + || signature.result != shape(helper.role().result()) + { + return Err("control helper signature disagrees with its logical storage roles"); + } + } + let raw = self + .signature(continuation) + .ok_or("raw continuation signature is missing")?; + if raw.parameters != [shape(FieldKind::Payload)] || raw.result != shape(FieldKind::Payload) + { + return Err("raw continuation signature disagrees with its unary protocol"); + } + Ok(()) + } +} diff --git a/crates/psrs-backend/src/cc/convert.rs b/crates/psrs-backend/src/cc/convert.rs index c9f0b1d8..f90b7deb 100644 --- a/crates/psrs-backend/src/cc/convert.rs +++ b/crates/psrs-backend/src/cc/convert.rs @@ -64,3 +64,28 @@ pub struct AggregateConvert { pub destination: ValueShape, pub plan: ValueConversion, } + +impl ValueConversion { + /// The declared output of a conversion plan. This does not validate its + /// input or representation metadata; CC verification discharges those checks. + pub(crate) fn output_shape(&self, source: ValueShape) -> ValueShape { + let reference = |id| { + ValueShape::Reference(super::Reference { + nullable: false, + heap: super::RefShape::Repr(id), + }) + }; + match self { + Self::Identity => source, + Self::BoxScalar { representation, .. } => reference(*representation), + Self::UnboxScalar { destination, .. } + | Self::RecoverReference { destination, .. } + | Self::FunctionAdapter { destination, .. } => *destination, + Self::EraseReference => super::payload::erased_shape(), + Self::ArrayMap { target, .. } | Self::ProductMap { target, .. } => reference(*target), + Self::Sequence(steps) => steps + .iter() + .fold(source, |shape, step| step.output_shape(shape)), + } + } +} diff --git a/crates/psrs-backend/src/cc/layout/aggregate.rs b/crates/psrs-backend/src/cc/layout/aggregate.rs index 29309b4f..898c0ba4 100644 --- a/crates/psrs-backend/src/cc/layout/aggregate.rs +++ b/crates/psrs-backend/src/cc/layout/aggregate.rs @@ -14,7 +14,7 @@ enum AggregateKey { Record(Vec<(String, ValueShape)>), } -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq)] pub(super) struct AggregateLayouts { pub(super) arrays: HashMap, pub(super) records: HashMap, @@ -28,8 +28,12 @@ pub(super) fn reserve_aggregate_layouts( ) -> AggregateLayouts { let mut arrays = HashMap::new(); let mut records = HashMap::new(); + let live_types = super::functions::live_type_ids(module); for index in 0..module.types.len() { let id = TypeId(index as u32); + if !live_types.contains(&id) { + continue; + } if array_element_type(module, id).is_some() { arrays.insert(id, representations.reserve()); } else if module.is_record_type(id) && !module.record_is_open(id).unwrap_or(false) { @@ -47,6 +51,48 @@ pub(super) fn normalize_aggregate_layouts( enum_types: &HashSet, aggregate_types: &HashSet, newtype_ids: &HashSet, + nominal_types: &HashMap, + mut function_types: HashMap, + mut layouts: AggregateLayouts, + representations: &mut RepresentationTable, +) -> Result<(AggregateLayouts, HashMap), Vec> { + // Aggregate keys contain closure signatures, whose keys in turn contain + // aggregate handles. Intern both until neither identity changes. + let bound = representations.representations.len() + representations.signatures.len() + 2; + for _ in 0..bound { + let previous_layouts = layouts.clone(); + let previous_functions = function_types.clone(); + let previous_table = representations.clone(); + (layouts, function_types) = normalize_pass( + module, + enum_types, + aggregate_types, + newtype_ids, + nominal_types, + function_types, + layouts, + representations, + )?; + if layouts == previous_layouts + && function_types == previous_functions + && *representations == previous_table + { + return Ok((layouts, function_types)); + } + } + Err(layout_error( + module.span, + "aggregate and callable layout normalization did not converge", + )) +} + +#[allow(clippy::too_many_arguments)] +fn normalize_pass( + module: &Module, + enum_types: &HashSet, + aggregate_types: &HashSet, + newtype_ids: &HashSet, + nominal_types: &HashMap, mut function_types: HashMap, layouts: AggregateLayouts, representations: &mut RepresentationTable, @@ -57,6 +103,7 @@ pub(super) fn normalize_aggregate_layouts( enum_types, aggregate_types, newtype_ids, + nominal_types, function_types: function_types.clone(), representations, arrays: layouts.arrays, @@ -100,6 +147,24 @@ pub(super) fn normalize_aggregate_layouts( } remap_shape(&mut signature.result, &remapped); } + for representation in &mut representations_table.representations { + match representation { + Representation::Box { value } => remap_shape(value, &remapped), + Representation::Product { fields } => { + for field in fields { + remap_shape(field, &remapped); + } + } + Representation::Variant { cases } => { + for case in cases { + for field in &mut case.fields { + remap_shape(field, &remapped); + } + } + } + Representation::Array { element } => remap_shape(element, &remapped), + } + } canonicalize_signatures(representations_table, &mut function_types); Ok((layouts, function_types)) } @@ -109,6 +174,7 @@ struct Builder<'a> { enum_types: &'a HashSet, aggregate_types: &'a HashSet, newtype_ids: &'a HashSet, + nominal_types: &'a HashMap, function_types: HashMap, representations: &'a mut RepresentationTable, arrays: HashMap, @@ -127,35 +193,34 @@ impl Builder<'_> { if !self.active.insert(id) { return Ok(handle); } - let (key, representation, labels) = - if let Some(element) = array_element_type(self.module, id) { - let shape = self.value_shape(element, span)?; - ( - AggregateKey::Array(shape), - Representation::Array { element: shape }, - None, - ) - } else { - let Some(fields) = self.module.record_fields(id) else { - self.active.remove(&id); - return Err(layout_error( - span, - "aggregate type has no array or record layout", - )); - }; - let mut fields = fields - .iter() - .map(|(label, ty)| Ok((label.clone(), self.value_shape(*ty, span)?))) - .collect::, Vec>>()?; - fields.sort_by(|left, right| left.0.cmp(&right.0)); - let labels = fields.iter().map(|(label, _)| label.clone()).collect(); - let shapes = fields.iter().map(|(_, shape)| *shape).collect(); - ( - AggregateKey::Record(fields), - Representation::Product { fields: shapes }, - Some(labels), - ) + let (key, representation, labels) = if array_element_type(self.module, id).is_some() { + let shape = crate::cc::payload::erased_shape(); + ( + AggregateKey::Array(shape), + Representation::Array { element: shape }, + None, + ) + } else { + let Some(fields) = self.module.record_fields(id) else { + self.active.remove(&id); + return Err(layout_error( + span, + "aggregate type has no array or record layout", + )); }; + let mut fields = fields + .iter() + .map(|(label, ty)| Ok((label.clone(), self.value_shape(*ty, span)?))) + .collect::, Vec>>()?; + fields.sort_by(|left, right| left.0.cmp(&right.0)); + let labels = fields.iter().map(|(label, _)| label.clone()).collect(); + let shapes = fields.iter().map(|(_, shape)| *shape).collect(); + ( + AggregateKey::Record(fields), + Representation::Product { fields: shapes }, + Some(labels), + ) + }; let canonical = if let Some(canonical) = self.canonical.get(&key).copied() { canonical } else { @@ -196,6 +261,7 @@ impl Builder<'_> { self.enum_types, self.aggregate_types, self.newtype_ids, + self.nominal_types, &self.arrays, &self.records, &self.function_types, diff --git a/crates/psrs-backend/src/cc/layout/captures.rs b/crates/psrs-backend/src/cc/layout/captures.rs index 883d63fc..c04d470a 100644 --- a/crates/psrs-backend/src/cc/layout/captures.rs +++ b/crates/psrs-backend/src/cc/layout/captures.rs @@ -11,6 +11,10 @@ pub(super) fn module_has_integer_capture(module: &CoreModule) -> bool { } fn expression_has_integer_capture(expression: &Expr, module: &CoreModule) -> bool { + psrs_span::with_sufficient_stack(|| expression_has_integer_capture_inner(expression, module)) +} + +fn expression_has_integer_capture_inner(expression: &Expr, module: &CoreModule) -> bool { match &expression.kind { ExprKind::Lambda { binder, body } => { let mut bound = HashSet::from([binder.id]); @@ -79,6 +83,14 @@ fn free_integer_local( expression: &Expr, module: &CoreModule, bound: &mut HashSet, +) -> bool { + psrs_span::with_sufficient_stack(|| free_integer_local_inner(expression, module, bound)) +} + +fn free_integer_local_inner( + expression: &Expr, + module: &CoreModule, + bound: &mut HashSet, ) -> bool { match &expression.kind { ExprKind::Local(id) => { diff --git a/crates/psrs-backend/src/cc/layout/control/contracts.rs b/crates/psrs-backend/src/cc/layout/control/contracts.rs new file mode 100644 index 00000000..7f1f75de --- /dev/null +++ b/crates/psrs-backend/src/cc/layout/control/contracts.rs @@ -0,0 +1,284 @@ +//! Quote the authoritative Core proof before the immutable CC table is frozen. + +use super::ControlLayout; +use crate::BackendError; +use crate::cc::control::ControlImplementation; +use crate::cc::{ + ControlContract, ControlContractId, ControlDomain, RepresentationTable, SourceTypeIdentityId, + TypeConstructorIdentity as C, TypeIdentity as T, +}; +use psrs_core::{CheckedControlContract, ExprKind, Module, Type, TypeConstructor, TypeId}; +use psrs_hir::Intrinsic; +use std::collections::HashSet; + +pub(in crate::cc::layout) fn append( + module: &Module, + table: &mut RepresentationTable, + layout: ControlLayout, +) -> Result, Vec> { + let mut origin = None::; + let mut identities = Vec::::new(); + let mut pending = module + .declarations + .iter() + .enumerate() + .map(|(owner, declaration)| (owner, &declaration.value)) + .collect::>(); + while let Some((owner, expression)) = pending.pop() { + match &expression.kind { + ExprKind::IntrinsicCall { + intrinsic, + arguments, + } => { + if intrinsic.descriptor().category == psrs_hir::IntrinsicCategory::Control { + if matches!( + intrinsic, + Intrinsic::ControlRun | Intrinsic::ControlOnce | Intrinsic::ControlCancel + ) { + origin = Some(origin.map_or(owner, |previous| previous.min(owner))); + } + let proof = psrs_core::checked_control_contract( + module, + *intrinsic, + expression.ty, + &arguments + .iter() + .map(|argument| argument.ty) + .collect::>(), + ) + .map_err(|message| { + vec![BackendError::invalid_ir( + "P8 control contract planning", + expression.span, + message, + )] + })?; + let quoted = quote(module, &proof).ok_or_else(|| { + vec![BackendError::invalid_ir( + "P8 control contract planning", + expression.span, + "control contract contains a dangling or cyclic type", + )] + })?; + let mut intern = |id: TypeId, quoted: &T| -> SourceTypeIdentityId { + if let Some(index) = identities + .iter() + .position(|other| psrs_core::equivalent_types(id, *other, module)) + { + return SourceTypeIdentityId(index as u32); + } + identities.push(id); + table.plan_control_identity(quoted.clone()) + }; + let result_identity = intern(proof.result(), "ed.result); + let argument_identities = proof + .arguments() + .iter() + .zip("ed.arguments) + .map(|(id, quoted)| intern(*id, quoted)) + .collect(); + let contract = ControlContract { + result_identity, + argument_identities, + implementation: implementation(layout, proof.operation()), + operation: proof.operation(), + result: quoted.result, + arguments: quoted.arguments, + source: quoted.source, + target: quoted.target, + }; + table.plan_control_contract(contract); + } + pending.extend(arguments.iter().map(|value| (owner, value))); + } + ExprKind::Constructor { arguments, .. } + | ExprKind::Array { + elements: arguments, + } => pending.extend(arguments.iter().map(|value| (owner, value))), + ExprKind::Record { fields } => { + pending.extend(fields.iter().map(|(_, value)| (owner, value))) + } + ExprKind::RecordUpdate { record, fields } => { + pending.push((owner, record)); + pending.extend(fields.iter().map(|(_, value)| (owner, value))); + } + ExprKind::FieldAccess { record, .. } => pending.push((owner, record)), + ExprKind::RepresentationCast { value, .. } => pending.push((owner, value)), + ExprKind::Application(left, right) => { + pending.push((owner, left)); + pending.push((owner, right)); + } + ExprKind::Lambda { body, .. } => pending.push((owner, body)), + ExprKind::Let { bindings, body } => { + pending.extend(bindings.iter().map(|binding| (owner, &binding.value))); + pending.push((owner, body)); + } + ExprKind::If { + condition, + then_branch, + else_branch, + } => { + pending.extend( + [ + condition.as_ref(), + then_branch.as_ref(), + else_branch.as_ref(), + ] + .into_iter() + .map(|value| (owner, value)), + ); + } + ExprKind::Case { + scrutinee, + branches, + } => { + pending.push((owner, scrutinee)); + pending.extend(branches.iter().map(|branch| (owner, &branch.value))); + } + _ => {} + } + } + Ok(origin.map(|index| module.declarations[index].symbol.module)) +} + +pub(in crate::cc) fn lookup( + module: &Module, + table: &RepresentationTable, + proof: &CheckedControlContract, +) -> Option { + let quoted = quote(module, proof)?; + table + .control_contracts() + .iter() + .position(|candidate| { + candidate.operation == proof.operation() + && candidate.result == quoted.result + && candidate.arguments == quoted.arguments + && candidate.source == quoted.source + && candidate.target == quoted.target + }) + .map(|index| ControlContractId(index as u32)) +} + +struct QuotedContract { + result: T, + arguments: Vec, + source: Option, + target: Option, +} + +fn quote(module: &Module, proof: &CheckedControlContract) -> Option { + let domain = |domain: psrs_core::ControlDomain| { + Some(ControlDomain { + request: ty(module, domain.request, &mut HashSet::new())?, + answer: ty(module, domain.answer, &mut HashSet::new())?, + }) + }; + Some(QuotedContract { + result: ty(module, proof.result(), &mut HashSet::new())?, + arguments: proof + .arguments() + .iter() + .map(|id| ty(module, *id, &mut HashSet::new())) + .collect::>()?, + source: match proof.source() { + Some(value) => Some(domain(value)?), + None => None, + }, + target: match proof.target() { + Some(value) => Some(domain(value)?), + None => None, + }, + }) +} + +fn ty(module: &Module, id: TypeId, visiting: &mut HashSet) -> Option { + psrs_span::with_sufficient_stack(|| { + if !visiting.insert(id) { + return None; + } + let result = match module.types.get(id.0 as usize)? { + Type::Variable(variable) => T::Variable(*variable), + Type::Constructor(constructor) => T::Constructor(match constructor { + TypeConstructor::Function => C::Function, + TypeConstructor::Record => C::Record, + TypeConstructor::Row => C::Row, + TypeConstructor::Array => C::Array, + TypeConstructor::Int => C::Int, + TypeConstructor::Number => C::Number, + TypeConstructor::Boolean => C::Boolean, + TypeConstructor::String => C::String, + TypeConstructor::Char => C::Char, + TypeConstructor::Unit => C::Unit, + TypeConstructor::Type => C::Type, + TypeConstructor::Constraint => C::Constraint, + TypeConstructor::Symbol => C::Symbol, + TypeConstructor::User(id) => C::User(*id), + }), + Type::Application(head, argument) => T::Application( + Box::new(ty(module, *head, visiting)?), + Box::new(ty(module, *argument, visiting)?), + ), + Type::ForAll { variables, body } => T::ForAll { + variables: variables.clone(), + body: Box::new(ty(module, *body, visiting)?), + }, + Type::Closure { parameters, result } => T::Closure { + parameters: parameters + .iter() + .map(|id| ty(module, *id, visiting)) + .collect::>()?, + result: Box::new(ty(module, *result, visiting)?), + }, + Type::RowEmpty => T::RowEmpty, + Type::RowExtend { + label, + ty: field, + tail, + } => T::RowExtend { + label: label.clone(), + ty: Box::new(ty(module, *field, visiting)?), + tail: Box::new(ty(module, *tail, visiting)?), + }, + Type::TypeLevelString(value) => T::TypeLevelString(value.clone()), + Type::TypeLevelInt(value) => T::TypeLevelInt(*value), + }; + visiting.remove(&id); + Some(result) + }) +} + +fn implementation(layout: ControlLayout, operation: Intrinsic) -> ControlImplementation { + use Intrinsic::*; + let case = match operation { + ControlReturn => crate::control::schema::ComputationCase::Return, + ControlPerform => crate::control::schema::ComputationCase::Perform, + ControlBind => crate::control::schema::ComputationCase::Bind, + ControlHandle => crate::control::schema::ComputationCase::Handle, + ControlResume => crate::control::schema::ComputationCase::OwnedResume, + ControlRun => { + return ControlImplementation::Call { + signature: layout.run, + role: crate::cc::ControlHelperRole::Run, + }; + } + ControlOnce => { + return ControlImplementation::Call { + signature: layout.once, + role: crate::cc::ControlHelperRole::Once, + }; + } + ControlCancel => { + return ControlImplementation::Call { + signature: layout.cancel, + role: crate::cc::ControlHelperRole::Cancel, + }; + } + _ => unreachable!("checked control operation"), + } + .tag(); + ControlImplementation::Frame { + representation: layout.computation, + case, + } +} diff --git a/crates/psrs-backend/src/cc/layout/control/mod.rs b/crates/psrs-backend/src/cc/layout/control/mod.rs new file mode 100644 index 00000000..e4a49b98 --- /dev/null +++ b/crates/psrs-backend/src/cc/layout/control/mod.rs @@ -0,0 +1,208 @@ +//! Predeclared representations for the persistent control abstract machine. + +mod contracts; + +pub(in crate::cc::layout) use contracts::append as append_contracts; +pub(in crate::cc) use contracts::lookup; + +use crate::cc::payload::erased_shape; +use crate::cc::{ + ReprId, Representation, RepresentationTable, Signature, SignatureId, ValueShape, VariantCase, +}; + +use psrs_hir::TypeId as HirTypeId; +use std::collections::HashMap; + +/// Reserve builtin carriers before any source signature or aggregate layout. +pub(in crate::cc) fn reserve(table: &mut RepresentationTable) -> HashMap { + HashMap::from([ + (HirTypeId::CONTROL, table.reserve()), + (HirTypeId::RESUMPTION, table.reserve()), + ]) +} + +#[derive(Clone, Copy)] +pub(in crate::cc) struct ControlLayout { + pub computation: ReprId, + pub sequence: ReprId, + pub pair: ReprId, + pub continuation: SignatureId, + pub executor: SignatureId, + pub run: SignatureId, + pub context: ReprId, + pub clone_sequence: SignatureId, + pub rebuild_sequence: SignatureId, + pub owner: ReprId, + pub callback: ReprId, + pub boundary: ReprId, + pub status: ReprId, + pub start: SignatureId, + pub boundary_check: SignatureId, + pub once: SignatureId, + pub cancel: SignatureId, + pub replay_result: ReprId, +} + +pub(in crate::cc) fn append( + table: &mut RepresentationTable, + continuation: SignatureId, + nominal_types: &HashMap, +) -> ControlLayout { + let erased = erased_shape(); + let computation = nominal_types[&HirTypeId::CONTROL]; + let sequence = table.reserve(); + let context = table.reserve(); + let owner = nominal_types[&HirTypeId::RESUMPTION]; + let boundary = table.reserve(); + let callback = crate::cc::layout::protocols::array(table, erased); + let status = crate::cc::layout::protocols::array(table, ValueShape::Integer); + let replay_result = table.reserve(); + let labels = vec!["_1".to_owned(), "_2".to_owned()]; + let pair = crate::cc::layout::protocols::record(table, &labels).unwrap_or_else(|| { + let pair = table.reserve(); + table.set( + pair, + Representation::Product { + fields: vec![erased; 2], + }, + ); + table.set_product_labels(pair, labels); + pair + }); + let schema = crate::cc::ControlSchema { + computation, + sequence, + context, + extent: boundary, + status, + owner, + callback, + result_pair: pair, + replay_result, + }; + let shape = |kind| schema.shape(kind, continuation); + use crate::control::schema::{ + ComputationCase, ContextCase, ControlHelperRole as R, ExtentCase, FieldKind, OwnerCase, + SequenceCase, + }; + for (repr, cases) in [ + ( + computation, + ComputationCase::ALL + .iter() + .map(|case| (case.tag(), case.fields())) + .collect::>(), + ), + ( + sequence, + SequenceCase::ALL + .iter() + .map(|case| (case.tag(), case.fields())) + .collect(), + ), + ( + context, + ContextCase::ALL + .iter() + .map(|case| (case.tag(), case.fields())) + .collect(), + ), + ( + owner, + OwnerCase::ALL + .iter() + .map(|case| (case.tag(), case.fields())) + .collect(), + ), + ( + boundary, + ExtentCase::ALL + .iter() + .map(|case| (case.tag(), case.fields())) + .collect(), + ), + ] { + table.set( + repr, + Representation::Variant { + cases: cases + .into_iter() + .map(|(tag, fields)| VariantCase { + tag, + fields: fields.iter().map(|kind| shape(*kind)).collect(), + }) + .collect(), + }, + ); + } + table.set( + replay_result, + Representation::Product { + fields: vec![shape(FieldKind::Sequence), shape(FieldKind::Extent)], + }, + ); + let mut signature = |role: R| { + table.add_signature(Signature { + parameters: role.parameters().iter().map(|kind| shape(*kind)).collect(), + result: shape(role.result()), + }) + }; + let executor = signature(R::Executor); + let run = signature(R::Run); + let clone_sequence = signature(R::CloneSequence); + let rebuild_sequence = signature(R::RebuildSequence); + let start = signature(R::Start); + let boundary_check = signature(R::Check); + let once = signature(R::Once); + let cancel = signature(R::Cancel); + ControlLayout { + computation, + sequence, + pair, + continuation, + executor, + run, + context, + clone_sequence, + rebuild_sequence, + owner, + callback, + boundary, + status, + start, + boundary_check, + once, + cancel, + replay_result, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn control_cells_reuse_existing_canonical_array_layouts() { + let mut table = RepresentationTable::default(); + let erased = erased_shape(); + let callback = crate::cc::layout::protocols::array(&mut table, erased); + let status = + crate::cc::layout::protocols::array(&mut table, crate::cc::ValueShape::Integer); + let continuation = table.add_signature(Signature { + parameters: vec![erased], + result: erased, + }); + let nominal_types = reserve(&mut table); + let layout = append(&mut table, continuation, &nominal_types); + assert_eq!(layout.callback, callback); + assert_eq!(layout.status, status); + assert_eq!( + table + .representations + .iter() + .filter(|repr| matches!(repr, Representation::Array { .. })) + .count(), + 2 + ); + } +} diff --git a/crates/psrs-backend/src/cc/layout/declaration.rs b/crates/psrs-backend/src/cc/layout/declaration.rs new file mode 100644 index 00000000..61247b40 --- /dev/null +++ b/crates/psrs-backend/src/cc/layout/declaration.rs @@ -0,0 +1,74 @@ +//! The syntactic calling boundary of a checked Core declaration. + +use crate::BackendError; +use psrs_core::{Declaration, ExprKind, Module, TypeId}; +use psrs_span::TextRange; + +pub(in crate::cc) struct DeclarationCall { + pub parameters: Vec<(TypeId, TextRange)>, + pub result: TypeId, +} + +/// Only the leading lambdas belong to this declaration's call. A case, let, +/// alias, quantified result or explicit closure result produces a value whose +/// own parameters must be called separately. +pub(in crate::cc) fn declaration_call_parts( + module: &Module, + declaration: &Declaration, +) -> Result> { + let mut ty = super::unquantified_type(module, declaration.ty); + let mut value = &declaration.value; + let mut parameters = Vec::new(); + if let Some((closure_parameters, result)) = psrs_core::closure_parts(&module.types, ty) { + let mut peeled = 0; + for parameter in closure_parameters { + let ExprKind::Lambda { binder, body } = &value.kind else { + break; + }; + if !module.types_equivalent(binder.ty, *parameter) { + return Err(vec![BackendError::new( + "P8 closure conversion", + binder.span, + "lambda binder type differs from the function parameter type", + )]); + } + parameters.push((binder.ty, binder.span)); + value = body; + peeled += 1; + } + if peeled == closure_parameters.len() { + ty = result; + } else if peeled != 0 { + return Err(vec![BackendError::new( + "P8 closure conversion", + declaration.span, + "closure declaration is missing a parameter", + )]); + } + } + while let ExprKind::Lambda { binder, body } = &value.kind { + if psrs_core::closure_parts(&module.types, ty).is_some() { + break; + } + let Some((parameter, result)) = psrs_core::arrow_parts(&module.types, ty) else { + break; + }; + if !module.types_equivalent(parameter, binder.ty) { + return Err(vec![BackendError::new( + "P8 closure conversion", + binder.span, + "lambda binder type differs from the function parameter type", + )]); + } + parameters.push((binder.ty, binder.span)); + ty = result; + value = body; + if psrs_core::forall_parts(&module.types, ty).is_some() { + break; + } + } + Ok(DeclarationCall { + parameters, + result: ty, + }) +} diff --git a/crates/psrs-backend/src/cc/layout/functions/mod.rs b/crates/psrs-backend/src/cc/layout/functions/mod.rs index fa633aec..39781590 100644 --- a/crates/psrs-backend/src/cc/layout/functions/mod.rs +++ b/crates/psrs-backend/src/cc/layout/functions/mod.rs @@ -1,32 +1,36 @@ -use super::{Signature, function_arrow_parameters, is_callable_type, scalar_type}; +use super::{Signature, is_callable_type, scalar_type}; use crate::BackendError; use crate::cc::{RefShape, ReprId, Representation, RepresentationTable, SignatureId, ValueShape}; use psrs_core::{Module as CoreModule, TypeId}; use psrs_hir::TypeId as HirTypeId; use std::collections::{HashMap, HashSet}; +pub(crate) mod parts; +mod projection; mod reachable; +use projection::CallProjection; use reachable::referenced_types; +pub(super) fn live_type_ids(module: &CoreModule) -> HashSet { + reachable::live_type_ids(module) +} + +#[allow(clippy::too_many_arguments)] pub(super) fn append_function_types( module: &CoreModule, enum_types: &HashSet, aggregate_types: &HashSet, newtype_ids: &HashSet, + nominal_types: &HashMap, array_types: &HashMap, record_types: &HashMap, representations: &mut RepresentationTable, ) -> Result> { // Constructor schemes and unused library declarations leave callable types - // in the linked table. Laying those out would demand a runtime shape for an - // ADT the program never references. Both source arrows and the closure - // representation of a registered callable constructor are callable, so both - // receive a signature here. - let referenced = referenced_types( - module, - array_types.keys().chain(record_types.keys()).copied(), - ); + // in the linked table. Only types referenced by remaining declarations and + // constructor fields need runtime signatures. + let referenced = referenced_types(module, std::iter::empty()); let function_ids = module .types .iter() @@ -61,6 +65,7 @@ pub(super) fn append_function_types( enum_types, aggregate_types, newtype_ids, + nominal_types, array_types, record_types, &provisional_function_types, @@ -106,39 +111,27 @@ pub(crate) fn function_signature( enum_types: &HashSet, aggregate_types: &HashSet, newtype_ids: &HashSet, + nominal_types: &HashMap, array_types: &HashMap, record_types: &HashMap, function_types: &HashMap, ) -> Result> { - let (parameter_ids, result_id) = function_arrow_parameters(module, id); - let parameters = parameter_ids - .into_iter() - .map(|parameter| { - scalar_type( - module, - parameter, - module.span, - enum_types, - aggregate_types, - newtype_ids, - array_types, - record_types, - function_types, - ) - }) - .collect::, _>>()?; - let result = scalar_type( - module, - result_id, - module.span, - enum_types, - aggregate_types, - newtype_ids, - array_types, - record_types, - function_types, - )?; - Ok(Signature { parameters, result }) + let projection = CallProjection::checked(module, id)?; + let signature = projection.physical_signature(|source, ty| { + scalar_type( + source, + ty, + module.span, + enum_types, + aggregate_types, + newtype_ids, + nominal_types, + array_types, + record_types, + function_types, + ) + })?; + Ok(signature) } /// Re-interns the signature table after aggregate normalization has rewritten diff --git a/crates/psrs-backend/src/cc/layout/functions/parts.rs b/crates/psrs-backend/src/cc/layout/functions/parts.rs new file mode 100644 index 00000000..7db2c57a --- /dev/null +++ b/crates/psrs-backend/src/cc/layout/functions/parts.rs @@ -0,0 +1,25 @@ +//! Checked source call boundaries shared by layout and newtype protocols. +use super::super::layout_error; +use crate::BackendError; +use psrs_core::{Module as CoreModule, TypeId}; + +/// Derives the calling convention of a value. +/// +/// A closure contributes exactly the parameter list it was born with. Its +/// result stays a value, even when that value is a function or another +/// closure. An ordinary function flattens every arrow: the parameters are the +/// arrow domains and the result is the codomain. A quantifier in a codomain +/// stops the walk so that polymorphic result stays a separate closure. +pub(crate) fn function_arrow_parameters(module: &CoreModule, id: TypeId) -> (Vec, TypeId) { + checked_function_arrow_parameters(module, id) + .expect("P8 layout validated live callable signatures before expression lowering") +} + +/// Checks the complete call boundary before registering a physical signature. +/// A non-callable value has zero parameters; dangling/cyclic spines are errors. +pub(crate) fn checked_function_arrow_parameters( + module: &CoreModule, + id: TypeId, +) -> Result<(Vec, TypeId), Vec> { + psrs_core::call_parts(&module.types, id).map_err(|message| layout_error(module.span, message)) +} diff --git a/crates/psrs-backend/src/cc/layout/functions/projection/mod.rs b/crates/psrs-backend/src/cc/layout/functions/projection/mod.rs new file mode 100644 index 00000000..bcd944a6 --- /dev/null +++ b/crates/psrs-backend/src/cc/layout/functions/projection/mod.rs @@ -0,0 +1,41 @@ +//! Checked logical-to-physical calling signatures. This plans storage only; +//! expression lowering retains the calling convention of every invocation. +use super::super::layout_error; +use crate::BackendError; +use crate::cc::{Signature, ValueShape}; +use psrs_core::{Module, TypeId}; + +pub(super) struct CallProjection<'a> { + source: &'a Module, + parameters: Vec, + result: TypeId, +} + +impl<'a> CallProjection<'a> { + pub(super) fn checked(source: &'a Module, ty: TypeId) -> Result> { + let (parameters, result) = psrs_core::call_parts(&source.types, ty) + .map_err(|message| layout_error(source.span, message))?; + Ok(Self { + source, + parameters, + result, + }) + } + + /// Only payload types reach the physical layout callback. The source arena + /// is retained explicitly so the projected TypeIds cannot be interpreted in + /// a rewritten physical arena. An empty parameter list is a real nullary + /// call, not permission to evaluate the action at initialization time. + pub(super) fn physical_signature( + &self, + mut shape: impl FnMut(&Module, TypeId) -> Result>, + ) -> Result> { + let parameters = self + .parameters + .iter() + .map(|ty| shape(self.source, *ty)) + .collect::, _>>()?; + let result = shape(self.source, self.result)?; + Ok(Signature { parameters, result }) + } +} diff --git a/crates/psrs-backend/src/cc/layout/functions/reachable.rs b/crates/psrs-backend/src/cc/layout/functions/reachable.rs index 479de563..714b2bf5 100644 --- a/crates/psrs-backend/src/cc/layout/functions/reachable.rs +++ b/crates/psrs-backend/src/cc/layout/functions/reachable.rs @@ -1,14 +1,13 @@ use psrs_core::{Expr, ExprKind, Module as CoreModule, Pattern, PatternKind, Type, TypeId}; use std::collections::HashSet; -/// Callable types that occur on a remaining declaration, expression, or -/// constructor field, or reserved aggregate layout. Other residual callable -/// types left behind by pruned library code are omitted. +/// Types referenced by remaining declarations, expressions, constructor fields, +/// and any explicitly supplied aggregate layout roots. pub(super) fn referenced_types( module: &CoreModule, aggregate_roots: impl Iterator, ) -> HashSet { - let mut referenced = HashSet::new(); + let mut referenced = live_type_ids(module); let mut visiting = HashSet::new(); // Every reserved aggregate is normalized by the layout builder. Its nested // callable fields therefore need signatures even when its Core type is a @@ -16,6 +15,12 @@ pub(super) fn referenced_types( for ty in aggregate_roots { record_type(module, ty, &mut visiting, &mut referenced); } + referenced +} + +pub(super) fn live_type_ids(module: &CoreModule) -> HashSet { + let mut referenced = HashSet::new(); + let mut visiting = HashSet::new(); for declaration in &module.declarations { record_type(module, declaration.ty, &mut visiting, &mut referenced); record_expr(module, &declaration.value, &mut visiting, &mut referenced); @@ -25,6 +30,22 @@ pub(super) fn referenced_types( record_type(module, *field, &mut visiting, &mut referenced); } } + // Runtime and WIT import schemes publish their payload layouts from the + // provider contract. Those TypeIds are not always shared with a remaining + // declaration body, and canonical guest projection keys the import by this + // scheme rather than by a structurally equal declaration type. + for checked in &module.external_types { + let import_layout = module.externals.iter().any(|external| { + external.symbol == checked.symbol + && matches!( + external.kind, + psrs_hir::ExternalKind::Runtime { .. } | psrs_hir::ExternalKind::Wit { .. } + ) + }); + if import_layout { + record_type(module, checked.ty, &mut visiting, &mut referenced); + } + } referenced } @@ -33,6 +54,15 @@ fn record_expr( expression: &Expr, visiting: &mut HashSet, referenced: &mut HashSet, +) { + psrs_span::with_sufficient_stack(|| record_expr_inner(module, expression, visiting, referenced)) +} + +fn record_expr_inner( + module: &CoreModule, + expression: &Expr, + visiting: &mut HashSet, + referenced: &mut HashSet, ) { record_type(module, expression.ty, visiting, referenced); match &expression.kind { @@ -145,14 +175,12 @@ fn record_type( if !visiting.insert(id) { return; } + referenced.insert(id); match module.types.get(id.0 as usize) { Some(Type::Application(function, argument)) => { // An ordinary arrow and a registered callable constructor // application are both callable closures and need a representation // signature even though they are not a dedicated type node. - if super::super::is_callable_type(module, id) { - referenced.insert(id); - } record_type(module, *function, visiting, referenced); record_type(module, *argument, visiting, referenced); } @@ -164,13 +192,9 @@ fn record_type( // Keep a signature entry for the quantified value itself as well // as for its body. Use sites retain the scheme TypeId on binders, // while expression TypeIds may name its instantiated body. - if super::super::is_callable_type(module, id) { - referenced.insert(id); - } record_type(module, *body, visiting, referenced); } Some(Type::Closure { parameters, result }) => { - referenced.insert(id); for parameter in parameters { record_type(module, *parameter, visiting, referenced); } diff --git a/crates/psrs-backend/src/cc/layout/mod.rs b/crates/psrs-backend/src/cc/layout/mod.rs index 15e3dbfc..a9994c83 100644 --- a/crates/psrs-backend/src/cc/layout/mod.rs +++ b/crates/psrs-backend/src/cc/layout/mod.rs @@ -8,15 +8,21 @@ use std::collections::{HashMap, HashSet}; mod aggregate; mod captures; +pub(in crate::cc) mod control; +mod declaration; mod functions; +pub(crate) mod protocols; mod scalar; +mod type_dependency; #[cfg(test)] mod tests; use captures::module_has_integer_capture; +pub(super) use declaration::declaration_call_parts; pub(crate) use functions::function_signature; pub(super) use scalar::{declaration_shape, is_abstract_type, scalar_type}; +use type_dependency::depends_on_type_variable; /// The runtime value shape of a Core primitive constructor, keyed by the /// constructor. This is the single mapping from a source primitive to its @@ -149,6 +155,11 @@ fn layoutable_field_type_inner( let Some(type_id) = user_type_id(module, id) else { return false; }; + // An uninhabited applied data type is an erased reference, which a + // variant case may carry like any other referenced payload. + if is_uninhabited_data_type(module, type_id) { + return true; + } if !newtype_ids.contains(&type_id) { return false; } @@ -184,6 +195,11 @@ pub(super) fn type_layout( .iter() .any(|declaration| depends_on_type_variable(module, declaration.ty)) || module_has_integer_capture(module) + || module + .types + .iter() + .enumerate() + .any(|(index, _)| array_element_type(module, TypeId(index as u32)).is_some()) || module .constructors .iter() @@ -216,6 +232,14 @@ pub(super) fn type_layout( None }; + let uses_control = module.types.iter().any(|ty| matches!(ty, + Type::Constructor(TypeConstructor::User(id)) if *id == HirTypeId::CONTROL || *id == HirTypeId::RESUMPTION)); + let nominal_types = if uses_control { + control::reserve(&mut representations) + } else { + HashMap::new() + }; + // Reserve aggregate handles before signatures so closure types can refer // to them. Normalize afterward and rewrite signature references to the // canonical handles. @@ -225,6 +249,7 @@ pub(super) fn type_layout( enum_types, aggregate_types, newtype_ids, + &nominal_types, &reserved_aggregates.arrays, &reserved_aggregates.records, &mut representations, @@ -234,6 +259,7 @@ pub(super) fn type_layout( enum_types, aggregate_types, newtype_ids, + &nominal_types, function_layout.function_types, reserved_aggregates, &mut representations, @@ -242,7 +268,11 @@ pub(super) fn type_layout( let record_types = aggregate_layouts.records; let mut constructor_types = HashMap::new(); - let mut aggregate_ids = aggregate_types.iter().copied().collect::>(); + let mut aggregate_ids = aggregate_types + .iter() + .copied() + .filter(|id| *id != HirTypeId::CONTROL && *id != HirTypeId::RESUMPTION) + .collect::>(); aggregate_ids.sort_by_key(|id| (id.module.0, id.index)); for type_id in aggregate_ids { let id = representations.reserve(); @@ -273,6 +303,7 @@ pub(super) fn type_layout( enum_types, aggregate_types, newtype_ids, + &nominal_types, &array_types, &record_types, &function_types, @@ -287,25 +318,53 @@ pub(super) fn type_layout( representations.set(id, Representation::Variant { cases }); } + protocols::append(&mut representations); + let function_slot = crate::boundary::function_slot_protocol(&mut representations); + let protocols = crate::boundary::payload_erased_protocols( + &mut representations, + &function_types, + module.span, + )?; + let control_layout = + uses_control.then(|| control::append(&mut representations, function_slot, &nominal_types)); + let control_owner = match control_layout { + Some(control) => control::append_contracts(module, &mut representations, control)?, + None => None, + }; Ok(TypeLayout { + function_slot, + protocols, representations, array_types, record_types, constructor_types, + nominal_types, boxed_integer_type, boxed_number_type, function_types, + control_layout, + control_owner, }) } pub(super) struct TypeLayout { + pub(super) function_slot: SignatureId, + /// The payload-erased protocol signature of each callable constructor, + /// keyed by its concrete signature. Built from the registered + /// representation owners and the module's callable signatures. + pub(super) protocols: HashMap, pub(super) representations: RepresentationTable, pub(super) array_types: HashMap, pub(super) record_types: HashMap, pub(super) constructor_types: HashMap, + pub(super) nominal_types: HashMap, pub(super) boxed_integer_type: Option, pub(super) boxed_number_type: Option, pub(super) function_types: HashMap, + /// Source module of the first declaration that emits machine helpers. + pub(super) control_owner: Option, + /// The shared frame representation for `Control f a` values. + pub(super) control_layout: Option, } pub(super) fn newtype_field_type(module: &CoreModule, type_id: HirTypeId) -> Option { @@ -331,40 +390,28 @@ pub(super) fn user_type_id(module: &CoreModule, mut id: TypeId) -> Option bool { + !module.opaque_ids.contains(&type_id) + && !module + .constructors + .iter() + .any(|constructor| constructor.type_id == type_id) +} + /// Whether a Core type is a callable closure value: an ordinary function arrow /// or a closure born with a fixed parameter list. -pub(super) fn is_callable_type(module: &CoreModule, id: TypeId) -> bool { +pub(crate) fn is_callable_type(module: &CoreModule, id: TypeId) -> bool { let id = unquantified_type(module, id); psrs_core::arrow_parts(&module.types, id).is_some() || psrs_core::closure_parts(&module.types, id).is_some() } -/// Derives the calling convention of a value. -/// -/// A closure contributes exactly the parameter list it was born with. Its -/// result stays a value, even when that value is a function or another -/// closure. An ordinary function flattens every arrow: the parameters are the -/// arrow domains and the result is the codomain. A quantifier in a codomain -/// stops the walk so that polymorphic result stays a separate closure. -pub(crate) fn function_arrow_parameters(module: &CoreModule, id: TypeId) -> (Vec, TypeId) { - let id = unquantified_type(module, id); - if let Some((parameters, result)) = psrs_core::closure_parts(&module.types, id) { - return (parameters.to_vec(), result); - } - let mut parameters = Vec::new(); - let mut current = id; - while let Some((parameter, result)) = psrs_core::arrow_parts(&module.types, current) { - parameters.push(parameter); - current = result; - // A quantifier in the codomain starts a new polymorphic closure - // boundary. Keep that type intact as the result value; unwrapping it - // there would merge its arrows into the current closure's arity. - if psrs_core::forall_parts(&module.types, current).is_some() { - break; - } - } - (parameters, current) -} +pub(crate) use functions::parts::{checked_function_arrow_parameters, function_arrow_parameters}; /// Returns the runtime-facing body of a type after removing quantifiers that /// wrap the value itself. A quantifier reached in an arrow's codomain is kept @@ -406,33 +453,3 @@ pub(super) fn array_element_type(module: &CoreModule, id: TypeId) -> Option None, } } - -pub(super) fn depends_on_type_variable(module: &CoreModule, id: TypeId) -> bool { - fn visit(module: &CoreModule, id: TypeId, visiting: &mut HashSet) -> bool { - if !visiting.insert(id) { - return false; - } - let result = match module.types.get(id.0 as usize) { - Some(Type::Variable(_)) => true, - Some(Type::Application(function, argument)) => { - visit(module, *function, visiting) || visit(module, *argument, visiting) - } - Some(Type::ForAll { body, .. }) => visit(module, *body, visiting), - Some(Type::RowExtend { ty, tail, .. }) => { - visit(module, *ty, visiting) || visit(module, *tail, visiting) - } - Some(Type::Closure { parameters, result }) => { - parameters - .iter() - .any(|parameter| visit(module, *parameter, visiting)) - || visit(module, *result, visiting) - } - Some(Type::RowEmpty) => false, - _ => false, - }; - visiting.remove(&id); - result - } - - visit(module, id, &mut HashSet::new()) -} diff --git a/crates/psrs-backend/src/cc/layout/protocols.rs b/crates/psrs-backend/src/cc/layout/protocols.rs new file mode 100644 index 00000000..fdf53f86 --- /dev/null +++ b/crates/psrs-backend/src/cc/layout/protocols.rs @@ -0,0 +1,69 @@ +//! Aggregate owners' canonical protocols for bare polymorphic value slots. +use super::super::{RefShape, Reference, ReprId, Representation, RepresentationTable, ValueShape}; + +pub(super) fn append(table: &mut RepresentationTable) { + let erased = ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Erased, + }); + // Register the canonical erased-element array only for a module that has an + // array layout to normalize. A bare slot can hold an array only when a + // concrete array type exists, so a program without arrays must not gain a + // dead representation that would claim a newtype allocated storage. + let has_array = table + .representations + .iter() + .any(|representation| matches!(representation, Representation::Array { .. })); + if has_array { + array(table, erased); + // Text codecs explicitly cross from source slots to integer buffers. + array(table, ValueShape::Integer); + } + let labels = table + .product_labels + .values() + .cloned() + .collect::>(); + for labels in labels { + if table.product_labels.iter().any(|(id, existing)| { + *existing == labels + && matches!(table.representation(*id), Some(Representation::Product { fields }) + if fields.len() == labels.len() && fields.iter().all(|field| *field == erased)) + }) { + continue; + } + let id = table.reserve(); + table.set( + id, + Representation::Product { + fields: vec![erased; labels.len()], + }, + ); + table.set_product_labels(id, labels); + } +} + +pub(crate) fn record(table: &RepresentationTable, labels: &[String]) -> Option { + let erased = ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Erased, + }); + table.product_labels.iter().find_map(|(id, existing)| { + (*existing == labels + && matches!(table.representation(*id), Some(Representation::Product { fields }) + if fields.len() == labels.len() && fields.iter().all(|field| *field == erased))) + .then_some(*id) + }) +} + +/// Reuse the authoritative array representation for a physical element shape. +pub(super) fn array(table: &mut RepresentationTable, element: ValueShape) -> ReprId { + if let Some(index) = table.representations.iter().position(|representation| { + matches!(representation, Representation::Array { element: existing } if *existing == element) + }) { + return ReprId(index as u32); + } + let id = table.reserve(); + table.set(id, Representation::Array { element }); + id +} diff --git a/crates/psrs-backend/src/cc/layout/scalar.rs b/crates/psrs-backend/src/cc/layout/scalar.rs index cb37d31f..2ffe36dc 100644 --- a/crates/psrs-backend/src/cc/layout/scalar.rs +++ b/crates/psrs-backend/src/cc/layout/scalar.rs @@ -1,10 +1,9 @@ use super::{ - function_type_signature, is_callable_type, layout_error, newtype_field_type, - primitive_shape_of, unquantified_type, user_type_id, + function_type_signature, is_callable_type, is_uninhabited_data_type, layout_error, + newtype_field_type, primitive_shape_of, unquantified_type, user_type_id, }; use crate::BackendError; use crate::cc::{RefShape, Reference, ReprId, Signature, SignatureId, ValueShape}; -use psrs_core::ExprKind; use psrs_core::{Module as CoreModule, Type, TypeConstructor, TypeId}; use psrs_hir::TypeId as HirTypeId; use psrs_span::TextRange; @@ -17,88 +16,34 @@ pub(crate) fn declaration_shape( enum_types: &HashSet, aggregate_types: &HashSet, newtype_ids: &HashSet, + nominal_types: &HashMap, array_types: &HashMap, record_types: &HashMap, function_types: &HashMap, ) -> Result> { - // Declaration-level quantifiers describe the polymorphic value, not an - // extra runtime layer around its closure. Strip only those leading - // quantifiers; a quantifier in a result type remains a separate closure. - let mut ty = unquantified_type(module, declaration.ty); - let mut parameters = Vec::new(); - let mut value = &declaration.value; - if let Some((closure_parameters, result)) = psrs_core::closure_parts(&module.types, ty) { - // The value is this closure when it binds the closure's parameter - // list. An alias of a closure value contributes no parameters. - let closure_parameters = closure_parameters.to_vec(); - let mut peeled = 0; - for parameter_ty in &closure_parameters { - let ExprKind::Lambda { binder, body } = &value.kind else { - break; - }; - if binder.ty != *parameter_ty { - return Err(vec![BackendError::new( - "P8 closure conversion", - binder.span, - "lambda binder type differs from the function parameter type", - )]); - } - parameters.push(scalar_type( + let call = super::declaration_call_parts(module, declaration)?; + let ty = call.result; + let nominal_result = nominal_shape(module, ty, nominal_types); + let parameters = call + .parameters + .into_iter() + .map(|(ty, span)| { + scalar_type( module, - binder.ty, - binder.span, + ty, + span, enum_types, aggregate_types, newtype_ids, + nominal_types, array_types, record_types, function_types, - )?); - value = body; - peeled += 1; - } - if peeled == closure_parameters.len() { - ty = result; - } else if peeled != 0 { - return Err(vec![BackendError::new( - "P8 closure conversion", - declaration.span, - "closure declaration is missing a parameter", - )]); - } - } - while let ExprKind::Lambda { binder, body } = &value.kind { - // A closure result is a value of this function. Its parameter list is - // not part of this signature. - if psrs_core::closure_parts(&module.types, ty).is_some() { - break; - } - let Some((parameter, result)) = psrs_core::arrow_parts(&module.types, ty) else { - break; - }; - if parameter != binder.ty { - return Err(vec![BackendError::new( - "P8 closure conversion", - binder.span, - "lambda binder type differs from the function parameter type", - )]); - } - ty = result; - parameters.push(scalar_type( - module, - binder.ty, - binder.span, - enum_types, - aggregate_types, - newtype_ids, - array_types, - record_types, - function_types, - )?); - value = body; - if psrs_core::forall_parts(&module.types, ty).is_some() { - break; - } + ) + }) + .collect::, _>>()?; + if let Some(result) = nominal_result { + return Ok(Signature { parameters, result }); } if is_callable_type(module, ty) { return Ok(Signature { @@ -141,6 +86,17 @@ pub(crate) fn declaration_shape( result: ValueShape::Integer, }) } + // A constructorless user data type is uninhabited: no expression builds + // one, so its values come only from unchecked coercions. It crosses the + // erased boundary like any other abstract type. + Some(Type::Constructor(TypeConstructor::User(id))) + if is_uninhabited_data_type(module, *id) => + { + Ok(Signature { + parameters, + result: erased_reference(), + }) + } Some(_) if module.is_record_type(ty) => { if module.record_is_open(ty).unwrap_or(false) { Err(vec![BackendError::new( @@ -181,7 +137,22 @@ pub(crate) fn declaration_shape( "the first backend slice cannot represent aggregate or parameterized types", )]); }; - if enum_types.contains(&type_id) { + if module.opaque_ids.contains(&type_id) { + // An applied foreign type is not a nullary handle. It has no + // constructors, so its value uses the erased reference until a + // later calling convention gives that type its own layout. + Ok(Signature { + parameters, + result: erased_reference(), + }) + } else if is_uninhabited_data_type(module, type_id) { + // An applied constructorless algebraic type is uninhabited and + // only appears through unchecked coercions. + Ok(Signature { + parameters, + result: erased_reference(), + }) + } else if enum_types.contains(&type_id) { Ok(Signature { parameters, result: ValueShape::Integer, @@ -207,6 +178,7 @@ pub(crate) fn declaration_shape( enum_types, aggregate_types, newtype_ids, + nominal_types, array_types, record_types, function_types, @@ -232,6 +204,7 @@ pub(crate) fn declaration_shape( enum_types, aggregate_types, newtype_ids, + nominal_types, array_types, record_types, function_types, @@ -266,6 +239,7 @@ pub(crate) fn scalar_type( enum_types: &HashSet, aggregate_types: &HashSet, newtype_ids: &HashSet, + nominal_types: &HashMap, array_types: &HashMap, record_types: &HashMap, function_types: &HashMap, @@ -276,6 +250,9 @@ pub(crate) fn scalar_type( return callable_value_shape(module, id, span, function_types); } let id = unquantified_type(module, id); + if let Some(shape) = nominal_shape(module, id, nominal_types) { + return Ok(shape); + } if let Some(shape) = primitive_shape_of(module, id) { return Ok(shape); } @@ -294,6 +271,13 @@ pub(crate) fn scalar_type( Some(Type::Constructor(TypeConstructor::User(id))) if module.opaque_ids.contains(id) => { Ok(ValueShape::Integer) } + // A constructorless user data type is uninhabited, so it has no + // concrete value to lay out beyond the erased reference. + Some(Type::Constructor(TypeConstructor::User(id))) + if is_uninhabited_data_type(module, *id) => + { + Ok(erased_reference()) + } Some(Type::Application(_, _)) if array_types.contains_key(&id) => { Ok(ValueShape::Reference(Reference { nullable: false, @@ -328,6 +312,12 @@ pub(crate) fn scalar_type( "aggregate and parameterized types are not supported by the first backend slice", )]); }; + if module.opaque_ids.contains(&type_id) { + return Ok(erased_reference()); + } + if is_uninhabited_data_type(module, type_id) { + return Ok(erased_reference()); + } if enum_types.contains(&type_id) { Ok(ValueShape::Integer) } else if aggregate_types.contains(&type_id) { @@ -343,6 +333,7 @@ pub(crate) fn scalar_type( enum_types, aggregate_types, newtype_ids, + nominal_types, array_types, record_types, function_types, @@ -368,6 +359,7 @@ pub(crate) fn scalar_type( enum_types, aggregate_types, newtype_ids, + nominal_types, array_types, record_types, function_types, @@ -393,6 +385,13 @@ pub(crate) fn scalar_type( } } +fn erased_reference() -> ValueShape { + ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Erased, + }) +} + fn aggregate_value_type() -> ValueShape { ValueShape::Reference(Reference { nullable: false, @@ -436,3 +435,15 @@ pub(in crate::cc) fn is_abstract_type(module: &CoreModule, mut id: TypeId) -> bo } false } + +fn nominal_shape( + module: &CoreModule, + ty: TypeId, + nominal_types: &HashMap, +) -> Option { + let repr = nominal_types.get(&user_type_id(module, ty)?)?; + Some(ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Repr(*repr), + })) +} diff --git a/crates/psrs-backend/src/cc/layout/tests/arrays.rs b/crates/psrs-backend/src/cc/layout/tests/arrays.rs new file mode 100644 index 00000000..ede67235 --- /dev/null +++ b/crates/psrs-backend/src/cc/layout/tests/arrays.rs @@ -0,0 +1,97 @@ +use super::*; + +#[test] +fn source_arrays_share_erased_element_storage() { + let mut module = empty_module(vec![ + Type::Variable(TypeVariableId(0)), + Type::Constructor(TypeConstructor::Array), + Type::Application(TypeId(1), TypeId(0)), + Type::Constructor(psrs_core::TypeConstructor::Int), + Type::Application(TypeId(1), TypeId(3)), + Type::Application(TypeId(1), TypeId(2)), + ]); + // `Array Int` and `Array (Array a)` reach every element shape under test. + root_types(&mut module, [TypeId(4), TypeId(5)]); + let layout = layout_for(&module); + let generic = layout.array_types[&TypeId(2)]; + let concrete = layout.array_types[&TypeId(4)]; + let nested = layout.array_types[&TypeId(5)]; + assert_eq!( + generic, concrete, + "source arrays preserve storage through instantiation" + ); + assert_eq!( + generic, nested, + "nested arrays use the same storage protocol" + ); + assert_eq!( + layout.representations.representation(generic), + Some(&Representation::Array { + element: ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Erased, + }), + }) + ); + assert_eq!( + layout.representations.representation(concrete), + Some(&Representation::Array { + element: crate::cc::payload::erased_shape(), + }) + ); + assert_eq!( + layout.representations.representation(nested), + Some(&Representation::Array { + element: crate::cc::payload::erased_shape(), + }) + ); +} + +#[test] +fn a_wit_import_scheme_lays_out_array_payloads_without_a_declaration_root() { + // Canonical guest projection keys a WIT import by its checked scheme TypeId. + // That node is not always shared with a remaining declaration, so the + // scheme itself must be a live layout root. + let int = TypeId(0); + let array_int = TypeId(2); + let mut types = vec![ + Type::Constructor(TypeConstructor::Int), + Type::Constructor(TypeConstructor::Array), + Type::Application(TypeId(1), TypeId(0)), + ]; + let scheme = push_arrow(&mut types, int, array_int); + let symbol = SymbolId::new(ModuleId::INTRINSICS, 3); + let module = Module { + type_names: Vec::new(), + id: ModuleId(2), + name: "WitArrayImportLayout".into(), + externals: vec![psrs_hir::ExternalSymbol { + source_module: None, + symbol, + name: "bytes".into(), + kind: psrs_hir::ExternalKind::Wit { + interface: "wasi:random/random".into(), + function: "get-random-bytes".into(), + }, + signature: None, + }], + external_types: vec![psrs_core::ExternalType { + symbol, + source_module: ModuleId(2), + ty: scheme, + }], + types, + newtype_ids: Vec::new(), + opaque_ids: Vec::new(), + callable_types: Vec::new(), + constructors: Vec::new(), + declarations: Vec::new(), + entry: None, + span: psrs_span::TextRange::new(0, 8), + }; + let layout = layout_for(&module); + assert!( + layout.array_types.contains_key(&array_int), + "WIT Array Int import scheme must reserve an array layout" + ); +} diff --git a/crates/psrs-backend/src/cc/layout/tests/mod.rs b/crates/psrs-backend/src/cc/layout/tests/mod.rs index 57c6da6e..3c8d754b 100644 --- a/crates/psrs-backend/src/cc/layout/tests/mod.rs +++ b/crates/psrs-backend/src/cc/layout/tests/mod.rs @@ -1,3 +1,5 @@ +mod arrays; + use super::*; use crate::cc::{RefShape, Reference}; use psrs_core::{ @@ -5,7 +7,9 @@ use psrs_core::{ }; use psrs_hir::{LocalId, ModuleId, SymbolId, TypeId as HirTypeId, TypeVariableId}; +mod opaque; mod records; +mod uninhabited; fn push_arrow(types: &mut Vec, parameter: TypeId, result: TypeId) -> TypeId { let head = TypeId(types.len() as u32); @@ -42,54 +46,37 @@ fn layout_for(module: &Module) -> TypeLayout { type_layout(module, &enums, &aggregates, &newtypes).expect("layout should succeed") } -#[test] -fn canonical_arrays_key_by_element_shape() { - let module = empty_module(vec![ - Type::Variable(TypeVariableId(0)), - Type::Constructor(TypeConstructor::Array), - Type::Application(TypeId(1), TypeId(0)), - Type::Constructor(psrs_core::TypeConstructor::Int), - Type::Application(TypeId(1), TypeId(3)), - Type::Application(TypeId(1), TypeId(2)), - ]); - let layout = layout_for(&module); - let generic = layout.array_types[&TypeId(2)]; - let concrete = layout.array_types[&TypeId(4)]; - let nested = layout.array_types[&TypeId(5)]; - assert_ne!(generic, concrete, "Array a and Array Int must differ"); - assert_eq!( - layout.representations.representation(generic), - Some(&Representation::Array { - element: ValueShape::Reference(Reference { - nullable: false, - heap: RefShape::Erased, - }), - }) - ); - assert_eq!( - layout.representations.representation(concrete), - Some(&Representation::Array { - element: ValueShape::Integer, - }) - ); - assert_eq!( - layout.representations.representation(nested), - Some(&Representation::Array { - element: ValueShape::Reference(Reference { - nullable: false, - heap: RefShape::Repr(generic), - }), - }) - ); +/// Roots aggregate types through declarations so the layout builder treats +/// them as live. The production builder only lays out types reachable from a +/// declaration or a constructor field, so a fixture with neither has no +/// arrays or records to normalize. +fn root_types(module: &mut Module, roots: impl IntoIterator) { + for (index, ty) in roots.into_iter().enumerate() { + module.declarations.push(Declaration { + symbol: SymbolId::new(module.id, index as u32), + name: format!("root{index}"), + name_span: psrs_span::TextRange::new(0, 1), + quantified: Vec::new(), + ty, + value: Expr { + kind: ExprKind::Unit, + ty, + span: psrs_span::TextRange::new(0, 1), + }, + span: psrs_span::TextRange::new(0, 1), + }); + } } #[test] fn recursive_aggregate_normalization_terminates() { - let module = empty_module(vec![ + let mut module = empty_module(vec![ Type::Application(TypeId(2), TypeId(1)), Type::Application(TypeId(2), TypeId(0)), Type::Constructor(TypeConstructor::Array), ]); + // The two mutually recursive arrays are unreachable without a root. + root_types(&mut module, [TypeId(0)]); let layout = layout_for(&module); let first = layout.array_types[&TypeId(0)]; let second = layout.array_types[&TypeId(1)]; @@ -101,9 +88,9 @@ fn recursive_aggregate_normalization_terminates() { layout.representations.representation(second), Some(Representation::Array { .. }) )); - assert_ne!( + assert_eq!( first, second, - "mutually recursive arrays keep distinct canonical handles" + "recursive source arrays share canonical erased storage" ); } @@ -122,6 +109,69 @@ fn quantifier_erasure_terminates_on_a_malformed_cycle() { ); } +#[test] +fn bound_rank_n_record_fields_do_not_make_dictionary_layout_dependent() { + fn push_closed_record(types: &mut Vec, field_type: TypeId) -> TypeId { + let empty = TypeId(types.len() as u32); + types.push(Type::RowEmpty); + let row = TypeId(types.len() as u32); + types.push(Type::RowExtend { + label: "method".into(), + ty: field_type, + tail: empty, + }); + let record_constructor = TypeId(types.len() as u32); + types.push(Type::Constructor(TypeConstructor::Record)); + let record = TypeId(types.len() as u32); + types.push(Type::Application(record_constructor, row)); + record + } + + let variable = TypeVariableId(0); + let mut types = vec![ + Type::Variable(variable), + Type::Constructor(TypeConstructor::Int), + ]; + let identity = push_arrow(&mut types, TypeId(0), TypeId(0)); + let polymorphic_identity = TypeId(types.len() as u32); + types.push(Type::ForAll { + variables: vec![variable], + body: identity, + }); + let dictionary = push_closed_record(&mut types, polymorphic_identity); + let genuinely_dependent = push_closed_record(&mut types, TypeId(0)); + let module = empty_module(types); + + fn parameter_shape(module: &Module, ty: TypeId, representation: ReprId) -> ValueShape { + let record_types = HashMap::from([(ty, representation)]); + super::scalar::scalar_type( + module, + ty, + module.span, + &HashSet::new(), + &HashSet::new(), + &HashSet::new(), + &HashMap::new(), + &HashMap::new(), + &record_types, + &HashMap::new(), + ) + .expect("a closed record parameter has a canonical product shape") + } + + assert!(!depends_on_type_variable(&module, dictionary)); + assert!(depends_on_type_variable(&module, genuinely_dependent)); + for (ty, representation) in [(dictionary, ReprId(7)), (genuinely_dependent, ReprId(8))] { + assert_eq!( + parameter_shape(&module, ty, representation), + ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Repr(representation), + }) + ); + } +} + #[test] fn equal_normalized_function_signatures_share_one_signature_id() { let array = TypeId(0); @@ -191,9 +241,9 @@ fn equal_normalized_function_signatures_share_one_signature_id() { let layout = type_layout(&module, &enums, &aggregates, &newtypes) .expect("distinct function types should normalize and intern"); - assert_ne!( + assert_eq!( layout.array_types[&array_int], layout.array_types[&array_string], - "Array Int and Array String are distinct semantic shapes with distinct canonical arrays" + "distinct source element types share physical array storage" ); assert_eq!( layout.array_types[&array_int], layout.array_types[&array_int_b], @@ -205,9 +255,9 @@ fn equal_normalized_function_signatures_share_one_signature_id() { first, second, "function types that are equal after normalization must share one SignatureId" ); - assert_ne!( + assert_eq!( layout.function_types[&f_int], layout.function_types[&f_string], - "arrays with different semantic element shapes keep distinct signatures" + "array function signatures share a physical storage convention" ); let signature = layout .representations @@ -369,7 +419,7 @@ fn an_opaque_handle_and_an_array_of_handles_have_scalar_layouts() { let opaque = HirTypeId::new(module_id, 0); let handle = TypeId(0); let array_handle = TypeId(2); - let module = Module { + let mut module = Module { type_names: Vec::new(), id: module_id, name: "OpaqueHandleLayoutTest".into(), @@ -388,6 +438,7 @@ fn an_opaque_handle_and_an_array_of_handles_have_scalar_layouts() { entry: None, span: psrs_span::TextRange::new(0, 40), }; + root_types(&mut module, [array_handle]); let newtypes = HashSet::new(); let enums = enum_type_ids(&module, &newtypes); let aggregates = aggregate_type_ids(&module, &newtypes); @@ -402,7 +453,7 @@ fn an_opaque_handle_and_an_array_of_handles_have_scalar_layouts() { .representations .representation(layout.array_types[&array_handle]), Some(&Representation::Array { - element: ValueShape::Integer, + element: crate::cc::payload::erased_shape(), }) ); let _ = handle; diff --git a/crates/psrs-backend/src/cc/layout/tests/opaque.rs b/crates/psrs-backend/src/cc/layout/tests/opaque.rs new file mode 100644 index 00000000..2fa1aaa5 --- /dev/null +++ b/crates/psrs-backend/src/cc/layout/tests/opaque.rs @@ -0,0 +1,51 @@ +use super::{declaration_shape, empty_module}; +use crate::cc::{RefShape, Reference, ValueShape}; +use psrs_core::{Declaration, Expr, ExprKind, Type, TypeConstructor, TypeId}; +use psrs_hir::{ModuleId, SymbolId, TypeId as HirTypeId}; +use std::collections::{HashMap, HashSet}; + +#[test] +fn an_applied_opaque_foreign_type_uses_the_erased_reference() { + let opaque = HirTypeId::new(ModuleId(1), 0); + let mut module = empty_module(vec![ + Type::Constructor(TypeConstructor::User(opaque)), + Type::Constructor(TypeConstructor::Int), + Type::Application(TypeId(0), TypeId(1)), + ]); + module.opaque_ids.push(opaque); + let applied = TypeId(2); + module.declarations.push(Declaration { + symbol: SymbolId::new(module.id, 0), + name: "value".to_owned(), + name_span: psrs_span::TextRange::new(0, 1), + quantified: Vec::new(), + ty: applied, + value: Expr { + kind: ExprKind::Unit, + ty: applied, + span: psrs_span::TextRange::new(0, 1), + }, + span: psrs_span::TextRange::new(0, 1), + }); + let signature = declaration_shape( + &module.declarations[0], + &module, + &HashSet::new(), + &HashSet::new(), + &HashSet::new(), + &HashMap::new(), + &HashMap::new(), + &HashMap::new(), + &HashMap::new(), + ) + .expect("an applied opaque type should have a layout"); + assert!(signature.parameters.is_empty()); + assert_eq!(signature.result, erased()); +} + +fn erased() -> ValueShape { + ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Erased, + }) +} diff --git a/crates/psrs-backend/src/cc/layout/tests/records.rs b/crates/psrs-backend/src/cc/layout/tests/records.rs index 436dbfe1..b619191b 100644 --- a/crates/psrs-backend/src/cc/layout/tests/records.rs +++ b/crates/psrs-backend/src/cc/layout/tests/records.rs @@ -170,7 +170,8 @@ fn canonical_record_keys_sort_labels_and_share_equal_keyed_records() { ]; let first_record = push_record(&mut types, vec![("x", TypeId(0)), ("y", TypeId(1))]); let second_record = push_record(&mut types, vec![("y", TypeId(1)), ("x", TypeId(0))]); - let module = empty_module(types); + let mut module = empty_module(types); + root_types(&mut module, [first_record, second_record]); let layout = layout_for(&module); let first = layout.record_types[&first_record]; let second = layout.record_types[&second_record]; @@ -195,3 +196,50 @@ fn canonical_record_keys_sort_labels_and_share_equal_keyed_records() { }) ); } + +#[test] +fn record_and_callable_keys_converge_together_through_nested_layouts() { + let mut types = vec![ + Type::Variable(TypeVariableId(0)), + Type::Variable(TypeVariableId(1)), + Type::Constructor(TypeConstructor::Int), + ]; + let first_inner = push_record(&mut types, vec![("payload", TypeId(0))]); + let second_inner = push_record(&mut types, vec![("payload", TypeId(1))]); + let first_method = push_arrow(&mut types, first_inner, TypeId(0)); + let second_method = push_arrow(&mut types, second_inner, TypeId(1)); + let first_outer = push_record(&mut types, vec![("run", first_method)]); + let second_outer = push_record(&mut types, vec![("run", second_method)]); + let first_consumer = push_arrow(&mut types, first_outer, TypeId(2)); + let second_consumer = push_arrow(&mut types, second_outer, TypeId(2)); + let mut module = empty_module(types); + root_types(&mut module, [first_consumer, second_consumer]); + let layout = layout_for(&module); + assert_eq!( + layout.record_types[&first_inner], + layout.record_types[&second_inner] + ); + assert_eq!( + layout.function_types[&first_method], + layout.function_types[&second_method] + ); + assert_eq!( + layout.record_types[&first_outer], + layout.record_types[&second_outer] + ); + assert_eq!( + layout.function_types[&first_consumer], + layout.function_types[&second_consumer] + ); + let signature = layout + .representations + .signature(layout.function_types[&first_consumer]) + .unwrap(); + assert_eq!( + signature.parameters, + vec![ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Repr(layout.record_types[&first_outer]), + })] + ); +} diff --git a/crates/psrs-backend/src/cc/layout/tests/uninhabited.rs b/crates/psrs-backend/src/cc/layout/tests/uninhabited.rs new file mode 100644 index 00000000..4e68ba7b --- /dev/null +++ b/crates/psrs-backend/src/cc/layout/tests/uninhabited.rs @@ -0,0 +1,88 @@ +//! A constructorless user `data` declaration is uninhabited. Its values come +//! only from unchecked coercions, so the backend lays it out as an erased +//! reference, like any other abstract type. Foreign data stays a resource +//! handle and is excluded. +use super::{declaration_shape, empty_module}; +use crate::cc::{RefShape, Reference, ValueShape}; +use psrs_core::{ConstructorInfo, Declaration, Expr, ExprKind, Type, TypeConstructor, TypeId}; +use psrs_hir::{ModuleId, SymbolId, TypeId as HirTypeId}; +use std::collections::{HashMap, HashSet}; + +fn declaration(module: &mut psrs_core::Module, ty: TypeId) -> Declaration { + let declaration = Declaration { + symbol: SymbolId::new(module.id, 0), + name: "value".to_owned(), + name_span: psrs_span::TextRange::new(0, 1), + quantified: Vec::new(), + ty, + value: Expr { + kind: ExprKind::Unit, + ty, + span: psrs_span::TextRange::new(0, 1), + }, + span: psrs_span::TextRange::new(0, 1), + }; + module.declarations.push(declaration.clone()); + declaration +} + +fn shape(module: &psrs_core::Module, declaration: &Declaration) -> ValueShape { + declaration_shape( + declaration, + module, + &HashSet::new(), + &HashSet::new(), + &HashSet::new(), + &HashMap::new(), + &HashMap::new(), + &HashMap::new(), + &HashMap::new(), + ) + .expect("an uninhabited type should have a layout") + .result +} + +fn erased() -> ValueShape { + ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Erased, + }) +} + +#[test] +fn a_bare_constructorless_data_type_uses_the_erased_reference() { + let empty = HirTypeId::new(ModuleId(1), 0); + let mut module = empty_module(vec![Type::Constructor(TypeConstructor::User(empty))]); + let declaration = declaration(&mut module, TypeId(0)); + assert_eq!(shape(&module, &declaration), erased()); +} + +#[test] +fn an_applied_constructorless_data_type_uses_the_erased_reference() { + let empty = HirTypeId::new(ModuleId(1), 0); + let mut module = empty_module(vec![ + Type::Constructor(TypeConstructor::User(empty)), + Type::Constructor(TypeConstructor::Int), + Type::Application(TypeId(0), TypeId(1)), + ]); + let declaration = declaration(&mut module, TypeId(2)); + assert_eq!(shape(&module, &declaration), erased()); +} + +#[test] +fn a_user_type_with_a_constructor_is_not_treated_as_uninhabited() { + // The same nominal type becomes an enum as soon as it has a nullary + // constructor, so the erased fallback must not swallow it. + let inhabited = HirTypeId::new(ModuleId(1), 0); + let mut module = empty_module(vec![Type::Constructor(TypeConstructor::User(inhabited))]); + module.constructors.push(ConstructorInfo { + symbol: SymbolId::new(module.id, 1), + name: "Only".to_owned(), + type_id: inhabited, + tag: 0, + field_count: 0, + field_types: Vec::new(), + parameters: Vec::new(), + }); + assert!(!super::is_uninhabited_data_type(&module, inhabited)); +} diff --git a/crates/psrs-backend/src/cc/layout/type_dependency.rs b/crates/psrs-backend/src/cc/layout/type_dependency.rs new file mode 100644 index 00000000..527e65e7 --- /dev/null +++ b/crates/psrs-backend/src/cc/layout/type_dependency.rs @@ -0,0 +1,54 @@ +use super::CoreModule; +use psrs_core::{Type, TypeId}; +use psrs_hir::TypeVariableId; +use std::collections::{HashMap, HashSet}; + +pub(super) fn depends_on_type_variable(module: &CoreModule, id: TypeId) -> bool { + fn visit( + module: &CoreModule, + id: TypeId, + bound: &mut HashMap, + visiting: &mut HashSet, + ) -> bool { + if !visiting.insert(id) { + return false; + } + let result = match module.types.get(id.0 as usize) { + Some(Type::Variable(variable)) => !bound.contains_key(variable), + Some(Type::Application(function, argument)) => { + visit(module, *function, bound, visiting) + || visit(module, *argument, bound, visiting) + } + Some(Type::ForAll { variables, body }) => { + for variable in variables { + *bound.entry(*variable).or_default() += 1; + } + let dependent = visit(module, *body, bound, visiting); + for variable in variables { + if let Some(count) = bound.get_mut(variable) { + *count -= 1; + if *count == 0 { + bound.remove(variable); + } + } + } + dependent + } + Some(Type::RowExtend { ty, tail, .. }) => { + visit(module, *ty, bound, visiting) || visit(module, *tail, bound, visiting) + } + Some(Type::Closure { parameters, result }) => { + parameters + .iter() + .any(|parameter| visit(module, *parameter, bound, visiting)) + || visit(module, *result, bound, visiting) + } + Some(Type::RowEmpty) => false, + _ => false, + }; + visiting.remove(&id); + result + } + + visit(module, id, &mut HashMap::new(), &mut HashSet::new()) +} diff --git a/crates/psrs-backend/src/cc/lower/array.rs b/crates/psrs-backend/src/cc/lower/array/mod.rs similarity index 92% rename from crates/psrs-backend/src/cc/lower/array.rs rename to crates/psrs-backend/src/cc/lower/array/mod.rs index d44cafb5..8593c03a 100644 --- a/crates/psrs-backend/src/cc/lower/array.rs +++ b/crates/psrs-backend/src/cc/lower/array/mod.rs @@ -1,3 +1,5 @@ +mod storage; + use super::super::{Assignment, AssignmentKind, RefShape, Reference, ValueId, ValueShape}; use super::FunctionLowerer; use crate::BackendError; @@ -18,10 +20,10 @@ impl FunctionLowerer<'_> { "array expression has no representation requirement", )]); }; - let values = elements - .iter() - .map(|element| self.lower_value(element, assignments)) - .collect::, _>>()?; + let mut values = Vec::with_capacity(elements.len()); + for element in elements { + values.push(self.lower_array_element(element, assignments)?); + } let destination = self.fresh(ty); assignments.push(Assignment { destination, @@ -65,7 +67,7 @@ impl FunctionLowerer<'_> { span: expression.span, }); let index = self.lower_value(index, assignments)?; - let new_value = self.lower_value(new_value, assignments)?; + let new_value = self.lower_array_element(new_value, assignments)?; assignments.push(Assignment { destination, kind: AssignmentKind::ArraySet { diff --git a/crates/psrs-backend/src/cc/lower/array/storage.rs b/crates/psrs-backend/src/cc/lower/array/storage.rs new file mode 100644 index 00000000..7d87c9ea --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/array/storage.rs @@ -0,0 +1,86 @@ +//! Low-level initialized allocation and unsafe writes; library code owns loops. +use super::super::FunctionLowerer; +use crate::BackendError; +use crate::cc::{Assignment, AssignmentKind, ValueId, ValueShape}; +use psrs_core::Expr; + +impl FunctionLowerer<'_> { + pub(in crate::cc::lower) fn lower_array_element( + &mut self, + value: &Expr, + assignments: &mut Vec, + ) -> Result> { + let shape = self.value_shape(value.ty, value.span)?; + let lowered = self.lower_value(value, assignments)?; + let plan = self.erase_payload(shape, value.span)?; + Ok(self.emit_conversion( + lowered, + shape, + crate::cc::payload::erased_shape(), + plan, + value.span, + assignments, + )) + } + pub(in crate::cc::lower) fn lower_array_fill( + &mut self, + expression: &Expr, + length: &Expr, + value: &Expr, + ty: ValueShape, + assignments: &mut Vec, + ) -> Result> { + let representation = *self.array_types.get(&expression.ty).ok_or_else(|| { + vec![BackendError::invalid_ir( + "P8 closure conversion", + expression.span, + "arrayFill has no checked array representation", + )] + })?; + let length = self.lower_value(length, assignments)?; + let value = self.lower_array_element(value, assignments)?; + let destination = self.fresh(ty); + assignments.push(Assignment { + destination, + span: expression.span, + kind: AssignmentKind::ArrayFill { + destination, + representation, + length, + value, + }, + }); + Ok(destination) + } + pub(in crate::cc::lower) fn lower_array_write( + &mut self, + expression: &Expr, + array: &Expr, + index: &Expr, + new_value: &Expr, + assignments: &mut Vec, + ) -> Result> { + let representation = *self.array_types.get(&array.ty).ok_or_else(|| { + vec![BackendError::invalid_ir( + "P8 closure conversion", + expression.span, + "arrayWrite has no checked array representation", + )] + })?; + let value = self.lower_value(array, assignments)?; + let index = self.lower_value(index, assignments)?; + let new_value = self.lower_array_element(new_value, assignments)?; + assignments.push(Assignment { + destination: value, + span: expression.span, + kind: AssignmentKind::ArraySet { + destination: value, + representation, + value, + index, + new_value, + }, + }); + Ok(value) + } +} diff --git a/crates/psrs-backend/src/cc/lower/call/application.rs b/crates/psrs-backend/src/cc/lower/call/application.rs index 0a323cd6..58dc599c 100644 --- a/crates/psrs-backend/src/cc/lower/call/application.rs +++ b/crates/psrs-backend/src/cc/lower/call/application.rs @@ -2,10 +2,10 @@ use super::super::super::layout::{function_signature, function_type_signature}; use super::super::super::{Assignment, AssignmentKind, ValueId}; use super::super::{FunctionLowerer, Signature, ValueShape}; use super::helpers::{ - callable_parameter_types, callable_result_type, collect_application, + callable_instantiation, callable_parameter_types, callable_result_type, collect_application, conversion_reconstructs_aggregate, function_value_types, persist_reference, restore_reference, }; -use super::partial::PartialApplication; +use super::partial::{IndirectPartialApplication, PartialApplication}; use super::{ApplicationLowering, CallShape}; use crate::BackendError; use psrs_core::{Expr, ExprKind}; @@ -54,6 +54,7 @@ impl ApplicationLowering for FunctionLowerer<'_> { assignments, ); } + let evidence = callable_instantiation(self.module, self.boundary, function, head.ty); self.check_call_shape( &signature, arguments.len(), @@ -73,12 +74,13 @@ impl ApplicationLowering for FunctionLowerer<'_> { )] })?; let source_shape = self.value_shape(argument.ty, argument.span)?; - let conversion = self.typed_conversion( + let conversion = self.typed_conversion_with_instantiation( argument.ty, source_type, source_shape, *expected, expression.span, + evidence.as_ref(), )?; conversions.push((source_shape, conversion)); } @@ -119,6 +121,7 @@ impl ApplicationLowering for FunctionLowerer<'_> { assignments.push(Assignment { destination: call_result, kind: AssignmentKind::DirectCall { + contract: None, function, arguments: values, }, @@ -132,12 +135,13 @@ impl ApplicationLowering for FunctionLowerer<'_> { "call target has no declaration result type", )] })?; - let conversion = self.typed_conversion( + let conversion = self.typed_conversion_with_instantiation( source_type, expression.ty, signature.result, result_type, expression.span, + evidence.as_ref(), )?; Ok(self.emit_conversion( call_result, @@ -171,16 +175,11 @@ impl FunctionLowerer<'_> { self.enum_types, self.aggregate_types, self.newtype_ids, + self.nominal_types, self.array_types, self.record_types, self.function_types, )?; - self.check_call_shape( - &signature, - arguments.len(), - signature.result, - expression.span, - )?; let Some(signature_id) = function_type_signature(self.module, self.function_types, head.ty) else { return Err(vec![BackendError::new( @@ -189,6 +188,28 @@ impl FunctionLowerer<'_> { "higher-order call has no runtime function type", )]); }; + // An under-applied callee that is not a top-level declaration (a local + // closure or a dictionary method) captures the supplied arguments and + // exposes the remaining parameters through a generated closure. + if arguments.len() < signature.parameters.len() { + return self.lower_indirect_partial_application( + IndirectPartialApplication { + expression, + head, + arguments, + signature: &signature, + signature_id, + result_type, + }, + assignments, + ); + } + self.check_call_shape( + &signature, + arguments.len(), + signature.result, + expression.span, + )?; let (source_parameters, source_result) = function_value_types(self.module, head.ty); // The callee expression is lowered at its own use type, so its runtime // value already matches `head.ty`; no side-table adaptation is needed. diff --git a/crates/psrs-backend/src/cc/lower/call/helpers.rs b/crates/psrs-backend/src/cc/lower/call/helpers.rs index 51a1813c..e0668b8c 100644 --- a/crates/psrs-backend/src/cc/lower/call/helpers.rs +++ b/crates/psrs-backend/src/cc/lower/call/helpers.rs @@ -160,11 +160,16 @@ pub(super) fn declaration_parameter_types( else { return Vec::new(); }; - function_arrow_parameters(module, declaration.ty).0 + super::super::super::layout::declaration_call_parts(module, declaration) + .expect("declaration calling boundary was checked before expression lowering") + .parameters + .into_iter() + .map(|(ty, _)| ty) + .collect() } /// The value type a declaration produces after its ordinary arguments: its -/// declared type with every arrow peeled, stopping before a callable +/// declared type with its leading lambda parameters peeled, stopping before a callable /// constructor's hidden parameters. For `discard :: Effect a -> (a -> Effect /// b) -> Effect b` this is `Effect b`, not the value `b` inside the effect. pub(super) fn declaration_result_type( @@ -175,7 +180,11 @@ pub(super) fn declaration_result_type( .declarations .iter() .find(|declaration| declaration.symbol == symbol)?; - Some(function_arrow_parameters(module, declaration.ty).1) + Some( + super::super::super::layout::declaration_call_parts(module, declaration) + .expect("declaration calling boundary was checked before expression lowering") + .result, + ) } /// The result type of a (possibly curried) function type: the value produced @@ -200,7 +209,12 @@ pub(super) fn callable_parameter_types( { return declaration_parameter_types(module, symbol); } - function_parameter_types(module, callable_type) + let scheme = module + .external_types + .iter() + .find(|external| external.symbol == symbol) + .map_or(callable_type, |external| external.ty); + function_parameter_types(module, scheme) } pub(super) fn callable_result_type( @@ -209,11 +223,36 @@ pub(super) fn callable_result_type( callable_type: psrs_core::TypeId, ) -> Option { declaration_result_type(module, symbol).or_else(|| { - let (_, result) = function_arrow_parameters(module, callable_type); + let scheme = module + .external_types + .iter() + .find(|external| external.symbol == symbol) + .map_or(callable_type, |external| external.ty); + let (_, result) = function_arrow_parameters(module, scheme); module.types.get(result.0 as usize).map(|_| result) }) } +pub(super) fn callable_instantiation<'a>( + module: &'a CoreModule, + boundary: &crate::boundary::BoundaryEvidence<'a>, + symbol: SymbolId, + instance: psrs_core::TypeId, +) -> Option> { + if let Some(declaration) = module + .declarations + .iter() + .find(|declaration| declaration.symbol == symbol) + { + return boundary.checked_instantiation(declaration.ty, &declaration.quantified, instance); + } + let external = module + .external_types + .iter() + .find(|external| external.symbol == symbol)?; + boundary.instantiation_at(external.ty, instance) +} + fn function_parameter_types( module: &CoreModule, type_id: psrs_core::TypeId, diff --git a/crates/psrs-backend/src/cc/lower/call/mod.rs b/crates/psrs-backend/src/cc/lower/call/mod.rs index de4e25bc..90bfda36 100644 --- a/crates/psrs-backend/src/cc/lower/call/mod.rs +++ b/crates/psrs-backend/src/cc/lower/call/mod.rs @@ -9,6 +9,29 @@ use psrs_span::TextRange; pub(super) use helpers::{is_function_type, persist_reference, restore_reference}; +impl super::FunctionLowerer<'_> { + pub(super) fn lower_external_function_value( + &mut self, + expression: &Expr, + function: psrs_hir::SymbolId, + signature: &Signature, + result_type: ValueShape, + assignments: &mut Vec, + ) -> Result> { + self.lower_partial_global_application( + partial::PartialApplication { + expression, + function, + source_signature: signature, + arguments: Vec::new(), + result_type, + callable_type: expression.ty, + }, + assignments, + ) + } +} + pub(super) trait CallShape { fn check_call_shape( &self, diff --git a/crates/psrs-backend/src/cc/lower/call/partial.rs b/crates/psrs-backend/src/cc/lower/call/partial/global.rs similarity index 63% rename from crates/psrs-backend/src/cc/lower/call/partial.rs rename to crates/psrs-backend/src/cc/lower/call/partial/global.rs index 4e3d2756..d260cb1c 100644 --- a/crates/psrs-backend/src/cc/lower/call/partial.rs +++ b/crates/psrs-backend/src/cc/lower/call/partial/global.rs @@ -1,29 +1,7 @@ -use super::super::super::layout::function_type_signature; -use super::super::super::{ - Assignment, AssignmentKind, Function, RefShape, Reference, ValueConversion, ValueId, -}; -use super::super::lambda::LambdaLowering; -use super::super::{FunctionLowerer, Signature, ValueShape}; -use super::helpers::{ - callable_parameter_types, callable_result_type, closure_value_type, closure_value_type_for, - conversion_reconstructs_aggregate, function_result_type, persist_reference, restore_reference, -}; -use crate::BackendError; -use psrs_core::Expr; -use psrs_core::TypeId; -use psrs_hir::SymbolId; - -pub(super) struct PartialApplication<'a> { - pub(super) expression: &'a Expr, - pub(super) function: SymbolId, - pub(super) source_signature: &'a Signature, - pub(super) arguments: Vec<&'a Expr>, - pub(super) result_type: ValueShape, - pub(super) callable_type: TypeId, -} +use super::*; impl FunctionLowerer<'_> { - pub(super) fn lower_partial_global_application( + pub(in crate::cc::lower::call) fn lower_partial_global_application( &mut self, application: PartialApplication<'_>, assignments: &mut Vec, @@ -62,6 +40,16 @@ impl FunctionLowerer<'_> { "partial application has an incomplete declaration signature", )]); } + let evidence = callable_instantiation(self.module, self.boundary, function, callable_type); + let (use_parameters, _) = function_arrow_parameters(self.module, callable_type); + let remaining_count = source_signature.parameters.len() - arguments.len(); + if target_signature.parameters.len() < remaining_count { + return Err(vec![BackendError::invalid_ir( + "P8 closure conversion", + expression.span, + "partial application target omits a declaration parameter", + )]); + } let capture_conversions = arguments .iter() .enumerate() @@ -69,12 +57,13 @@ impl FunctionLowerer<'_> { let source_type = declared_parameters[index]; let expected = source_signature.parameters[index]; let source_shape = self.value_shape(argument.ty, argument.span)?; - let conversion = self.typed_conversion( + let conversion = self.typed_conversion_with_instantiation( argument.ty, source_type, source_shape, expected, expression.span, + evidence.as_ref(), )?; Ok((source_shape, conversion)) }) @@ -151,17 +140,30 @@ impl FunctionLowerer<'_> { // but the callee's declaration may store a polymorphic parameter erased. // Convert it the way a saturated call does; an identity conversion when // the shapes already agree keeps the monomorphic case unchanged. - for (index, parameter) in remaining_parameters.into_iter().enumerate() { + for (index, parameter) in remaining_parameters + .iter() + .copied() + .take(remaining_count) + .enumerate() + { let position = captured.len() + index; let source_type = declared_parameters[position]; let source_shape = remaining_shapes[index]; let expected = source_signature.parameters[position]; - let conversion = nested.typed_conversion( - source_type, + let use_type = use_parameters.get(position).copied().ok_or_else(|| { + vec![BackendError::invalid_ir( + "P8 closure conversion", + expression.span, + "partial application parameter has no checked use type", + )] + })?; + let conversion = nested.typed_conversion_with_instantiation( + use_type, source_type, source_shape, expected, expression.span, + evidence.as_ref(), )?; let converted = nested.emit_conversion( parameter, @@ -177,20 +179,111 @@ impl FunctionLowerer<'_> { nested_assignments.push(Assignment { destination: result, kind: AssignmentKind::DirectCall { + contract: None, function, arguments: call_arguments, }, span: expression.span, }); + let (result, source_result_shape, source_result_type) = if remaining_parameters.len() + > remaining_count + { + let source_type = callable_result_type(self.module, function, callable_type) + .ok_or_else(|| { + vec![BackendError::invalid_ir( + "P8 closure conversion", + expression.span, + "partial application result has no declaration type", + )] + })?; + // Peel only the declaration's checked ordinary call prefix. Any + // remaining arrows belong to the value returned by that call. + let mut cursor = callable_type; + for _ in 0..source_signature.parameters.len() { + while let Some((_, body)) = psrs_core::forall_parts(&self.module.types, cursor) { + cursor = body; + } + let (_, tail) = + psrs_core::arrow_parts(&self.module.types, cursor).ok_or_else(|| { + vec![BackendError::invalid_ir( + "P8 closure conversion", + expression.span, + "partial application calling prefix has no checked use arrow", + )] + })?; + cursor = tail; + } + let returned_signature = + function_type_signature(self.module, self.function_types, cursor).ok_or_else( + || { + vec![BackendError::invalid_ir( + "P8 closure conversion", + expression.span, + "partial application returned callable has no signature", + )] + }, + )?; + let returned = self + .representations + .signature(returned_signature) + .cloned() + .ok_or_else(|| { + vec![BackendError::invalid_ir( + "P8 closure conversion", + expression.span, + "partial application returned callable signature is absent", + )] + })?; + let tail_parameters = &remaining_parameters[remaining_count..]; + if returned.parameters != remaining_shapes[remaining_count..] { + return Err(vec![BackendError::invalid_ir( + "P8 closure conversion", + expression.span, + "partial application returned callable parameters disagree with its target", + )]); + } + let returned_shape = closure_value_type_for(returned_signature); + let conversion = nested.typed_conversion_with_instantiation( + source_type, + cursor, + source_signature.result, + returned_shape, + expression.span, + evidence.as_ref(), + )?; + let callable = nested.emit_conversion( + result, + source_signature.result, + returned_shape, + conversion, + expression.span, + &mut nested_assignments, + ); + let destination = nested.fresh(returned.result); + nested_assignments.push(Assignment { + destination, + kind: AssignmentKind::IndirectCall { + function: callable, + signature: returned_signature, + arguments: tail_parameters.to_vec(), + }, + span: expression.span, + }); + let use_result = function_result_type(self.module, cursor); + (destination, returned.result, Some(use_result)) + } else { + (result, source_signature.result, None) + }; // The callee's declaration result is the un-instantiated shape, but the // closure signature is the partially applied expression's result. A // polymorphic declaration such as `pure :: a -> Effect a` returns an // erased value while the expression's result is concrete, so convert // before the generated function returns. - let result_conversion = if source_signature.result == target_signature.result { + let result_conversion = if source_result_shape == target_signature.result { ValueConversion::Identity } else { - let source_result_type = callable_result_type(self.module, function, callable_type) + let source_result_type = source_result_type + .or_else(|| callable_result_type(self.module, function, callable_type)) .ok_or_else(|| { vec![BackendError::new( "P8 closure conversion", @@ -199,17 +292,18 @@ impl FunctionLowerer<'_> { )] })?; let destination_result_type = function_result_type(self.module, expression.ty); - nested.typed_conversion( + nested.typed_conversion_with_instantiation( source_result_type, destination_result_type, - source_signature.result, + source_result_shape, target_signature.result, expression.span, + evidence.as_ref(), )? }; let converted_result = nested.emit_conversion( result, - source_signature.result, + source_result_shape, target_signature.result, result_conversion, expression.span, diff --git a/crates/psrs-backend/src/cc/lower/call/partial/indirect.rs b/crates/psrs-backend/src/cc/lower/call/partial/indirect.rs new file mode 100644 index 00000000..76599bf7 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/call/partial/indirect.rs @@ -0,0 +1,222 @@ +use super::*; + +impl FunctionLowerer<'_> { + /// Under-application of a local closure or dictionary method. Mirrors + /// [`Self::lower_partial_global_application`] but calls the captured callee + /// value indirectly instead of a declaration symbol. + pub(in crate::cc::lower::call) fn lower_indirect_partial_application( + &mut self, + application: IndirectPartialApplication<'_>, + assignments: &mut Vec, + ) -> Result> { + let IndirectPartialApplication { + expression, + head, + arguments, + signature, + signature_id, + result_type, + } = application; + let Some(target_signature_id) = + function_type_signature(self.module, self.function_types, expression.ty) + else { + return Err(vec![BackendError::new( + "P8 closure conversion", + expression.span, + "partial application has no runtime function type", + )]); + }; + let Some(target_signature) = self.representations.signature(target_signature_id).cloned() + else { + return Err(vec![BackendError::new( + "P8 closure conversion", + expression.span, + "partial application has no target call signature", + )]); + }; + let source_parameter_types = function_arrow_parameters(self.module, head.ty).0; + if source_parameter_types.len() != signature.parameters.len() { + return Err(vec![BackendError::new( + "P8 closure conversion", + expression.span, + "partial application has an incomplete callee signature", + )]); + } + + // Convert each supplied argument to the callee's expected parameter + // shape; each becomes a capture of the generated closure. + let mut capture_conversions = Vec::with_capacity(arguments.len()); + for (index, argument) in arguments.iter().enumerate() { + let expected = signature.parameters[index]; + let source_shape = self.value_shape(argument.ty, argument.span)?; + let conversion = self.typed_conversion( + argument.ty, + source_parameter_types[index], + source_shape, + expected, + expression.span, + )?; + capture_conversions.push((source_shape, conversion)); + } + + let callee = self.lower_value(head, assignments)?; + let callee_type = self + .values + .iter() + .find(|declaration| declaration.id == callee) + .map(|declaration| declaration.ty) + .ok_or_else(|| { + vec![BackendError::new( + "P8 closure conversion", + expression.span, + "partial application callee has no runtime type", + )] + })?; + let mut captured = Vec::with_capacity(arguments.len() + 1); + captured.push(callee); + for (index, argument) in arguments.iter().enumerate() { + let value = self.lower_value(argument, assignments)?; + let expected = signature.parameters[index]; + let (source_shape, conversion) = capture_conversions[index].clone(); + let converted = self.emit_conversion( + value, + source_shape, + expected, + conversion, + expression.span, + assignments, + ); + captured.push(converted); + } + + let mut nested = self.child_lowerer(); + let closure_parameter = nested.fresh(closure_value_type()); + let mut parameters = vec![closure_parameter]; + let mut remaining_parameters = Vec::with_capacity(target_signature.parameters.len()); + for expected in &target_signature.parameters { + let parameter = nested.fresh(*expected); + parameters.push(parameter); + remaining_parameters.push(parameter); + } + let mut nested_assignments = Vec::with_capacity(signature.parameters.len() + 1); + let mut call_arguments = Vec::with_capacity(signature.parameters.len()); + let callee_capture = nested.fresh(callee_type); + nested_assignments.push(Assignment { + destination: callee_capture, + kind: AssignmentKind::ClosureGetCapture { + closure: closure_parameter, + index: 0, + }, + span: expression.span, + }); + for (index, expected) in signature.parameters[..arguments.len()].iter().enumerate() { + let destination = nested.fresh(*expected); + nested_assignments.push(Assignment { + destination, + kind: AssignmentKind::ClosureGetCapture { + closure: closure_parameter, + index: (index + 1) as u32, + }, + span: expression.span, + }); + call_arguments.push(destination); + } + // A remaining parameter carries the expression's concrete shape, but + // the callee stores its parameter erased. Convert it the way the + // declaration partial application does. + for (index, parameter) in remaining_parameters.into_iter().enumerate() { + let position = arguments.len() + index; + let source_type = source_parameter_types[position]; + let source_shape = target_signature.parameters[index]; + let expected = signature.parameters[position]; + let conversion = nested.typed_conversion( + source_type, + source_type, + source_shape, + expected, + expression.span, + )?; + let converted = nested.emit_conversion( + parameter, + source_shape, + expected, + conversion, + expression.span, + &mut nested_assignments, + ); + call_arguments.push(converted); + } + if signature.result != target_signature.result { + return Err(vec![BackendError::new( + "P8 closure conversion", + expression.span, + "partial application result does not match the callee result", + )]); + } + let result = nested.fresh(signature.result); + nested_assignments.push(Assignment { + destination: result, + kind: AssignmentKind::IndirectCall { + function: callee_capture, + signature: signature_id, + arguments: call_arguments, + }, + span: expression.span, + }); + + let symbol = self.generated_symbols.borrow_mut().fresh(self.owner); + let generated = Function { + symbol, + name: format!("partial_indirect_{}", expression.span.start), + parameters, + values: nested.values, + assignments: nested_assignments, + result, + result_type: target_signature.result, + span: expression.span, + }; + crate::cc::verify::verify_function(&generated, self.signatures, self.representations)?; + self.generated.extend(nested.generated); + self.generated.push(generated); + + let closure = self.fresh(closure_value_type_for(target_signature_id)); + assignments.push(Assignment { + destination: closure, + kind: AssignmentKind::FunctionRef { + function: symbol, + signature: target_signature_id, + captures: captured, + }, + span: expression.span, + }); + if result_type == closure_value_type_for(target_signature_id) { + Ok(closure) + } else if result_type + == ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Erased, + }) + { + let erased = self.fresh(result_type); + assignments.push(Assignment { + destination: erased, + kind: AssignmentKind::RepresentationCast { + destination: erased, + value: closure, + reference: Reference { + nullable: false, + heap: RefShape::Erased, + }, + }, + span: expression.span, + }); + Ok(erased) + } else { + Err(vec![BackendError::new( + "P8 closure conversion", + expression.span, + "partial application result has the wrong runtime type", + )]) + } + } +} diff --git a/crates/psrs-backend/src/cc/lower/call/partial/mod.rs b/crates/psrs-backend/src/cc/lower/call/partial/mod.rs new file mode 100644 index 00000000..a85a9c5f --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/call/partial/mod.rs @@ -0,0 +1,41 @@ +use super::super::super::layout::{function_arrow_parameters, function_type_signature}; +use super::super::super::{ + Assignment, AssignmentKind, Function, RefShape, Reference, SignatureId, ValueConversion, + ValueId, +}; +use super::super::lambda::LambdaLowering; +use super::super::{FunctionLowerer, Signature, ValueShape}; +use super::helpers::{ + callable_instantiation, callable_parameter_types, callable_result_type, closure_value_type, + closure_value_type_for, conversion_reconstructs_aggregate, function_result_type, + persist_reference, restore_reference, +}; +use crate::BackendError; +use psrs_core::Expr; +use psrs_core::TypeId; +use psrs_hir::SymbolId; + +pub(super) struct PartialApplication<'a> { + pub(super) expression: &'a Expr, + pub(super) function: SymbolId, + pub(super) source_signature: &'a Signature, + pub(super) arguments: Vec<&'a Expr>, + pub(super) result_type: ValueShape, + pub(super) callable_type: TypeId, +} + +/// An under-applied callee that is not a top-level declaration: a local +/// closure or a dictionary method reached through a field access. The supplied +/// arguments are captured and the remaining parameters are exposed by a +/// generated closure that calls the callee indirectly. +pub(super) struct IndirectPartialApplication<'a> { + pub(super) expression: &'a Expr, + pub(super) head: &'a Expr, + pub(super) arguments: Vec<&'a Expr>, + pub(super) signature: &'a Signature, + pub(super) signature_id: SignatureId, + pub(super) result_type: ValueShape, +} + +mod global; +mod indirect; diff --git a/crates/psrs-backend/src/cc/lower/constructor.rs b/crates/psrs-backend/src/cc/lower/constructor.rs index e81e3af7..3871617a 100644 --- a/crates/psrs-backend/src/cc/lower/constructor.rs +++ b/crates/psrs-backend/src/cc/lower/constructor.rs @@ -120,12 +120,23 @@ impl FunctionLowerer<'_> { "constructor field storage does not match its normalized template", )]); } - let conversion = self.typed_conversion( + let evidence = self + .boundary + .checked_instantiation(template_type, &constructor.parameters, argument.ty) + .ok_or_else(|| { + vec![BackendError::invalid_ir( + "P8 closure conversion", + expression.span, + "constructor field has no checked scoped instantiation", + )] + })?; + let conversion = self.typed_conversion_with_instantiation( argument.ty, template_type, source_shape, template_shape, expression.span, + Some(&evidence), )?; let value = self.emit_conversion( value, diff --git a/crates/psrs-backend/src/cc/lower/control/builder.rs b/crates/psrs-backend/src/cc/lower/control/builder.rs new file mode 100644 index 00000000..0320c097 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/builder.rs @@ -0,0 +1,297 @@ +//! CC construction helpers for steps over predeclared machine layouts. + +use super::fields::ControlEmitter; +use super::{ControlFunctions, ControlLayout}; +use crate::cc::payload::erased_shape; +use crate::cc::{ + Assignment, AssignmentKind, Function, RefShape, Reference, ReprId, RepresentationTable, + SignatureId, TagCase, ValueDecl, ValueId, ValueShape, +}; +use psrs_hir::SymbolId; +use psrs_span::TextRange; + +pub(super) struct Machine<'a> { + pub representations: &'a RepresentationTable, + pub values: Vec, + pub layout: ControlLayout, + pub functions: ControlFunctions, + pub span: TextRange, +} + +pub(super) fn repr(id: ReprId) -> ValueShape { + reference(RefShape::Repr(id)) +} + +fn reference(heap: RefShape) -> ValueShape { + ValueShape::Reference(Reference { + nullable: false, + heap, + }) +} + +impl<'a> Machine<'a> { + pub fn new( + representations: &'a RepresentationTable, + layout: ControlLayout, + functions: ControlFunctions, + span: TextRange, + ) -> Self { + Self { + representations, + values: Vec::new(), + layout, + functions, + span, + } + } + + pub fn emit( + &mut self, + shape: ValueShape, + body: &mut Vec, + kind: impl FnOnce(ValueId) -> AssignmentKind, + ) -> ValueId { + let destination = self.fresh_value(shape); + body.push(Assignment { + destination, + kind: kind(destination), + span: self.span, + }); + destination + } + + pub fn cast( + &mut self, + value: ValueId, + shape: ValueShape, + body: &mut Vec, + ) -> ValueId { + if self.value_shape(value) == shape { + return value; + } + let ValueShape::Reference(reference) = shape else { + unreachable!("machine casts only references"); + }; + self.emit(shape, body, |destination| { + AssignmentKind::RepresentationCast { + destination, + value, + reference, + } + }) + } + + pub fn node( + &mut self, + representation: ReprId, + case: u32, + fields: Vec, + body: &mut Vec, + ) -> ValueId { + let fields = self + .control_fields(representation, case, fields, self.span, body) + .expect("machine construction must match its predeclared frame schema"); + self.emit(repr(representation), body, |destination| { + AssignmentKind::VariantNew { + destination, + representation, + case, + fields, + } + }) + } + + pub fn field( + &mut self, + value: ValueId, + representation: ReprId, + case: u32, + field: u32, + body: &mut Vec, + ) -> ValueId { + let value = self.cast(value, repr(representation), body); + let shape = self.control_field_shape(representation, case, field); + self.emit(shape, body, |destination| AssignmentKind::VariantGet { + destination, + representation, + case, + field, + value, + }) + } + + pub fn pair(&mut self, state: ValueId, value: ValueId, body: &mut Vec) -> ValueId { + let state = self.cast(state, erased_shape(), body); + let value = self.cast(value, erased_shape(), body); + let representation = self.layout.pair; + let value = self.emit(repr(representation), body, |destination| { + AssignmentKind::ProductNew { + destination, + representation, + arguments: vec![state, value], + } + }); + self.cast(value, erased_shape(), body) + } + + pub fn pair_field( + &mut self, + value: ValueId, + field: u32, + body: &mut Vec, + ) -> ValueId { + let representation = self.layout.pair; + let value = self.cast(value, repr(representation), body); + self.emit(erased_shape(), body, |destination| { + AssignmentKind::ProductGet { + destination, + representation, + field, + value, + } + }) + } + + pub fn capture( + &mut self, + closure: ValueId, + index: u32, + shape: ValueShape, + body: &mut Vec, + ) -> ValueId { + self.emit(shape, body, |_| AssignmentKind::ClosureGetCapture { + closure, + index, + }) + } + + pub fn closure( + &mut self, + function: SymbolId, + signature: SignatureId, + captures: Vec, + body: &mut Vec, + ) -> ValueId { + self.emit(reference(RefShape::Closure(signature)), body, |_| { + AssignmentKind::FunctionRef { + function, + signature, + captures, + } + }) + } + + /// Use the common curry protocol; every intermediate result is erased. + pub fn apply( + &mut self, + mut function: ValueId, + arguments: &[ValueId], + body: &mut Vec, + ) -> ValueId { + let signature = self.layout.continuation; + for argument in arguments { + let argument = self.cast(*argument, erased_shape(), body); + let callable = self.cast(function, reference(RefShape::Closure(signature)), body); + function = self.emit(erased_shape(), body, |_| AssignmentKind::IndirectCall { + function: callable, + signature, + arguments: vec![argument], + }); + } + function + } + + pub fn call( + &mut self, + function: SymbolId, + arguments: Vec, + body: &mut Vec, + ) -> ValueId { + let signature = self.functions.signature(self.layout, function); + self.control_call(function, signature, arguments, self.span, body) + .expect("generated call must match its predeclared signature") + } + + pub fn value_shape(&self, value: ValueId) -> ValueShape { + self.values + .iter() + .find(|declaration| declaration.id == value) + .map(|declaration| declaration.ty) + .expect("declared machine value") + } + + pub fn switch( + &mut self, + value: ValueId, + representation: ReprId, + cases: Vec, + body: &mut Vec, + ) -> ValueId { + let value = self.cast(value, repr(representation), body); + let tag = self.emit(ValueShape::Integer, body, |destination| { + AssignmentKind::VariantTag { + destination, + representation, + value, + } + }); + let shape = self.value_shape(cases.first().expect("machine switch has cases").value); + let trap = self.fresh_value(shape); + let default_assignments = vec![Assignment { + destination: trap, + kind: AssignmentKind::Unreachable, + span: self.span, + }]; + self.emit(shape, body, |_| AssignmentKind::TagSwitch { + value: tag, + cases, + default_assignments, + default_value: trap, + }) + } + + pub fn finish( + mut self, + symbol: SymbolId, + name: &str, + parameters: Vec, + mut assignments: Vec, + result: ValueId, + ) -> Function { + let result_type = if symbol == self.functions.resume { + erased_shape() + } else { + self.representations + .signature(self.functions.signature(self.layout, symbol)) + .expect("predeclared machine signature") + .result + }; + let result = self + .control_values(vec![result], vec![result_type], self.span, &mut assignments) + .expect("machine result must match its predeclared signature")[0]; + Function { + symbol, + name: name.into(), + parameters, + assignments, + result, + result_type, + values: self.values, + span: self.span, + } + } +} + +impl ControlEmitter for Machine<'_> { + fn table(&self) -> &RepresentationTable { + self.representations + } + fn declarations(&self) -> &[ValueDecl] { + &self.values + } + fn fresh_value(&mut self, shape: ValueShape) -> ValueId { + let id = ValueId(self.values.len() as u32); + self.values.push(ValueDecl { id, ty: shape }); + id + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/executor.rs b/crates/psrs-backend/src/cc/lower/control/executor.rs new file mode 100644 index 00000000..26a045ea --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/executor.rs @@ -0,0 +1,46 @@ +//! A flat driver built from a checked construction plan; only the driver +//! traverses saved continuation sequences. +//! +//! The transition program is described in [`super::plan`], checked there, and +//! expanded here. This module keeps only the executor's entry construction and +//! the `TagCase` helper shared with the other machine functions. + +use super::builder::{Machine, repr}; +use super::fields::ControlEmitter; +use super::plan; +use crate::cc::payload::erased_shape; +use crate::cc::{Assignment, Function, TagCase, ValueId}; + +impl Machine<'_> { + pub(super) fn executor(mut self) -> Function { + let state = self.fresh_value(erased_shape()); + let signature = self + .representations + .signature(self.layout.executor) + .unwrap(); + let handler = self.fresh_value(signature.parameters[1]); + let computation = self.fresh_value(repr(self.layout.computation)); + let sequence = self.fresh_value(repr(self.layout.sequence)); + let boundary = self.fresh_value(repr(self.layout.boundary)); + let context = self.fresh_value(repr(self.layout.context)); + let parameters = vec![state, handler, computation, sequence, boundary, context]; + let locals = [state, handler, computation, sequence, boundary, context]; + let program = plan::build(plan::Program::Executor); + let mut body = Vec::new(); + let result = self.expand(&program, &locals, &mut body); + let representations = self.representations; + let symbol = self.functions.executor; + let function = self.finish(symbol, "control_executor", parameters, body, result); + plan::check_boundary(&function, representations, &program) + .expect("executor boundary check must pass"); + function + } +} + +pub(super) fn arm(tag: u32, assignments: Vec, value: ValueId) -> TagCase { + TagCase { + tag: tag as i32, + assignments, + value, + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/fields.rs b/crates/psrs-backend/src/cc/lower/control/fields.rs new file mode 100644 index 00000000..6fca427e --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/fields.rs @@ -0,0 +1,163 @@ +//! Recover machine fields at the explicit erased carrier/callback boundary. + +use super::{FunctionLowerer, control_error}; +use crate::BackendError; +use crate::cc::payload::erased_shape; +use crate::cc::{ + Assignment, AssignmentKind, ReprId, Representation, RepresentationTable, SignatureId, + ValueDecl, ValueId, ValueShape, +}; +use psrs_hir::SymbolId; +use psrs_span::TextRange; + +/// Minimal emission capability shared by source factories and machine helpers. +/// No source types, local environments or mutable layout planning are exposed. +pub(super) trait ControlEmitter { + fn table(&self) -> &RepresentationTable; + fn declarations(&self) -> &[ValueDecl]; + fn fresh_value(&mut self, shape: ValueShape) -> ValueId; + + fn control_field_shape(&self, representation: ReprId, case: u32, field: u32) -> ValueShape { + let Some(Representation::Variant { cases }) = self.table().representation(representation) + else { + unreachable!("machine fields require a predeclared variant"); + }; + cases + .iter() + .find(|candidate| candidate.tag == case) + .and_then(|case| case.fields.get(field as usize)) + .copied() + .expect("machine projection must name a predeclared field") + } + + fn control_fields( + &mut self, + representation: ReprId, + case: u32, + fields: Vec, + span: TextRange, + assignments: &mut Vec, + ) -> Result, Vec> { + let Some(Representation::Variant { cases }) = self.table().representation(representation) + else { + return Err(control_error( + span, + "control frame has no variant representation", + )); + }; + let shapes = cases + .iter() + .find(|candidate| candidate.tag == case) + .map(|case| case.fields.clone()) + .ok_or_else(|| control_error(span, "control frame has an unknown case"))?; + self.control_values(fields, shapes, span, assignments) + } + + fn control_values( + &mut self, + fields: Vec, + shapes: Vec, + span: TextRange, + assignments: &mut Vec, + ) -> Result, Vec> { + if shapes.len() != fields.len() { + return Err(control_error( + span, + "control values have an incompatible count", + )); + } + fields + .into_iter() + .zip(shapes) + .map(|(value, shape)| { + // Source payloads and user callbacks use the erased protocol. + // Machine helpers keep their predeclared nominal shapes. + let source = self + .declarations() + .iter() + .find(|declaration| declaration.id == value) + .map(|declaration| declaration.ty) + .ok_or_else(|| control_error(span, "control field has no value declaration"))?; + if source == shape { + return Ok(value); + } + if !matches!(source, ValueShape::Reference(_)) + || (source != erased_shape() && shape != erased_shape()) + { + return Err(control_error( + span, + "control field has an incompatible representation", + )); + } + // These are references to existing machine objects, not source + // Array/Function payloads. Payload recovery would adapt their + // contents and could copy a shared mutable status cell. + let ValueShape::Reference(reference) = shape else { + return Err(control_error( + span, + "control field requires a reference shape", + )); + }; + let destination = self.fresh_value(shape); + assignments.push(Assignment { + destination, + kind: AssignmentKind::RepresentationCast { + destination, + value, + reference, + }, + span, + }); + Ok(destination) + }) + .collect() + } + + fn control_call( + &mut self, + function: SymbolId, + signature: SignatureId, + arguments: Vec, + span: TextRange, + assignments: &mut Vec, + ) -> Result> { + let contract = self + .table() + .control_helpers() + .iter() + .position(|helper| helper.function() == function) + .map(|index| crate::cc::ControlHelperId(index as u32)) + .ok_or_else(|| { + control_error(span, "control call has no predeclared semantic helper") + })?; + let signature = self + .table() + .signature(signature) + .ok_or_else(|| control_error(span, "control call has no predeclared signature"))? + .clone(); + let arguments = self.control_values(arguments, signature.parameters, span, assignments)?; + let destination = self.fresh_value(signature.result); + assignments.push(Assignment { + destination, + kind: AssignmentKind::DirectCall { + contract: Some(contract), + function, + arguments, + }, + span, + }); + Ok(destination) + } +} + +impl ControlEmitter for FunctionLowerer<'_> { + fn table(&self) -> &RepresentationTable { + self.representations + } + fn declarations(&self) -> &[ValueDecl] { + &self.values + } + fn fresh_value(&mut self, shape: ValueShape) -> ValueId { + self.fresh(shape) + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/foreign.rs b/crates/psrs-backend/src/cc/lower/control/foreign.rs new file mode 100644 index 00000000..e805ea32 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/foreign.rs @@ -0,0 +1,38 @@ +//! Host-resource values cannot be stored in a software continuation. +use super::{FunctionLowerer, control_error}; +use crate::BackendError; +use psrs_core::{Expr, Type, TypeConstructor}; +use std::collections::HashSet; + +impl FunctionLowerer<'_> { + pub(super) fn reject_host_resource_captures( + &self, + argument: &Expr, + ) -> Result<(), Vec> { + let mut bound = HashSet::new(); + let mut captures = Vec::new(); + super::super::lambda::collect_captures(argument, &mut bound, &mut captures); + for local in captures { + let Some(&ty) = self.local_types.get(&local) else { + continue; + }; + if self.is_host_resource(ty) { + return Err(control_error( + argument.span, + "control callback cannot capture a host resource handle", + )); + } + } + Ok(()) + } + + fn is_host_resource(&self, ty: psrs_core::TypeId) -> bool { + match self.module.types.get(ty.0 as usize) { + Some(Type::Constructor(TypeConstructor::User(id))) => { + self.module.opaque_ids.contains(id) + } + Some(Type::Application(head, _)) => self.is_host_resource(*head), + _ => false, + } + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/lifetime.rs b/crates/psrs-backend/src/cc/lower/control/lifetime.rs new file mode 100644 index 00000000..f36bb4b0 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/lifetime.rs @@ -0,0 +1,198 @@ +//! Mutable status cells are private; frame payloads and sequences stay immutable. + +use super::builder::{Machine, repr}; +use super::executor::arm; +use super::fields::ControlEmitter; +use crate::cc::{Assignment, AssignmentKind, Function, TagCase, ValueId, ValueShape}; +use crate::control::schema::ExtentCase; + +impl Machine<'_> { + pub(super) fn integer(&mut self, n: i32, body: &mut Vec) -> ValueId { + self.emit(ValueShape::Integer, body, |_| AssignmentKind::Constant(n)) + } + + pub(super) fn new_cell( + &mut self, + representation: crate::cc::ReprId, + value: ValueId, + body: &mut Vec, + ) -> ValueId { + self.emit(repr(representation), body, |destination| { + AssignmentKind::ArrayNew { + destination, + representation, + elements: vec![value], + } + }) + } + + pub(super) fn read_cell( + &mut self, + representation: crate::cc::ReprId, + shape: ValueShape, + cell: ValueId, + body: &mut Vec, + ) -> ValueId { + let cell = self.cast(cell, repr(representation), body); + let index = self.integer(0, body); + self.emit(shape, body, |destination| AssignmentKind::ArrayGet { + destination, + representation, + value: cell, + index, + }) + } + + pub(super) fn write_cell( + &mut self, + representation: crate::cc::ReprId, + cell: ValueId, + new_value: ValueId, + body: &mut Vec, + ) { + let cell = self.cast(cell, repr(representation), body); + let Some(crate::cc::Representation::Array { element }) = + self.representations.representation(representation) + else { + unreachable!("machine cell is an array"); + }; + let new_value = self + .control_values(vec![new_value], vec![*element], self.span, body) + .expect("machine write must preserve its cell representation")[0]; + let index = self.integer(0, body); + body.push(Assignment { + destination: cell, + kind: AssignmentKind::ArraySet { + destination: cell, + representation, + value: cell, + index, + new_value, + }, + span: self.span, + }); + } + + pub(super) fn new_status(&mut self, body: &mut Vec) -> ValueId { + let zero = self.integer(0, body); + self.new_cell(self.layout.status, zero, body) + } + + pub(super) fn status(&mut self, cell: ValueId, body: &mut Vec) -> ValueId { + self.read_cell(self.layout.status, ValueShape::Integer, cell, body) + } + + pub(super) fn set_status(&mut self, cell: ValueId, n: i32, body: &mut Vec) { + let new_value = self.integer(n, body); + self.write_cell(self.layout.status, cell, new_value, body); + } + + pub(super) fn guard_pending(&mut self, cell: ValueId, body: &mut Vec) { + let status = self.status(cell, body); + self.select(status, vec![arm(0, vec![], cell)], body); + } + + pub(super) fn select( + &mut self, + value: ValueId, + cases: Vec, + body: &mut Vec, + ) -> ValueId { + let shape = self.value_shape(cases.first().expect("machine select has cases").value); + let trap = self.fresh_value(shape); + let default_assignments = vec![Assignment { + destination: trap, + kind: AssignmentKind::Unreachable, + span: self.span, + }]; + self.emit(shape, body, |_| AssignmentKind::TagSwitch { + value, + cases, + default_assignments, + default_value: trap, + }) + } + + pub(super) fn new_boundary( + &mut self, + parent: Option, + body: &mut Vec, + ) -> ValueId { + let cell = self.new_status(body); + let (case, fields) = match parent { + Some(parent) => (ExtentCase::Scoped.tag(), vec![cell, parent]), + None => (ExtentCase::Root.tag(), vec![cell]), + }; + self.node(self.layout.boundary, case, fields, body) + } + + pub(super) fn check_boundary( + &mut self, + boundary: ValueId, + body: &mut Vec, + ) -> ValueId { + let function = self.functions.check; + self.call(function, vec![boundary], body) + } + + pub(super) fn close_boundary( + &mut self, + boundary: ValueId, + body: &mut Vec, + ) -> ValueId { + let function = self.functions.close; + self.call(function, vec![boundary], body) + } + + pub(super) fn start_boundary(mut self) -> Function { + let program = super::plan::build(super::plan::Program::Start); + let mut body = Vec::new(); + let result = self.expand(&program, &[], &mut body); + let representations = self.representations; + let symbol = self.functions.start; + let function = self.finish(symbol, "control_start_boundary", vec![], body, result); + super::plan::check_boundary(&function, representations, &program) + .expect("start_boundary boundary check must pass"); + function + } + + pub(super) fn boundary_check(mut self) -> Function { + let boundary = self.fresh_value(repr(self.layout.boundary)); + let params = [boundary]; + let program = super::plan::build(super::plan::Program::Check); + let mut body = Vec::new(); + let result = self.expand(&program, ¶ms, &mut body); + let representations = self.representations; + let symbol = self.functions.check; + let function = self.finish( + symbol, + "control_check_boundary", + vec![boundary], + body, + result, + ); + super::plan::check_boundary(&function, representations, &program) + .expect("boundary_check boundary check must pass"); + function + } + + pub(super) fn boundary_close(mut self) -> Function { + let boundary = self.fresh_value(repr(self.layout.boundary)); + let params = [boundary]; + let program = super::plan::build(super::plan::Program::Close); + let mut body = Vec::new(); + let result = self.expand(&program, ¶ms, &mut body); + let representations = self.representations; + let symbol = self.functions.close; + let function = self.finish( + symbol, + "control_close_boundary", + vec![boundary], + body, + result, + ); + super::plan::check_boundary(&function, representations, &program) + .expect("boundary_close boundary check must pass"); + function + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/mod.rs b/crates/psrs-backend/src/cc/lower/control/mod.rs new file mode 100644 index 00000000..90497ea7 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/mod.rs @@ -0,0 +1,262 @@ +//! Lower the inert control surface to a persistent continuation machine. + +use super::FunctionLowerer; +use crate::BackendError; +use crate::cc::layout::control::ControlLayout; +use crate::cc::payload::erased_shape; +use crate::cc::{Assignment, AssignmentKind, ValueId, ValueShape}; +use psrs_core::Expr; +use psrs_hir::Intrinsic; +use psrs_span::TextRange; + +pub(crate) use plan::{Family, Shape}; + +mod builder; +mod executor; +mod fields; +mod foreign; +mod lifetime; +mod operands; +mod owned; +mod plan; +mod planning; +mod replay; +mod scope; +use builder::Machine; +use fields::ControlEmitter; +pub(in crate::cc) use planning::ControlFunctions; +pub(in crate::cc) use planning::plan_control_helpers; +pub(crate) use planning::{plan_external_targets, plan_shape_for}; + +impl FunctionLowerer<'_> { + pub(super) fn lower_control( + &mut self, + expression: &Expr, + intrinsic: Intrinsic, + arguments: &[Expr], + ty: ValueShape, + assignments: &mut Vec, + ) -> Result> { + let proof = psrs_core::checked_control_contract( + self.module, + intrinsic, + expression.ty, + &arguments + .iter() + .map(|argument| argument.ty) + .collect::>(), + ) + .map_err(|message| control_error(expression.span, message))?; + let contract = + crate::cc::layout::control::lookup(self.module, self.representations, &proof) + .ok_or_else(|| { + control_error( + expression.span, + "control use has no predeclared checked contract", + ) + })?; + let fields = self.lower_control_operands(contract, arguments, assignments)?; + let (value, implementation) = + self.lower_control_frame(expression, intrinsic, fields.clone(), ty, assignments)?; + let destination = self.fresh(ty); + assignments.push(Assignment { + destination, + kind: AssignmentKind::ControlEvidence { + value, + implementation, + arguments: fields, + contract, + }, + span: expression.span, + }); + Ok(destination) + } + + fn lower_control_frame( + &mut self, + expression: &Expr, + intrinsic: Intrinsic, + fields: Vec, + ty: ValueShape, + assignments: &mut Vec, + ) -> Result<(ValueId, ValueId), Vec> { + let layout = self.control_layout.ok_or_else(|| { + control_error( + expression.span, + "control primitive used without a frame representation", + ) + })?; + if matches!(intrinsic, Intrinsic::ControlOnce | Intrinsic::ControlCancel) { + let functions = self.ensure_control_functions(layout, expression.span)?; + let function = if intrinsic == Intrinsic::ControlOnce { + functions.once + } else { + functions.cancel + }; + let result = self.control_call( + function, + functions.signature(layout, function), + fields, + expression.span, + assignments, + )?; + if intrinsic == Intrinsic::ControlCancel { + let unit = self.fresh(ty); + assignments.push(Assignment { + destination: unit, + kind: AssignmentKind::Constant(0), + span: expression.span, + }); + return Ok((unit, result)); + } + if ty != builder::repr(layout.owner) { + return Err(control_error( + expression.span, + "resumption result has no nominal owner shape", + )); + } + return Ok((result, result)); + } + if intrinsic == Intrinsic::ControlRun { + let functions = self.ensure_control_functions(layout, expression.span)?; + let result = self.control_call( + functions.run, + layout.run, + fields, + expression.span, + assignments, + )?; + let conversion = self.recover_payload(ty, expression.span)?; + return Ok(( + self.emit_conversion( + result, + erased_shape(), + ty, + conversion, + expression.span, + assignments, + ), + result, + )); + } + let case = match intrinsic { + Intrinsic::ControlReturn => crate::control::schema::ComputationCase::Return, + Intrinsic::ControlPerform => crate::control::schema::ComputationCase::Perform, + Intrinsic::ControlBind => crate::control::schema::ComputationCase::Bind, + Intrinsic::ControlHandle => crate::control::schema::ComputationCase::Handle, + Intrinsic::ControlResume => crate::control::schema::ComputationCase::OwnedResume, + _ => return Err(control_error(expression.span, "not a control primitive")), + } + .tag(); + let destination = self.fresh(ty); + let fields = self.control_fields( + layout.computation, + case, + fields, + expression.span, + assignments, + )?; + assignments.push(Assignment { + destination, + kind: AssignmentKind::VariantNew { + destination, + representation: layout.computation, + case, + fields, + }, + span: expression.span, + }); + Ok((destination, destination)) + } + + fn ensure_control_functions( + &mut self, + layout: ControlLayout, + span: TextRange, + ) -> Result> { + let functions = self + .control_functions + .borrow() + .ok_or_else(|| control_error(span, "control helpers were not predeclared"))?; + if functions.emitted { + return Ok(functions); + } + for (symbol, id, _) in functions.contracts(layout) { + let signature = self + .representations + .signature(id) + .ok_or_else(|| { + control_error( + span, + "control lifetime function has no predeclared signature", + ) + })? + .clone(); + self.generated_signatures + .borrow_mut() + .insert(symbol, signature); + } + let mut resume = Machine::new(self.representations, layout, functions, span); + let receiver = resume.fresh_value(super::lambda::closure_value_type()); + let value = resume.fresh_value(erased_shape()); + let params = [receiver, value]; + let program = plan::build(plan::Program::RawResume); + let mut assignments = Vec::new(); + let result = resume.expand(&program, ¶ms, &mut assignments); + let representations = resume.representations; + let resume = resume.finish( + functions.resume, + "control_resume", + vec![receiver, value], + assignments, + result, + ); + plan::check_boundary(&resume, representations, &program) + .expect("raw resume boundary check must pass"); + self.verify_synthesized(&resume)?; + self.generated.push(resume); + let executor = Machine::new(self.representations, layout, functions, span).executor(); + self.verify_synthesized(&executor)?; + self.generated.push(executor); + for function in [ + Machine::new(self.representations, layout, functions, span).run(), + Machine::new(self.representations, layout, functions, span).clone_sequence(), + Machine::new(self.representations, layout, functions, span).rebuild_sequence(), + Machine::new(self.representations, layout, functions, span).start_boundary(), + Machine::new(self.representations, layout, functions, span).boundary_check(), + Machine::new(self.representations, layout, functions, span).boundary_close(), + Machine::new(self.representations, layout, functions, span).owned(), + Machine::new(self.representations, layout, functions, span).cancel(), + ] { + self.verify_synthesized(&function)?; + self.generated.push(function); + } + *self.control_functions.borrow_mut() = Some(ControlFunctions { + emitted: true, + ..functions + }); + Ok(functions) + } +} + +fn control_error(span: TextRange, message: &'static str) -> Vec { + vec![BackendError::invalid_ir( + "P8 closure conversion", + span, + message, + )] +} + +/// Re-check a generated machine body against its checked plan. This is the +/// replaced recognizer's entry point: it runs on the emitted CC at the module +/// verification boundary and rejects any body whose ordered operations, +/// operands, dispatch or returned value disagree with the construction plan. +pub(crate) fn verify_boundary( + function: &crate::cc::Function, + table: &crate::cc::RepresentationTable, +) -> Result<(), &'static str> { + match planning::plan_for_symbol(function.symbol, table) { + Some(plan) => plan::check_boundary(function, table, &plan), + None => Ok(()), + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/operands.rs b/crates/psrs-backend/src/cc/lower/control/operands.rs new file mode 100644 index 00000000..c007e244 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/operands.rs @@ -0,0 +1,116 @@ +//! Lower each source operand into its predeclared storage role. + +use super::{FunctionLowerer, control_error}; +use crate::BackendError; +use crate::cc::control::ControlImplementation; +use crate::cc::payload::{PayloadPlanner, erased_shape}; +use crate::cc::{ + Assignment, ControlContractId, RefShape, Reference, Representation, ValueId, ValueShape, +}; +use psrs_core::Expr; + +impl FunctionLowerer<'_> { + pub(super) fn lower_control_operands( + &mut self, + contract: ControlContractId, + arguments: &[Expr], + assignments: &mut Vec, + ) -> Result, Vec> { + let proof = &self.representations.control_contracts()[contract.0 as usize]; + let shapes = match proof.implementation { + ControlImplementation::Frame { + representation, + case, + } => { + let Some(Representation::Variant { cases }) = + self.representations.representation(representation) + else { + return Err(control_error( + self.module.span, + "control operand plan has no variant", + )); + }; + cases + .iter() + .find(|candidate| candidate.tag == case) + .map(|case| case.fields.clone()) + .ok_or_else(|| { + control_error(self.module.span, "control operand plan has no case") + })? + } + ControlImplementation::Call { signature, .. } => self + .representations + .signature(signature) + .ok_or_else(|| { + control_error(self.module.span, "control operand plan has no signature") + })? + .parameters + .iter() + .take(arguments.len()) + .copied() + .collect(), + }; + if shapes.len() != arguments.len() { + return Err(control_error( + self.module.span, + "control operand plan has an incompatible count", + )); + } + arguments + .iter() + .zip(shapes) + .map(|(argument, target)| { + if target == erased_shape() { + return self.lower_array_element(argument, assignments); + } + let source = self.value_shape(argument.ty, argument.span)?; + if let ValueShape::Reference(Reference { + heap: RefShape::Closure(_), + .. + }) = target + { + self.reject_host_resource_captures(argument)?; + } + let value = self.lower_value(argument, assignments)?; + if let ValueShape::Reference(Reference { + heap: RefShape::Closure(_), + .. + }) = target + { + let ValueShape::Reference(Reference { + heap: RefShape::Closure(signature), + .. + }) = source + else { + return Err(control_error( + argument.span, + "control callback has no callable representation", + )); + }; + let plan = self.payload_callable(signature, true, argument.span)?; + if plan.output_shape(source) != target { + return Err(control_error( + argument.span, + "control callback disagrees with its unary protocol", + )); + } + return Ok(self.emit_conversion( + value, + source, + target, + plan, + argument.span, + assignments, + )); + } + if source != target { + return Err(control_error( + argument.span, + "control carrier disagrees with its nominal representation", + )); + } + Ok(value) + }) + .collect() + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/owned.rs b/crates/psrs-backend/src/cc/lower/control/owned.rs new file mode 100644 index 00000000..3e2adca2 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/owned.rs @@ -0,0 +1,39 @@ +//! One-shot ownership is consumed at interpretation, before invoking user code. +//! +//! The factory and cancellation bodies are built from the checked plans in +//! [`super::plan`]. + +use super::builder::{Machine, repr}; +use super::fields::ControlEmitter; +use crate::cc::Function; + +impl Machine<'_> { + pub(super) fn owned(mut self) -> Function { + let signature = self.representations.signature(self.layout.once).unwrap(); + let continuation = self.fresh_value(signature.parameters[0]); + let params = [continuation]; + let program = super::plan::build(super::plan::Program::Once); + let mut body = Vec::new(); + let result = self.expand(&program, ¶ms, &mut body); + let representations = self.representations; + let symbol = self.functions.once; + let function = self.finish(symbol, "control_once", vec![continuation], body, result); + super::plan::check_boundary(&function, representations, &program) + .expect("once boundary check must pass"); + function + } + + pub(super) fn cancel(mut self) -> Function { + let owner = self.fresh_value(repr(self.layout.owner)); + let params = [owner]; + let program = super::plan::build(super::plan::Program::Cancel); + let mut body = Vec::new(); + let result = self.expand(&program, ¶ms, &mut body); + let representations = self.representations; + let symbol = self.functions.cancel; + let function = self.finish(symbol, "control_cancel", vec![owner], body, result); + super::plan::check_boundary(&function, representations, &program) + .expect("cancel boundary check must pass"); + function + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/boundary/mod.rs b/crates/psrs-backend/src/cc/lower/control/plan/boundary/mod.rs new file mode 100644 index 00000000..439ca001 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/boundary/mod.rs @@ -0,0 +1,253 @@ +//! Independent, ordered validation of ordinary CC against its construction plan. +//! +//! Casts may bridge planned reference shapes; generic CC verification checks +//! their legality. Every other assignment must implement the next planned +//! operation, including unused effects and trapping defaults. This is a P8 +//! construction check, not a validator for arbitrary optimized programs. + +mod ops; +mod primitives; + +use super::ir::{Case, Exec, Operand, state_index}; +use crate::cc::branch_facts::Definitions; +use crate::cc::{ + Assignment, AssignmentKind as A, ControlSchema, Function, ReprId, RepresentationTable, ValueId, +}; + +type Checked = Result; +const MISMATCH: &str = + "emitted machine operands or ordered operations disagree with the checked plan"; + +pub(crate) fn check_boundary( + function: &Function, + table: &RepresentationTable, + plan: &super::CheckedPlan, +) -> Checked<()> { + let checker = Checker { + table, + schema: table + .control_schema() + .ok_or("boundary check has no checked machine schema")?, + definitions: crate::cc::value_identity::definitions(&function.assignments), + params: &function.parameters, + shapes: function + .values + .iter() + .map(|value| (value.id, value.ty)) + .collect(), + used_casts: std::cell::RefCell::new(std::collections::HashSet::new()), + pending_casts: std::cell::RefCell::new(Vec::new()), + }; + let mut body = function.assignments.as_slice(); + let result = checker.exec(plan.exec(), &mut Vec::new(), &mut body)?; + checker.equal(result, function.result)?; + checker.end(&mut body)?; + Ok(()) +} + +struct Checker<'a> { + table: &'a RepresentationTable, + schema: ControlSchema, + definitions: Definitions<'a>, + params: &'a [ValueId], + shapes: std::collections::HashMap, + used_casts: std::cell::RefCell>, + pending_casts: std::cell::RefCell>, +} + +impl Checker<'_> { + fn strip(&self, mut value: ValueId) -> Checked { + let mut active = std::collections::HashSet::new(); + while let Some(A::RepresentationCast { value: source, .. }) = + self.definitions.get(&value).copied() + { + if !active.insert(value) { + return Err(MISMATCH); + } + value = *source; + } + Ok(value) + } + + fn equal(&self, a: ValueId, b: ValueId) -> Checked<()> { + self.used_casts.borrow_mut().extend([a, b]); + if self.strip(a)? == self.strip(b)? { + Ok(()) + } else { + Err(MISMATCH) + } + } + + fn values(&self, actual: &[ValueId], expected: &[ValueId]) -> Checked<()> { + if actual.len() != expected.len() { + return Err(MISMATCH); + } + for (a, b) in actual.iter().zip(expected) { + self.equal(*a, *b)?; + } + Ok(()) + } + + fn operand(&self, value: &Operand, env: &[ValueId]) -> Checked { + match value { + Operand::State(slot) => self.params.get(state_index(*slot)), + Operand::Param(index) => self.params.get(*index), + Operand::Temp(index) => env.get(*index), + } + .copied() + .ok_or(MISMATCH) + } + + fn operands(&self, values: &[Operand], env: &[ValueId]) -> Checked> { + values + .iter() + .map(|value| self.operand(value, env)) + .collect() + } + + fn representation(&self, case: Case) -> ReprId { + match case { + Case::Computation(_) => self.schema.computation, + Case::Sequence(_) => self.schema.sequence, + Case::Context(_) => self.schema.context, + Case::Extent(_) => self.schema.extent, + Case::Owner(_) => self.schema.owner, + } + } + + fn skip_casts(&self, body: &mut &[Assignment]) { + while let Some(Assignment { + destination, + kind: A::RepresentationCast { + value, reference, .. + }, + .. + }) = body.first() + { + let target = crate::cc::ValueShape::Reference(*reference); + // Erasure and same-shape aliases are total. A recovery must feed + // the immediately following checked operation or actual exit; + // it cannot move across a guard, store or user callback. + if target != crate::cc::payload::erased_shape() + && self.shapes.get(value) != Some(&target) + { + self.pending_casts.borrow_mut().push(*destination); + } + *body = &body[1..]; + } + } + + fn consumed_casts(&self) -> Checked<()> { + if self + .pending_casts + .borrow() + .iter() + .any(|value| !self.used_casts.borrow().contains(value)) + { + return Err(MISMATCH); + } + self.pending_casts.borrow_mut().clear(); + Ok(()) + } + + fn next<'a>(&self, body: &mut &'a [Assignment]) -> Checked<&'a Assignment> { + self.consumed_casts()?; + self.skip_casts(body); + let (next, rest) = body.split_first().ok_or(MISMATCH)?; + *body = rest; + Ok(next) + } + + fn end(&self, body: &mut &[Assignment]) -> Checked<()> { + self.consumed_casts()?; + self.skip_casts(body); + self.consumed_casts()?; + if body.is_empty() { + Ok(()) + } else { + Err(MISMATCH) + } + } + + fn reject(&self, body: &[Assignment], result: ValueId) -> Checked<()> { + let mut body = body; + let assignment = self.next(&mut body)?; + if !matches!(assignment.kind, A::Unreachable) { + return Err(MISMATCH); + } + self.equal(assignment.destination, result)?; + self.end(&mut body) + } + + fn exec( + &self, + plan: &Exec, + env: &mut Vec, + body: &mut &[Assignment], + ) -> Checked { + match plan { + Exec::Steps { ops, next } => { + let base = env.len(); + for op in ops { + let value = self.op(op, env, body)?; + env.push(value); + } + let value = self.exec(next, env, body)?; + env.truncate(base); + Ok(value) + } + Exec::Switch { source, case, arms } => { + let source = self.operand(source, env)?; + let tag = self.tag(source, self.representation(*case), body)?; + let assignment = self.next(body)?; + let A::TagSwitch { + value, + cases, + default_assignments, + default_value, + } = &assignment.kind + else { + return Err(MISMATCH); + }; + self.equal(*value, tag)?; + if cases.len() != arms.len() { + return Err(MISMATCH); + } + for (actual, (tag, arm)) in cases.iter().zip(arms) { + if actual.tag != *tag as i32 { + return Err(MISMATCH); + } + let mut branch = actual.assignments.as_slice(); + let result = self.exec(arm, &mut env.clone(), &mut branch)?; + self.equal(result, actual.value)?; + self.end(&mut branch)?; + } + self.reject(default_assignments, *default_value)?; + Ok(assignment.destination) + } + Exec::Advance { + state, + handler, + computation, + sequence, + extent, + context, + } => { + let args = [state, handler, computation, sequence, extent, context] + .iter() + .map(|value| self.operand(value, env)) + .collect::>>()?; + self.call(crate::cc::ControlHelperRole::Executor, &args, body) + } + Exec::Tail { role, args } => self.call(*role, &self.operands(args, env)?, body), + Exec::Return { value } | Exec::Publish { result: value } => self.operand(value, env), + Exec::Reject { .. } => { + let assignment = self.next(body)?; + if !matches!(assignment.kind, A::Unreachable) { + return Err(MISMATCH); + } + Ok(assignment.destination) + } + } + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/boundary/ops.rs b/crates/psrs-backend/src/cc/lower/control/plan/boundary/ops.rs new file mode 100644 index 00000000..deb80fd6 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/boundary/ops.rs @@ -0,0 +1,189 @@ +//! Interpret each planned operation as an independent emitted-CC obligation. +use super::super::ir::{Op, Product}; +use super::*; +use crate::cc::ControlHelperRole as R; +use crate::control::schema::{ContextCase, ExtentCase, SequenceCase, context}; + +impl Checker<'_> { + pub(super) fn op( + &self, + op: &Op, + env: &[ValueId], + body: &mut &[Assignment], + ) -> Checked { + let operand = |value| self.operand(value, env); + match op { + Op::Project { + source, + case, + field, + } => self.project( + operand(source)?, + self.representation(*case), + case.tag(), + *field, + body, + ), + Op::Node { case, fields } => self.node( + self.representation(*case), + case.tag(), + &self.operands(fields, env)?, + body, + ), + Op::Capture { + closure, + index, + case, + } => { + let a = self.next(body)?; + let A::ClosureGetCapture { + closure: actual, + index: slot, + } = &a.kind + else { + return Err(MISMATCH); + }; + if slot != index { + return Err(MISMATCH); + } + self.equal(*actual, operand(closure)?)?; + let expected = crate::cc::ValueShape::Reference(crate::cc::Reference { + nullable: false, + heap: crate::cc::RefShape::Repr(self.representation(*case)), + }); + if self.shapes.get(&a.destination) != Some(&expected) { + return Err(MISMATCH); + } + Ok(a.destination) + } + Op::Apply { callee, args } => { + let mut callee = operand(callee)?; + let signature = self.table.control_resumption().ok_or(MISMATCH)?.signature(); + for arg in args { + let a = self.next(body)?; + let A::IndirectCall { + function, + signature: actual, + arguments, + } = &a.kind + else { + return Err(MISMATCH); + }; + if *actual != signature { + return Err(MISMATCH); + } + self.equal(*function, callee)?; + self.values(arguments, &[operand(arg)?])?; + callee = a.destination; + } + Ok(callee) + } + Op::Helper { role, args } => self.call(*role, &self.operands(args, env)?, body), + Op::ResumeClosure { captures } => { + let raw = self.table.control_resumption().ok_or(MISMATCH)?; + let a = self.next(body)?; + let A::FunctionRef { + function, + signature, + captures: actual, + } = &a.kind + else { + return Err(MISMATCH); + }; + if (*function, *signature) != (raw.function(), raw.signature()) { + return Err(MISMATCH); + } + self.values(actual, &self.operands(captures, env)?)?; + Ok(a.destination) + } + Op::Pair { state, value } => self.product( + self.schema.result_pair, + &[operand(state)?, operand(value)?], + body, + ), + Op::PairField { pair, field } => { + self.product_field(operand(pair)?, self.schema.result_pair, *field, body) + } + Op::ReplayField { result, field } => { + self.product_field(operand(result)?, self.schema.replay_result, *field, body) + } + Op::FreshExtent { parent } => { + let status = self.new_status(body)?; + let (case, fields) = match parent { + Some(parent) => (ExtentCase::Scoped, vec![status, operand(parent)?]), + None => (ExtentCase::Root, vec![status]), + }; + self.node(self.schema.extent, case.tag(), &fields, body) + } + Op::ContextExtent { context } => self.context_extent(operand(context)?, body), + Op::CheckBoundary { extent } => self.call(R::Check, &[operand(extent)?], body), + Op::CloseBoundary { extent } => self.call(R::Close, &[operand(extent)?], body), + Op::GuardPending { cell } => { + let cell = operand(cell)?; + self.guard(cell, body)?; + Ok(cell) + } + Op::SetStatus { cell, value } => { + let cell = operand(cell)?; + let status = self.constant(*value, body)?; + self.write(self.schema.status, cell, status, body)?; + Ok(cell) + } + Op::ReadCallback { cell } => self.read(self.schema.callback, operand(cell)?, body), + Op::WriteCallback { cell, value } => { + let cell = operand(cell)?; + self.write(self.schema.callback, cell, operand(value)?, body)?; + Ok(cell) + } + Op::NewCallbackCell { value } => { + self.new_cell(self.schema.callback, operand(value)?, body) + } + Op::NewStatus => self.new_status(body), + Op::Product { + product: Product::ReplayResult, + fields, + } => self.product( + self.schema.replay_result, + &self.operands(fields, env)?, + body, + ), + Op::EmptySequence => { + self.node(self.schema.sequence, SequenceCase::Empty.tag(), &[], body) + } + } + } + + fn context_extent(&self, source: ValueId, body: &mut &[Assignment]) -> Checked { + let tag = self.tag(source, self.schema.context, body)?; + let a = self.next(body)?; + let A::TagSwitch { + value, + cases, + default_assignments, + default_value, + } = &a.kind + else { + return Err(MISMATCH); + }; + self.equal(*value, tag)?; + if cases.len() != ContextCase::ALL.len() { + return Err(MISMATCH); + } + for (actual, case) in cases.iter().zip(ContextCase::ALL) { + if actual.tag != case.tag() as i32 { + return Err(MISMATCH); + } + let field = match case { + ContextCase::Root => context::ROOT_EXTENT, + ContextCase::Scoped => context::SCOPED_EXTENT, + }; + let mut branch = actual.assignments.as_slice(); + let result = + self.project(source, self.schema.context, case.tag(), field, &mut branch)?; + self.equal(result, actual.value)?; + self.end(&mut branch)?; + } + self.reject(default_assignments, *default_value)?; + Ok(a.destination) + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/boundary/primitives.rs b/crates/psrs-backend/src/cc/lower/control/plan/boundary/primitives.rs new file mode 100644 index 00000000..4e5f9509 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/boundary/primitives.rs @@ -0,0 +1,257 @@ +//! Check ordered CC operations without invoking the machine emitter. +use super::*; +use crate::cc::ControlHelperRole; + +impl Checker<'_> { + pub(super) fn tag( + &self, + source: ValueId, + repr: ReprId, + body: &mut &[Assignment], + ) -> Checked { + let a = self.next(body)?; + let A::VariantTag { + representation, + value, + .. + } = &a.kind + else { + return Err(MISMATCH); + }; + if *representation != repr { + return Err(MISMATCH); + } + self.equal(*value, source)?; + Ok(a.destination) + } + + pub(super) fn project( + &self, + source: ValueId, + repr: ReprId, + tag: u32, + field: u32, + body: &mut &[Assignment], + ) -> Checked { + let a = self.next(body)?; + let A::VariantGet { + representation, + case, + field: index, + value, + .. + } = &a.kind + else { + return Err(MISMATCH); + }; + if (*representation, *case, *index) != (repr, tag, field) { + return Err(MISMATCH); + } + self.equal(*value, source)?; + Ok(a.destination) + } + + pub(super) fn node( + &self, + repr: ReprId, + tag: u32, + args: &[ValueId], + body: &mut &[Assignment], + ) -> Checked { + let a = self.next(body)?; + let A::VariantNew { + representation, + case, + fields, + .. + } = &a.kind + else { + return Err(MISMATCH); + }; + if (*representation, *case) != (repr, tag) { + return Err(MISMATCH); + } + self.values(fields, args)?; + Ok(a.destination) + } + + pub(super) fn product( + &self, + repr: ReprId, + args: &[ValueId], + body: &mut &[Assignment], + ) -> Checked { + let a = self.next(body)?; + let A::ProductNew { + representation, + arguments, + .. + } = &a.kind + else { + return Err(MISMATCH); + }; + if *representation != repr { + return Err(MISMATCH); + } + self.values(arguments, args)?; + Ok(a.destination) + } + + pub(super) fn product_field( + &self, + source: ValueId, + repr: ReprId, + index: u32, + body: &mut &[Assignment], + ) -> Checked { + let a = self.next(body)?; + let A::ProductGet { + representation, + field, + value, + .. + } = &a.kind + else { + return Err(MISMATCH); + }; + if (*representation, *field) != (repr, index) { + return Err(MISMATCH); + } + self.equal(*value, source)?; + Ok(a.destination) + } + + pub(super) fn call( + &self, + role: ControlHelperRole, + args: &[ValueId], + body: &mut &[Assignment], + ) -> Checked { + let helper = self.table.control_helper(role).ok_or(MISMATCH)?; + let a = self.next(body)?; + let A::DirectCall { + function, + arguments, + contract, + } = &a.kind + else { + return Err(MISMATCH); + }; + if *function != helper.function() + || contract.and_then(|id| self.table.control_helpers().get(id.0 as usize)) + != Some(helper) + { + return Err(MISMATCH); + } + self.values(arguments, args)?; + Ok(a.destination) + } + + pub(super) fn constant(&self, expected: i32, body: &mut &[Assignment]) -> Checked { + let a = self.next(body)?; + if !matches!(a.kind, A::Constant(n) if n == expected) { + return Err(MISMATCH); + } + Ok(a.destination) + } + + pub(super) fn new_cell( + &self, + repr: ReprId, + value: ValueId, + body: &mut &[Assignment], + ) -> Checked { + let a = self.next(body)?; + let A::ArrayNew { + representation, + elements, + .. + } = &a.kind + else { + return Err(MISMATCH); + }; + if *representation != repr { + return Err(MISMATCH); + } + self.values(elements, &[value])?; + Ok(a.destination) + } + + pub(super) fn read( + &self, + repr: ReprId, + cell: ValueId, + body: &mut &[Assignment], + ) -> Checked { + let zero = self.constant(0, body)?; + let a = self.next(body)?; + let A::ArrayGet { + representation, + value, + index, + .. + } = &a.kind + else { + return Err(MISMATCH); + }; + if *representation != repr { + return Err(MISMATCH); + } + self.equal(*value, cell)?; + self.equal(*index, zero)?; + Ok(a.destination) + } + + pub(super) fn write( + &self, + repr: ReprId, + cell: ValueId, + expected: ValueId, + body: &mut &[Assignment], + ) -> Checked<()> { + let zero = self.constant(0, body)?; + let a = self.next(body)?; + let A::ArraySet { + representation, + value, + index, + new_value, + .. + } = &a.kind + else { + return Err(MISMATCH); + }; + if *representation != repr { + return Err(MISMATCH); + } + self.equal(*value, cell)?; + self.equal(*index, zero)?; + self.equal(*new_value, expected) + } + + pub(super) fn new_status(&self, body: &mut &[Assignment]) -> Checked { + let zero = self.constant(0, body)?; + self.new_cell(self.schema.status, zero, body) + } + + pub(super) fn guard(&self, cell: ValueId, body: &mut &[Assignment]) -> Checked<()> { + let status = self.read(self.schema.status, cell, body)?; + let a = self.next(body)?; + let A::TagSwitch { + value, + cases, + default_assignments, + default_value, + } = &a.kind + else { + return Err(MISMATCH); + }; + self.equal(*value, status)?; + if cases.len() != 1 || cases[0].tag != 0 { + return Err(MISMATCH); + } + self.end(&mut cases[0].assignments.as_slice())?; + self.equal(cases[0].value, cell)?; + self.reject(default_assignments, *default_value) + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/check/mod.rs b/crates/psrs-backend/src/cc/lower/control/plan/check/mod.rs new file mode 100644 index 00000000..55b96296 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/check/mod.rs @@ -0,0 +1,100 @@ +//! Check all named actions and typed, lexically scoped construction operands. +mod operations; +mod scope; +#[cfg(test)] +mod tests; + +use super::ir::{Case, Exec, Operand, state_index}; +use crate::control::execution::{ActionId, Outcome, Program, Selection}; +use crate::control::schema::{FieldKind, OwnerCase}; +use scope::{Info, Scope}; +use std::collections::HashSet; + +type Checked = Result; + +#[cfg(test)] +pub(crate) fn check(exec: &Exec) -> Checked<()> { + check_for(Program::Executor, exec) +} + +pub(crate) fn check_for(program: Program, exec: &Exec) -> Checked<()> { + let mut reached = HashSet::new(); + walk(exec, &mut Vec::new(), program, &mut reached)?; + for action in ActionId::ALL { + if action.selection().0 == program && !reached.contains(&action) { + return Err("checked machine plan is missing a required action"); + } + } + let scope = Scope::new(program); + scope.exec(exec) +} + +fn selection(case: Case, tag: u32) -> Checked { + use crate::control::schema::{ + ComputationCase as C, ContextCase as X, ExtentCase as E, SequenceCase as Q, + }; + match case { + Case::Computation(_) => C::ALL + .into_iter() + .find(|case| case.tag() == tag) + .map(Selection::Computation), + Case::Sequence(_) => Q::ALL + .into_iter() + .find(|case| case.tag() == tag) + .map(Selection::Sequence), + Case::Context(_) => X::ALL + .into_iter() + .find(|case| case.tag() == tag) + .map(Selection::Context), + Case::Extent(_) => E::ALL + .into_iter() + .find(|case| case.tag() == tag) + .map(Selection::Extent), + Case::Owner(_) => None, + } + .ok_or("machine plan dispatch invents an action selection") +} + +fn walk( + exec: &Exec, + path: &mut Vec, + program: Program, + reached: &mut HashSet, +) -> Checked<()> { + match exec { + Exec::Steps { next, .. } => return walk(next, path, program, reached), + Exec::Switch { case, arms, .. } => { + let mut tags = arms.iter().map(|(tag, _)| *tag).collect::>(); + tags.sort_unstable(); + if tags != case.tags() { + return Err("machine plan dispatch must cover every schema case exactly once"); + } + for (tag, arm) in arms { + path.push(selection(*case, *tag)?); + walk(arm, path, program, reached)?; + path.pop(); + } + return Ok(()); + } + _ => {} + } + let action = ActionId::ALL + .into_iter() + .find(|action| action.selection() == (program, path.as_slice())) + .ok_or("machine plan has an undeclared action path")?; + let outcome = match exec { + Exec::Advance { .. } => Outcome::Advance, + Exec::Publish { .. } => Outcome::Publish, + Exec::Return { .. } => Outcome::Return, + Exec::Tail { role, .. } => Outcome::Tail(*role), + Exec::Reject { .. } => Outcome::Reject, + _ => unreachable!(), + }; + if action.outcome() != outcome { + return Err("machine plan action has the wrong terminal outcome"); + } + if !reached.insert(action) { + return Err("machine plan repeats an action"); + } + Ok(()) +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/check/operations.rs b/crates/psrs-backend/src/cc/lower/control/plan/check/operations.rs new file mode 100644 index 00000000..69d3e14c --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/check/operations.rs @@ -0,0 +1,130 @@ +//! Logical type and provenance preconditions of construction operations. +use super::super::ir::{Op, Product}; +use super::*; +use crate::control::schema::{ExtentCase, SequenceCase}; + +impl Scope { + pub(super) fn op(&self, op: &Op) -> Checked { + use FieldKind as K; + let kind = match op { + Op::Project { + source, + case, + field, + } => return self.projection(source, *case, *field), + Op::Node { case, fields } => { + self.arguments(fields, case.fields())?; + return Ok(Info::node(*case)); + } + Op::Capture { + closure, + index, + case, + } => { + if self.program != Program::RawResume { + return Err("only raw resume may read machine closure captures"); + } + self.require(closure, K::Callable)?; + let expected = match *index { + 0 => K::Sequence, + 1 => K::Extent, + _ => return Err("raw resume capture index is outside its protocol"), + }; + if case.kind() != expected { + return Err("raw resume capture has the wrong carrier role"); + } + expected + } + Op::Apply { callee, args } => { + self.require(callee, K::Callable)?; + if args.is_empty() { + return Err("machine callback application requires an argument"); + } + for arg in args { + self.info(arg)?; + } + K::Payload + } + Op::Helper { role, args } => { + self.arguments(args, role.parameters())?; + role.result() + } + Op::ResumeClosure { captures } => { + self.arguments(captures, &[K::Sequence, K::Extent])?; + K::Callable + } + Op::Pair { state, value } => { + self.info(state)?; + self.info(value)?; + K::ResultPair + } + Op::PairField { pair, field } => { + self.require(pair, K::ResultPair)?; + if *field > 1 { + return Err("machine result pair field is out of range"); + } + K::Payload + } + Op::ReplayField { result, field } => { + self.require(result, K::ReplayResult)?; + match *field { + 0 => K::Sequence, + 1 => K::Extent, + _ => return Err("machine replay result field is out of range"), + } + } + Op::FreshExtent { parent } => { + if let Some(parent) = parent { + self.require(parent, K::Extent)?; + } + return Ok(Info::node(Case::Extent(if parent.is_some() { + ExtentCase::Scoped + } else { + ExtentCase::Root + }))); + } + Op::ContextExtent { context } => { + self.require(context, K::Context)?; + K::Extent + } + Op::CheckBoundary { extent } | Op::CloseBoundary { extent } => { + self.require(extent, K::Extent)?; + K::Extent + } + Op::GuardPending { cell } => { + self.require(cell, K::Status)?; + K::Status + } + Op::SetStatus { cell, value } => { + self.require(cell, K::Status)?; + if !(0..=3).contains(value) { + return Err("machine status write is outside the lifecycle domain"); + } + K::Status + } + Op::ReadCallback { cell } => { + self.require(cell, K::Callback)?; + K::Payload + } + Op::WriteCallback { cell, value } => { + self.require(cell, K::Callback)?; + self.info(value)?; + K::Callback + } + Op::NewCallbackCell { value } => { + self.require(value, K::Callable)?; + K::Callback + } + Op::NewStatus => K::Status, + Op::Product { + product: Product::ReplayResult, + fields, + } => { + self.arguments(fields, &[K::Sequence, K::Extent])?; + K::ReplayResult + } + Op::EmptySequence => return Ok(Info::node(Case::Sequence(SequenceCase::Empty))), + }; + Ok(Info::new(kind)) + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/check/scope.rs b/crates/psrs-backend/src/cc/lower/control/plan/check/scope.rs new file mode 100644 index 00000000..1aaf9488 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/check/scope.rs @@ -0,0 +1,164 @@ +//! Scope and logical storage roles are checked before allocating CC values. +use super::super::ir::Family; +use super::*; +use std::collections::HashMap; + +#[derive(Clone, Copy)] +pub(super) struct Info { + pub kind: FieldKind, + pub tag: Option, +} + +impl Info { + pub(super) fn new(kind: FieldKind) -> Self { + Self { kind, tag: None } + } + pub(super) fn node(case: Case) -> Self { + Self { + kind: case.kind(), + tag: Some(case.tag()), + } + } +} + +#[derive(Clone)] +pub(super) struct Scope { + pub program: Program, + pub temps: Vec, + pub refinements: HashMap, +} + +impl Scope { + pub(super) fn new(program: Program) -> Self { + Self { + program, + temps: Vec::new(), + refinements: HashMap::new(), + } + } + + fn key(&self, operand: &Operand) -> Checked { + Ok(match operand { + Operand::State(state) => { + if self.program != Program::Executor { + return Err("helper plan cannot read executor state"); + } + Operand::Param(state_index(*state)) + } + _ => operand.clone(), + }) + } + + pub(super) fn info(&self, operand: &Operand) -> Checked { + let key = self.key(operand)?; + let mut info = match key { + Operand::Param(index) => self.program.parameters().get(index).copied().map(Info::new), + Operand::Temp(index) => self.temps.get(index).copied(), + Operand::State(_) => unreachable!(), + } + .ok_or("machine plan operand is outside its lexical scope")?; + if let Some((_, tag)) = self.refinements.get(&key) { + info.tag = Some(*tag); + } + Ok(info) + } + + pub(super) fn require(&self, operand: &Operand, kind: FieldKind) -> Checked<()> { + let actual = self.info(operand)?.kind; + // Payload erasure accepts references. Callback results may recover a + // computation, callable or result pair at their checked protocol use. + if actual == kind + || kind == FieldKind::Payload + || (actual == FieldKind::Payload + && matches!( + kind, + FieldKind::Computation | FieldKind::Callable | FieldKind::ResultPair + )) + { + Ok(()) + } else { + Err("machine plan operand has the wrong logical storage role") + } + } + + pub(super) fn arguments(&self, args: &[Operand], expected: &[FieldKind]) -> Checked<()> { + if args.len() != expected.len() { + return Err("machine plan call or allocation has the wrong arity"); + } + for (arg, kind) in args.iter().zip(expected) { + self.require(arg, *kind)?; + } + Ok(()) + } + + pub(super) fn projection(&self, source: &Operand, case: Case, field: u32) -> Checked { + self.require(source, case.kind())?; + let info = self.info(source)?; + let singleton = case == Case::Owner(OwnerCase::OneShot); + if !singleton && info.tag != Some(case.tag()) { + return Err("machine plan projection lacks its own case refinement"); + } + let kind = *case + .fields() + .get(field as usize) + .ok_or("machine plan projection names an invalid field")?; + Ok(Info::new(kind)) + } + + pub(super) fn exec(mut self, exec: &Exec) -> Checked<()> { + match exec { + Exec::Steps { ops, next } => { + for op in ops { + let info = self.op(op)?; + self.temps.push(info); + } + self.exec(next) + } + Exec::Switch { source, case, arms } => { + self.require(source, case.kind())?; + let key = self.key(source)?; + for (tag, arm) in arms { + let mut branch = self.clone(); + branch + .refinements + .insert(key.clone(), (case.family(), *tag)); + branch.exec(arm)?; + } + Ok(()) + } + Exec::Advance { + state, + handler, + computation, + sequence, + extent, + context, + } => { + if self.program != Program::Executor { + return Err("only the executor may advance machine state"); + } + self.arguments( + &[ + state.clone(), + handler.clone(), + computation.clone(), + sequence.clone(), + extent.clone(), + context.clone(), + ], + crate::cc::ControlHelperRole::Executor.parameters(), + ) + } + Exec::Tail { role, args } => { + self.arguments(args, role.parameters())?; + if role.result() != self.program.result() { + return Err("machine plan tail call has the wrong result role"); + } + Ok(()) + } + Exec::Publish { result } => self.require(result, FieldKind::ResultPair), + Exec::Return { value } => self.require(value, self.program.result()), + Exec::Reject { .. } => Ok(()), + } + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/check/tests.rs b/crates/psrs-backend/src/cc/lower/control/plan/check/tests.rs new file mode 100644 index 00000000..b741cd36 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/check/tests.rs @@ -0,0 +1,246 @@ +use super::super::ir::{Op, State}; +use super::*; +use crate::control::schema::{ComputationCase as C, ContextCase as X, SequenceCase as Q}; + +fn build(program: Program) -> Exec { + match program { + Program::Executor => super::super::executor(), + Program::Run => super::super::run::program(), + Program::CloneSequence => super::super::clone_sequence(), + Program::RebuildSequence => super::super::rebuild_sequence(), + Program::Check => super::super::boundary_check(), + Program::Close => super::super::boundary_close(), + Program::Start => super::super::start_boundary(), + Program::Once => super::super::once(), + Program::Cancel => super::super::cancel(), + Program::RawResume => super::super::raw_resume(), + } +} + +fn edit(plan: &mut Exec, change: &mut impl FnMut(&mut Op) -> bool) -> bool { + match plan { + Exec::Steps { ops, next } => ops.iter_mut().any(&mut *change) || edit(next, change), + Exec::Switch { arms, .. } => arms.iter_mut().any(|(_, arm)| edit(arm, change)), + _ => false, + } +} + +fn reject_op(program: Program, message: &str, change: impl FnMut(&mut Op) -> bool) { + let mut plan = build(program); + assert!(edit(&mut plan, &mut { change })); + let error = check_for(program, &plan).unwrap_err(); + assert!(error.contains(message), "{error}"); +} + +fn drop_path(exec: &mut Exec, path: &[Selection]) { + match exec { + Exec::Steps { next, .. } => drop_path(next, path), + Exec::Switch { case, arms, .. } => { + let index = arms + .iter() + .position(|(tag, _)| selection(*case, *tag).unwrap() == path[0]) + .unwrap(); + if path.len() == 1 { + arms.remove(index); + } else { + drop_path(&mut arms[index].1, &path[1..]); + } + } + _ => { + assert!(path.is_empty()); + *exec = Exec::Tail { + role: crate::cc::ControlHelperRole::Close, + args: Vec::new(), + }; + } + } +} + +#[test] +fn every_required_action_is_checked_including_helpers_and_rejection_arms() { + let mut unique = HashSet::new(); + for action in ActionId::ALL { + let (program, path) = action.selection(); + assert!( + unique.insert((program, path)), + "duplicate registry path for {action:?}" + ); + let mut plan = build(program); + check_for(program, &plan).unwrap(); + drop_path(&mut plan, path); + assert!( + check_for(program, &plan).is_err(), + "missing {action:?} was accepted" + ); + } + for (program, expected) in [ + (Program::Executor, 20), + (Program::Run, 1), + (Program::CloneSequence, 7), + (Program::RebuildSequence, 4), + (Program::Check, 2), + (Program::Close, 2), + (Program::Start, 1), + (Program::Once, 1), + (Program::Cancel, 1), + (Program::RawResume, 1), + ] { + assert_eq!( + ActionId::ALL + .into_iter() + .filter(|a| a.selection().0 == program) + .count(), + expected + ); + } +} + +#[test] +fn scoped_actions_cannot_disappear_behind_complete_root_actions() { + let mut plan = build(Program::Executor); + drop_path( + &mut plan, + &[ + Selection::Computation(C::Perform), + Selection::Context(X::Scoped), + ], + ); + assert!(check(&plan).is_err()); +} + +#[test] +fn projections_require_the_selected_case_of_their_own_source() { + reject_op(Program::Executor, "case refinement", |op| { + if let Op::Project { + source: Operand::State(State::Sequence), + case, + .. + } = op + && *case == Case::Sequence(Q::Cons) + { + *case = Case::Sequence(Q::Append); + return true; + } + false + }); +} + +#[test] +fn forward_and_out_of_range_operands_are_rejected_before_expansion() { + for source in [Operand::Temp(0), Operand::Temp(999), Operand::Param(6)] { + reject_op(Program::Executor, "lexical scope", |op| { + if let Op::Project { source: actual, .. } = op { + *actual = source.clone(); + return true; + } + false + }); + } +} + +#[test] +fn branch_local_temporaries_cannot_escape_to_siblings() { + let mut plan = build(Program::CloneSequence); + let Exec::Switch { arms, .. } = &mut plan else { + unreachable!() + }; + let Exec::Switch { arms, .. } = &mut arms[0].1 else { + unreachable!() + }; + let Exec::Tail { args, .. } = &mut arms[0].1 else { + unreachable!() + }; + args[0] = Operand::Temp(0); + assert_eq!( + check_for(Program::CloneSequence, &plan), + Err("machine plan operand is outside its lexical scope") + ); +} + +#[test] +fn allocation_and_helper_parameters_keep_their_logical_roles() { + reject_op(Program::Executor, "logical storage role", |op| { + if let Op::Node { + case: Case::Sequence(Q::Cons), + fields, + } = op + { + fields[1] = Operand::State(State::Extent); + return true; + } + false + }); + reject_op(Program::Executor, "wrong arity", |op| { + if let Op::Helper { args, .. } = op { + args.pop(); + return true; + } + false + }); +} + +#[test] +fn helper_cannot_inherit_executor_state() { + reject_op(Program::Once, "executor state", |op| { + if let Op::NewCallbackCell { value } = op { + *value = Operand::State(State::Handler); + return true; + } + false + }); +} + +#[test] +fn capture_slots_and_product_indices_have_checked_domains() { + reject_op(Program::RawResume, "capture index", |op| { + if let Op::Capture { index, .. } = op { + *index = 2; + return true; + } + false + }); + reject_op(Program::Executor, "pair field", |op| { + if let Op::PairField { field, .. } = op { + *field = 2; + return true; + } + false + }); + reject_op(Program::Executor, "replay result field", |op| { + if let Op::ReplayField { field, .. } = op { + *field = 2; + return true; + } + false + }); +} + +#[test] +fn lifecycle_status_values_have_a_checked_domain() { + reject_op(Program::Cancel, "lifecycle domain", |op| { + if let Op::SetStatus { value, .. } = op { + *value = 4; + return true; + } + false + }); +} + +#[test] +fn equal_signature_tail_calls_keep_their_semantic_role() { + let mut plan = build(Program::Check); + let Exec::Switch { arms, .. } = &mut plan else { + unreachable!() + }; + let Exec::Steps { next, .. } = &mut arms[1].1 else { + unreachable!() + }; + let Exec::Tail { role, .. } = next.as_mut() else { + unreachable!() + }; + *role = crate::cc::ControlHelperRole::Close; + assert_eq!( + check_for(Program::Check, &plan), + Err("machine plan action has the wrong terminal outcome") + ); +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/driver.rs b/crates/psrs-backend/src/cc/lower/control/plan/driver.rs new file mode 100644 index 00000000..fedb799d --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/driver.rs @@ -0,0 +1,237 @@ +//! Perform, Bind and raw Resume transitions. + +use super::ir::*; +use super::returns::enter_target; +use crate::cc::ControlHelperRole; +use crate::control::schema::{ + ComputationCase, ContextCase, SequenceCase, computation as c, context as x, replay, result_pair, +}; + +/// Perform applies the current dispatch clause with a fresh raw resumption. +pub(super) fn perform(cursor: &mut Cursor) -> Exec { + let mut ops = Vec::new(); + let request = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Computation), + case: Case::Computation(ComputationCase::Perform), + field: c::PAYLOAD, + }, + ); + let arms = ContextCase::ALL + .iter() + .map(|case| { + let mut cursor = cursor.clone(); + let arm = match case { + ContextCase::Root => perform_root(&mut cursor, request.clone()), + ContextCase::Scoped => perform_scoped(&mut cursor, request.clone()), + }; + (case.tag(), arm) + }) + .collect(); + steps( + ops, + switch( + state(State::Context), + Case::Context(ContextCase::Root), + arms, + ), + ) +} + +pub(super) fn perform_root(cursor: &mut Cursor, request: Operand) -> Exec { + let mut ops = Vec::new(); + let extent = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Context), + case: Case::Context(ContextCase::Root), + field: x::ROOT_EXTENT, + }, + ); + let continuation = cursor.bind( + &mut ops, + Op::ResumeClosure { + captures: vec![state(State::Sequence), extent], + }, + ); + let pair = cursor.bind( + &mut ops, + Op::Apply { + callee: state(State::Handler), + args: vec![state(State::Value), request, continuation], + }, + ); + let next_state = cursor.bind( + &mut ops, + Op::PairField { + pair: pair.clone(), + field: result_pair::STATE, + }, + ); + let next = cursor.bind( + &mut ops, + Op::PairField { + pair, + field: result_pair::VALUE, + }, + ); + let empty = cursor.bind(&mut ops, Op::EmptySequence); + steps( + ops, + advance_all( + next_state, + state(State::Handler), + next, + empty, + state(State::Extent), + state(State::Context), + ), + ) +} + +pub(super) fn perform_scoped(cursor: &mut Cursor, request: Operand) -> Exec { + let mut ops = Vec::new(); + let extent = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Context), + case: Case::Context(ContextCase::Scoped), + field: x::SCOPED_EXTENT, + }, + ); + let continuation = cursor.bind( + &mut ops, + Op::ResumeClosure { + captures: vec![state(State::Sequence), extent], + }, + ); + let clause = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Context), + case: Case::Context(ContextCase::Scoped), + field: x::SCOPED_OPERATION, + }, + ); + let target = cursor.bind( + &mut ops, + Op::Apply { + callee: clause, + args: vec![request, continuation], + }, + ); + steps(ops, enter_target(cursor, target)) +} + +pub(super) fn bind(cursor: &mut Cursor) -> Exec { + let mut ops = Vec::new(); + let inner = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Computation), + case: Case::Computation(ComputationCase::Bind), + field: c::BIND_INNER, + }, + ); + let continuation = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Computation), + case: Case::Computation(ComputationCase::Bind), + field: c::BIND_CONTINUATION, + }, + ); + let pending = cursor.bind( + &mut ops, + Op::Node { + case: Case::Sequence(SequenceCase::Cons), + fields: vec![continuation, state(State::Sequence)], + }, + ); + steps(ops, advance_here(inner, pending)) +} + +/// Raw resume checks its checkpoint and replays the saved sequence before the +/// consumer queue. +pub(super) fn resume(cursor: &mut Cursor) -> Exec { + let mut ops = Vec::new(); + let value = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Computation), + case: Case::Computation(ComputationCase::Resume), + field: c::RESUME_VALUE, + }, + ); + let saved = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Computation), + case: Case::Computation(ComputationCase::Resume), + field: c::RESUME_SAVED, + }, + ); + let checkpoint = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Computation), + case: Case::Computation(ComputationCase::Resume), + field: c::RESUME_CHECKPOINT, + }, + ); + cursor.bind(&mut ops, Op::CheckBoundary { extent: checkpoint }); + let empty = cursor.bind(&mut ops, Op::EmptySequence); + let parent = cursor.bind( + &mut ops, + Op::ContextExtent { + context: state(State::Context), + }, + ); + let cloned = cursor.bind( + &mut ops, + Op::Helper { + role: ControlHelperRole::CloneSequence, + args: vec![saved, empty.clone(), empty, state(State::Extent), parent], + }, + ); + let replay_queue = cursor.bind( + &mut ops, + Op::ReplayField { + result: cloned.clone(), + field: replay::QUEUE, + }, + ); + let active = cursor.bind( + &mut ops, + Op::ReplayField { + result: cloned, + field: replay::ACTIVE, + }, + ); + let pending = cursor.bind( + &mut ops, + Op::Node { + case: Case::Sequence(SequenceCase::Append), + fields: vec![replay_queue, state(State::Sequence)], + }, + ); + let next = cursor.bind( + &mut ops, + Op::Node { + case: Case::Computation(ComputationCase::Return), + fields: vec![value], + }, + ); + steps( + ops, + advance_all( + state(State::Value), + state(State::Handler), + next, + pending, + active, + state(State::Context), + ), + ) +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/expand.rs b/crates/psrs-backend/src/cc/lower/control/plan/expand.rs new file mode 100644 index 00000000..314ea837 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/expand.rs @@ -0,0 +1,259 @@ +//! Expansion of the checked plan into ordinary CC. + +use super::super::builder::{Machine, repr}; +use super::ir::{Exec, Op, Operand, Product, Shape, state_index}; +use crate::cc::payload::erased_shape; +use crate::cc::{Assignment, AssignmentKind, ControlHelperRole, ValueId}; +use crate::control::schema::SequenceCase; + +impl Machine<'_> { + /// Emit ordinary CC from the checked plan. `params` are the enclosing + /// function's parameters; `Operand::State` indexes the executor's six named + /// slots and `Operand::Param` a helper's explicit parameters. + pub(in crate::cc::lower::control) fn expand( + &mut self, + plan: &super::CheckedPlan, + params: &[ValueId], + body: &mut Vec, + ) -> ValueId { + self.expand_exec(plan.exec(), &mut Vec::new(), params, body) + } + + fn expand_exec( + &mut self, + exec: &Exec, + env: &mut Vec, + params: &[ValueId], + body: &mut Vec, + ) -> ValueId { + match exec { + Exec::Steps { ops, next } => { + let base = env.len(); + for op in ops { + let value = self.expand_op(op, env, params, body); + env.push(value); + } + let result = self.expand_exec(next, env, params, body); + env.truncate(base); + result + } + Exec::Switch { source, case, arms } => { + let value = self.operand(source, env, params); + let value = self.cast(value, repr(case.repr(self.layout)), body); + let mut cases = Vec::new(); + for (tag, arm) in arms { + let mut arm_body = Vec::new(); + let mut arm_env = env.clone(); + let result = self.expand_exec(arm, &mut arm_env, params, &mut arm_body); + cases.push(super::super::executor::arm(*tag, arm_body, result)); + } + self.switch(value, case.repr(self.layout), cases, body) + } + Exec::Advance { + state, + handler, + computation, + sequence, + extent, + context, + } => { + let state = self.operand(state, env, params); + let handler = self.operand(handler, env, params); + let computation = self.operand(computation, env, params); + let sequence = self.operand(sequence, env, params); + let extent = self.operand(extent, env, params); + let context = self.operand(context, env, params); + let function = self.functions.executor; + self.call( + function, + vec![state, handler, computation, sequence, extent, context], + body, + ) + } + Exec::Publish { result } | Exec::Return { value: result } => { + self.operand(result, env, params) + } + Exec::Tail { role, args } => { + let function = self.function_for(*role); + let args = args + .iter() + .map(|operand| self.operand(operand, env, params)) + .collect(); + self.call(function, args, body) + } + Exec::Reject { shape } => { + let shape = self.planned_shape(*shape); + self.emit(shape, body, |_| AssignmentKind::Unreachable) + } + } + } + + fn expand_op( + &mut self, + op: &Op, + env: &[ValueId], + params: &[ValueId], + body: &mut Vec, + ) -> ValueId { + match op { + Op::Project { + source, + case, + field, + } => { + let source = self.operand(source, env, params); + self.field(source, case.repr(self.layout), case.tag(), *field, body) + } + Op::Node { case, fields } => { + let fields = fields + .iter() + .map(|operand| self.operand(operand, env, params)) + .collect(); + self.node(case.repr(self.layout), case.tag(), fields, body) + } + Op::Capture { + closure, + index, + case, + } => { + let closure = self.operand(closure, env, params); + let shape = repr(case.repr(self.layout)); + self.capture(closure, *index, shape, body) + } + Op::Apply { callee, args } => { + let callee = self.operand(callee, env, params); + let args = args + .iter() + .map(|operand| self.operand(operand, env, params)) + .collect::>(); + self.apply(callee, &args, body) + } + Op::Helper { role, args } => { + let function = self.function_for(*role); + let args = args + .iter() + .map(|operand| self.operand(operand, env, params)) + .collect(); + self.call(function, args, body) + } + Op::ResumeClosure { captures } => { + let captures = captures + .iter() + .map(|operand| self.operand(operand, env, params)) + .collect(); + let function = self.functions.resume; + let signature = self.layout.continuation; + self.closure(function, signature, captures, body) + } + Op::Pair { state, value } => { + let state = self.operand(state, env, params); + let value = self.operand(value, env, params); + self.pair(state, value, body) + } + Op::PairField { pair, field } => { + let pair = self.operand(pair, env, params); + self.pair_field(pair, *field, body) + } + Op::ReplayField { result, field } => { + let result = self.operand(result, env, params); + self.replay_field(result, *field, body) + } + Op::FreshExtent { parent } => { + let parent = parent + .as_ref() + .map(|operand| self.operand(operand, env, params)); + self.new_boundary(parent, body) + } + Op::ContextExtent { context } => { + let context = self.operand(context, env, params); + self.context_extent(context, body) + } + Op::CheckBoundary { extent } => { + let extent = self.operand(extent, env, params); + self.check_boundary(extent, body) + } + Op::CloseBoundary { extent } => { + let extent = self.operand(extent, env, params); + self.close_boundary(extent, body) + } + Op::GuardPending { cell } => { + let cell = self.operand(cell, env, params); + self.guard_pending(cell, body); + cell + } + Op::SetStatus { cell, value } => { + let cell = self.operand(cell, env, params); + self.set_status(cell, *value, body); + cell + } + Op::ReadCallback { cell } => { + let cell = self.operand(cell, env, params); + self.read_cell(self.layout.callback, erased_shape(), cell, body) + } + Op::WriteCallback { cell, value } => { + let cell = self.operand(cell, env, params); + let value = self.operand(value, env, params); + self.write_cell(self.layout.callback, cell, value, body); + cell + } + Op::NewCallbackCell { value } => { + let value = self.operand(value, env, params); + let value = self.cast(value, erased_shape(), body); + self.new_cell(self.layout.callback, value, body) + } + Op::NewStatus => self.new_status(body), + Op::Product { product, fields } => { + let representation = self.planned_product(*product); + let fields = fields + .iter() + .map(|operand| self.operand(operand, env, params)) + .collect(); + self.emit(repr(representation), body, |destination| { + AssignmentKind::ProductNew { + destination, + representation, + arguments: fields, + } + }) + } + Op::EmptySequence => { + let representation = self.layout.sequence; + self.node(representation, SequenceCase::Empty.tag(), Vec::new(), body) + } + } + } + + fn operand(&self, operand: &Operand, env: &[ValueId], params: &[ValueId]) -> ValueId { + match operand { + Operand::State(slot) => params[state_index(*slot)], + Operand::Param(index) => params[*index], + Operand::Temp(index) => env[*index], + } + } + + fn planned_shape(&self, shape: Shape) -> crate::cc::ValueShape { + match shape { + Shape::ReplayResult => repr(self.layout.replay_result), + } + } + + fn planned_product(&self, product: Product) -> crate::cc::ReprId { + match product { + Product::ReplayResult => self.layout.replay_result, + } + } + + fn function_for(&self, role: ControlHelperRole) -> psrs_hir::SymbolId { + match role { + ControlHelperRole::Executor => self.functions.executor, + ControlHelperRole::Run => self.functions.run, + ControlHelperRole::CloneSequence => self.functions.clone_sequence, + ControlHelperRole::RebuildSequence => self.functions.rebuild_sequence, + ControlHelperRole::Start => self.functions.start, + ControlHelperRole::Check => self.functions.check, + ControlHelperRole::Close => self.functions.close, + ControlHelperRole::Once => self.functions.once, + ControlHelperRole::Cancel => self.functions.cancel, + } + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/handle.rs b/crates/psrs-backend/src/cc/lower/control/plan/handle.rs new file mode 100644 index 00000000..e66cc65a --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/handle.rs @@ -0,0 +1,338 @@ +//! Handle and owned-transfer transitions. + +use super::ir::*; +use crate::control::schema::{ + ComputationCase, ContextCase, OwnerCase, SequenceCase, computation as c, owner as o, + sequence as q, +}; + +/// Handle normalizes a leading Append, then enters the source under a fresh or +/// shared extent. +pub(super) fn handle(cursor: &mut Cursor) -> Exec { + let mut ops = Vec::new(); + let ret = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Computation), + case: Case::Computation(ComputationCase::Handle), + field: c::HANDLE_RETURN, + }, + ); + let op = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Computation), + case: Case::Computation(ComputationCase::Handle), + field: c::HANDLE_OPERATION, + }, + ); + let inner = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Computation), + case: Case::Computation(ComputationCase::Handle), + field: c::HANDLE_SOURCE, + }, + ); + let arms = SequenceCase::ALL + .iter() + .map(|case| { + let mut cursor = cursor.clone(); + let arm = match case { + SequenceCase::Empty | SequenceCase::Cons => { + handle_enter_fresh(&mut cursor, ret.clone(), op.clone(), inner.clone()) + } + SequenceCase::Finish => { + handle_enter_finish(&mut cursor, ret.clone(), op.clone(), inner.clone()) + } + SequenceCase::Append => handle_normalize(&mut cursor), + }; + (case.tag(), arm) + }) + .collect(); + steps( + ops, + switch( + state(State::Sequence), + Case::Sequence(SequenceCase::Empty), + arms, + ), + ) +} + +pub(super) fn handle_enter_fresh( + cursor: &mut Cursor, + ret: Operand, + op: Operand, + inner: Operand, +) -> Exec { + let mut ops = Vec::new(); + let extent = cursor.bind( + &mut ops, + Op::FreshExtent { + parent: Some(state(State::Extent)), + }, + ); + let context = cursor.bind( + &mut ops, + Op::Node { + case: Case::Context(ContextCase::Scoped), + fields: vec![ + ret, + op, + state(State::Sequence), + state(State::Extent), + state(State::Context), + extent.clone(), + ], + }, + ); + let empty = cursor.bind(&mut ops, Op::EmptySequence); + steps( + ops, + advance_all( + state(State::Value), + state(State::Handler), + inner, + empty, + extent, + context, + ), + ) +} + +pub(super) fn handle_enter_finish( + cursor: &mut Cursor, + ret: Operand, + op: Operand, + inner: Operand, +) -> Exec { + let mut ops = Vec::new(); + let extent = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Sequence), + case: Case::Sequence(SequenceCase::Finish), + field: q::FINISH_EXTENT, + }, + ); + let saved = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Sequence), + case: Case::Sequence(SequenceCase::Finish), + field: q::FINISH_REST, + }, + ); + let parent = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Sequence), + case: Case::Sequence(SequenceCase::Finish), + field: q::FINISH_PARENT, + }, + ); + let context = cursor.bind( + &mut ops, + Op::Node { + case: Case::Context(ContextCase::Scoped), + fields: vec![ + ret, + op, + saved, + parent, + state(State::Context), + extent.clone(), + ], + }, + ); + let empty = cursor.bind(&mut ops, Op::EmptySequence); + steps( + ops, + advance_all( + state(State::Value), + state(State::Handler), + inner, + empty, + extent, + context, + ), + ) +} + +pub(super) fn handle_normalize(cursor: &mut Cursor) -> Exec { + let mut ops = Vec::new(); + let left = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Sequence), + case: Case::Sequence(SequenceCase::Append), + field: q::APPEND_LEFT, + }, + ); + let right = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Sequence), + case: Case::Sequence(SequenceCase::Append), + field: q::APPEND_RIGHT, + }, + ); + let arms = SequenceCase::ALL + .iter() + .map(|case| { + let mut cursor = cursor.clone(); + let arm = match case { + SequenceCase::Empty => advance_here(state(State::Computation), right.clone()), + SequenceCase::Cons | SequenceCase::Append | SequenceCase::Finish => { + let (head_field, rest_field, parent_field) = match case { + SequenceCase::Cons => (q::CONS_HEAD, q::CONS_TAIL, None), + SequenceCase::Append => (q::APPEND_LEFT, q::APPEND_RIGHT, None), + SequenceCase::Finish => { + (q::FINISH_EXTENT, q::FINISH_REST, Some(q::FINISH_PARENT)) + } + SequenceCase::Empty => unreachable!(), + }; + let mut ops = Vec::new(); + let head = cursor.bind( + &mut ops, + Op::Project { + source: left.clone(), + case: Case::Sequence(*case), + field: head_field, + }, + ); + let rest = cursor.bind( + &mut ops, + Op::Project { + source: left.clone(), + case: Case::Sequence(*case), + field: rest_field, + }, + ); + let rest = cursor.bind( + &mut ops, + Op::Node { + case: Case::Sequence(SequenceCase::Append), + fields: vec![rest, right.clone()], + }, + ); + let mut fields = vec![head, rest]; + if let Some(parent_field) = parent_field { + let parent = cursor.bind( + &mut ops, + Op::Project { + source: left.clone(), + case: Case::Sequence(*case), + field: parent_field, + }, + ); + fields.push(parent); + } + let pending = cursor.bind( + &mut ops, + Op::Node { + case: Case::Sequence(*case), + fields, + }, + ); + steps( + ops, + advance_all( + state(State::Value), + state(State::Handler), + state(State::Computation), + pending, + state(State::Extent), + state(State::Context), + ), + ) + } + }; + (case.tag(), arm) + }) + .collect(); + steps( + ops, + switch(left, Case::Sequence(SequenceCase::Append), arms), + ) +} + +/// Owned transfer guards Pending, marks Running, loads and clears the callback, +/// applies it, then marks Consumed. +pub(super) fn owned_resume(cursor: &mut Cursor) -> Exec { + let mut ops = Vec::new(); + let owner = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Computation), + case: Case::Computation(ComputationCase::OwnedResume), + field: c::OWNED_OWNER, + }, + ); + let value = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Computation), + case: Case::Computation(ComputationCase::OwnedResume), + field: c::OWNED_VALUE, + }, + ); + let callback = cursor.bind( + &mut ops, + Op::Project { + source: owner.clone(), + case: Case::Owner(OwnerCase::OneShot), + field: o::CALLBACK, + }, + ); + let status = cursor.bind( + &mut ops, + Op::Project { + source: owner, + case: Case::Owner(OwnerCase::OneShot), + field: o::STATUS, + }, + ); + cursor.bind( + &mut ops, + Op::GuardPending { + cell: status.clone(), + }, + ); + cursor.bind( + &mut ops, + Op::SetStatus { + cell: status.clone(), + value: 1, + }, + ); + let continuation = cursor.bind( + &mut ops, + Op::ReadCallback { + cell: callback.clone(), + }, + ); + cursor.bind( + &mut ops, + Op::WriteCallback { + cell: callback, + value: status.clone(), + }, + ); + let next = cursor.bind( + &mut ops, + Op::Apply { + callee: continuation, + args: vec![value], + }, + ); + cursor.bind( + &mut ops, + Op::SetStatus { + cell: status, + value: 2, + }, + ); + steps(ops, advance_here(next, state(State::Sequence))) +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/helpers.rs b/crates/psrs-backend/src/cc/lower/control/plan/helpers.rs new file mode 100644 index 00000000..df5b2d5d --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/helpers.rs @@ -0,0 +1,429 @@ +//! Checked plans for the machine's helper bodies: replay, lifecycle and the +//! one-shot factories. Each builder names its parameters positionally; the +//! expander binds them to the function's fresh parameter values. + +use super::ir::{Case, Cursor, Exec, Op, Operand, Product, Shape, steps, switch}; +use crate::cc::ControlHelperRole; +use crate::control::schema::{ + ComputationCase, ExtentCase, OwnerCase, SequenceCase, extent as e, owner as o, sequence as q, +}; + +fn param(index: usize) -> Operand { + Operand::Param(index) +} + +fn project(source: Operand, case: Case, field: u32) -> Op { + Op::Project { + source, + case, + field, + } +} + +fn tail(role: ControlHelperRole, args: Vec) -> Exec { + Exec::Tail { role, args } +} + +/// Flatten the current queue into a reversed sequence with an explicit stack. +/// +/// Parameters: `queue`, `todo`, `reversed`, `active`, `parent`. +pub(crate) fn clone_sequence() -> Exec { + let cursor = Cursor::new(); + let arms = SequenceCase::ALL + .iter() + .map(|case| { + let mut cursor = cursor.clone(); + let arm = match case { + SequenceCase::Empty => clone_todo(&cursor), + SequenceCase::Cons => { + let mut ops = Vec::new(); + let head = cursor.bind( + &mut ops, + project(param(0), Case::Sequence(SequenceCase::Cons), q::CONS_HEAD), + ); + let rest = cursor.bind( + &mut ops, + project(param(0), Case::Sequence(SequenceCase::Cons), q::CONS_TAIL), + ); + let reversed = cursor.bind( + &mut ops, + Op::Node { + case: Case::Sequence(SequenceCase::Cons), + fields: vec![head, param(2)], + }, + ); + steps( + ops, + tail( + ControlHelperRole::CloneSequence, + vec![rest, param(1), reversed, param(3), param(4)], + ), + ) + } + SequenceCase::Append => { + let mut ops = Vec::new(); + let left = cursor.bind( + &mut ops, + project( + param(0), + Case::Sequence(SequenceCase::Append), + q::APPEND_LEFT, + ), + ); + let right = cursor.bind( + &mut ops, + project( + param(0), + Case::Sequence(SequenceCase::Append), + q::APPEND_RIGHT, + ), + ); + let todo = cursor.bind( + &mut ops, + Op::Node { + case: Case::Sequence(SequenceCase::Append), + fields: vec![right, param(1)], + }, + ); + steps( + ops, + tail( + ControlHelperRole::CloneSequence, + vec![left, todo, param(2), param(3), param(4)], + ), + ) + } + SequenceCase::Finish => { + let mut ops = Vec::new(); + let extent = cursor.bind( + &mut ops, + project( + param(0), + Case::Sequence(SequenceCase::Finish), + q::FINISH_EXTENT, + ), + ); + let rest = cursor.bind( + &mut ops, + project( + param(0), + Case::Sequence(SequenceCase::Finish), + q::FINISH_REST, + ), + ); + cursor.bind( + &mut ops, + Op::CheckBoundary { + extent: extent.clone(), + }, + ); + let reversed = cursor.bind( + &mut ops, + Op::Node { + case: Case::Sequence(SequenceCase::Finish), + fields: vec![extent, param(2), param(4)], + }, + ); + steps( + ops, + tail( + ControlHelperRole::CloneSequence, + vec![rest, param(1), reversed, param(3), param(4)], + ), + ) + } + }; + (case.tag(), arm) + }) + .collect(); + switch(param(0), Case::Sequence(SequenceCase::Empty), arms) +} + +/// The traversal stack is built only from Append nodes; any other pending node +/// is a rejection, not a silent fallthrough. +fn clone_todo(cursor: &Cursor) -> Exec { + let arms = SequenceCase::ALL + .iter() + .map(|case| { + let mut cursor = cursor.clone(); + let arm = match case { + SequenceCase::Empty => tail( + ControlHelperRole::RebuildSequence, + vec![param(2), param(1), param(3), param(4)], + ), + SequenceCase::Append => { + let mut ops = Vec::new(); + let next = cursor.bind( + &mut ops, + project( + param(1), + Case::Sequence(SequenceCase::Append), + q::APPEND_LEFT, + ), + ); + let rest = cursor.bind( + &mut ops, + project( + param(1), + Case::Sequence(SequenceCase::Append), + q::APPEND_RIGHT, + ), + ); + steps( + ops, + tail( + ControlHelperRole::CloneSequence, + vec![next, rest, param(2), param(3), param(4)], + ), + ) + } + SequenceCase::Cons | SequenceCase::Finish => Exec::Reject { + shape: Shape::ReplayResult, + }, + }; + (case.tag(), arm) + }) + .collect(); + switch(param(1), Case::Sequence(SequenceCase::Empty), arms) +} + +/// Rebuild the flattened queue, giving every Finish a fresh child extent. +/// +/// Parameters: `reversed`, `result_queue`, `active`, `parent`. +pub(crate) fn rebuild_sequence() -> Exec { + let cursor = Cursor::new(); + let arms = SequenceCase::ALL + .iter() + .map(|case| { + let mut cursor = cursor.clone(); + let arm = match case { + SequenceCase::Empty => { + let mut ops = Vec::new(); + let result = cursor.bind( + &mut ops, + Op::Product { + product: Product::ReplayResult, + fields: vec![param(1), param(2)], + }, + ); + steps(ops, Exec::Return { value: result }) + } + SequenceCase::Cons => { + let mut ops = Vec::new(); + let head = cursor.bind( + &mut ops, + project(param(0), Case::Sequence(SequenceCase::Cons), q::CONS_HEAD), + ); + let rest = cursor.bind( + &mut ops, + project(param(0), Case::Sequence(SequenceCase::Cons), q::CONS_TAIL), + ); + let next = cursor.bind( + &mut ops, + Op::Node { + case: Case::Sequence(SequenceCase::Cons), + fields: vec![head, param(1)], + }, + ); + steps( + ops, + tail( + ControlHelperRole::RebuildSequence, + vec![rest, next, param(2), param(3)], + ), + ) + } + SequenceCase::Append => Exec::Reject { + shape: Shape::ReplayResult, + }, + SequenceCase::Finish => { + let mut ops = Vec::new(); + // The saved finish's extent is inspected but replaced by a + // fresh activation extent, matching the replay contract. + let _saved = cursor.bind( + &mut ops, + project( + param(0), + Case::Sequence(SequenceCase::Finish), + q::FINISH_EXTENT, + ), + ); + let rest = cursor.bind( + &mut ops, + project( + param(0), + Case::Sequence(SequenceCase::Finish), + q::FINISH_REST, + ), + ); + let extent = cursor.bind( + &mut ops, + Op::FreshExtent { + parent: Some(param(3)), + }, + ); + let next = cursor.bind( + &mut ops, + Op::Node { + case: Case::Sequence(SequenceCase::Finish), + fields: vec![extent.clone(), param(1), param(2)], + }, + ); + steps( + ops, + tail( + ControlHelperRole::RebuildSequence, + vec![rest, next, extent.clone(), extent], + ), + ) + } + }; + (case.tag(), arm) + }) + .collect(); + switch(param(0), Case::Sequence(SequenceCase::Empty), arms) +} + +/// Guard the current extent's own status and continue with its parent. +/// +/// Parameters: `boundary`. +pub(crate) fn boundary_check() -> Exec { + let arms = ExtentCase::ALL + .iter() + .map(|case| { + let mut cursor = Cursor::new(); + let mut ops = Vec::new(); + let cell = cursor.bind(&mut ops, project(param(0), Case::Extent(*case), e::STATUS)); + cursor.bind(&mut ops, Op::GuardPending { cell }); + let arm = match case { + ExtentCase::Root => Exec::Return { value: param(0) }, + ExtentCase::Scoped => { + let parent = + cursor.bind(&mut ops, project(param(0), Case::Extent(*case), e::PARENT)); + tail(ControlHelperRole::Check, vec![parent]) + } + }; + (case.tag(), steps(ops, arm)) + }) + .collect(); + switch(param(0), Case::Extent(ExtentCase::Root), arms) +} + +/// Close the current extent's own status and return it. +/// +/// Parameters: `boundary`. +pub(crate) fn boundary_close() -> Exec { + let arms = ExtentCase::ALL + .iter() + .map(|case| { + let mut cursor = Cursor::new(); + let mut ops = Vec::new(); + let cell = cursor.bind(&mut ops, project(param(0), Case::Extent(*case), e::STATUS)); + cursor.bind(&mut ops, Op::SetStatus { cell, value: 1 }); + (case.tag(), steps(ops, Exec::Return { value: param(0) })) + }) + .collect(); + switch(param(0), Case::Extent(ExtentCase::Root), arms) +} + +/// Allocate a fresh root extent. +pub(crate) fn start_boundary() -> Exec { + let mut cursor = Cursor::new(); + let mut ops = Vec::new(); + let extent = cursor.bind(&mut ops, Op::FreshExtent { parent: None }); + steps(ops, Exec::Return { value: extent }) +} + +/// Allocate a one-shot owner object. +/// +/// Parameters: `continuation`. +pub(crate) fn once() -> Exec { + let mut cursor = Cursor::new(); + let mut ops = Vec::new(); + let callback = cursor.bind(&mut ops, Op::NewCallbackCell { value: param(0) }); + let status = cursor.bind(&mut ops, Op::NewStatus); + let owner = cursor.bind( + &mut ops, + Op::Node { + case: Case::Owner(OwnerCase::OneShot), + fields: vec![callback, status], + }, + ); + steps(ops, Exec::Return { value: owner }) +} + +/// Guard Pending, mark Cancelled, clear the callback and return the owner. +/// +/// Parameters: `owner`. +pub(crate) fn cancel() -> Exec { + let mut cursor = Cursor::new(); + let mut ops = Vec::new(); + let status = cursor.bind( + &mut ops, + project(param(0), Case::Owner(OwnerCase::OneShot), o::STATUS), + ); + cursor.bind( + &mut ops, + Op::GuardPending { + cell: status.clone(), + }, + ); + cursor.bind( + &mut ops, + Op::SetStatus { + cell: status.clone(), + value: 3, + }, + ); + let callback = cursor.bind( + &mut ops, + project(param(0), Case::Owner(OwnerCase::OneShot), o::CALLBACK), + ); + cursor.bind( + &mut ops, + Op::WriteCallback { + cell: callback, + value: status, + }, + ); + steps(ops, Exec::Return { value: param(0) }) +} + +/// The raw-resume closure adapter: read its saved queue and checkpoint +/// captures, check the checkpoint, and enter the raw `Resume` frame. +/// +/// Parameters: `receiver`, `value`. +pub(crate) fn raw_resume() -> Exec { + let mut cursor = Cursor::new(); + let mut ops = Vec::new(); + let saved = cursor.bind( + &mut ops, + Op::Capture { + closure: param(0), + index: 0, + case: Case::Sequence(SequenceCase::Empty), + }, + ); + let boundary = cursor.bind( + &mut ops, + Op::Capture { + closure: param(0), + index: 1, + case: Case::Extent(ExtentCase::Root), + }, + ); + cursor.bind( + &mut ops, + Op::CheckBoundary { + extent: boundary.clone(), + }, + ); + let result = cursor.bind( + &mut ops, + Op::Node { + case: Case::Computation(ComputationCase::Resume), + fields: vec![param(1), saved, boundary], + }, + ); + steps(ops, Exec::Return { value: result }) +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/ir.rs b/crates/psrs-backend/src/cc/lower/control/plan/ir.rs new file mode 100644 index 00000000..045c3f22 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/ir.rs @@ -0,0 +1,313 @@ +//! The plan's named machine IR and construction helpers. + +use crate::cc::layout::control::ControlLayout; +use crate::cc::{ControlHelperRole, ReprId}; +use crate::control::schema::{ComputationCase, ContextCase, ExtentCase, OwnerCase, SequenceCase}; + +/// A named executor parameter. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(crate) enum State { + Value, + Handler, + Computation, + Sequence, + Extent, + Context, +} + +pub(crate) fn state_index(state: State) -> usize { + match state { + State::Value => 0, + State::Handler => 1, + State::Computation => 2, + State::Sequence => 3, + State::Extent => 4, + State::Context => 5, + } +} + +/// A value available to a transition. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) enum Operand { + State(State), + /// A caller-supplied parameter of a helper body. + Param(usize), + /// A result bound earlier in the enclosing scope. + Temp(usize), +} + +pub(crate) fn state(slot: State) -> Operand { + Operand::State(slot) +} + +/// The machine case family a projection or allocation belongs to. +/// The machine carrier a case belongs to, without its inner case value. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum Family { + Computation, + Sequence, + Context, + Extent, + Owner, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(crate) enum Case { + Computation(ComputationCase), + Sequence(SequenceCase), + Context(ContextCase), + Extent(ExtentCase), + Owner(OwnerCase), +} + +impl Case { + pub(crate) fn fields(self) -> &'static [crate::control::schema::FieldKind] { + match self { + Self::Computation(case) => case.fields(), + Self::Sequence(case) => case.fields(), + Self::Context(case) => case.fields(), + Self::Extent(case) => case.fields(), + Self::Owner(case) => case.fields(), + } + } + + pub(crate) fn kind(self) -> crate::control::schema::FieldKind { + use crate::control::schema::FieldKind as K; + match self { + Self::Computation(_) => K::Computation, + Self::Sequence(_) => K::Sequence, + Self::Context(_) => K::Context, + Self::Extent(_) => K::Extent, + Self::Owner(_) => K::Owner, + } + } + pub(crate) fn family(self) -> Family { + match self { + Self::Computation(_) => Family::Computation, + Self::Sequence(_) => Family::Sequence, + Self::Context(_) => Family::Context, + Self::Extent(_) => Family::Extent, + Self::Owner(_) => Family::Owner, + } + } + + pub(crate) fn repr(self, layout: ControlLayout) -> ReprId { + match self { + Self::Computation(_) => layout.computation, + Self::Sequence(_) => layout.sequence, + Self::Context(_) => layout.context, + Self::Extent(_) => layout.boundary, + Self::Owner(_) => layout.owner, + } + } + + pub(crate) fn tag(self) -> u32 { + match self { + Self::Computation(case) => case.tag(), + Self::Sequence(case) => case.tag(), + Self::Context(case) => case.tag(), + Self::Extent(case) => case.tag(), + Self::Owner(case) => case.tag(), + } + } + + /// The cases a dispatch on this family must cover. + pub(crate) fn tags(self) -> Vec { + match self { + Self::Computation(_) => ComputationCase::ALL.iter().map(|c| c.tag()).collect(), + Self::Sequence(_) => SequenceCase::ALL.iter().map(|c| c.tag()).collect(), + Self::Context(_) => ContextCase::ALL.iter().map(|c| c.tag()).collect(), + Self::Extent(_) => ExtentCase::ALL.iter().map(|c| c.tag()).collect(), + Self::Owner(_) => OwnerCase::ALL.iter().map(|c| c.tag()).collect(), + } + } +} + +/// One ordered construction step. Every step binds a result so an effectful step +/// is sequenced before the outcome even when its result is unused. +#[derive(Clone, Debug)] +pub(crate) enum Op { + Project { + source: Operand, + case: Case, + field: u32, + }, + Node { + case: Case, + fields: Vec, + }, + /// Read a capture slot of a closure; `case` names the expected shape. + Capture { + closure: Operand, + index: u32, + case: Case, + }, + Apply { + callee: Operand, + args: Vec, + }, + Helper { + role: ControlHelperRole, + args: Vec, + }, + ResumeClosure { + captures: Vec, + }, + Pair { + state: Operand, + value: Operand, + }, + PairField { + pair: Operand, + field: u32, + }, + ReplayField { + result: Operand, + field: u32, + }, + FreshExtent { + parent: Option, + }, + ContextExtent { + context: Operand, + }, + CheckBoundary { + extent: Operand, + }, + CloseBoundary { + extent: Operand, + }, + GuardPending { + cell: Operand, + }, + SetStatus { + cell: Operand, + value: i32, + }, + ReadCallback { + cell: Operand, + }, + WriteCallback { + cell: Operand, + value: Operand, + }, + /// Allocate the one-element callback cell holding a reference value. + NewCallbackCell { + value: Operand, + }, + /// Allocate a fresh one-element integer status cell initialised to zero. + NewStatus, + /// Allocate a planned product of erased fields. + Product { + product: Product, + fields: Vec, + }, + EmptySequence, +} + +/// A program over the machine's named case refinements. +#[derive(Clone, Debug)] +pub(crate) enum Exec { + /// Bind each operation's result in order, then continue. + Steps { ops: Vec, next: Box }, + /// Refine one carrier by case; every schema case has exactly one arm. + Switch { + source: Operand, + case: Case, + arms: Vec<(u32, Exec)>, + }, + /// Continue the executor with a complete named state. + Advance { + state: Operand, + handler: Operand, + computation: Operand, + sequence: Operand, + extent: Operand, + context: Operand, + }, + /// Return the published result pair. + Publish { result: Operand }, + /// Return one value from a helper body. + Return { value: Operand }, + /// Call a helper as the body's final action and return its result. + Tail { + role: ControlHelperRole, + args: Vec, + }, + /// Trap with a value of the given planned shape, matching a sibling arm. + Reject { shape: Shape }, +} + +/// A planned representation the builder names without a concrete [`ReprId`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum Product { + ReplayResult, +} + +/// The shape of a rejected arm, named without a concrete representation. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum Shape { + ReplayResult, +} + +/// Assigns temporary indices in emission order. A scope's arms share the +/// cursor position they start from, matching the cloned environment the +/// expander gives each arm. +#[derive(Clone)] +pub(crate) struct Cursor { + next: usize, +} + +impl Cursor { + pub(crate) fn new() -> Self { + Self { next: 0 } + } + + pub(crate) fn bind(&mut self, ops: &mut Vec, op: Op) -> Operand { + let operand = Operand::Temp(self.next); + ops.push(op); + self.next += 1; + operand + } +} + +pub(crate) fn steps(ops: Vec, next: Exec) -> Exec { + Exec::Steps { + ops, + next: Box::new(next), + } +} + +pub(crate) fn switch(source: Operand, case: Case, arms: Vec<(u32, Exec)>) -> Exec { + Exec::Switch { source, case, arms } +} + +pub(crate) fn advance_all( + state: Operand, + handler: Operand, + computation: Operand, + sequence: Operand, + extent: Operand, + context: Operand, +) -> Exec { + Exec::Advance { + state, + handler, + computation, + sequence, + extent, + context, + } +} + +/// Advance while preserving the current extent and dispatch context. +pub(crate) fn advance_here(computation: Operand, sequence: Operand) -> Exec { + advance_all( + state(State::Value), + state(State::Handler), + computation, + sequence, + state(State::Extent), + state(State::Context), + ) +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/mod.rs b/crates/psrs-backend/src/cc/lower/control/plan/mod.rs new file mode 100644 index 00000000..31807cd3 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/mod.rs @@ -0,0 +1,190 @@ +//! A temporary checked construction plan for the flat driver. +//! +//! The plan names each transition's selected cases, its ordered projections, +//! allocations, applications, helper calls and extent/status operations, and +//! exactly one terminal outcome. It is effect-agnostic and independent of any +//! concrete operation or handler. +//! +//! The executor is *built* from this plan: [`Machine::expand`] emits ordinary CC +//! from the same structure that [`check`] independently validates. An emitter +//! defect therefore fails the structural check before the generic CC verifiers +//! see a body at all. +//! +//! The module is split by responsibility: [`ir`] names the plan structure and +//! its construction helpers, [`returns`]/[`driver`]/[`handle`] build the +//! transitions, [`check`] validates the plan and [`expand`] emits ordinary CC. + +mod boundary; +mod check; +mod driver; +mod expand; +mod handle; +mod helpers; +mod ir; +#[cfg(test)] +mod model; +mod returns; +mod run; +mod shape; + +pub(crate) use crate::control::execution::Program; +pub(crate) use boundary::check_boundary; +#[cfg(test)] +pub(super) use check::check; +#[cfg(test)] +pub(super) use check::check_for; +use helpers::{ + boundary_check, boundary_close, cancel, clone_sequence, once, raw_resume, rebuild_sequence, + start_boundary, +}; +pub(crate) use ir::{Exec as PlanExec, Family}; +pub(crate) use shape::{Shape, shape as shape_of}; + +use crate::control::schema::ComputationCase; +use ir::{Case, Cursor, Exec, State, state, switch}; + +/// Only this module can publish a plan as checked. Expansion and boundary +/// validation consume this immutable value rather than an arbitrary program. +pub(crate) struct CheckedPlan { + exec: Exec, +} + +impl CheckedPlan { + pub(in crate::cc::lower::control) fn exec(&self) -> &Exec { + &self.exec + } +} + +pub(super) fn build(program: Program) -> CheckedPlan { + let exec = match program { + Program::Executor => executor(), + Program::Run => run::program(), + Program::CloneSequence => clone_sequence(), + Program::RebuildSequence => rebuild_sequence(), + Program::Check => boundary_check(), + Program::Close => boundary_close(), + Program::Start => start_boundary(), + Program::Once => once(), + Program::Cancel => cancel(), + Program::RawResume => raw_resume(), + }; + check::check_for(program, &exec) + .expect("machine construction plan must satisfy its complete contract"); + CheckedPlan { exec } +} + +/// Build the complete flat-driver program. +fn executor() -> Exec { + let cursor = Cursor::new(); + let arms = ComputationCase::ALL + .iter() + .map(|case| { + let mut cursor = cursor.clone(); + let arm = match case { + ComputationCase::Return => returns::returned(&mut cursor), + ComputationCase::Perform => driver::perform(&mut cursor), + ComputationCase::Bind => driver::bind(&mut cursor), + ComputationCase::Resume => driver::resume(&mut cursor), + ComputationCase::Handle => handle::handle(&mut cursor), + ComputationCase::OwnedResume => handle::owned_resume(&mut cursor), + }; + (case.tag(), arm) + }) + .collect(); + switch( + state(State::Computation), + Case::Computation(ComputationCase::Return), + arms, + ) +} + +#[cfg(test)] +mod tests { + use super::ir::{Case, Exec, Op, Operand, Shape, State}; + use super::*; + use crate::control::schema::ComputationCase; + + #[test] + fn executor_plan_is_complete() { + check(&executor()).expect("the driver plan covers every required action"); + } + + #[test] + fn helper_plans_are_well_formed() { + for (program, plan) in [ + (Program::CloneSequence, clone_sequence()), + (Program::RebuildSequence, rebuild_sequence()), + (Program::Check, boundary_check()), + (Program::Close, boundary_close()), + (Program::Start, start_boundary()), + (Program::Once, once()), + (Program::Cancel, cancel()), + (Program::RawResume, raw_resume()), + ] { + check_for(program, &plan).expect("helper plan must be well-formed"); + } + } + + #[test] + fn dispatch_rejects_an_invented_case() { + let plan = Exec::Switch { + source: Operand::State(State::Computation), + case: Case::Computation(ComputationCase::Return), + arms: vec![( + 99, + Exec::Publish { + result: Operand::State(State::Value), + }, + )], + }; + assert!(check(&plan).is_err()); + } + + #[test] + fn dispatch_rejects_a_duplicate_case() { + let plan = Exec::Switch { + source: Operand::State(State::Computation), + case: Case::Computation(ComputationCase::Return), + arms: vec![ + ( + 0, + Exec::Reject { + shape: Shape::ReplayResult, + }, + ), + ( + 0, + Exec::Reject { + shape: Shape::ReplayResult, + }, + ), + ], + }; + assert!(check_for(Program::Executor, &plan).is_err()); + } + + #[test] + fn node_field_arity_is_checked() { + let mut plan = once(); + let Exec::Steps { ops, .. } = &mut plan else { + unreachable!() + }; + let Op::Node { fields, .. } = ops.last_mut().unwrap() else { + unreachable!() + }; + fields.pop(); + assert_eq!( + check_for(Program::Once, &plan), + Err("machine plan call or allocation has the wrong arity") + ); + } + + #[test] + fn dropping_an_action_fails_the_check() { + let mut plan = executor(); + if let Exec::Switch { arms, .. } = &mut plan { + arms.retain(|(tag, _)| *tag != ComputationCase::Perform.tag()); + } + assert!(check(&plan).is_err()); + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/model/behavior.rs b/crates/psrs-backend/src/cc/lower/control/plan/model/behavior.rs new file mode 100644 index 00000000..68f909ed --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/model/behavior.rs @@ -0,0 +1,449 @@ +use super::super::ir::Family; +use super::compare::equivalent; +use super::fixtures::{Fixture, Queue, queues}; +use super::heap::*; +use super::interpret::Interpreter; +use super::reference::Reference; +use crate::control::execution::{ActionId, Program}; + +pub(super) fn assert_matches( + interpreter: &mut Interpreter, + input: &Heap, + program: Program, + args: Vec, +) -> Checked { + let mut expected = input.clone(); + let expected_step = Reference::new(&mut expected).run(program, &args); + interpreter.reset(input.clone()); + let actual_step = interpreter.run(program, args.clone()); + assert!( + equivalent( + &interpreter.heap, + &actual_step, + &expected, + &expected_step, + input.objects.len() + ), + "{program:?} {args:?}: plan={actual_step:?}, reference={expected_step:?}\nplan events={:?}\nreference events={:?}\ninput={:?}", + interpreter.heap.events, + expected.events, + input.objects + ); + actual_step +} + +#[test] +fn every_action_matches_operands_state_identity_and_ordered_behavior() { + let mut f = Fixture::new(); + let mut interpreter = Interpreter::new(f.heap.clone()); + let queues = f.queues(); + assert_matches( + &mut interpreter, + &f.heap, + Program::Run, + vec![f.state, f.handler, f.next], + ) + .unwrap(); + for context in [f.root, f.scoped] { + let computation = f.heap.node(Node::Return(f.answer)); + for queue in &queues { + assert_matches( + &mut interpreter, + &f.heap, + Program::Executor, + f.args(computation, *queue, context), + ) + .unwrap(); + } + let computation = f.heap.node(Node::Perform(f.request)); + assert_matches( + &mut interpreter, + &f.heap, + Program::Executor, + f.args(computation, f.consumer, context), + ) + .unwrap(); + let computation = f.heap.node(Node::Handle(f.ret, f.op, f.next)); + for queue in &queues { + assert_matches( + &mut interpreter, + &f.heap, + Program::Executor, + f.args(computation, *queue, context), + ) + .unwrap(); + } + } + let computation = f.heap.node(Node::Bind(f.next, f.callback)); + assert_matches( + &mut interpreter, + &f.heap, + Program::Executor, + f.args(computation, f.consumer, f.scoped), + ) + .unwrap(); + let saved = f.heap.node(Node::Finish(f.checkpoint, f.saved, f.parent)); + let computation = f.heap.node(Node::Resume(f.answer, saved, f.checkpoint)); + assert_matches( + &mut interpreter, + &f.heap, + Program::Executor, + f.args(computation, f.consumer, f.root), + ) + .unwrap(); + let computation = f.heap.node(Node::OwnedResume(f.owner, f.answer)); + assert_matches( + &mut interpreter, + &f.heap, + Program::Executor, + f.args(computation, f.consumer, f.scoped), + ) + .unwrap(); + + for queue in &queues { + assert_matches( + &mut interpreter, + &f.heap, + Program::CloneSequence, + vec![*queue, f.empty, f.empty, f.active, f.parent], + ) + .unwrap(); + } + let todo = f.heap.node(Node::Append(f.saved, f.empty)); + assert_matches( + &mut interpreter, + &f.heap, + Program::CloneSequence, + vec![f.empty, todo, f.saved, f.active, f.parent], + ) + .unwrap(); + for invalid in [f.saved, saved] { + assert_eq!( + assert_matches( + &mut interpreter, + &f.heap, + Program::CloneSequence, + vec![f.empty, invalid, f.empty, f.active, f.parent] + ), + Err(Fault::Trap) + ); + } + for reversed in [f.empty, f.saved, saved] { + assert_matches( + &mut interpreter, + &f.heap, + Program::RebuildSequence, + vec![reversed, f.consumer, f.active, f.parent], + ) + .unwrap(); + } + let append = f.heap.node(Node::Append(f.saved, f.consumer)); + assert_eq!( + assert_matches( + &mut interpreter, + &f.heap, + Program::RebuildSequence, + vec![append, f.consumer, f.active, f.parent] + ), + Err(Fault::Trap) + ); + let child = f.heap.extent(Some(f.checkpoint)); + for program in [Program::Check, Program::Close] { + for extent in [f.checkpoint, child] { + assert_matches(&mut interpreter, &f.heap, program, vec![extent]).unwrap(); + } + } + for (program, args) in [ + (Program::Start, vec![]), + (Program::Once, vec![f.raw]), + (Program::Cancel, vec![f.owner]), + (Program::RawResume, vec![f.raw, f.answer]), + ] { + assert_matches(&mut interpreter, &f.heap, program, args).unwrap(); + } + assert_eq!(interpreter.actions.len(), ActionId::ALL.len()); + for action in ActionId::ALL { + assert!( + interpreter.actions.contains(&action), + "missing executed action {action:?}" + ); + } +} + +#[test] +fn failed_pending_and_callback_traps_preserve_the_executed_prefix() { + let mut interpreter = Interpreter::new(Heap::default()); + for status in 0..=3 { + for callback_traps in [false, true] { + let mut f = Fixture::new(); + f.owner_status(status); + if callback_traps { + f.heap.objects[f.callback] = Object::Function { + label: "queue-callback", + result: Err(Fault::CallbackTrap), + }; + } + let computation = f.heap.node(Node::OwnedResume(f.owner, f.answer)); + let result = assert_matches( + &mut interpreter, + &f.heap, + Program::Executor, + f.args(computation, f.consumer, f.scoped), + ); + if status != 0 { + assert_eq!(result, Err(Fault::Trap)); + assert_eq!(interpreter.heap.events.len(), 1); + } else if callback_traps { + assert_eq!(result, Err(Fault::CallbackTrap)); + let Node::Owner { callback, status } = interpreter.heap.get_node(f.owner).unwrap() + else { + unreachable!() + }; + assert_eq!(interpreter.heap.objects[status], Object::Status(1)); + assert_eq!( + interpreter.heap.objects[callback], + Object::CallbackCell(status) + ); + } else { + result.unwrap(); + } + let cancelled = + assert_matches(&mut interpreter, &f.heap, Program::Cancel, vec![f.owner]); + if status != 0 { + assert_eq!(cancelled, Err(Fault::Trap)); + assert_eq!(interpreter.heap.events.len(), 1); + } else { + cancelled.unwrap(); + } + } + } +} + +#[test] +fn callback_failures_cannot_complete_their_target_transition() { + let mut interpreter = Interpreter::new(Heap::default()); + for callback in 0..4 { + let mut f = Fixture::new(); + let callee = [f.handler, f.ret, f.op, f.callback][callback]; + let Object::Function { result, .. } = &mut f.heap.objects[callee] else { + unreachable!() + }; + *result = Err(Fault::CallbackTrap); + let (computation, queue, context) = match callback { + 0 => (f.heap.node(Node::Perform(f.request)), f.consumer, f.root), + 1 => (f.heap.node(Node::Return(f.answer)), f.empty, f.scoped), + 2 => (f.heap.node(Node::Perform(f.request)), f.consumer, f.scoped), + _ => ( + f.heap.node(Node::Return(f.answer)), + f.heap.node(Node::Cons(f.callback, f.empty)), + f.root, + ), + }; + assert_eq!( + assert_matches( + &mut interpreter, + &f.heap, + Program::Executor, + f.args(computation, queue, context) + ), + Err(Fault::CallbackTrap) + ); + assert_eq!(interpreter.heap.events.len(), 1); + } +} + +#[test] +fn expiry_checks_every_ancestor_and_raw_invocation_remains_inert() { + let mut interpreter = Interpreter::new(Heap::default()); + for closed in 0..3 { + let mut f = Fixture::new(); + let parent = f.heap.extent(Some(f.parent)); + let child = f.heap.extent(Some(parent)); + f.expire([child, parent, f.parent][closed]); + assert_eq!( + assert_matches(&mut interpreter, &f.heap, Program::Check, vec![child]), + Err(Fault::Trap) + ); + assert_eq!(interpreter.heap.events.len(), closed + 1); + let raw = f.heap.alloc(Object::Raw { + queue: f.saved, + checkpoint: child, + }); + assert_eq!( + assert_matches( + &mut interpreter, + &f.heap, + Program::RawResume, + vec![raw, f.answer] + ), + Err(Fault::Trap) + ); + } + let mut f = Fixture::new(); + let raw_queue = f.heap.node(Node::Cons(f.raw, f.consumer)); + let computation = f.heap.node(Node::Return(f.answer)); + let result = assert_matches( + &mut interpreter, + &f.heap, + Program::Executor, + f.args(computation, raw_queue, f.scoped), + ) + .unwrap(); + let Step::Advance(state) = result else { + panic!("raw queue callback must construct the next computation"); + }; + assert_eq!( + interpreter.heap.get_node(state[2]).unwrap(), + Node::Resume(f.answer, f.saved, f.checkpoint) + ); + assert_eq!(state[3], f.consumer); + assert_eq!(state[5], f.scoped); +} + +#[test] +fn unknown_machine_tags_reject_without_unaccounted_work() { + let mut interpreter = Interpreter::new(Heap::default()); + for family in [ + Family::Computation, + Family::Sequence, + Family::Context, + Family::Extent, + ] { + let mut f = Fixture::new(); + let unknown = f.heap.alloc(Object::Unknown(family, 99)); + let computation = f.heap.node(Node::Return(f.answer)); + let (program, args) = match family { + Family::Computation => (Program::Executor, f.args(unknown, f.empty, f.root)), + Family::Sequence => (Program::Executor, f.args(computation, unknown, f.root)), + Family::Context => (Program::Executor, f.args(computation, f.empty, unknown)), + Family::Extent => (Program::Check, vec![unknown]), + Family::Owner => unreachable!(), + }; + assert_eq!( + assert_matches(&mut interpreter, &f.heap, program, args), + Err(Fault::Trap) + ); + assert!(interpreter.heap.events.is_empty()); + } +} + +fn flatten_input(queue: &Queue, f: &Fixture, callbacks: &mut Vec, finishes: &mut usize) { + match queue { + Queue::Empty => {} + Queue::Cons(alternative, rest) => { + callbacks.push(if *alternative { f.op } else { f.callback }); + flatten_input(rest, f, callbacks, finishes); + } + Queue::Finish(rest) => { + *finishes += 1; + flatten_input(rest, f, callbacks, finishes); + } + Queue::Append(left, right) => { + flatten_input(left, f, callbacks, finishes); + flatten_input(right, f, callbacks, finishes); + } + } +} + +fn replay_contents(heap: &Heap, result: &Step) -> (Vec, Vec, Value) { + let Step::Return(value) = result else { + panic!("replay must return a product"); + }; + let Object::Replay(mut queue, active) = *heap.get(*value).unwrap() else { + panic!("replay result"); + }; + let mut callbacks = Vec::new(); + let mut finishes = Vec::new(); + loop { + match heap.get_node(queue).unwrap() { + Node::Empty => break, + Node::Cons(callback, rest) => { + callbacks.push(callback); + queue = rest; + } + Node::Finish(extent, rest, _) => { + finishes.push(extent); + queue = rest; + } + other => panic!("replay must be linear, got {other:?}"), + } + } + (callbacks, finishes, active) +} + +#[test] +fn bounded_replay_preserves_order_sharing_freshness_and_three_extent_roles() { + let expressions = queues(3); + assert_eq!(expressions.len(), 1805); + let mut interpreter = Interpreter::new(Heap::default()); + for expression in expressions { + let mut f = Fixture::new(); + let queue = f.queue(&expression); + let args = vec![queue, f.empty, f.empty, f.active, f.parent]; + let first = assert_matches( + &mut interpreter, + &f.heap, + Program::CloneSequence, + args.clone(), + ) + .unwrap(); + let (callbacks, finishes, active) = replay_contents(&interpreter.heap, &first); + let mut expected_callbacks = Vec::new(); + let mut count = 0; + flatten_input(&expression, &f, &mut expected_callbacks, &mut count); + assert_eq!(callbacks, expected_callbacks, "{expression:?}"); + assert_eq!(finishes.len(), count); + let (cons_count, finish_count, append_count) = expression.counts(); + let queue_allocations = interpreter.heap.objects[f.heap.objects.len()..].iter().filter(|object| matches!(object,Object::Node(node) if node.case().family() == Family::Sequence)).count(); + assert_eq!( + queue_allocations, + 2 * (cons_count + finish_count) + append_count + ); + assert_eq!(active, finishes.first().copied().unwrap_or(f.active)); + let unique = finishes + .iter() + .copied() + .collect::>(); + assert_eq!(unique.len(), count); + for (index, extent) in finishes.iter().enumerate() { + assert!(*extent >= f.heap.objects.len()); + let Node::ScopedExtent { status, parent } = interpreter.heap.get_node(*extent).unwrap() + else { + panic!("activation must be a fresh child"); + }; + assert_eq!(interpreter.heap.objects[status], Object::Status(0)); + assert_eq!(parent, finishes.get(index + 1).copied().unwrap_or(f.parent)); + } + assert_eq!( + &interpreter.heap.objects[..f.heap.objects.len()], + f.heap.objects.as_slice() + ); + let before_second = interpreter.heap.clone(); + let second = assert_matches( + &mut interpreter, + &before_second, + Program::CloneSequence, + args, + ) + .unwrap(); + let (_, second_finishes, _) = replay_contents(&interpreter.heap, &second); + assert!( + second_finishes + .iter() + .all(|extent| !unique.contains(extent)) + ); + let mut expired = f.clone(); + expired.expire(f.checkpoint); + let result = assert_matches( + &mut interpreter, + &expired.heap, + Program::CloneSequence, + vec![queue, f.empty, f.empty, f.active, f.parent], + ); + if count == 0 { + result.unwrap(); + } else { + assert_eq!(result, Err(Fault::Trap)); + } + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/model/compare.rs b/crates/psrs-backend/src/cc/lower/control/plan/model/compare.rs new file mode 100644 index 00000000..807034e7 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/model/compare.rs @@ -0,0 +1,210 @@ +//! Compare observable transitions modulo fresh allocation IDs, preserving +//! input identity and aliasing. Replay traversal temporaries are budgeted by +//! the flat reference model; other allocations must agree exactly by role. +use super::heap::*; +use std::collections::{HashMap, HashSet}; + +fn queue_allocations(heap: &Heap, original: usize) -> usize { + heap.objects[original..].iter().filter(|object| matches!(object,Object::Node(node) if node.case().family() == super::super::ir::Family::Sequence)).count() +} + +fn allocations( + heap: &Heap, + original: usize, +) -> HashMap< + ( + std::mem::Discriminant, + Option, + ), + usize, +> { + let mut counts = HashMap::new(); + for object in &heap.objects[original..] { + let case = match object { + Object::Node(node) if node.case().family() == super::super::ir::Family::Sequence => { + continue; + } + Object::Node(node) => Some(node.case()), + _ => None, + }; + *counts + .entry((std::mem::discriminant(object), case)) + .or_default() += 1; + } + counts +} + +pub(super) fn equivalent( + a: &Heap, + result_a: &Checked, + b: &Heap, + result_b: &Checked, + original: usize, +) -> bool { + let mut compare = Compare { + a, + b, + original, + forward: HashMap::new(), + backward: HashMap::new(), + visited: HashSet::new(), + }; + let result = match (result_a, result_b) { + (Ok(Step::Return(a)), Ok(Step::Return(b))) + | (Ok(Step::Publish(a)), Ok(Step::Publish(b))) => compare.value(*a, *b), + (Ok(Step::Advance(a)), Ok(Step::Advance(b))) => compare.values(a, b), + (Err(a), Err(b)) => a == b && matches!(a, Fault::Trap | Fault::CallbackTrap), + _ => false, + }; + result + && allocations(a, original) == allocations(b, original) + && b.queue_work + .checked_sub(a.queue_work) + .and_then(|work| queue_allocations(b, original).checked_add(work)) + == Some(queue_allocations(a, original)) + && a.events.len() == b.events.len() + && a.events + .iter() + .zip(&b.events) + .all(|(a, b)| compare.event(a, b)) + && (0..original).all(|value| compare.value(value, value)) +} + +struct Compare<'a> { + a: &'a Heap, + b: &'a Heap, + original: usize, + forward: HashMap, + backward: HashMap, + visited: HashSet<(Value, Value)>, +} + +impl Compare<'_> { + fn values(&mut self, a: &[Value], b: &[Value]) -> bool { + a.len() == b.len() && a.iter().zip(b).all(|(a, b)| self.value(*a, *b)) + } + fn value(&mut self, a: Value, b: Value) -> bool { + if a < self.original || b < self.original { + if a != b { + return false; + } + } else { + if self.forward.get(&a).is_some_and(|other| *other != b) + || self.backward.get(&b).is_some_and(|other| *other != a) + { + return false; + } + self.forward.insert(a, b); + self.backward.insert(b, a); + } + if !self.visited.insert((a, b)) { + return true; + } + let (Some(a), Some(b)) = (self.a.objects.get(a), self.b.objects.get(b)) else { + return false; + }; + match (a, b) { + (Object::Node(a), Object::Node(b)) => { + a.case() == b.case() && self.values(&a.fields(), &b.fields()) + } + (Object::Atom(a), Object::Atom(b)) => a == b, + (Object::Status(a), Object::Status(b)) => a == b, + (Object::CallbackCell(a), Object::CallbackCell(b)) => self.value(*a, *b), + ( + Object::Function { + label: a, + result: ar, + }, + Object::Function { + label: b, + result: br, + }, + ) => { + a == b + && match (ar, br) { + (Ok(a), Ok(b)) => self.value(*a, *b), + (Err(a), Err(b)) => a == b, + _ => false, + } + } + ( + Object::Raw { + queue: a, + checkpoint: ae, + }, + Object::Raw { + queue: b, + checkpoint: be, + }, + ) => self.value(*a, *b) && self.value(*ae, *be), + (Object::Pair(a, aa), Object::Pair(b, ba)) + | (Object::Replay(a, aa), Object::Replay(b, ba)) => { + self.value(*a, *b) && self.value(*aa, *ba) + } + (Object::Unknown(a, at), Object::Unknown(b, bt)) => a == b && at == bt, + _ => false, + } + } + fn event(&mut self, a: &Event, b: &Event) -> bool { + match (a, b) { + (Event::ReadStatus(a, av), Event::ReadStatus(b, bv)) + | (Event::WriteStatus(a, av), Event::WriteStatus(b, bv)) => { + av == bv && self.value(*a, *b) + } + (Event::ReadCallback(a, av), Event::ReadCallback(b, bv)) + | (Event::WriteCallback(a, av), Event::WriteCallback(b, bv)) => { + self.value(*a, *b) && self.value(*av, *bv) + } + (Event::Apply(a, args_a), Event::Apply(b, args_b)) => { + self.value(*a, *b) && self.values(args_a, args_b) + } + _ => false, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn fresh_id_normalization_preserves_aliasing_and_ignores_unreachable_work_nodes() { + let mut a = Heap::default(); + let mut b = Heap::default(); + a.alloc(Object::Atom("input")); + b.alloc(Object::Atom("input")); + a.node(Node::Empty); // An unreachable traversal temporary. + b.queue_work = 1; + let ae = a.extent(None); + let be = b.extent(None); + a.extent(None); + let other = b.extent(None); + let ar = Ok(Step::Return(a.alloc(Object::Pair(ae, ae)))); + let br = Ok(Step::Return(b.alloc(Object::Pair(be, be)))); + assert!(equivalent(&a, &ar, &b, &br, 1)); + let Ok(Step::Return(pair)) = br else { + unreachable!() + }; + b.objects[pair] = Object::Pair(be, other); + assert!(!equivalent(&a, &ar, &b, &br, 1)); + } + + #[test] + fn equal_shapes_cannot_hide_input_replacement_or_an_incomplete_evaluation() { + let mut a = Heap::default(); + let extent = a.extent(None); + let original = a.objects.len(); + let mut b = a.clone(); + let copy = b.node(b.get_node(extent).unwrap()); + assert!(!equivalent( + &a, + &Ok(Step::Return(extent)), + &b, + &Ok(Step::Return(copy)), + original + )); + for fault in [Fault::InvalidModel, Fault::Exhausted] { + assert!(!equivalent(&a, &Err(fault), &a, &Err(fault), original)); + } + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/model/continuations.rs b/crates/psrs-backend/src/cc/lower/control/plan/model/continuations.rs new file mode 100644 index 00000000..373d4a0b --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/model/continuations.rs @@ -0,0 +1,173 @@ +//! Composition after individual transitions: actual shared queue occurrences, +//! bounded execution to completion, and ownership retained across replay. +use super::{ + behavior::assert_matches, + fixtures::{Fixture, queues}, + heap::*, + interpret::Interpreter, +}; +use crate::control::execution::Program; + +fn drive(interpreter: &mut Interpreter, mut args: Vec) -> Checked { + for _ in 0..256 { + let input = interpreter.heap.clone(); + match assert_matches(interpreter, &input, Program::Executor, args)? { + Step::Advance(next) => args = next, + other => return Ok(other), + } + } + panic!("bounded computation did not finish within 256 transitions"); +} + +#[test] +fn bounded_executor_resume_preserves_saved_checkpoint_and_current_dispatch() { + let mut interpreter = Interpreter::new(Heap::default()); + for queue in queues(2) { + for scoped in [false, true] { + let mut f = Fixture::new(); + let saved = f.queue(&queue); + let source_checkpoint = f.heap.extent(None); + let computation = f + .heap + .node(Node::Resume(f.answer, saved, source_checkpoint)); + let context = if scoped { f.scoped } else { f.root }; + let args = f.args(computation, f.consumer, context); + assert_matches(&mut interpreter, &f.heap, Program::Executor, args.clone()).unwrap(); + f.expire(source_checkpoint); + assert_eq!( + assert_matches(&mut interpreter, &f.heap, Program::Executor, args), + Err(Fault::Trap) + ); + assert_eq!(interpreter.heap.events.len(), 1); + } + } +} + +#[test] +fn left_and_right_bind_association_complete_with_the_same_ordered_callbacks() { + for depth in 0..16 { + for right in [false, true] { + let mut f = Fixture::new(); + let mut computation = f.heap.node(Node::Return(f.answer)); + for _ in 0..depth { + computation = if right { + let callback = f.heap.alloc(Object::Function { + label: "right-bind", + result: Ok(computation), + }); + let answer = f.heap.node(Node::Return(f.answer)); + f.heap.node(Node::Bind(answer, callback)) + } else { + f.heap.node(Node::Bind(computation, f.callback)) + }; + } + let mut interpreter = Interpreter::new(f.heap.clone()); + let result = drive(&mut interpreter, f.args(computation, f.empty, f.root)).unwrap(); + assert!(matches!(result, Step::Publish(_))); + assert_eq!( + interpreter + .heap + .events + .iter() + .filter(|event| matches!(event, Event::Apply(..))) + .count(), + depth + ); + } + } +} + +#[test] +fn shared_subqueues_replay_each_occurrence_without_cloning_owned_callbacks() { + let mut f = Fixture::new(); + let owned_computation = f.heap.node(Node::OwnedResume(f.owner, f.answer)); + let callback = f.heap.alloc(Object::Function { + label: "owned-callback", + result: Ok(owned_computation), + }); + let finish = f.heap.node(Node::Finish(f.checkpoint, f.empty, f.parent)); + let shared = f.heap.node(Node::Cons(callback, finish)); + let captured = f.heap.node(Node::Append(shared, shared)); + let args = vec![captured, f.empty, f.empty, f.active, f.parent]; + let mut interpreter = Interpreter::new(f.heap.clone()); + let Step::Return(replay) = + assert_matches(&mut interpreter, &f.heap, Program::CloneSequence, args).unwrap() + else { + unreachable!() + }; + let Object::Replay(queue, active) = *interpreter.heap.get(replay).unwrap() else { + unreachable!() + }; + assert_eq!( + &interpreter.heap.objects[..f.heap.objects.len()], + f.heap.objects.as_slice() + ); + let computation = interpreter.heap.node(Node::Return(f.answer)); + let state = vec![f.state, f.handler, computation, queue, active, f.root]; + assert_eq!(drive(&mut interpreter, state), Err(Fault::Trap)); + let calls = interpreter + .heap + .events + .iter() + .filter(|event| matches!(event,Event::Apply(callee,_) if *callee == callback)) + .count(); + assert_eq!( + calls, 2, + "both shared occurrences must remain in the replay" + ); + let Node::Owner { + callback: slot, + status, + } = interpreter.heap.get_node(f.owner).unwrap() + else { + unreachable!() + }; + assert_eq!(interpreter.heap.objects[status], Object::Status(2)); + assert_eq!(interpreter.heap.objects[slot], Object::CallbackCell(status)); + assert_eq!( + interpreter + .heap + .events + .iter() + .filter(|event| matches!(event,Event::Apply(callee,_) if *callee == f.callback)) + .count(), + 1 + ); +} + +#[test] +fn scoped_target_completion_closes_its_extent_and_restores_caller_work() { + let mut f = Fixture::new(); + let source = f.heap.node(Node::Perform(f.request)); + let body = f.heap.node(Node::Bind(source, f.callback)); + let computation = f.heap.node(Node::Handle(f.ret, f.op, body)); + let caller_queue = f.heap.node(Node::Cons(f.ret, f.empty)); + let mut interpreter = Interpreter::new(f.heap.clone()); + let result = drive(&mut interpreter, f.args(computation, caller_queue, f.root)).unwrap(); + assert!(matches!(result, Step::Publish(_))); + assert!( + !interpreter + .heap + .events + .iter() + .any(|event| matches!(event,Event::Apply(callee,_) if *callee == f.callback)), + "abandoned source work must not run" + ); + assert!( + interpreter + .heap + .events + .iter() + .any(|event| matches!(event,Event::Apply(callee,_) if *callee == f.ret)), + "caller work must run after target completion" + ); + assert_eq!( + interpreter + .heap + .events + .iter() + .filter(|event| matches!(event, Event::WriteStatus(_, 1))) + .count(), + 1 + ); +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/model/failure_prefixes.rs b/crates/psrs-backend/src/cc/lower/control/plan/model/failure_prefixes.rs new file mode 100644 index 00000000..b9170545 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/model/failure_prefixes.rs @@ -0,0 +1,74 @@ +//! Exceptional paths retain the work before failure without treating any +//! failed execution as successful normal completion. +use super::super::ir::Family; +use super::{behavior::assert_matches, fixtures::Fixture, heap::*, interpret::Interpreter}; +use crate::control::execution::Program; + +#[test] +fn replay_checks_later_distinct_finish_extents_before_rebuilding_any_activation() { + let mut interpreter = Interpreter::new(Heap::default()); + for expired_parent in [false, true] { + let mut f = Fixture::new(); + let stale = f.heap.extent(None); + let child = f.heap.extent(Some(stale)); + f.expire(if expired_parent { stale } else { child }); + let good = f.heap.node(Node::Finish(f.checkpoint, f.saved, f.parent)); + let bad = f.heap.node(Node::Finish(child, f.empty, f.parent)); + let captured = f.heap.node(Node::Append(good, bad)); + assert_eq!( + assert_matches( + &mut interpreter, + &f.heap, + Program::CloneSequence, + vec![captured, f.empty, f.empty, f.active, f.parent] + ), + Err(Fault::Trap) + ); + assert_eq!( + interpreter.heap.events.len(), + if expired_parent { 3 } else { 2 } + ); + assert!(interpreter.heap.objects[f.heap.objects.len()..].iter().all(|object| matches!(object,Object::Node(node) if node.case().family() == Family::Sequence)),"failed clone must not publish fresh activations or a result product"); + assert_eq!( + &interpreter.heap.objects[..f.heap.objects.len()], + f.heap.objects.as_slice() + ); + } +} + +#[test] +fn rejected_reverse_tails_keep_the_normal_rebuild_prefix() { + let mut interpreter = Interpreter::new(Heap::default()); + for unknown in [false, true] { + let mut f = Fixture::new(); + let bad = if unknown { + f.heap.alloc(Object::Unknown(Family::Sequence, 99)) + } else { + f.heap.node(Node::Append(f.saved, f.consumer)) + }; + let reversed = f.heap.node(Node::Finish(f.checkpoint, bad, f.parent)); + for program in [Program::RebuildSequence, Program::CloneSequence] { + let args = if program == Program::RebuildSequence { + vec![reversed, f.empty, f.active, f.parent] + } else { + vec![f.saved, f.empty, reversed, f.active, f.parent] + }; + assert_eq!( + assert_matches(&mut interpreter, &f.heap, program, args), + Err(Fault::Trap) + ); + let new = &interpreter.heap.objects[f.heap.objects.len()..]; + assert_eq!( + new.iter() + .filter(|object| matches!(object, Object::Node(Node::ScopedExtent { .. }))) + .count(), + 1 + ); + assert!( + !new.iter() + .any(|object| matches!(object, Object::Replay(..))), + "a rejected prefix cannot publish a replay result" + ); + } + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/model/fixtures.rs b/crates/psrs-backend/src/cc/lower/control/plan/model/fixtures.rs new file mode 100644 index 00000000..aafbe414 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/model/fixtures.rs @@ -0,0 +1,215 @@ +//! Distinct values for every semantic role; execution, dispatch checkpoint and +//! restoration parent deliberately differ. Cases are generated from inputs, +//! not from the plan or its action-path registry. +use super::heap::*; + +#[derive(Clone)] +pub(super) struct Fixture { + pub heap: Heap, + pub state: Value, + pub handler: Value, + pub answer: Value, + pub request: Value, + pub next: Value, + pub ret: Value, + pub op: Value, + pub callback: Value, + pub empty: Value, + pub consumer: Value, + pub saved: Value, + pub active: Value, + pub checkpoint: Value, + pub parent: Value, + pub root: Value, + pub scoped: Value, + pub owner: Value, + pub raw: Value, +} + +impl Fixture { + pub(super) fn new() -> Self { + let mut heap = Heap::default(); + let state = heap.alloc(Object::Atom("state")); + let next_state = heap.alloc(Object::Atom("next-state")); + let answer = heap.alloc(Object::Atom("answer")); + let request = heap.alloc(Object::Atom("request")); + let payload = heap.alloc(Object::Atom("callback-answer")); + let next = heap.node(Node::Return(payload)); + let pair = heap.alloc(Object::Pair(next_state, next)); + let handler = heap.alloc(Object::Function { + label: "root-handler", + result: Ok(pair), + }); + let ret = heap.alloc(Object::Function { + label: "return-clause", + result: Ok(next), + }); + let op = heap.alloc(Object::Function { + label: "operation-clause", + result: Ok(next), + }); + let callback = heap.alloc(Object::Function { + label: "queue-callback", + result: Ok(next), + }); + let empty = heap.node(Node::Empty); + let active = heap.extent(None); + let checkpoint = heap.extent(None); + let parent = heap.extent(None); + let root = heap.node(Node::RootContext(checkpoint)); + let consumer = heap.node(Node::Cons(op, empty)); + let saved = heap.node(Node::Cons(ret, empty)); + let scoped = heap.node(Node::ScopedContext { + ret, + op, + saved, + parent, + caller: root, + extent: checkpoint, + }); + let callback_slot = heap.alloc(Object::CallbackCell(callback)); + let status = heap.alloc(Object::Status(0)); + let owner = heap.node(Node::Owner { + callback: callback_slot, + status, + }); + let raw = heap.alloc(Object::Raw { + queue: saved, + checkpoint, + }); + Self { + heap, + state, + handler, + answer, + request, + next, + ret, + op, + callback, + empty, + consumer, + saved, + active, + checkpoint, + parent, + root, + scoped, + owner, + raw, + } + } + pub(super) fn args(&self, computation: Value, queue: Value, context: Value) -> Vec { + vec![ + self.state, + self.handler, + computation, + queue, + self.active, + context, + ] + } + pub(super) fn owner_status(&mut self, n: i32) { + let Node::Owner { status, .. } = self.heap.get_node(self.owner).unwrap() else { + unreachable!() + }; + self.heap.objects[status] = Object::Status(n); + } + pub(super) fn expire(&mut self, extent: Value) { + let status = match self.heap.get_node(extent).unwrap() { + Node::RootExtent(status) | Node::ScopedExtent { status, .. } => status, + _ => unreachable!(), + }; + self.heap.objects[status] = Object::Status(1); + } + pub(super) fn queues(&mut self) -> Vec { + let cons = self.heap.node(Node::Cons(self.callback, self.saved)); + let finish = self + .heap + .node(Node::Finish(self.checkpoint, self.saved, self.parent)); + let nested = self.heap.node(Node::Append(self.saved, self.consumer)); + let mut queues = vec![self.empty, cons, finish]; + for left in [self.empty, cons, nested, finish] { + queues.push(self.heap.node(Node::Append(left, self.consumer))); + } + queues + } +} + +/// Queue expressions through depth three, with two shared callback identities +/// and repeated Finish extent occurrences. Both Append subtrees are generated. +#[derive(Clone, Debug)] +pub(super) enum Queue { + Empty, + Cons(bool, Box), + Finish(Box), + Append(Box, Box), +} + +pub(super) fn queues(depth: usize) -> Vec { + if depth == 0 { + return vec![Queue::Empty]; + } + let previous = queues(depth - 1); + let mut result = vec![Queue::Empty]; + for rest in &previous { + result.push(Queue::Cons(false, Box::new(rest.clone()))); + result.push(Queue::Cons(true, Box::new(rest.clone()))); + result.push(Queue::Finish(Box::new(rest.clone()))); + } + for left in &previous { + for right in &previous { + result.push(Queue::Append( + Box::new(left.clone()), + Box::new(right.clone()), + )); + } + } + result +} + +impl Queue { + pub(super) fn counts(&self) -> (usize, usize, usize) { + match self { + Self::Empty => (0, 0, 0), + Self::Cons(_, rest) => { + let (c, f, a) = rest.counts(); + (c + 1, f, a) + } + Self::Finish(rest) => { + let (c, f, a) = rest.counts(); + (c, f + 1, a) + } + Self::Append(left, right) => { + let (c, f, a) = left.counts(); + let (c2, f2, a2) = right.counts(); + (c + c2, f + f2, a + a2 + 1) + } + } + } +} + +impl Fixture { + pub(super) fn queue(&mut self, queue: &Queue) -> Value { + match queue { + Queue::Empty => self.empty, + Queue::Cons(alternative, rest) => { + let rest = self.queue(rest); + self.heap.node(Node::Cons( + if *alternative { self.op } else { self.callback }, + rest, + )) + } + Queue::Finish(rest) => { + let rest = self.queue(rest); + self.heap + .node(Node::Finish(self.checkpoint, rest, self.parent)) + } + Queue::Append(left, right) => { + let left = self.queue(left); + let right = self.queue(right); + self.heap.node(Node::Append(left, right)) + } + } + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/model/heap.rs b/crates/psrs-backend/src/cc/lower/control/plan/model/heap.rs new file mode 100644 index 00000000..688370c7 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/model/heap.rs @@ -0,0 +1,205 @@ +//! Typed test values. The reference semantics matches these variants directly; +//! only the plan interpreter uses the independent positional adapter below. +use super::super::ir::{Case, Family}; +use crate::control::schema::{ + ComputationCase as C, ContextCase as X, ExtentCase as E, OwnerCase as O, SequenceCase as Q, +}; + +pub(super) type Value = usize; +pub(super) type Checked = Result; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum Fault { + Trap, + CallbackTrap, + InvalidModel, + Exhausted, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(super) enum Node { + Return(Value), + Perform(Value), + Bind(Value, Value), + Resume(Value, Value, Value), + Handle(Value, Value, Value), + OwnedResume(Value, Value), + Empty, + Cons(Value, Value), + Append(Value, Value), + Finish(Value, Value, Value), + RootContext(Value), + ScopedContext { + ret: Value, + op: Value, + saved: Value, + parent: Value, + caller: Value, + extent: Value, + }, + RootExtent(Value), + ScopedExtent { + status: Value, + parent: Value, + }, + Owner { + callback: Value, + status: Value, + }, +} + +impl Node { + pub(super) fn case(&self) -> Case { + match self { + Self::Return(_) => Case::Computation(C::Return), + Self::Perform(_) => Case::Computation(C::Perform), + Self::Bind(..) => Case::Computation(C::Bind), + Self::Resume(..) => Case::Computation(C::Resume), + Self::Handle(..) => Case::Computation(C::Handle), + Self::OwnedResume(..) => Case::Computation(C::OwnedResume), + Self::Empty => Case::Sequence(Q::Empty), + Self::Cons(..) => Case::Sequence(Q::Cons), + Self::Append(..) => Case::Sequence(Q::Append), + Self::Finish(..) => Case::Sequence(Q::Finish), + Self::RootContext(_) => Case::Context(X::Root), + Self::ScopedContext { .. } => Case::Context(X::Scoped), + Self::RootExtent(_) => Case::Extent(E::Root), + Self::ScopedExtent { .. } => Case::Extent(E::Scoped), + Self::Owner { .. } => Case::Owner(O::OneShot), + } + } + + pub(super) fn fields(&self) -> Vec { + match *self { + Self::Return(v) | Self::Perform(v) | Self::RootContext(v) | Self::RootExtent(v) => { + vec![v] + } + Self::Bind(a, b) | Self::OwnedResume(a, b) | Self::Cons(a, b) | Self::Append(a, b) => { + vec![a, b] + } + Self::Resume(a, b, c) | Self::Handle(a, b, c) | Self::Finish(a, b, c) => vec![a, b, c], + Self::Empty => vec![], + Self::ScopedContext { + ret, + op, + saved, + parent, + caller, + extent, + } => vec![ret, op, saved, parent, caller, extent], + Self::ScopedExtent { status, parent } => vec![status, parent], + Self::Owner { callback, status } => vec![callback, status], + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(super) enum Object { + Node(Node), + Atom(&'static str), + Status(i32), + CallbackCell(Value), + Function { + label: &'static str, + result: Checked, + }, + Raw { + queue: Value, + checkpoint: Value, + }, + Pair(Value, Value), + Replay(Value, Value), + Unknown(Family, u32), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(super) enum Event { + ReadStatus(Value, i32), + WriteStatus(Value, i32), + ReadCallback(Value, Value), + WriteCallback(Value, Value), + Apply(Value, Vec), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(super) enum Step { + Return(Value), + Publish(Value), + Advance(Vec), +} + +#[derive(Clone, Debug, Default)] +pub(super) struct Heap { + pub objects: Vec, + pub events: Vec, + /// The flat reference replay budgets the traversal nodes it does not build. + /// The plan interpreter allocates those nodes instead of using this counter. + pub queue_work: usize, +} + +impl Heap { + pub(super) fn alloc(&mut self, object: Object) -> Value { + let id = self.objects.len(); + self.objects.push(object); + id + } + pub(super) fn node(&mut self, node: Node) -> Value { + self.alloc(Object::Node(node)) + } + pub(super) fn get(&self, value: Value) -> Checked<&Object> { + self.objects.get(value).ok_or(Fault::InvalidModel) + } + pub(super) fn get_node(&self, value: Value) -> Checked { + match self.get(value)? { + Object::Node(node) => Ok(node.clone()), + Object::Unknown(..) => Err(Fault::Trap), + _ => Err(Fault::InvalidModel), + } + } + pub(super) fn extent(&mut self, parent: Option) -> Value { + let status = self.alloc(Object::Status(0)); + self.node(match parent { + Some(parent) => Node::ScopedExtent { status, parent }, + None => Node::RootExtent(status), + }) + } + pub(super) fn status(&mut self, cell: Value) -> Checked { + let Object::Status(value) = self.get(cell)? else { + return Err(Fault::InvalidModel); + }; + let value = *value; + self.events.push(Event::ReadStatus(cell, value)); + Ok(value) + } + pub(super) fn set_status(&mut self, cell: Value, value: i32) -> Checked<()> { + let Some(Object::Status(status)) = self.objects.get_mut(cell) else { + return Err(Fault::InvalidModel); + }; + *status = value; + self.events.push(Event::WriteStatus(cell, value)); + Ok(()) + } + pub(super) fn callback(&mut self, cell: Value) -> Checked { + let Object::CallbackCell(value) = self.get(cell)? else { + return Err(Fault::InvalidModel); + }; + let value = *value; + self.events.push(Event::ReadCallback(cell, value)); + Ok(value) + } + pub(super) fn set_callback(&mut self, cell: Value, value: Value) -> Checked<()> { + let Some(Object::CallbackCell(slot)) = self.objects.get_mut(cell) else { + return Err(Fault::InvalidModel); + }; + *slot = value; + self.events.push(Event::WriteCallback(cell, value)); + Ok(()) + } + pub(super) fn invoke(&mut self, function: Value, args: Vec) -> Checked { + self.events.push(Event::Apply(function, args)); + match self.get(function)? { + Object::Function { result, .. } => *result, + _ => Err(Fault::InvalidModel), + } + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/model/interpret.rs b/crates/psrs-backend/src/cc/lower/control/plan/model/interpret.rs new file mode 100644 index 00000000..1d9fd516 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/model/interpret.rs @@ -0,0 +1,221 @@ +//! Execute the actual plan with concrete references, state cells and callbacks. +use super::super::ir::{Case, Exec, Operand, State}; +use super::heap::*; +use crate::control::execution::{ActionId, Program, Selection}; +use crate::control::schema::ControlHelperRole as R; +use std::collections::{HashMap, HashSet}; +use std::rc::Rc; + +pub(super) enum Flow { + Exit(Step), + Tail(Program, Vec), +} + +pub(super) struct Interpreter { + pub heap: Heap, + pub actions: HashSet, + pub plans: HashMap>, + fuel: usize, +} + +pub(super) fn program(role: R) -> Program { + match role { + R::Executor => Program::Executor, + R::Run => Program::Run, + R::CloneSequence => Program::CloneSequence, + R::RebuildSequence => Program::RebuildSequence, + R::Start => Program::Start, + R::Check => Program::Check, + R::Close => Program::Close, + R::Once => Program::Once, + R::Cancel => Program::Cancel, + } +} + +impl Interpreter { + pub(super) fn new(heap: Heap) -> Self { + let plans = [ + Program::Executor, + Program::Run, + Program::CloneSequence, + Program::RebuildSequence, + Program::Start, + Program::Check, + Program::Close, + Program::Once, + Program::Cancel, + Program::RawResume, + ] + .into_iter() + .map(|program| { + ( + program, + Rc::new(super::super::build(program).exec().clone()), + ) + }) + .collect(); + Self { + heap, + plans, + actions: HashSet::new(), + fuel: 100_000, + } + } + + pub(super) fn tick(&mut self) -> Checked<()> { + self.fuel = self.fuel.checked_sub(1).ok_or(Fault::Exhausted)?; + Ok(()) + } + pub(super) fn reset(&mut self, heap: Heap) { + self.heap = heap; + self.fuel = 100_000; + } + + pub(super) fn run(&mut self, mut program: Program, mut params: Vec) -> Checked { + loop { + self.tick()?; + let plan = self + .plans + .get(&program) + .cloned() + .ok_or(Fault::InvalidModel)?; + let mut path = Vec::new(); + let flow = self.exec(&plan, ¶ms, &mut Vec::new(), &mut path); + if let Some(action) = ActionId::ALL + .into_iter() + .find(|action| action.selection() == (program, path.as_slice())) + { + self.actions.insert(action); + } + match flow? { + Flow::Exit(step) => return Ok(step), + Flow::Tail(target, args) => { + program = target; + params = args; + } + } + } + } + + pub(super) fn call(&mut self, target: Program, args: Vec) -> Checked { + let mut step = self.run(target, args)?; + loop { + match step { + Step::Return(value) => return Ok(value), + Step::Publish(value) if target == Program::Executor => return Ok(value), + Step::Advance(params) if target == Program::Executor => { + step = self.run(Program::Executor, params)?; + } + _ => return Err(Fault::InvalidModel), + } + } + } + + pub(super) fn apply(&mut self, function: Value, args: Vec) -> Checked { + if matches!(self.heap.get(function)?, Object::Raw { .. }) { + self.heap.events.push(Event::Apply(function, args.clone())); + let [value] = args.as_slice() else { + return Err(Fault::InvalidModel); + }; + self.call(Program::RawResume, vec![function, *value]) + } else { + self.heap.invoke(function, args) + } + } + + pub(super) fn operand(operand: &Operand, params: &[Value], env: &[Value]) -> Checked { + match operand { + Operand::State(state) => params.get(match state { + State::Value => 0, + State::Handler => 1, + State::Computation => 2, + State::Sequence => 3, + State::Extent => 4, + State::Context => 5, + }), + Operand::Param(index) => params.get(*index), + Operand::Temp(index) => env.get(*index), + } + .copied() + .ok_or(Fault::InvalidModel) + } + + pub(super) fn operands( + values: &[Operand], + params: &[Value], + env: &[Value], + ) -> Checked> { + values + .iter() + .map(|value| Self::operand(value, params, env)) + .collect() + } + + fn exec( + &mut self, + exec: &Exec, + params: &[Value], + env: &mut Vec, + path: &mut Vec, + ) -> Checked { + self.tick()?; + match exec { + Exec::Steps { ops, next } => { + for op in ops { + let value = self.op(op, params, env)?; + env.push(value); + } + self.exec(next, params, env, path) + } + Exec::Switch { source, case, arms } => { + let source = Self::operand(source, params, env)?; + let (family, tag) = match self.heap.get(source)? { + Object::Node(node) => (node.case().family(), node.case().tag()), + Object::Unknown(family, tag) => (*family, *tag), + _ => return Err(Fault::InvalidModel), + }; + if family != case.family() { + return Err(Fault::InvalidModel); + } + let arm = arms + .iter() + .find(|(candidate, _)| *candidate == tag) + .ok_or(Fault::Trap)?; + let actual = self.heap.get_node(source)?.case(); + path.push(match actual { + Case::Computation(case) => Selection::Computation(case), + Case::Sequence(case) => Selection::Sequence(case), + Case::Context(case) => Selection::Context(case), + Case::Extent(case) => Selection::Extent(case), + Case::Owner(_) => return Err(Fault::InvalidModel), + }); + self.exec(&arm.1, params, &mut env.clone(), path) + } + Exec::Advance { + state, + handler, + computation, + sequence, + extent, + context, + } => { + let values = [state, handler, computation, sequence, extent, context] + .into_iter() + .map(|o| Self::operand(o, params, env)) + .collect::>>()?; + Ok(Flow::Exit(Step::Advance(values))) + } + Exec::Publish { result } => Ok(Flow::Exit(Step::Publish(Self::operand( + result, params, env, + )?))), + Exec::Return { value } => { + Ok(Flow::Exit(Step::Return(Self::operand(value, params, env)?))) + } + Exec::Tail { role, args } => Ok(Flow::Tail( + program(*role), + Self::operands(args, params, env)?, + )), + Exec::Reject { .. } => Err(Fault::Trap), + } + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/model/mod.rs b/crates/psrs-backend/src/cc/lower/control/plan/model/mod.rs new file mode 100644 index 00000000..23819926 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/model/mod.rs @@ -0,0 +1,223 @@ +//! Bounded construction-model tests, separate from optimized target execution. +//! +//! `interpret` executes actual plans with a concrete heap. `reference` implements +//! direct typed operational semantics and flat-list replay, without consuming +//! the plans. `behavior` compares outcomes, ordered observations, source identity +//! and fresh-reference aliasing; `mutations` proves that semantically wrong but +//! structurally checked plans are distinguishable. These tests do not interpret +//! arbitrary source callback bodies or prove optimizer preservation. +//! +//! The terminal-shape tables retained below are complementary structural tests. + +mod behavior; +mod compare; +mod continuations; +mod failure_prefixes; +mod fixtures; +mod heap; +mod interpret; +mod mutations; +mod operations; +mod reference; +mod runs; + +use super::ir::{Case, Exec}; +use super::shape::Terminal; +use crate::control::schema::{ComputationCase, ContextCase, ExtentCase, SequenceCase}; + +/// One trace: the refinement path and its terminal outcome. +type Trace = (Vec<(Case, u32)>, Terminal); +/// A named plan and the traces the semantics require of it. +type Planned = (&'static str, Exec, Vec); + +/// The terminal outcome reached by the plan along exactly `path`. +pub(super) fn terminal_of(exec: &Exec, path: &[(Case, u32)]) -> Option { + match path.split_first() { + None => leaf(exec), + Some(((case, tag), rest)) => match exec { + Exec::Steps { next, .. } => terminal_of(next, path), + Exec::Switch { + case: actual, arms, .. + } if same_family(*actual, *case) => { + let arm = arms.iter().find(|(candidate, _)| candidate == tag)?; + terminal_of(&arm.1, rest) + } + _ => None, + }, + } +} + +fn leaf(exec: &Exec) -> Option { + match exec { + Exec::Steps { next, .. } => leaf(next), + Exec::Advance { .. } => Some(Terminal::Advance), + Exec::Publish { .. } | Exec::Return { .. } => Some(Terminal::Value), + Exec::Tail { .. } => Some(Terminal::Tail), + Exec::Reject { .. } => Some(Terminal::Reject), + Exec::Switch { .. } => None, + } +} + +/// A switch declares one case as a family marker; the refined tag is the path. +fn same_family(left: Case, right: Case) -> bool { + std::mem::discriminant(&left) == std::mem::discriminant(&right) +} + +/// The number of terminal traces the plan has. +pub(super) fn trace_count(exec: &Exec) -> usize { + match exec { + Exec::Steps { next, .. } => trace_count(next), + Exec::Switch { arms, .. } => arms.iter().map(|(_, arm)| trace_count(arm)).sum(), + _ => 1, + } +} + +fn c(case: ComputationCase) -> Case { + Case::Computation(case) +} +fn s(case: SequenceCase) -> Case { + Case::Sequence(case) +} +fn x(case: ContextCase) -> Case { + Case::Context(case) +} +fn e(case: ExtentCase) -> Case { + Case::Extent(case) +} + +/// The transition traces required by the semantics, independent of the builder. +pub(super) fn expected() -> Vec { + use ComputationCase::*; + use ContextCase::*; + use SequenceCase::*; + use Terminal::*; + vec![ + // Return: publish at the root, or apply the return clause and advance. + (vec![(c(Return), 0), (s(Empty), 0), (x(Root), 0)], Value), + (vec![(c(Return), 0), (s(Empty), 0), (x(Scoped), 1)], Advance), + (vec![(c(Return), 0), (s(Cons), 1)], Advance), + (vec![(c(Return), 0), (s(Append), 2), (s(Empty), 0)], Advance), + (vec![(c(Return), 0), (s(Append), 2), (s(Cons), 1)], Advance), + ( + vec![(c(Return), 0), (s(Append), 2), (s(Append), 2)], + Advance, + ), + ( + vec![(c(Return), 0), (s(Append), 2), (s(Finish), 3)], + Advance, + ), + (vec![(c(Return), 0), (s(Finish), 3)], Advance), + // Perform: root applies the handler, scoped applies the operation clause. + (vec![(c(Perform), 1), (x(Root), 0)], Advance), + (vec![(c(Perform), 1), (x(Scoped), 1)], Advance), + // Inert frames advance directly. + (vec![(c(Bind), 2)], Advance), + (vec![(c(Resume), 3)], Advance), + // Handle: normalize Append, otherwise enter under a fresh or shared extent. + (vec![(c(Handle), 4), (s(Empty), 0)], Advance), + (vec![(c(Handle), 4), (s(Cons), 1)], Advance), + (vec![(c(Handle), 4), (s(Append), 2), (s(Empty), 0)], Advance), + (vec![(c(Handle), 4), (s(Append), 2), (s(Cons), 1)], Advance), + ( + vec![(c(Handle), 4), (s(Append), 2), (s(Append), 2)], + Advance, + ), + ( + vec![(c(Handle), 4), (s(Append), 2), (s(Finish), 3)], + Advance, + ), + (vec![(c(Handle), 4), (s(Finish), 3)], Advance), + (vec![(c(OwnedResume), 5)], Advance), + ] +} + +/// The helper bodies' required traces, independent of their builders. +pub(super) fn helper_expected() -> Vec { + use ExtentCase::*; + use SequenceCase::*; + use Terminal::*; + vec![ + ( + "clone_sequence", + super::clone_sequence(), + vec![ + (vec![(s(Empty), 0), (s(Empty), 0)], Tail), + (vec![(s(Empty), 0), (s(Append), 2)], Tail), + (vec![(s(Empty), 0), (s(Cons), 1)], Reject), + (vec![(s(Empty), 0), (s(Finish), 3)], Reject), + (vec![(s(Cons), 1)], Tail), + (vec![(s(Append), 2)], Tail), + (vec![(s(Finish), 3)], Tail), + ], + ), + ( + "rebuild_sequence", + super::rebuild_sequence(), + vec![ + (vec![(s(Empty), 0)], Value), + (vec![(s(Cons), 1)], Tail), + (vec![(s(Append), 2)], Reject), + (vec![(s(Finish), 3)], Tail), + ], + ), + ( + "boundary_check", + super::boundary_check(), + vec![(vec![(e(Root), 0)], Value), (vec![(e(Scoped), 1)], Tail)], + ), + ( + "boundary_close", + super::boundary_close(), + vec![(vec![(e(Root), 0)], Value), (vec![(e(Scoped), 1)], Value)], + ), + ( + "start_boundary", + super::start_boundary(), + vec![(vec![], Value)], + ), + ("once", super::once(), vec![(vec![], Value)]), + ("cancel", super::cancel(), vec![(vec![], Value)]), + ("raw_resume", super::raw_resume(), vec![(vec![], Value)]), + ] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn executor_terminal_shape_matches_the_specification() { + let plan = super::super::executor(); + let expected = expected(); + assert_eq!( + trace_count(&plan), + expected.len(), + "plan trace count disagrees with the model" + ); + for (path, terminal) in &expected { + assert_eq!( + terminal_of(&plan, path).as_ref(), + Some(terminal), + "plan has no matching terminal for {path:?}" + ); + } + } + + #[test] + fn helper_terminal_shapes_match_the_specification() { + for (name, plan, expected) in helper_expected() { + assert_eq!( + trace_count(&plan), + expected.len(), + "{name} trace count disagrees with the model" + ); + for (path, terminal) in &expected { + assert_eq!( + terminal_of(&plan, path).as_ref(), + Some(terminal), + "{name} has no matching terminal for {path:?}" + ); + } + } + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/model/mutations/flows.rs b/crates/psrs-backend/src/cc/lower/control/plan/model/mutations/flows.rs new file mode 100644 index 00000000..582ee26b --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/model/mutations/flows.rs @@ -0,0 +1,155 @@ +use super::*; + +#[test] +fn swapped_same_shaped_clauses_do_not_pass_the_behavioral_model() { + let mut f = Fixture::new(); + let computation = f.heap.node(Node::Return(f.answer)); + let args = f.args(computation, f.empty, f.scoped); + detects(f, Program::Executor, args, |plan| { + assert!(edit(arm(plan, C::Return), &mut |op| { + if let Op::Project { + case: Case::Context(X::Scoped), + field, + .. + } = op + && *field == 0 + { + *field = 1; + return true; + } + false + })); + }); +} + +#[test] +fn target_finish_restores_its_caller_extent_not_its_owned_extent() { + let mut f = Fixture::new(); + let computation = f.heap.node(Node::Return(f.answer)); + let args = f.args(computation, f.empty, f.scoped); + detects(f, Program::Executor, args, |plan| { + assert!(edit(arm(plan, C::Return), &mut |op| { + if let Op::Project { + case: Case::Context(X::Scoped), + field, + .. + } = op + && *field == 3 + { + *field = 5; + return true; + } + false + })); + }); +} + +#[test] +fn callbacks_receive_the_current_answer_instead_of_the_root_state() { + let mut f = Fixture::new(); + let queue = f.heap.node(Node::Cons(f.callback, f.empty)); + let computation = f.heap.node(Node::Return(f.answer)); + let args = f.args(computation, queue, f.root); + detects(f, Program::Executor, args, |plan| { + let Exec::Steps { next, .. } = arm(plan, C::Return) else { + unreachable!() + }; + let Exec::Switch { arms, .. } = next.as_mut() else { + unreachable!() + }; + let cons = &mut arms + .iter_mut() + .find(|(tag, _)| *tag == Q::Cons.tag()) + .unwrap() + .1; + assert!(edit(cons, &mut |op| { + if let Op::Apply { args, .. } = op { + args[0] = Operand::State(State::Value); + return true; + } + false + })); + }); +} + +#[test] +fn bind_enters_its_inner_computation_instead_of_reentering_itself() { + let mut f = Fixture::new(); + let computation = f.heap.node(Node::Bind(f.next, f.callback)); + let args = f.args(computation, f.consumer, f.scoped); + detects(f, Program::Executor, args, |plan| { + let Exec::Steps { next, .. } = arm(plan, C::Bind) else { + unreachable!() + }; + let Exec::Advance { computation, .. } = next.as_mut() else { + unreachable!() + }; + *computation = Operand::State(State::Computation); + }); +} + +#[test] +fn root_handler_state_and_next_computation_do_not_share_a_projection() { + let mut f = Fixture::new(); + let computation = f.heap.node(Node::Perform(f.request)); + let args = f.args(computation, f.consumer, f.root); + detects(f, Program::Executor, args, |plan| { + assert!(edit(arm(plan, C::Perform), &mut |op| { + if let Op::PairField { field, .. } = op + && *field == 0 + { + *field = 1; + return true; + } + false + })); + }); +} + +#[test] +fn finish_closes_its_owned_extent_not_its_restore_parent() { + let mut f = Fixture::new(); + let computation = f.heap.node(Node::Return(f.answer)); + let queue = f.heap.node(Node::Finish(f.checkpoint, f.empty, f.parent)); + let args = f.args(computation, queue, f.scoped); + detects(f, Program::Executor, args, |plan| { + let Exec::Steps { next, .. } = arm(plan, C::Return) else { + unreachable!() + }; + let Exec::Switch { arms, .. } = next.as_mut() else { + unreachable!() + }; + let finish = &mut arms + .iter_mut() + .find(|(tag, _)| *tag == Q::Finish.tag()) + .unwrap() + .1; + assert!(edit(finish, &mut |op| { + if let Op::CloseBoundary { extent } = op { + *extent = Operand::State(State::Extent); + return true; + } + false + })); + }); +} + +#[test] +fn handle_keeps_caller_and_owned_extents_distinct() { + let mut f = Fixture::new(); + let computation = f.heap.node(Node::Handle(f.ret, f.op, f.next)); + let args = f.args(computation, f.empty, f.scoped); + detects(f, Program::Executor, args, |plan| { + assert!(edit(arm(plan, C::Handle), &mut |op| { + if let Op::Node { + case: Case::Context(X::Scoped), + fields, + } = op + { + fields[3] = fields[5].clone(); + return true; + } + false + })); + }); +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/model/mutations/lifetime.rs b/crates/psrs-backend/src/cc/lower/control/plan/model/mutations/lifetime.rs new file mode 100644 index 00000000..6514941d --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/model/mutations/lifetime.rs @@ -0,0 +1,148 @@ +use super::*; + +#[test] +fn unused_private_allocations_are_not_hidden_by_fresh_id_normalization() { + for allocation in [ + Op::NewStatus, + Op::FreshExtent { parent: None }, + Op::EmptySequence, + ] { + let f = Fixture::new(); + detects(f, Program::Start, vec![], |plan| { + let Exec::Steps { ops, .. } = plan else { + unreachable!() + }; + ops.push(allocation); + }); + } +} + +#[test] +fn owned_phases_remain_ordered_even_when_the_final_status_matches() { + let mut f = Fixture::new(); + let computation = f.heap.node(Node::OwnedResume(f.owner, f.answer)); + let args = f.args(computation, f.consumer, f.root); + detects(f, Program::Executor, args, |plan| { + assert!(edit(arm(plan, C::OwnedResume), &mut |op| { + if let Op::SetStatus { value, .. } = op + && *value == 1 + { + *value = 2; + return true; + } + false + })); + }); +} + +#[test] +fn ownership_transfer_releases_the_callback_instead_of_retaining_it() { + let mut f = Fixture::new(); + let computation = f.heap.node(Node::OwnedResume(f.owner, f.answer)); + let args = f.args(computation, f.consumer, f.root); + detects(f, Program::Executor, args, |plan| { + assert!(edit(arm(plan, C::OwnedResume), &mut |op| { + if let Op::WriteCallback { value, .. } = op { + *value = Operand::Temp(6); + return true; + } + false + })); + }); +} + +#[test] +fn cancellation_marks_cancelled_instead_of_consumed() { + let f = Fixture::new(); + let args = vec![f.owner]; + detects(f, Program::Cancel, args, |plan| { + assert!(edit(plan, &mut |op| { + if let Op::SetStatus { value, .. } = op { + *value = 2; + return true; + } + false + })); + }); +} + +#[test] +fn checking_an_expired_extent_cannot_reopen_it() { + let mut f = Fixture::new(); + f.expire(f.checkpoint); + let args = vec![f.checkpoint]; + detects(f, Program::Check, args, |plan| { + assert!(edit(plan, &mut |op| { + if let Op::GuardPending { cell } = op { + *op = Op::SetStatus { + cell: cell.clone(), + value: 0, + }; + return true; + } + false + })); + }); +} + +#[test] +fn close_cannot_leave_an_extent_live() { + let f = Fixture::new(); + let args = vec![f.checkpoint]; + detects(f, Program::Close, args, |plan| { + assert!(edit(plan, &mut |op| { + if let Op::SetStatus { value, .. } = op { + *value = 0; + return true; + } + false + })); + }); +} + +#[test] +fn raw_resume_checks_its_saved_checkpoint_before_publishing_a_frame() { + let mut f = Fixture::new(); + f.expire(f.checkpoint); + let args = vec![f.raw, f.answer]; + detects(f, Program::RawResume, args, |plan| { + assert!(edit(plan, &mut |op| { + if let Op::CheckBoundary { extent } = op { + *op = Op::FreshExtent { + parent: Some(extent.clone()), + }; + return true; + } + false + })); + }); +} + +#[test] +fn run_cannot_omit_normal_root_cleanup_or_publish_an_unrelated_result() { + for wrong_result in [false, true] { + let f = Fixture::new(); + let args = vec![f.state, f.handler, f.next]; + detects(f, Program::Run, args, |plan| { + let Exec::Steps { ops, next } = plan else { + unreachable!() + }; + if wrong_result { + **next = Exec::Return { + value: Operand::Param(0), + }; + } else { + let close = ops + .iter_mut() + .find(|op| matches!(op, Op::CloseBoundary { .. })) + .unwrap(); + let Op::CloseBoundary { extent } = close else { + unreachable!() + }; + *close = Op::CheckBoundary { + extent: extent.clone(), + }; + } + }); + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/model/mutations/mod.rs b/crates/psrs-backend/src/cc/lower/control/plan/model/mutations/mod.rs new file mode 100644 index 00000000..f42e19fa --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/model/mutations/mod.rs @@ -0,0 +1,62 @@ +//! The model must detect semantically wrong plans that still pass logical +//! type/scope, action coverage and terminal-shape checking. +mod flows; +mod lifetime; +mod replay; + +use super::super::{ + build, check, + ir::{Case, Exec, Op, Operand, State}, +}; +use super::{ + compare::equivalent, fixtures::Fixture, heap::*, interpret::Interpreter, reference::Reference, +}; +use crate::control::{ + execution::Program, + schema::{ComputationCase as C, ContextCase as X, SequenceCase as Q}, +}; +use std::rc::Rc; + +fn edit(plan: &mut Exec, change: &mut impl FnMut(&mut Op) -> bool) -> bool { + match plan { + Exec::Steps { ops, next } => ops.iter_mut().any(&mut *change) || edit(next, change), + Exec::Switch { arms, .. } => arms.iter_mut().any(|(_, arm)| edit(arm, change)), + _ => false, + } +} + +fn arm(plan: &mut Exec, tag: C) -> &mut Exec { + let Exec::Switch { arms, .. } = plan else { + panic!("executor begins with computation selection"); + }; + &mut arms + .iter_mut() + .find(|(case, _)| *case == tag.tag()) + .unwrap() + .1 +} + +fn detects(f: Fixture, program: Program, args: Vec, change: impl FnOnce(&mut Exec)) { + let mut interpreter = Interpreter::new(f.heap.clone()); + let mut plan = build(program).exec().clone(); + change(&mut plan); + check::check_for(program,&plan).expect("the negative must preserve the checked plan's logical types, scope, action inventory and terminal roles"); + interpreter.plans.insert(program, Rc::new(plan)); + let actual = interpreter.run(program, args.clone()); + let mut expected_heap = f.heap.clone(); + let expected = Reference::new(&mut expected_heap).run(program, &args); + assert!( + !matches!(actual, Err(Fault::InvalidModel | Fault::Exhausted)), + "mutation must have a defined model execution: {actual:?}" + ); + assert!( + !equivalent( + &interpreter.heap, + &actual, + &expected_heap, + &expected, + f.heap.objects.len() + ), + "model accepted a semantically wrong, structurally checked {program:?} plan" + ); +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/model/mutations/replay.rs b/crates/psrs-backend/src/cc/lower/control/plan/model/mutations/replay.rs new file mode 100644 index 00000000..171089ca --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/model/mutations/replay.rs @@ -0,0 +1,128 @@ +use super::*; + +#[test] +fn perform_captures_the_dispatch_checkpoint_not_the_execution_extent() { + let mut f = Fixture::new(); + let computation = f.heap.node(Node::Perform(f.request)); + let args = f.args(computation, f.consumer, f.root); + detects(f, Program::Executor, args, |plan| { + assert!(edit(arm(plan, C::Perform), &mut |op| { + if let Op::ResumeClosure { captures } = op { + captures[1] = Operand::State(State::Extent); + return true; + } + false + })); + }); +} + +#[test] +fn resume_checks_the_saved_checkpoint_not_the_current_execution_extent() { + let mut f = Fixture::new(); + f.expire(f.checkpoint); + let computation = f.heap.node(Node::Resume(f.answer, f.saved, f.checkpoint)); + let args = f.args(computation, f.consumer, f.scoped); + detects(f, Program::Executor, args, |plan| { + assert!(edit(arm(plan, C::Resume), &mut |op| { + if let Op::CheckBoundary { extent } = op { + *extent = Operand::State(State::Extent); + return true; + } + false + })); + }); +} + +#[test] +fn clone_cannot_drop_the_left_queue_when_pending_right_work_exists() { + let mut f = Fixture::new(); + let queue = f.heap.node(Node::Append(f.saved, f.consumer)); + let args = vec![queue, f.empty, f.empty, f.active, f.parent]; + detects(f, Program::CloneSequence, args, |plan| { + assert!(edit(plan, &mut |op| { + if let Op::Project { + case: Case::Sequence(Q::Append), + source: Operand::Param(0), + field, + .. + } = op + && *field == 0 + { + *field = 1; + return true; + } + false + })); + }); +} + +#[test] +fn resume_appends_captured_work_before_consumer_work() { + let mut f = Fixture::new(); + let computation = f.heap.node(Node::Resume(f.answer, f.saved, f.checkpoint)); + let args = f.args(computation, f.consumer, f.scoped); + detects(f, Program::Executor, args, |plan| { + assert!(edit(arm(plan, C::Resume), &mut |op| { + if let Op::Node { + case: Case::Sequence(Q::Append), + fields, + } = op + { + fields.swap(0, 1); + return true; + } + false + })); + }); +} + +#[test] +fn replay_parents_fresh_activations_to_dispatch_not_execution() { + let mut f = Fixture::new(); + let queue = f.heap.node(Node::Finish(f.checkpoint, f.empty, f.parent)); + let args = vec![queue, f.empty, f.active, f.parent]; + detects(f, Program::RebuildSequence, args, |plan| { + assert!(edit(plan, &mut |op| { + if let Op::FreshExtent { parent } = op { + *parent = Some(Operand::Param(2)); + return true; + } + false + })); + }); +} + +#[test] +fn replay_must_not_reuse_a_saved_extent_as_a_fresh_activation() { + let mut f = Fixture::new(); + let queue = f.heap.node(Node::Finish(f.checkpoint, f.empty, f.parent)); + let args = vec![queue, f.empty, f.active, f.parent]; + detects(f, Program::RebuildSequence, args, |plan| { + assert!(edit(plan, &mut |op| { + if matches!(op, Op::FreshExtent { .. }) { + *op = Op::Project { + source: Operand::Param(0), + case: Case::Sequence(Q::Finish), + field: 0, + }; + return true; + } + false + })); + }); +} + +#[test] +fn replay_result_keeps_the_rebuilt_queue_and_matching_activation() { + let f = Fixture::new(); + let args = vec![f.empty, f.consumer, f.active, f.parent]; + detects(f, Program::RebuildSequence, args, |plan| { + assert!(edit(plan, &mut |op| { + if let Op::Product { fields, .. } = op { + fields[0] = Operand::Param(0); + return true; + } + false + })); + }); +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/model/operations.rs b/crates/psrs-backend/src/cc/lower/control/plan/model/operations.rs new file mode 100644 index 00000000..a98ddb54 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/model/operations.rs @@ -0,0 +1,168 @@ +//! Interpret plan operands and instructions; no reference transition is used. +use super::super::ir::{Case, Op, Product}; +use super::heap::*; +use super::interpret::{Interpreter, program}; +use crate::control::execution::Program; +use crate::control::schema::{ + ComputationCase as C, ContextCase as X, ExtentCase as E, OwnerCase as O, SequenceCase as Q, +}; + +impl Interpreter { + pub(super) fn op(&mut self, op: &Op, params: &[Value], env: &[Value]) -> Checked { + self.tick()?; + let value = |o| Self::operand(o, params, env); + match op { + Op::Project { + source, + case, + field, + } => { + let node = self.heap.get_node(value(source)?)?; + if node.case() != *case { + return Err(Fault::InvalidModel); + } + node.fields() + .get(*field as usize) + .copied() + .ok_or(Fault::InvalidModel) + } + Op::Node { case, fields } => { + let fields = Self::operands(fields, params, env)?; + let node = node(*case, &fields)?; + Ok(self.heap.node(node)) + } + Op::Capture { closure, index, .. } => { + let Object::Raw { queue, checkpoint } = self.heap.get(value(closure)?)? else { + return Err(Fault::InvalidModel); + }; + match *index { + 0 => Ok(*queue), + 1 => Ok(*checkpoint), + _ => Err(Fault::InvalidModel), + } + } + Op::Apply { callee, args } => { + self.apply(value(callee)?, Self::operands(args, params, env)?) + } + Op::Helper { role, args } => { + self.call(program(*role), Self::operands(args, params, env)?) + } + Op::ResumeClosure { captures } => { + let args = Self::operands(captures, params, env)?; + let [queue, checkpoint] = args.as_slice() else { + return Err(Fault::InvalidModel); + }; + Ok(self.heap.alloc(Object::Raw { + queue: *queue, + checkpoint: *checkpoint, + })) + } + Op::Pair { + state, + value: answer, + } => Ok(self.heap.alloc(Object::Pair(value(state)?, value(answer)?))), + Op::PairField { pair, field } => { + let Object::Pair(state, answer) = self.heap.get(value(pair)?)? else { + return Err(Fault::InvalidModel); + }; + match *field { + 0 => Ok(*state), + 1 => Ok(*answer), + _ => Err(Fault::InvalidModel), + } + } + Op::ReplayField { result, field } => { + let Object::Replay(queue, active) = self.heap.get(value(result)?)? else { + return Err(Fault::InvalidModel); + }; + match *field { + 0 => Ok(*queue), + 1 => Ok(*active), + _ => Err(Fault::InvalidModel), + } + } + Op::FreshExtent { parent } => { + Ok(self.heap.extent(parent.as_ref().map(value).transpose()?)) + } + Op::ContextExtent { context } => match self.heap.get_node(value(context)?)? { + Node::RootContext(extent) | Node::ScopedContext { extent, .. } => Ok(extent), + _ => Err(Fault::InvalidModel), + }, + Op::CheckBoundary { extent } => self.call(Program::Check, vec![value(extent)?]), + Op::CloseBoundary { extent } => self.call(Program::Close, vec![value(extent)?]), + Op::GuardPending { cell } => { + let cell = value(cell)?; + if self.heap.status(cell)? != 0 { + return Err(Fault::Trap); + } + Ok(cell) + } + Op::SetStatus { + cell, + value: status, + } => { + let cell = value(cell)?; + self.heap.set_status(cell, *status)?; + Ok(cell) + } + Op::ReadCallback { cell } => self.heap.callback(value(cell)?), + Op::WriteCallback { + cell, + value: replacement, + } => { + let cell = value(cell)?; + self.heap.set_callback(cell, value(replacement)?)?; + Ok(cell) + } + Op::NewCallbackCell { value: callback } => { + Ok(self.heap.alloc(Object::CallbackCell(value(callback)?))) + } + Op::NewStatus => Ok(self.heap.alloc(Object::Status(0))), + Op::Product { + product: Product::ReplayResult, + fields, + } => { + let fields = Self::operands(fields, params, env)?; + let [queue, active] = fields.as_slice() else { + return Err(Fault::InvalidModel); + }; + Ok(self.heap.alloc(Object::Replay(*queue, *active))) + } + Op::EmptySequence => Ok(self.heap.node(Node::Empty)), + } + } +} + +fn node(case: Case, fields: &[Value]) -> Checked { + Ok(match (case, fields) { + (Case::Computation(C::Return), [a]) => Node::Return(*a), + (Case::Computation(C::Perform), [a]) => Node::Perform(*a), + (Case::Computation(C::Bind), [a, b]) => Node::Bind(*a, *b), + (Case::Computation(C::Resume), [a, b, c]) => Node::Resume(*a, *b, *c), + (Case::Computation(C::Handle), [a, b, c]) => Node::Handle(*a, *b, *c), + (Case::Computation(C::OwnedResume), [a, b]) => Node::OwnedResume(*a, *b), + (Case::Sequence(Q::Empty), []) => Node::Empty, + (Case::Sequence(Q::Cons), [a, b]) => Node::Cons(*a, *b), + (Case::Sequence(Q::Append), [a, b]) => Node::Append(*a, *b), + (Case::Sequence(Q::Finish), [a, b, c]) => Node::Finish(*a, *b, *c), + (Case::Context(X::Root), [a]) => Node::RootContext(*a), + (Case::Context(X::Scoped), [a, b, c, d, e, f]) => Node::ScopedContext { + ret: *a, + op: *b, + saved: *c, + parent: *d, + caller: *e, + extent: *f, + }, + (Case::Extent(E::Root), [a]) => Node::RootExtent(*a), + (Case::Extent(E::Scoped), [a, b]) => Node::ScopedExtent { + status: *a, + parent: *b, + }, + (Case::Owner(O::OneShot), [a, b]) => Node::Owner { + callback: *a, + status: *b, + }, + _ => return Err(Fault::InvalidModel), + }) +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/model/reference/executor.rs b/crates/psrs-backend/src/cc/lower/control/plan/model/reference/executor.rs new file mode 100644 index 00000000..3d99fe4a --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/model/reference/executor.rs @@ -0,0 +1,176 @@ +//! Direct operational transitions over typed nodes. +use super::super::heap::*; +use super::Reference; + +impl Reference<'_> { + pub(super) fn step(&mut self, mut state: [Value; 6]) -> Checked { + let [s, h, computation, queue, extent, context] = state; + match self.heap.get_node(computation)? { + Node::Return(value) => match self.heap.get_node(queue)? { + Node::Empty => match self.heap.get_node(context)? { + Node::RootContext(_) => { + return Ok(Step::Publish(self.heap.alloc(Object::Pair(s, value)))); + } + Node::ScopedContext { ret, .. } => { + let target = self.apply(ret, vec![value])?; + return self.enter_target(state, target); + } + _ => return Err(Fault::InvalidModel), + }, + Node::Cons(callback, rest) => { + state[2] = self.apply(callback, vec![value])?; + state[3] = rest; + } + Node::Append(left, right) => return self.append_return(state, value, left, right), + Node::Finish(owned, rest, parent) => { + self.close(owned)?; + state[3] = rest; + state[4] = parent; + } + _ => return Err(Fault::InvalidModel), + }, + Node::Perform(request) => { + let checkpoint = self.dispatch_extent(context)?; + let raw = self.heap.alloc(Object::Raw { queue, checkpoint }); + match self.heap.get_node(context)? { + Node::RootContext(_) => { + let pair = self.apply(h, vec![s, request, raw])?; + let Object::Pair(next_state, next) = *self.heap.get(pair)? else { + return Err(Fault::InvalidModel); + }; + state[0] = next_state; + state[2] = next; + state[3] = self.heap.node(Node::Empty); + } + Node::ScopedContext { op, .. } => { + let target = self.apply(op, vec![request, raw])?; + return self.enter_target(state, target); + } + _ => return Err(Fault::InvalidModel), + } + } + Node::Bind(inner, callback) => { + state[2] = inner; + state[3] = self.heap.node(Node::Cons(callback, queue)); + } + Node::Resume(value, saved, checkpoint) => { + self.check(checkpoint)?; + let parent = self.dispatch_extent(context)?; + let empty = self.heap.node(Node::Empty); + let replay = self.clone_sequence(saved, empty, empty, extent, parent)?; + let Object::Replay(replayed, activation) = *self.heap.get(replay)? else { + return Err(Fault::InvalidModel); + }; + state[2] = self.heap.node(Node::Return(value)); + state[3] = self.heap.node(Node::Append(replayed, queue)); + state[4] = activation; + } + Node::Handle(ret, op, inner) => { + if let Node::Append(left, right) = self.heap.get_node(queue)? { + return self.normalize_handle(state, left, right); + } + let (saved, parent, owned) = match self.heap.get_node(queue)? { + Node::Finish(owned, rest, parent) => (rest, parent, owned), + Node::Empty | Node::Cons(..) => (queue, extent, self.heap.extent(Some(extent))), + _ => return Err(Fault::InvalidModel), + }; + state[2] = inner; + state[3] = self.heap.node(Node::Empty); + state[4] = owned; + state[5] = self.heap.node(Node::ScopedContext { + ret, + op, + saved, + parent, + caller: context, + extent: owned, + }); + } + Node::OwnedResume(owner, value) => { + let Node::Owner { callback, status } = self.heap.get_node(owner)? else { + return Err(Fault::InvalidModel); + }; + if self.heap.status(status)? != 0 { + return Err(Fault::Trap); + } + self.heap.set_status(status, 1)?; + let callback_value = self.heap.callback(callback)?; + self.heap.set_callback(callback, status)?; + state[2] = self.apply(callback_value, vec![value])?; + self.heap.set_status(status, 2)?; + } + _ => return Err(Fault::InvalidModel), + } + Ok(Step::Advance(state.into())) + } + + fn enter_target(&mut self, mut state: [Value; 6], target: Value) -> Checked { + let Node::ScopedContext { + saved, + parent, + caller, + extent, + .. + } = self.heap.get_node(state[5])? + else { + return Err(Fault::InvalidModel); + }; + state[2] = target; + state[3] = self.heap.node(Node::Finish(extent, saved, parent)); + state[4] = extent; + state[5] = caller; + Ok(Step::Advance(state.into())) + } + + fn append_return( + &mut self, + mut state: [Value; 6], + value: Value, + left: Value, + right: Value, + ) -> Checked { + state[3] = match self.heap.get_node(left)? { + Node::Empty => right, + Node::Cons(callback, rest) => { + let queue = self.heap.node(Node::Append(rest, right)); + state[2] = self.apply(callback, vec![value])?; + queue + } + Node::Append(first, second) => { + let tail = self.heap.node(Node::Append(second, right)); + self.heap.node(Node::Append(first, tail)) + } + Node::Finish(owned, rest, parent) => { + let tail = self.heap.node(Node::Append(rest, right)); + self.heap.node(Node::Finish(owned, tail, parent)) + } + _ => return Err(Fault::InvalidModel), + }; + Ok(Step::Advance(state.into())) + } + + fn normalize_handle( + &mut self, + mut state: [Value; 6], + left: Value, + right: Value, + ) -> Checked { + state[3] = match self.heap.get_node(left)? { + Node::Empty => right, + Node::Cons(callback, rest) => { + let tail = self.heap.node(Node::Append(rest, right)); + self.heap.node(Node::Cons(callback, tail)) + } + Node::Append(first, second) => { + let tail = self.heap.node(Node::Append(second, right)); + self.heap.node(Node::Append(first, tail)) + } + Node::Finish(owned, rest, parent) => { + let tail = self.heap.node(Node::Append(rest, right)); + self.heap.node(Node::Finish(owned, tail, parent)) + } + _ => return Err(Fault::InvalidModel), + }; + Ok(Step::Advance(state.into())) + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/model/reference/mod.rs b/crates/psrs-backend/src/cc/lower/control/plan/model/reference/mod.rs new file mode 100644 index 00000000..a8640f58 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/model/reference/mod.rs @@ -0,0 +1,139 @@ +//! Operational semantics over typed values. No plan, Op, Cursor, field-offset +//! constants or emitter is inspected here. Replay uses the separate flat model. +mod executor; +mod replay; +use super::heap::*; +use crate::control::execution::Program; + +pub(super) struct Reference<'a> { + pub heap: &'a mut Heap, + fuel: usize, +} + +impl<'a> Reference<'a> { + pub(super) fn new(heap: &'a mut Heap) -> Self { + Self { + heap, + fuel: 100_000, + } + } + pub(super) fn tick(&mut self) -> Checked<()> { + self.fuel = self.fuel.checked_sub(1).ok_or(Fault::Exhausted)?; + Ok(()) + } + pub(super) fn run(&mut self, program: Program, args: &[Value]) -> Checked { + self.tick()?; + let returned = match (program, args) { + (Program::Run, [state, handler, computation]) => { + let empty = self.heap.node(Node::Empty); + let root = self.heap.extent(None); + let context = self.heap.node(Node::RootContext(root)); + let mut current = vec![*state, *handler, *computation, empty, root, context]; + let result = loop { + let args: [Value; 6] = current.try_into().map_err(|_| Fault::InvalidModel)?; + match self.step(args)? { + Step::Advance(next) => current = next, + Step::Publish(result) => break result, + _ => return Err(Fault::InvalidModel), + } + }; + self.close(root)?; + result + } + (Program::Executor, [s, h, c, q, e, x]) => return self.step([*s, *h, *c, *q, *e, *x]), + (Program::CloneSequence, [queue, todo, reversed, active, parent]) => { + self.clone_sequence(*queue, *todo, *reversed, *active, *parent)? + } + (Program::RebuildSequence, [reversed, result, active, parent]) => { + self.rebuild_sequence(*reversed, *result, *active, *parent)? + } + (Program::Check, [extent]) => self.check(*extent)?, + (Program::Close, [extent]) => { + self.close(*extent)?; + *extent + } + (Program::Start, []) => self.heap.extent(None), + (Program::Once, [callback]) => { + let callback = self.heap.alloc(Object::CallbackCell(*callback)); + let status = self.heap.alloc(Object::Status(0)); + self.heap.node(Node::Owner { callback, status }) + } + (Program::Cancel, [owner]) => { + let Node::Owner { callback, status } = self.heap.get_node(*owner)? else { + return Err(Fault::InvalidModel); + }; + if self.heap.status(status)? != 0 { + return Err(Fault::Trap); + } + self.heap.set_status(status, 3)?; + self.heap.set_callback(callback, status)?; + *owner + } + (Program::RawResume, [receiver, value]) => { + let Object::Raw { queue, checkpoint } = *self.heap.get(*receiver)? else { + return Err(Fault::InvalidModel); + }; + self.check(checkpoint)?; + self.heap.node(Node::Resume(*value, queue, checkpoint)) + } + _ => return Err(Fault::InvalidModel), + }; + Ok(Step::Return(returned)) + } + + pub(super) fn check(&mut self, mut extent: Value) -> Checked { + loop { + self.tick()?; + let (status, parent) = match self.heap.get_node(extent)? { + Node::RootExtent(status) => (status, None), + Node::ScopedExtent { status, parent } => (status, Some(parent)), + _ => return Err(Fault::InvalidModel), + }; + if self.heap.status(status)? != 0 { + return Err(Fault::Trap); + } + match parent { + Some(parent) => extent = parent, + None => return Ok(extent), + } + } + } + + fn close(&mut self, extent: Value) -> Checked<()> { + match self.heap.get_node(extent)? { + Node::RootExtent(status) | Node::ScopedExtent { status, .. } => { + self.heap.set_status(status, 1) + } + _ => Err(Fault::InvalidModel), + } + } + + fn dispatch_extent(&self, context: Value) -> Checked { + match self.heap.get_node(context)? { + Node::RootContext(extent) | Node::ScopedContext { extent, .. } => Ok(extent), + _ => Err(Fault::InvalidModel), + } + } + + fn apply(&mut self, callee: Value, args: Vec) -> Checked { + if matches!(self.heap.get(callee)?, Object::Raw { .. }) { + self.heap.events.push(Event::Apply(callee, args.clone())); + let Step::Return(value) = self.run( + Program::RawResume, + &[ + callee, + *args + .first() + .filter(|_| args.len() == 1) + .ok_or(Fault::InvalidModel)?, + ], + )? + else { + return Err(Fault::InvalidModel); + }; + Ok(value) + } else { + self.heap.invoke(callee, args) + } + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/model/reference/replay.rs b/crates/psrs-backend/src/cc/lower/control/plan/model/reference/replay.rs new file mode 100644 index 00000000..a534cc51 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/model/reference/replay.rs @@ -0,0 +1,125 @@ +//! A flat occurrence-list replay specification, independent of the plan's +//! traversal-stack/reversed-node construction. Callback and owner references +//! are never copied; only activation extents and queue structure are fresh. +use super::super::heap::*; +use super::Reference; + +#[derive(Clone, Copy, Debug)] +pub(super) enum Frame { + Callback(Value), + Finish, +} + +impl Reference<'_> { + fn flatten(&mut self, queue: Value, output: &mut Vec) -> Checked<()> { + self.tick()?; + match self.heap.get_node(queue)? { + Node::Empty => {} + Node::Cons(callback, rest) => { + self.heap.queue_work += 1; + output.push(Frame::Callback(callback)); + self.flatten(rest, output)?; + } + Node::Append(left, right) => { + self.heap.queue_work += 1; + self.flatten(left, output)?; + self.flatten(right, output)?; + } + Node::Finish(extent, rest, _) => { + self.check(extent)?; + self.heap.queue_work += 1; + output.push(Frame::Finish); + self.flatten(rest, output)?; + } + _ => return Err(Fault::InvalidModel), + } + Ok(()) + } + + fn linear(&mut self, mut queue: Value) -> (Vec, Checked<()>) { + let mut frames = Vec::new(); + loop { + if let Err(error) = self.tick() { + return (frames, Err(error)); + } + match self.heap.get_node(queue) { + Ok(Node::Empty) => return (frames, Ok(())), + Ok(Node::Cons(callback, rest)) => { + frames.push(Frame::Callback(callback)); + queue = rest; + } + Ok(Node::Finish(_, rest, _)) => { + frames.push(Frame::Finish); + queue = rest; + } + Ok(Node::Append(..)) => return (frames, Err(Fault::Trap)), + Err(error) => return (frames, Err(error)), + _ => return (frames, Err(Fault::InvalidModel)), + } + } + } + + pub(super) fn clone_sequence( + &mut self, + queue: Value, + mut todo: Value, + reversed: Value, + active: Value, + parent: Value, + ) -> Checked { + let mut frames = Vec::new(); + self.flatten(queue, &mut frames)?; + loop { + self.tick()?; + match self.heap.get_node(todo)? { + Node::Empty => break, + Node::Append(next, rest) => { + self.flatten(next, &mut frames)?; + todo = rest; + } + Node::Cons(..) | Node::Finish(..) => return Err(Fault::Trap), + _ => return Err(Fault::InvalidModel), + } + } + let mut reversed_frames = frames.into_iter().rev().collect::>(); + let (prefix, end) = self.linear(reversed); + reversed_frames.extend(prefix); + self.rebuild_frames(&reversed_frames, todo, active, parent, end) + } + + pub(super) fn rebuild_sequence( + &mut self, + reversed: Value, + result: Value, + active: Value, + parent: Value, + ) -> Checked { + let (frames, end) = self.linear(reversed); + self.rebuild_frames(&frames, result, active, parent, end) + } + + fn rebuild_frames( + &mut self, + frames: &[Frame], + mut result: Value, + mut active: Value, + mut parent: Value, + end: Checked<()>, + ) -> Checked { + for frame in frames { + self.tick()?; + result = match *frame { + Frame::Callback(callback) => self.heap.node(Node::Cons(callback, result)), + Frame::Finish => { + let fresh = self.heap.extent(Some(parent)); + let next = self.heap.node(Node::Finish(fresh, result, active)); + active = fresh; + parent = fresh; + next + } + }; + } + end?; + Ok(self.heap.alloc(Object::Replay(result, active))) + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/model/runs.rs b/crates/psrs-backend/src/cc/lower/control/plan/model/runs.rs new file mode 100644 index 00000000..02b5cff6 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/model/runs.rs @@ -0,0 +1,57 @@ +//! Complete normal Run lifecycle, including fresh roots across invocations. +use super::{behavior::assert_matches, fixtures::Fixture, heap::*, interpret::Interpreter}; +use crate::control::execution::Program; + +#[test] +fn run_composes_dispatch_callbacks_and_normal_root_completion() { + let mut f = Fixture::new(); + let performed = f.heap.node(Node::Perform(f.request)); + let bound = f.heap.node(Node::Bind(f.next, f.callback)); + let handled = f.heap.node(Node::Handle(f.ret, f.op, f.next)); + for computation in [f.next, performed, bound, handled] { + let mut interpreter = Interpreter::new(f.heap.clone()); + assert_matches( + &mut interpreter, + &f.heap, + Program::Run, + vec![f.state, f.handler, computation], + ) + .unwrap(); + let roots = interpreter.heap.objects[f.heap.objects.len()..] + .iter() + .filter_map(|object| { + if let Object::Node(Node::RootExtent(status)) = object { + Some(*status) + } else { + None + } + }) + .collect::>(); + assert_eq!(roots.len(), 1, "each Run owns exactly one fresh root"); + assert_eq!( + interpreter.heap.status(roots[0]), + Ok(1), + "normal completion closes its root" + ); + let after_first = interpreter.heap.clone(); + assert_matches( + &mut interpreter, + &after_first, + Program::Run, + vec![f.state, f.handler, computation], + ) + .unwrap(); + let roots = interpreter.heap.objects[after_first.objects.len()..] + .iter() + .filter_map(|object| { + if let Object::Node(Node::RootExtent(status)) = object { + Some(*status) + } else { + None + } + }) + .collect::>(); + assert_eq!(roots.len(), 1, "reentry must allocate its own root"); + assert_eq!(interpreter.heap.status(roots[0]), Ok(1)); + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/returns.rs b/crates/psrs-backend/src/cc/lower/control/plan/returns.rs new file mode 100644 index 00000000..591da8e6 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/returns.rs @@ -0,0 +1,362 @@ +//! Return/queue transitions of the flat driver. + +use super::ir::*; +use crate::control::schema::{ + ComputationCase, ContextCase, SequenceCase, computation as c, context as x, sequence as q, +}; + +/// Return publishes or applies the current return clause, then consumes the +/// current queue. +pub(super) fn returned(cursor: &mut Cursor) -> Exec { + let mut ops = Vec::new(); + let value = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Computation), + case: Case::Computation(ComputationCase::Return), + field: c::PAYLOAD, + }, + ); + let arms = SequenceCase::ALL + .iter() + .map(|case| { + let mut cursor = cursor.clone(); + let arm = match case { + SequenceCase::Empty => return_empty(&cursor, value.clone()), + SequenceCase::Cons => return_cons(&mut cursor, value.clone()), + SequenceCase::Append => return_append(&mut cursor, value.clone()), + SequenceCase::Finish => return_finish(&mut cursor), + }; + (case.tag(), arm) + }) + .collect(); + steps( + ops, + switch( + state(State::Sequence), + Case::Sequence(SequenceCase::Empty), + arms, + ), + ) +} + +/// A return at an empty queue publishes at the root or enters a scoped target. +pub(super) fn return_empty(cursor: &Cursor, value: Operand) -> Exec { + let arms = ContextCase::ALL + .iter() + .map(|case| { + let mut cursor = cursor.clone(); + let arm = match case { + ContextCase::Root => { + let mut ops = Vec::new(); + let pair = cursor.bind( + &mut ops, + Op::Pair { + state: state(State::Value), + value: value.clone(), + }, + ); + steps(ops, Exec::Publish { result: pair }) + } + ContextCase::Scoped => return_clause(&mut cursor, value.clone()), + }; + (case.tag(), arm) + }) + .collect(); + switch( + state(State::Context), + Case::Context(ContextCase::Root), + arms, + ) +} + +/// Apply the scoped return clause and enter the target with a fresh finish. +pub(super) fn return_clause(cursor: &mut Cursor, value: Operand) -> Exec { + let mut ops = Vec::new(); + let ret = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Context), + case: Case::Context(ContextCase::Scoped), + field: x::SCOPED_RETURN, + }, + ); + let target = cursor.bind( + &mut ops, + Op::Apply { + callee: ret, + args: vec![value], + }, + ); + steps(ops, enter_target(cursor, target)) +} + +/// Build the finish record for the current scoped context and advance into the +/// target under the caller's context and the shared extent. +pub(super) fn enter_target(cursor: &mut Cursor, target: Operand) -> Exec { + let mut ops = Vec::new(); + let saved = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Context), + case: Case::Context(ContextCase::Scoped), + field: x::SCOPED_SAVED, + }, + ); + let parent = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Context), + case: Case::Context(ContextCase::Scoped), + field: x::SCOPED_PARENT, + }, + ); + let caller = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Context), + case: Case::Context(ContextCase::Scoped), + field: x::SCOPED_CALLER, + }, + ); + let extent = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Context), + case: Case::Context(ContextCase::Scoped), + field: x::SCOPED_EXTENT, + }, + ); + let finish = cursor.bind( + &mut ops, + Op::Node { + case: Case::Sequence(SequenceCase::Finish), + fields: vec![extent.clone(), saved, parent], + }, + ); + steps( + ops, + advance_all( + state(State::Value), + state(State::Handler), + target, + finish, + extent, + caller, + ), + ) +} + +pub(super) fn return_cons(cursor: &mut Cursor, value: Operand) -> Exec { + let mut ops = Vec::new(); + let head = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Sequence), + case: Case::Sequence(SequenceCase::Cons), + field: q::CONS_HEAD, + }, + ); + let tail = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Sequence), + case: Case::Sequence(SequenceCase::Cons), + field: q::CONS_TAIL, + }, + ); + let next = cursor.bind( + &mut ops, + Op::Apply { + callee: head, + args: vec![value], + }, + ); + steps(ops, advance_here(next, tail)) +} + +pub(super) fn return_append(cursor: &mut Cursor, value: Operand) -> Exec { + let mut ops = Vec::new(); + let left = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Sequence), + case: Case::Sequence(SequenceCase::Append), + field: q::APPEND_LEFT, + }, + ); + let right = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Sequence), + case: Case::Sequence(SequenceCase::Append), + field: q::APPEND_RIGHT, + }, + ); + steps(ops, append(cursor, value, left, right)) +} + +/// Reassociate an Append so left-to-right work and Finish ownership are kept. +pub(super) fn append(cursor: &Cursor, value: Operand, left: Operand, right: Operand) -> Exec { + let arms = SequenceCase::ALL + .iter() + .map(|case| { + let mut cursor = cursor.clone(); + let arm = match case { + SequenceCase::Empty => advance_here(state(State::Computation), right.clone()), + SequenceCase::Cons => { + let mut ops = Vec::new(); + let head = cursor.bind( + &mut ops, + Op::Project { + source: left.clone(), + case: Case::Sequence(SequenceCase::Cons), + field: q::CONS_HEAD, + }, + ); + let rest = cursor.bind( + &mut ops, + Op::Project { + source: left.clone(), + case: Case::Sequence(SequenceCase::Cons), + field: q::CONS_TAIL, + }, + ); + let pending = cursor.bind( + &mut ops, + Op::Node { + case: Case::Sequence(SequenceCase::Append), + fields: vec![rest, right.clone()], + }, + ); + let next = cursor.bind( + &mut ops, + Op::Apply { + callee: head, + args: vec![value.clone()], + }, + ); + steps(ops, advance_here(next, pending)) + } + SequenceCase::Append => { + let mut ops = Vec::new(); + let first = cursor.bind( + &mut ops, + Op::Project { + source: left.clone(), + case: Case::Sequence(SequenceCase::Append), + field: q::APPEND_LEFT, + }, + ); + let second = cursor.bind( + &mut ops, + Op::Project { + source: left.clone(), + case: Case::Sequence(SequenceCase::Append), + field: q::APPEND_RIGHT, + }, + ); + let rest = cursor.bind( + &mut ops, + Op::Node { + case: Case::Sequence(SequenceCase::Append), + fields: vec![second, right.clone()], + }, + ); + let pending = cursor.bind( + &mut ops, + Op::Node { + case: Case::Sequence(SequenceCase::Append), + fields: vec![first, rest], + }, + ); + steps(ops, advance_here(state(State::Computation), pending)) + } + SequenceCase::Finish => { + let mut ops = Vec::new(); + let extent = cursor.bind( + &mut ops, + Op::Project { + source: left.clone(), + case: Case::Sequence(SequenceCase::Finish), + field: q::FINISH_EXTENT, + }, + ); + let rest = cursor.bind( + &mut ops, + Op::Project { + source: left.clone(), + case: Case::Sequence(SequenceCase::Finish), + field: q::FINISH_REST, + }, + ); + let parent = cursor.bind( + &mut ops, + Op::Project { + source: left.clone(), + case: Case::Sequence(SequenceCase::Finish), + field: q::FINISH_PARENT, + }, + ); + let pending = cursor.bind( + &mut ops, + Op::Node { + case: Case::Sequence(SequenceCase::Append), + fields: vec![rest, right.clone()], + }, + ); + let finish = cursor.bind( + &mut ops, + Op::Node { + case: Case::Sequence(SequenceCase::Finish), + fields: vec![extent, pending, parent], + }, + ); + steps(ops, advance_here(state(State::Computation), finish)) + } + }; + (case.tag(), arm) + }) + .collect(); + switch(left, Case::Sequence(SequenceCase::Append), arms) +} + +pub(super) fn return_finish(cursor: &mut Cursor) -> Exec { + let mut ops = Vec::new(); + let extent = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Sequence), + case: Case::Sequence(SequenceCase::Finish), + field: q::FINISH_EXTENT, + }, + ); + let rest = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Sequence), + case: Case::Sequence(SequenceCase::Finish), + field: q::FINISH_REST, + }, + ); + let parent = cursor.bind( + &mut ops, + Op::Project { + source: state(State::Sequence), + case: Case::Sequence(SequenceCase::Finish), + field: q::FINISH_PARENT, + }, + ); + cursor.bind(&mut ops, Op::CloseBoundary { extent }); + steps( + ops, + advance_all( + state(State::Value), + state(State::Handler), + state(State::Computation), + rest, + parent, + state(State::Context), + ), + ) +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/run.rs b/crates/psrs-backend/src/cc/lower/control/plan/run.rs new file mode 100644 index 00000000..a4cc3ed3 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/run.rs @@ -0,0 +1,67 @@ +//! The complete normal lifetime of a source Run, before payload recovery. +use super::ir::{Case, Cursor, Exec, Op, Operand, steps}; +use crate::cc::ControlHelperRole as R; +use crate::control::schema::ContextCase; + +pub(super) fn program() -> Exec { + let mut cursor = Cursor::new(); + let mut ops = Vec::new(); + let empty = cursor.bind(&mut ops, Op::EmptySequence); + let root = cursor.bind( + &mut ops, + Op::Helper { + role: R::Start, + args: vec![], + }, + ); + let context = cursor.bind( + &mut ops, + Op::Node { + case: Case::Context(ContextCase::Root), + fields: vec![root.clone()], + }, + ); + let result = cursor.bind( + &mut ops, + Op::Helper { + role: R::Executor, + args: vec![ + Operand::Param(0), + Operand::Param(1), + Operand::Param(2), + empty, + root.clone(), + context, + ], + }, + ); + cursor.bind(&mut ops, Op::CloseBoundary { extent: root }); + steps(ops, Exec::Return { value: result }) +} + +impl super::super::builder::Machine<'_> { + pub(in crate::cc::lower::control) fn run(mut self) -> crate::cc::Function { + use super::super::fields::ControlEmitter; + let parameters = R::Run + .parameters() + .iter() + .map(|kind| { + let shape = self + .representations + .control_schema() + .unwrap() + .shape(*kind, self.layout.continuation); + self.fresh_value(shape) + }) + .collect::>(); + let plan = super::build(super::Program::Run); + let mut body = Vec::new(); + let result = self.expand(&plan, ¶meters, &mut body); + let table = self.representations; + let symbol = self.functions.run; + let function = self.finish(symbol, "control_run", parameters, body, result); + super::check_boundary(&function, table, &plan) + .expect("Run construction must match its checked plan"); + function + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/plan/shape.rs b/crates/psrs-backend/src/cc/lower/control/plan/shape.rs new file mode 100644 index 00000000..cd416e8b --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/plan/shape.rs @@ -0,0 +1,58 @@ +//! The dispatch/outcome shape of a plan, shared by the boundary checker. +//! +//! A [`Shape`] is the tree of machine-carrier switches a body performs and the +//! terminal outcome each path reaches. It is derived from the plan by +//! [`shape`] and from emitted CC by the P8 boundary checker; comparing the two +//! is how the boundary validates that expansion emitted the checked program. + +use super::ir::{Exec, Family}; + +/// The terminal outcome class of one plan path. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum Terminal { + /// Continue the flat driver with a complete state. + Advance, + /// Tail-call another machine helper. + Tail, + /// Publish or return a value from the body. + Value, + /// Trap on an unsupported alternative. + Reject, +} + +/// A dispatch/outcome tree. Switch arms are in schema order. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum Shape { + Switch(Family, Vec<(u32, Shape)>), + Leaf(Terminal), +} + +/// Derive the shape of a plan. +pub(crate) fn shape(exec: &Exec) -> Shape { + match exec { + Exec::Steps { next, .. } => shape(next), + Exec::Switch { case, arms, .. } => Shape::Switch( + case.family(), + arms.iter().map(|(tag, arm)| (*tag, shape(arm))).collect(), + ), + Exec::Advance { .. } => Shape::Leaf(Terminal::Advance), + Exec::Tail { .. } => Shape::Leaf(Terminal::Tail), + Exec::Publish { .. } | Exec::Return { .. } => Shape::Leaf(Terminal::Value), + Exec::Reject { .. } => Shape::Leaf(Terminal::Reject), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn shape_is_sensitive_to_a_dropped_dispatch_arm() { + let mut plan = super::super::executor(); + let before = shape(&plan); + if let Exec::Switch { arms, .. } = &mut plan { + arms.retain(|(tag, _)| *tag != 0); + } + assert_ne!(shape(&plan), before); + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/planning.rs b/crates/psrs-backend/src/cc/lower/control/planning.rs new file mode 100644 index 00000000..cbab42de --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/planning.rs @@ -0,0 +1,182 @@ +//! Publish machine bindings before source lowering; query immutable plans by symbol. +use super::{ControlLayout, plan}; +use crate::cc::SignatureId; +use psrs_hir::SymbolId; + +#[derive(Clone, Copy)] +pub(in crate::cc) struct ControlFunctions { + pub(super) emitted: bool, + pub(super) executor: SymbolId, + pub(super) run: SymbolId, + pub(super) resume: SymbolId, + pub(super) clone_sequence: SymbolId, + pub(super) rebuild_sequence: SymbolId, + pub(super) start: SymbolId, + pub(super) check: SymbolId, + pub(super) close: SymbolId, + pub(super) once: SymbolId, + pub(super) cancel: SymbolId, +} + +impl ControlFunctions { + pub(super) fn contracts( + self, + layout: ControlLayout, + ) -> [(SymbolId, SignatureId, crate::cc::ControlHelperRole); 9] { + use crate::cc::ControlHelperRole::*; + [ + (self.executor, layout.executor, Executor), + (self.run, layout.run, Run), + (self.clone_sequence, layout.clone_sequence, CloneSequence), + ( + self.rebuild_sequence, + layout.rebuild_sequence, + RebuildSequence, + ), + (self.start, layout.start, Start), + (self.check, layout.boundary_check, Check), + (self.close, layout.boundary_check, Close), + (self.once, layout.once, Once), + (self.cancel, layout.cancel, Cancel), + ] + } + + pub(super) fn signature(self, layout: ControlLayout, function: SymbolId) -> SignatureId { + self.contracts(layout) + .into_iter() + .find(|(symbol, _, _)| *symbol == function) + .map(|(_, signature, _)| signature) + .expect("predeclared control function") + } +} + +pub(in crate::cc) fn plan_control_helpers( + layout: &mut crate::cc::layout::TypeLayout, + allocator: &std::rc::Rc>, +) -> Option { + let control = layout.control_layout?; + let mut allocator = allocator.borrow_mut(); + let owner = layout.control_owner?; + let functions = ControlFunctions { + emitted: false, + executor: allocator.fresh(owner), + run: allocator.fresh(owner), + resume: allocator.fresh(owner), + clone_sequence: allocator.fresh(owner), + rebuild_sequence: allocator.fresh(owner), + start: allocator.fresh(owner), + check: allocator.fresh(owner), + close: allocator.fresh(owner), + once: allocator.fresh(owner), + cancel: allocator.fresh(owner), + }; + let resumption = crate::cc::ControlResumptionContract { + function: functions.resume, + signature: control.continuation, + }; + let schema = crate::cc::ControlSchema { + computation: control.computation, + sequence: control.sequence, + context: control.context, + extent: control.boundary, + status: control.status, + owner: control.owner, + callback: control.callback, + result_pair: control.pair, + replay_result: control.replay_result, + }; + let helpers = functions + .contracts(control) + .into_iter() + .map( + |(function, signature, role)| crate::cc::ControlHelperContract { + role, + function, + signature, + }, + ) + .collect(); + layout + .representations + .freeze_control_plan(helpers, resumption, schema) + .expect("machine planning must satisfy its named schema"); + Some(functions) +} + +/// The checked plan shape for a machine function symbol, if it is one. +pub(crate) fn plan_shape_for( + symbol: psrs_hir::SymbolId, + table: &crate::cc::RepresentationTable, +) -> Option { + plan_for_symbol(symbol, table).map(|plan| plan::shape_of(plan.exec())) +} + +pub(super) fn plan_for_symbol( + symbol: psrs_hir::SymbolId, + table: &crate::cc::RepresentationTable, +) -> Option { + use crate::cc::ControlHelperRole as R; + let role = table + .control_helpers() + .iter() + .find(|helper| helper.function() == symbol) + .map(|helper| helper.role()); + let resumption = table + .control_resumption() + .is_some_and(|raw| raw.function() == symbol); + let program = match role { + Some(R::Executor) => plan::Program::Executor, + Some(R::Run) => plan::Program::Run, + Some(R::CloneSequence) => plan::Program::CloneSequence, + Some(R::RebuildSequence) => plan::Program::RebuildSequence, + Some(R::Start) => plan::Program::Start, + Some(R::Check) => plan::Program::Check, + Some(R::Close) => plan::Program::Close, + Some(R::Once) => plan::Program::Once, + Some(R::Cancel) => plan::Program::Cancel, + None if resumption => plan::Program::RawResume, + None => return None, + }; + Some(plan::build(program)) +} + +/// The external helper symbols a machine function tail-calls, per its plan. +/// +/// Self tail calls are loopified into loop back-edges by P9, so they are +/// excluded; the MIR boundary check compares the rest against actual +/// `ReturnCall` targets. +pub(crate) fn plan_external_targets( + symbol: psrs_hir::SymbolId, + table: &crate::cc::RepresentationTable, +) -> Option> { + use crate::cc::ControlHelperRole; + fn collect(exec: &plan::PlanExec, out: &mut std::collections::HashSet) { + match exec { + plan::PlanExec::Steps { next, .. } => collect(next, out), + plan::PlanExec::Switch { arms, .. } => { + for (_, arm) in arms { + collect(arm, out); + } + } + plan::PlanExec::Advance { .. } => { + out.insert(ControlHelperRole::Executor); + } + plan::PlanExec::Tail { role, .. } => { + out.insert(*role); + } + _ => {} + } + } + let plan = plan_for_symbol(symbol, table)?; + let mut wanted = std::collections::HashSet::new(); + collect(plan.exec(), &mut wanted); + let mut targets = std::collections::HashSet::new(); + for role in wanted { + if let Some(helper) = table.control_helper(role) + && helper.function() != symbol + { + targets.insert(helper.function()); + } + } + Some(targets) +} diff --git a/crates/psrs-backend/src/cc/lower/control/replay.rs b/crates/psrs-backend/src/cc/lower/control/replay.rs new file mode 100644 index 00000000..44b29323 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/replay.rs @@ -0,0 +1,81 @@ +//! Instantiate lifecycle frames when replaying a persistent source checkpoint. +//! Ordinary callbacks and one-shot owner objects are shared, never cloned. +//! +//! The bodies are built from the checked plans in [`super::plan`]; only the +//! replay-result projection stays here because the executor also uses it. + +use super::builder::{Machine, repr}; +use super::fields::ControlEmitter; +use crate::cc::{Assignment, AssignmentKind, Function, ValueId}; + +impl Machine<'_> { + pub(super) fn clone_sequence(mut self) -> Function { + let queue = self.fresh_value(repr(self.layout.sequence)); + let todo = self.fresh_value(repr(self.layout.sequence)); + let reversed = self.fresh_value(repr(self.layout.sequence)); + let active = self.fresh_value(repr(self.layout.boundary)); + let parent = self.fresh_value(repr(self.layout.boundary)); + let params = [queue, todo, reversed, active, parent]; + let program = super::plan::build(super::plan::Program::CloneSequence); + let mut body = Vec::new(); + let result = self.expand(&program, ¶ms, &mut body); + let representations = self.representations; + let symbol = self.functions.clone_sequence; + let function = self.finish( + symbol, + "control_clone_sequence", + params.to_vec(), + body, + result, + ); + super::plan::check_boundary(&function, representations, &program) + .expect("clone_sequence boundary check must pass"); + function + } + + pub(super) fn rebuild_sequence(mut self) -> Function { + let reversed = self.fresh_value(repr(self.layout.sequence)); + let result_queue = self.fresh_value(repr(self.layout.sequence)); + let active = self.fresh_value(repr(self.layout.boundary)); + let parent = self.fresh_value(repr(self.layout.boundary)); + let params = [reversed, result_queue, active, parent]; + let program = super::plan::build(super::plan::Program::RebuildSequence); + let mut body = Vec::new(); + let result = self.expand(&program, ¶ms, &mut body); + let representations = self.representations; + let symbol = self.functions.rebuild_sequence; + let function = self.finish( + symbol, + "control_rebuild_sequence", + params.to_vec(), + body, + result, + ); + super::plan::check_boundary(&function, representations, &program) + .expect("rebuild_sequence boundary check must pass"); + function + } + + pub(super) fn replay_field( + &mut self, + value: ValueId, + field: u32, + body: &mut Vec, + ) -> ValueId { + let representation = self.layout.replay_result; + let Some(crate::cc::Representation::Product { fields }) = + self.representations.representation(representation) + else { + unreachable!("predeclared replay result product"); + }; + let shape = *fields + .get(field as usize) + .expect("predeclared replay result field"); + self.emit(shape, body, |destination| AssignmentKind::ProductGet { + destination, + representation, + field, + value, + }) + } +} diff --git a/crates/psrs-backend/src/cc/lower/control/scope.rs b/crates/psrs-backend/src/cc/lower/control/scope.rs new file mode 100644 index 00000000..cdb10b5a --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/control/scope.rs @@ -0,0 +1,31 @@ +//! Source dispatch and target lifetime are separate heap contexts. +//! +//! The executor's use of these relations now lives in [`super::plan`]; this +//! module keeps only the named context-extent projection they share. + +use super::builder::Machine; +use super::executor::arm; +use crate::cc::{Assignment, ValueId}; +use crate::control::schema::{ContextCase, context as x}; + +impl Machine<'_> { + /// The current dispatch checkpoint of a context: root field 0 or scoped + /// field 5. Named so replay and closure capture do not recover the offset. + pub(super) fn context_extent( + &mut self, + context: ValueId, + body: &mut Vec, + ) -> ValueId { + let mut cases = Vec::new(); + for case in ContextCase::ALL { + let mut branch = Vec::new(); + let index = match case { + ContextCase::Root => x::ROOT_EXTENT, + ContextCase::Scoped => x::SCOPED_EXTENT, + }; + let extent = self.field(context, self.layout.context, case.tag(), index, &mut branch); + cases.push(arm(case.tag(), branch, extent)); + } + self.switch(context, self.layout.context, cases, body) + } +} diff --git a/crates/psrs-backend/src/cc/lower/conversion/callable.rs b/crates/psrs-backend/src/cc/lower/conversion/callable.rs new file mode 100644 index 00000000..d613afff --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/conversion/callable.rs @@ -0,0 +1,215 @@ +//! Emission of callable plans with representation-only endpoints. + +use super::super::{FunctionLowerer, LambdaLowering}; +use crate::BackendError; +use crate::cc::{ + Assignment, AssignmentKind, Function, RefShape, Reference, SignatureId, ValueConversion, + ValueShape, +}; +use psrs_span::TextRange; + +fn closure(signature: SignatureId) -> ValueShape { + ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Closure(signature), + }) +} + +impl FunctionLowerer<'_> { + /// Emits a completed callable plan. No source matching occurs here: the + /// two signatures and recursive argument/result plans are already fixed. + pub(super) fn physical_callable_plan( + &mut self, + source: SignatureId, + target: SignatureId, + arguments: Vec, + result_plan: ValueConversion, + span: TextRange, + ) -> Result> { + if source == target + && arguments + .iter() + .all(|plan| matches!(plan, ValueConversion::Identity)) + && matches!(result_plan, ValueConversion::Identity) + { + return Ok(ValueConversion::Identity); + } + let source_shape = self + .representations + .signature(source) + .cloned() + .ok_or_else(|| { + super::conversion_error(span, "callable plan has no producer signature") + })?; + let target_shape = self + .representations + .signature(target) + .cloned() + .ok_or_else(|| { + super::conversion_error(span, "callable plan has no consumer signature") + })?; + if source_shape.parameters.len() != target_shape.parameters.len() + || arguments.len() != target_shape.parameters.len() + { + return Err(super::conversion_error( + span, + "callable plan has incompatible parameter counts", + )); + } + let mut body = self.child_lowerer(); + let receiver = body.fresh(ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Aggregate, + })); + let mut parameters = vec![receiver]; + let inputs = target_shape + .parameters + .iter() + .map(|shape| body.fresh(*shape)) + .collect::>(); + parameters.extend(inputs.iter().copied()); + let mut values = Vec::new(); + let mut assignments = Vec::new(); + let captured = body.fresh(super::erased_shape()); + assignments.push(Assignment { + destination: captured, + kind: AssignmentKind::ClosureGetCapture { + closure: receiver, + index: 0, + }, + span, + }); + let producer = body.fresh(closure(source)); + assignments.push(Assignment { + destination: producer, + kind: AssignmentKind::RepresentationCast { + destination: producer, + value: captured, + reference: Reference { + nullable: false, + heap: RefShape::Closure(source), + }, + }, + span, + }); + // Preserve references across recursive plans that can allocate loops. + let (producer, producer_shape) = super::super::call::persist_reference( + &mut body, + producer, + closure(source), + span, + &mut assignments, + ); + for (index, plan) in arguments.into_iter().enumerate() { + let parameter = inputs[index]; + let value = body.emit_conversion( + parameter, + target_shape.parameters[index], + source_shape.parameters[index], + plan, + span, + &mut assignments, + ); + values.push(super::super::call::persist_reference( + &mut body, + value, + source_shape.parameters[index], + span, + &mut assignments, + )); + } + let producer = match producer_shape { + Some(shape) => super::super::call::restore_reference( + &mut body, + producer, + shape, + span, + &mut assignments, + ), + None => producer, + }; + let values = values + .into_iter() + .map(|(value, shape)| match shape { + Some(shape) => super::super::call::restore_reference( + &mut body, + value, + shape, + span, + &mut assignments, + ), + None => value, + }) + .collect(); + let value = body.fresh(source_shape.result); + assignments.push(Assignment { + destination: value, + kind: AssignmentKind::IndirectCall { + function: producer, + signature: source, + arguments: values, + }, + span, + }); + let result = body.emit_conversion( + value, + source_shape.result, + target_shape.result, + result_plan, + span, + &mut assignments, + ); + let body_symbol = self.generated_symbols.borrow_mut().fresh(self.owner); + let function = Function { + symbol: body_symbol, + name: format!("protocol_adapter_{}", span.start), + parameters, + values: body.values, + assignments, + result, + result_type: target_shape.result, + span, + }; + super::super::super::verify::verify_function( + &function, + self.signatures, + self.representations, + )?; + self.generated.extend(body.generated); + self.generated.push(function); + + let mut factory = self.child_lowerer(); + let input = factory.fresh(closure(source)); + let output = factory.fresh(closure(target)); + let symbol = self.generated_symbols.borrow_mut().fresh(self.owner); + let function = Function { + symbol, + name: format!("protocol_adapter_factory_{}", span.start), + parameters: vec![input], + values: factory.values, + assignments: vec![Assignment { + destination: output, + kind: AssignmentKind::FunctionRef { + function: body_symbol, + signature: target, + captures: vec![input], + }, + span, + }], + result: output, + result_type: closure(target), + span, + }; + super::super::super::verify::verify_function( + &function, + self.signatures, + self.representations, + )?; + self.generated.push(function); + Ok(ValueConversion::FunctionAdapter { + function: symbol, + source: closure(source), + destination: closure(target), + }) + } +} diff --git a/crates/psrs-backend/src/cc/lower/conversion/function_slot.rs b/crates/psrs-backend/src/cc/lower/conversion/function_slot.rs new file mode 100644 index 00000000..aac0e6bd --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/conversion/function_slot.rs @@ -0,0 +1,317 @@ +//! One callable protocol for functions stored under bare polymorphic values. +//! Erasure curries flattened calls into unary erased segments; recovery applies +//! those segments and adapts every argument and the final result explicitly. +use super::super::LambdaLowering; +use super::*; +use crate::cc::{Function, Signature, SignatureId}; + +fn closure(signature: SignatureId) -> ValueShape { + ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Closure(signature), + }) +} + +impl FunctionLowerer<'_> { + fn slot_signature(&self, span: TextRange) -> Result> { + self.boundary.function_slot_signature().ok_or_else(|| { + conversion_error(span, "polymorphic function slot has no registered protocol") + }) + } + + pub(super) fn erase_function_slot( + &mut self, + source: SignatureId, + span: TextRange, + ) -> Result> { + let slot = self.slot_signature(span)?; + if source == slot { + return Ok(ValueConversion::Identity); + } + let shape = self + .representations + .signature(source) + .cloned() + .ok_or_else(|| conversion_error(span, "function slot producer has no signature"))?; + if shape.parameters.is_empty() { + return Err(conversion_error( + span, + "function slot producer requires an argument", + )); + } + let function = self.slot_segment(source, slot, &shape, 0, span)?; + self.slot_factory(source, slot, function, span) + } + + fn slot_segment( + &mut self, + source: SignatureId, + slot: SignatureId, + shape: &Signature, + prefix: usize, + span: TextRange, + ) -> Result> { + let mut body = self.child_lowerer(); + let receiver = body.fresh(ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Aggregate, + })); + let input_shape = erased_shape(); + let result_shape = erased_shape(); + let input = body.fresh(input_shape); + let mut assignments = Vec::new(); + let producer = body.slot_capture(receiver, 0, closure(source), span, &mut assignments); + let mut arguments = Vec::new(); + for index in 0..prefix { + arguments.push(body.slot_capture( + receiver, + index as u32 + 1, + shape.parameters[index], + span, + &mut assignments, + )); + } + let conversion = body.recover_payload(shape.parameters[prefix], span)?; + arguments.push(body.emit_conversion( + input, + input_shape, + shape.parameters[prefix], + conversion, + span, + &mut assignments, + )); + let result = if prefix + 1 == shape.parameters.len() { + let result = body.fresh(shape.result); + assignments.push(Assignment { + destination: result, + kind: AssignmentKind::IndirectCall { + function: producer, + signature: source, + arguments, + }, + span, + }); + let conversion = body.erase_payload(shape.result, span)?; + body.emit_conversion( + result, + shape.result, + result_shape, + conversion, + span, + &mut assignments, + ) + } else { + let next = body.slot_segment(source, slot, shape, prefix + 1, span)?; + let value = body.fresh(closure(slot)); + let mut captures = vec![producer]; + captures.extend(arguments); + assignments.push(Assignment { + destination: value, + kind: AssignmentKind::FunctionRef { + function: next, + signature: slot, + captures, + }, + span, + }); + body.emit_conversion( + value, + closure(slot), + erased_shape(), + ValueConversion::EraseReference, + span, + &mut assignments, + ) + }; + let symbol = self.generated_symbols.borrow_mut().fresh(self.owner); + let function = Function { + symbol, + name: format!("function_slot_segment_{prefix}_{}", span.start), + parameters: vec![receiver, input], + values: body.values, + assignments, + result, + result_type: result_shape, + span, + }; + super::super::super::verify::verify_function( + &function, + self.signatures, + self.representations, + )?; + self.generated.extend(body.generated); + self.generated.push(function); + Ok(symbol) + } + + pub(super) fn recover_function_slot( + &mut self, + target: SignatureId, + span: TextRange, + ) -> Result> { + let slot = self.slot_signature(span)?; + let shape = self + .representations + .signature(target) + .cloned() + .ok_or_else(|| conversion_error(span, "function slot consumer has no signature"))?; + if shape.parameters.is_empty() { + return Err(conversion_error( + span, + "function slot consumer requires an argument", + )); + } + let initial = slot; + let cast = ValueConversion::RecoverReference { + destination: closure(initial), + evidence: RecoveryEvidence::TypeInstantiation, + }; + if target == initial { + return Ok(cast); + } + let mut body = self.child_lowerer(); + let receiver = body.fresh(ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Aggregate, + })); + let inputs: Vec<_> = shape + .parameters + .iter() + .map(|shape| body.fresh(*shape)) + .collect(); + let mut parameters = vec![receiver]; + parameters.extend(inputs.iter().copied()); + let mut assignments = Vec::new(); + let mut current = body.slot_capture(receiver, 0, closure(initial), span, &mut assignments); + let mut result = current; + for (index, parameter_shape) in shape.parameters.iter().enumerate() { + let input = inputs[index]; + let argument_shape = erased_shape(); + let invoked = slot; + let conversion = body.erase_payload(*parameter_shape, span)?; + let argument = body.emit_conversion( + input, + *parameter_shape, + argument_shape, + conversion, + span, + &mut assignments, + ); + result = body.fresh(erased_shape()); + assignments.push(Assignment { + destination: result, + kind: AssignmentKind::IndirectCall { + function: current, + signature: invoked, + arguments: vec![argument], + }, + span, + }); + if index + 1 < shape.parameters.len() { + current = body.emit_conversion( + result, + erased_shape(), + closure(slot), + ValueConversion::RecoverReference { + destination: closure(slot), + evidence: RecoveryEvidence::TypeInstantiation, + }, + span, + &mut assignments, + ); + } + } + let source_result = erased_shape(); + let conversion = body.recover_payload(shape.result, span)?; + let result = body.emit_conversion( + result, + source_result, + shape.result, + conversion, + span, + &mut assignments, + ); + let symbol = self.generated_symbols.borrow_mut().fresh(self.owner); + let function = Function { + symbol, + name: format!("function_slot_recovery_{}", span.start), + parameters, + values: body.values, + assignments, + result, + result_type: shape.result, + span, + }; + super::super::super::verify::verify_function( + &function, + self.signatures, + self.representations, + )?; + self.generated.extend(body.generated); + self.generated.push(function); + let factory = self.slot_factory(initial, target, symbol, span)?; + Ok(sequence(vec![cast, factory])) + } + + fn slot_capture( + &mut self, + receiver: ValueId, + index: u32, + shape: ValueShape, + span: TextRange, + assignments: &mut Vec, + ) -> ValueId { + let value = self.fresh(shape); + assignments.push(Assignment { + destination: value, + kind: AssignmentKind::ClosureGetCapture { + closure: receiver, + index, + }, + span, + }); + value + } + + fn slot_factory( + &mut self, + source: SignatureId, + target: SignatureId, + body: psrs_hir::SymbolId, + span: TextRange, + ) -> Result> { + let mut factory = self.child_lowerer(); + let input = factory.fresh(closure(source)); + let output = factory.fresh(closure(target)); + let symbol = self.generated_symbols.borrow_mut().fresh(self.owner); + let function = Function { + symbol, + name: format!("function_slot_factory_{}", span.start), + parameters: vec![input], + values: factory.values, + assignments: vec![Assignment { + destination: output, + kind: AssignmentKind::FunctionRef { + function: body, + signature: target, + captures: vec![input], + }, + span, + }], + result: output, + result_type: closure(target), + span, + }; + super::super::super::verify::verify_function( + &function, + self.signatures, + self.representations, + )?; + self.generated.push(function); + Ok(ValueConversion::FunctionAdapter { + function: symbol, + source: closure(source), + destination: closure(target), + }) + } +} diff --git a/crates/psrs-backend/src/cc/lower/conversion/mod.rs b/crates/psrs-backend/src/cc/lower/conversion/mod.rs index 64ef4c90..6edfdf13 100644 --- a/crates/psrs-backend/src/cc/lower/conversion/mod.rs +++ b/crates/psrs-backend/src/cc/lower/conversion/mod.rs @@ -3,19 +3,26 @@ use super::super::layout::{ user_type_id, }; use super::super::{ - AggregateConvert, Assignment, AssignmentKind, BoxKind, RecoveryEvidence, RefShape, Reference, - ReprId, ValueConversion, ValueId, ValueShape, + AggregateConvert, Assignment, AssignmentKind, RecoveryEvidence, RefShape, Reference, ReprId, + ValueConversion, ValueId, ValueShape, }; use super::FunctionLowerer; use crate::BackendError; use psrs_core::TypeId; use psrs_span::TextRange; +mod callable; +mod function_slot; +mod product_map; +mod scalars; +mod transport; + pub(in crate::cc) struct VariantFieldConversion { pub(in crate::cc) variant: ReprId, pub(in crate::cc) tag: u32, pub(in crate::cc) field: u32, pub(in crate::cc) template_type: TypeId, + pub(in crate::cc) parameters: Vec, pub(in crate::cc) target_type: TypeId, pub(in crate::cc) target_shape: ValueShape, pub(in crate::cc) stored_shape: ValueShape, @@ -35,6 +42,7 @@ impl FunctionLowerer<'_> { self.enum_types, self.aggregate_types, self.newtype_ids, + self.nominal_types, self.array_types, self.record_types, self.function_types, @@ -48,6 +56,25 @@ impl FunctionLowerer<'_> { source_shape: ValueShape, destination_shape: ValueShape, span: TextRange, + ) -> Result> { + self.typed_conversion_with_instantiation( + source_type, + destination_type, + source_shape, + destination_shape, + span, + None, + ) + } + + pub(in crate::cc::lower) fn typed_conversion_with_instantiation( + &mut self, + source_type: TypeId, + destination_type: TypeId, + source_shape: ValueShape, + destination_shape: ValueShape, + span: TextRange, + instantiation: Option<&psrs_core::Instantiation<'_>>, ) -> Result> { self.plan_conversion( source_type, @@ -55,6 +82,7 @@ impl FunctionLowerer<'_> { source_shape, destination_shape, span, + instantiation, ) } @@ -65,7 +93,18 @@ impl FunctionLowerer<'_> { source_shape: ValueShape, destination_shape: ValueShape, span: TextRange, + instantiation: Option<&psrs_core::Instantiation<'_>>, ) -> Result> { + if let Some(plan) = self.constructor_transport( + source_type, + destination_type, + source_shape, + destination_shape, + span, + instantiation, + )? { + return Ok(plan); + } if source_shape == destination_shape { return Ok(ValueConversion::Identity); } @@ -74,6 +113,14 @@ impl FunctionLowerer<'_> { // incorrectly replace erased storage with the concrete argument shape. let source_type = self.conversion_template(source_type, span)?; let destination_type = self.conversion_template(destination_type, span)?; + // Erased callable storage uses the payload owner's curry protocol, + // rather than the concrete producer's closure signature. + if destination_shape == crate::cc::payload::erased_shape() { + return self.erase_payload(source_shape, span); + } + if source_shape == crate::cc::payload::erased_shape() { + return self.recover_payload(destination_shape, span); + } if super::call::is_function_type(self.module, source_type) && super::call::is_function_type(self.module, destination_type) && matches!( @@ -90,44 +137,14 @@ impl FunctionLowerer<'_> { source_shape, destination_shape, span, + instantiation, ); } if is_abstract_type(self.module, destination_type) { - return match source_shape { - ValueShape::Integer | ValueShape::Boolean => self - .box_plan(BoxKind::Integer, self.boxed_integer_type, span) - .map(|boxed| sequence(vec![boxed, ValueConversion::EraseReference])), - ValueShape::Number => self - .box_plan(BoxKind::Number, self.boxed_number_type, span) - .map(|boxed| sequence(vec![boxed, ValueConversion::EraseReference])), - // A `String` is already a GC reference in the `eq` hierarchy, so - // it is erased and recovered by cast, not by the integer box. - ValueShape::String | ValueShape::Reference(_) => { - Ok(ValueConversion::EraseReference) - } - }; + return self.erase_payload(source_shape, span); } if is_abstract_type(self.module, source_type) { - return match destination_shape { - ValueShape::Integer | ValueShape::Boolean => self.unbox_plan( - BoxKind::Integer, - self.boxed_integer_type, - destination_shape, - span, - ), - ValueShape::Number => self.unbox_plan( - BoxKind::Number, - self.boxed_number_type, - destination_shape, - span, - ), - ValueShape::String | ValueShape::Reference(_) => { - Ok(ValueConversion::RecoverReference { - destination: destination_shape, - evidence: RecoveryEvidence::TypeInstantiation, - }) - } - }; + return self.recover_payload(destination_shape, span); } // An abstract aggregate value and the concrete variant representation of // the same declaration are related by a reference cast (DEC-13). @@ -185,6 +202,7 @@ impl FunctionLowerer<'_> { source_element_shape, destination_element_shape, span, + instantiation, )?; return Ok(ValueConversion::ArrayMap { source: source_repr, @@ -202,10 +220,17 @@ impl FunctionLowerer<'_> { "record conversion requires closed rows", )); }; - let (Some(source_repr), Some(destination_repr)) = ( - self.record_types.get(&source_type).copied(), - self.record_types.get(&destination_type).copied(), - ) else { + let ( + ValueShape::Reference(Reference { + heap: RefShape::Repr(source_repr), + .. + }), + ValueShape::Reference(Reference { + heap: RefShape::Repr(destination_repr), + .. + }), + ) = (source_shape, destination_shape) + else { return Err(conversion_error( span, "record conversion has no canonical layout", @@ -229,8 +254,22 @@ impl FunctionLowerer<'_> { "generic record conversion requires identical closed field labels", )); } + let fields = |repr| match self.representations.representation(repr) { + Some(crate::cc::Representation::Product { fields }) + if fields.len() == labels.len() => + { + Some(fields.clone()) + } + _ => None, + }; + let source_shapes = fields(source_repr).ok_or_else(|| { + conversion_error(span, "source record protocol has invalid fields") + })?; + let destination_shapes = fields(destination_repr).ok_or_else(|| { + conversion_error(span, "target record protocol has invalid fields") + })?; let mut plans = Vec::with_capacity(labels.len()); - for label in &labels { + for (index, label) in labels.iter().enumerate() { let source_field = source_fields .iter() .find(|(name, _)| name == label) @@ -244,9 +283,10 @@ impl FunctionLowerer<'_> { plans.push(self.plan_conversion( source_field, destination_field, - self.value_shape(source_field, span)?, - self.value_shape(destination_field, span)?, + source_shapes[index], + destination_shapes[index], span, + instantiation, )?); } return Ok(ValueConversion::ProductMap { @@ -283,36 +323,6 @@ impl FunctionLowerer<'_> { Ok(ty) } - fn box_plan( - &self, - kind: BoxKind, - representation: Option, - span: TextRange, - ) -> Result> { - representation - .map(|representation| ValueConversion::BoxScalar { - kind, - representation, - }) - .ok_or_else(|| conversion_error(span, "erased scalar has no box representation")) - } - - fn unbox_plan( - &self, - kind: BoxKind, - representation: Option, - destination: ValueShape, - span: TextRange, - ) -> Result> { - representation - .map(|representation| ValueConversion::UnboxScalar { - kind, - representation, - destination, - }) - .ok_or_else(|| conversion_error(span, "erased scalar has no box representation")) - } - pub(in crate::cc) fn emit_conversion( &mut self, value: ValueId, @@ -325,6 +335,26 @@ impl FunctionLowerer<'_> { if matches!(plan, ValueConversion::Identity) { return value; } + if let Some(result) = + self.emit_product_map(value, source, destination, &plan, span, assignments) + { + return result; + } + if let ValueConversion::Sequence(steps) = plan { + let mut value = value; + let mut shape = source; + let length = steps.len(); + for (index, step) in steps.into_iter().enumerate() { + let next = if index + 1 == length { + destination + } else { + step.output_shape(shape) + }; + value = self.emit_conversion(value, shape, next, step, span, assignments); + shape = next; + } + return value; + } let result = self.fresh(destination); assignments.push(Assignment { destination: result, @@ -351,6 +381,7 @@ impl FunctionLowerer<'_> { tag, field, template_type, + parameters, target_type, target_shape, stored_shape, @@ -359,8 +390,12 @@ impl FunctionLowerer<'_> { let template_shape = self.value_shape(template_type, span)?; if stored_shape == erased_shape() && is_abstract_type(self.module, template_type) - && matches!(target_shape, ValueShape::Reference(_)) + && matches!(target_shape, ValueShape::Reference(reference) if !matches!(reference.heap, RefShape::Closure(_))) && target_shape != erased_shape() + && !matches!(target_shape, ValueShape::Reference(Reference { heap: RefShape::Repr(id), .. }) + if matches!(self.representations.representation(id), Some(crate::cc::Representation::Array { .. }))) + && !matches!(target_shape, ValueShape::Reference(Reference { heap: RefShape::Repr(id), .. }) + if self.representations.product_labels(id).is_some()) { return Ok(ValueConversion::RecoverReference { destination: target_shape, @@ -379,12 +414,19 @@ impl FunctionLowerer<'_> { "variant field storage does not match its normalized template", )]); } + let evidence = self + .boundary + .checked_instantiation(template_type, ¶meters, target_type) + .ok_or_else(|| { + conversion_error(span, "variant field has no checked scoped instantiation") + })?; self.plan_conversion( template_type, target_type, template_shape, target_shape, span, + Some(&evidence), ) } } @@ -397,12 +439,7 @@ pub(super) fn sequence(steps: Vec) -> ValueConversion { } } -pub(super) fn erased_shape() -> ValueShape { - ValueShape::Reference(Reference { - nullable: false, - heap: RefShape::Erased, - }) -} +pub(super) use crate::cc::payload::erased_shape; fn conversion_error(span: TextRange, message: &'static str) -> Vec { vec![BackendError::new("P8 closure conversion", span, message)] diff --git a/crates/psrs-backend/src/cc/lower/conversion/product_map.rs b/crates/psrs-backend/src/cc/lower/conversion/product_map.rs new file mode 100644 index 00000000..a27650b8 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/conversion/product_map.rs @@ -0,0 +1,98 @@ +//! Expose field conversions as CC operations before dependency publication. +use super::*; +use crate::cc::Representation; + +impl FunctionLowerer<'_> { + pub(super) fn emit_product_map( + &mut self, + value: ValueId, + source_shape: ValueShape, + destination_shape: ValueShape, + plan: &ValueConversion, + span: TextRange, + assignments: &mut Vec, + ) -> Option { + let ValueConversion::ProductMap { + source, + target, + labels, + fields, + } = plan + else { + return None; + }; + let shape = |id| { + ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Repr(id), + }) + }; + let Some(Representation::Product { + fields: source_fields, + }) = self.representations.representation(*source) + else { + return None; + }; + let Some(Representation::Product { + fields: target_fields, + }) = self.representations.representation(*target) + else { + return None; + }; + // Leave malformed plans intact so the aggregate verifier diagnoses them. + if source_shape != shape(*source) + || destination_shape != shape(*target) + || source_fields.len() != fields.len() + || target_fields.len() != fields.len() + || self.representations.product_labels(*source) != Some(labels.as_slice()) + || self.representations.product_labels(*target) != Some(labels.as_slice()) + || fields + .iter() + .zip(source_fields) + .zip(target_fields) + .any(|((plan, source), target)| plan.output_shape(*source) != *target) + { + return None; + } + let source_fields = source_fields.clone(); + let target_fields = target_fields.clone(); + let mut arguments = Vec::with_capacity(fields.len()); + for (index, ((plan, source_shape), target_shape)) in fields + .iter() + .zip(source_fields) + .zip(target_fields) + .enumerate() + { + let field = self.fresh(source_shape); + assignments.push(Assignment { + destination: field, + kind: AssignmentKind::ProductGet { + destination: field, + representation: *source, + field: index as u32, + value, + }, + span, + }); + arguments.push(self.emit_conversion( + field, + source_shape, + target_shape, + plan.clone(), + span, + assignments, + )); + } + let result = self.fresh(destination_shape); + assignments.push(Assignment { + destination: result, + kind: AssignmentKind::ProductNew { + destination: result, + representation: *target, + arguments, + }, + span, + }); + Some(result) + } +} diff --git a/crates/psrs-backend/src/cc/lower/conversion/scalars.rs b/crates/psrs-backend/src/cc/lower/conversion/scalars.rs new file mode 100644 index 00000000..d68fc27e --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/conversion/scalars.rs @@ -0,0 +1,52 @@ +use super::super::FunctionLowerer; +use super::conversion_error; +use crate::{ + BackendError, + cc::{BoxKind, ReprId, ValueConversion, ValueShape}, +}; +use psrs_span::TextRange; + +impl FunctionLowerer<'_> { + pub(in crate::cc) fn erase_payload( + &mut self, + shape: ValueShape, + span: TextRange, + ) -> Result> { + crate::cc::payload::PayloadPlanner::erase_payload(self, shape, span) + } + + pub(in crate::cc) fn recover_payload( + &mut self, + shape: ValueShape, + span: TextRange, + ) -> Result> { + crate::cc::payload::PayloadPlanner::recover_payload(self, shape, span) + } +} + +impl crate::cc::payload::PayloadPlanner for FunctionLowerer<'_> { + fn payload_table(&self) -> &crate::cc::RepresentationTable { + self.representations + } + fn payload_box(&self, kind: BoxKind) -> Option { + match kind { + BoxKind::Integer => self.boxed_integer_type, + BoxKind::Number => self.boxed_number_type, + } + } + fn payload_callable( + &mut self, + signature: crate::cc::SignatureId, + entering: bool, + span: TextRange, + ) -> Result> { + if entering { + self.erase_function_slot(signature, span) + } else { + self.recover_function_slot(signature, span) + } + } + fn payload_error(&self, span: TextRange, message: &'static str) -> Vec { + conversion_error(span, message) + } +} diff --git a/crates/psrs-backend/src/cc/lower/conversion/tests.rs b/crates/psrs-backend/src/cc/lower/conversion/tests.rs index d13c233d..4815b0dc 100644 --- a/crates/psrs-backend/src/cc/lower/conversion/tests.rs +++ b/crates/psrs-backend/src/cc/lower/conversion/tests.rs @@ -1,4 +1,5 @@ use super::*; +use crate::boundary::BoundaryEvidence; use crate::cc::RepresentationTable; use crate::cc::lower::GeneratedSymbolAllocator; use psrs_core::Type; @@ -36,6 +37,7 @@ fn unsupported_typed_boundary_reports_its_source_span() { let constructor_reprs = HashMap::new(); let function_types = HashMap::new(); let function_wrappers = HashMap::new(); + let boundary = BoundaryEvidence::empty(&module); let mut lowerer = FunctionLowerer { next_value: 0, values: Vec::new(), @@ -43,11 +45,18 @@ fn unsupported_typed_boundary_reports_its_source_span() { signatures: &signatures, representations: &representations, module: &module, + boundary: &boundary, enum_types: &ids, aggregate_types: &ids, newtype_ids: &ids, + nominal_types: &std::collections::HashMap::new(), boxed_integer_type: None, boxed_number_type: None, + control_layout: None, + control_functions: std::rc::Rc::new(std::cell::RefCell::new(None)), + generated_signatures: std::rc::Rc::new(std::cell::RefCell::new( + std::collections::HashMap::new(), + )), array_types: &reprs, record_types: &reprs, constructor_tags: &tags, @@ -69,6 +78,7 @@ fn unsupported_typed_boundary_reports_its_source_span() { ValueShape::Integer, ValueShape::Number, span, + None, ) .unwrap_err(); assert!( @@ -90,12 +100,12 @@ fn unsupported_typed_boundary_reports_its_source_span() { heap: RefShape::Repr(ReprId(3)), }); assert!(matches!( - lowerer.plan_conversion(TypeId(0), TypeId(1), aggregate, representation, span), + lowerer.plan_conversion(TypeId(0), TypeId(1), aggregate, representation, span, None), Ok(ValueConversion::RecoverReference { .. }) )); assert!(matches!( lowerer - .plan_conversion(TypeId(0), TypeId(1), representation, aggregate, span) + .plan_conversion(TypeId(0), TypeId(1), representation, aggregate, span, None) .expect("a representation should recover its aggregate supertype"), ValueConversion::RecoverReference { .. } )); diff --git a/crates/psrs-backend/src/cc/lower/conversion/transport.rs b/crates/psrs-backend/src/cc/lower/conversion/transport.rs new file mode 100644 index 00000000..ca3932ea --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/conversion/transport.rs @@ -0,0 +1,189 @@ +//! Constructor transport at checked generic boundaries. + +use super::super::FunctionLowerer; +use super::{conversion_error, sequence}; +use crate::{ + BackendError, + cc::{RecoveryEvidence, RefShape, Reference, ValueConversion, ValueShape}, +}; +use psrs_core::{Instantiation, Type, TypeConstructor, TypeId}; +use psrs_span::TextRange; + +fn abstract_head(module: &psrs_core::Module, ty: TypeId) -> Option { + let ty = super::super::super::layout::unquantified_type(module, ty); + let Type::Application(head, _) = module.types.get(ty.0 as usize)? else { + return None; + }; + let Type::Variable(variable) = module.types.get(head.0 as usize)? else { + return None; + }; + Some(*variable) +} + +fn callable(shape: ValueShape) -> Option { + match shape { + ValueShape::Reference(Reference { + heap: RefShape::Closure(id), + .. + }) => Some(id), + _ => None, + } +} + +impl FunctionLowerer<'_> { + pub(super) fn constructor_transport( + &mut self, + source: TypeId, + target: TypeId, + source_shape: ValueShape, + target_shape: ValueShape, + span: TextRange, + evidence: Option<&Instantiation<'_>>, + ) -> Result, Vec> { + let source_head = abstract_head(self.module, source); + let target_head = abstract_head(self.module, target); + let array_boundary = match ( + source_head, + target_head, + super::super::super::layout::array_element_type(self.module, source), + super::super::super::layout::array_element_type(self.module, target), + ) { + (Some(variable), None, _, Some(element)) => Some((variable, false, target, element)), + (None, Some(variable), Some(element), _) => Some((variable, true, source, element)), + _ => None, + }; + if let Some((variable, entering, concrete_type, element_type)) = array_boundary { + let (constructor, fixed) = evidence + .and_then(|proof| proof.constructor(variable)) + .ok_or_else(|| { + conversion_error(span, "array constructor transport has no checked binding") + })?; + if constructor != TypeConstructor::Array || !fixed.is_empty() { + return Err(conversion_error( + span, + "array transport binding is not the unary Array constructor", + )); + } + let erased_element = super::erased_shape(); + let protocol = self.representations.representations.iter().position(|representation| + matches!(representation, crate::cc::Representation::Array { element } if *element == erased_element)) + .map(|index| crate::cc::ReprId(index as u32)) + .ok_or_else(|| conversion_error(span, "Array owner has no erased storage protocol"))?; + let concrete = self + .array_types + .get(&concrete_type) + .copied() + .ok_or_else(|| conversion_error(span, "array transport has no concrete layout"))?; + let element_shape = self.value_shape(element_type, span)?; + let protocol_shape = ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Repr(protocol), + }); + if concrete == protocol { + return Ok(Some(if entering { + ValueConversion::EraseReference + } else { + ValueConversion::RecoverReference { + destination: target_shape, + evidence: RecoveryEvidence::TypeInstantiation, + } + })); + } + return Ok(Some(if entering { + let element = self.erase_payload(element_shape, span)?; + sequence(vec![ + ValueConversion::ArrayMap { + source: concrete, + target: protocol, + element: Box::new(element), + }, + ValueConversion::EraseReference, + ]) + } else { + let element = self.recover_payload(element_shape, span)?; + sequence(vec![ + ValueConversion::RecoverReference { + destination: protocol_shape, + evidence: RecoveryEvidence::TypeInstantiation, + }, + ValueConversion::ArrayMap { + source: protocol, + target: concrete, + element: Box::new(element), + }, + ]) + })); + } + let boundary = match ( + abstract_head(self.module, source), + abstract_head(self.module, target), + ) { + (Some(variable), None) if callable(target_shape).is_some() => (variable, false), + (None, Some(variable)) if callable(source_shape).is_some() => (variable, true), + _ => return Ok(None), + }; + let (variable, entering) = boundary; + let (constructor, arguments) = evidence.and_then(|proof| proof.constructor(variable)) + .ok_or_else(|| vec![BackendError::new("P8 closure conversion", span, + format!("abstract constructor transport has no checked constructor binding for {variable:?}, endpoints {source:?} -> {target:?}"))])?; + let parameter_types = self + .boundary + .protocol_parameters(constructor, &arguments) + .ok_or_else(|| { + conversion_error(span, "constructor transport has no representation protocol") + })?; + let parameters = parameter_types + .into_iter() + .map(|ty| self.value_shape(ty, span)) + .collect::, _>>()?; + let concrete = callable(if entering { source_shape } else { target_shape }).unwrap(); + let signature = self + .representations + .signature(concrete) + .cloned() + .ok_or_else(|| { + conversion_error(span, "constructor transport has no concrete signature") + })?; + if signature.parameters != parameters { + return Err(conversion_error( + span, + "constructor transport requires a callable segment adapter", + )); + } + if let Some(result_type) = self.boundary.protocol_result(constructor) + && signature.result != self.value_shape(result_type, span)? + { + return Err(conversion_error( + span, + "constructor transport result differs from its checked storage field", + )); + } + let protocol = self.boundary.protocol_signature(concrete).ok_or_else(|| { + conversion_error(span, "constructor transport has no registered protocol") + })?; + let payload = signature.result; + let protocol_shape = ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Closure(protocol), + }); + let identity_arguments = vec![ValueConversion::Identity; parameters.len()]; + let plan = if entering { + let result = self.erase_payload(payload, span)?; + let adapter = + self.physical_callable_plan(concrete, protocol, identity_arguments, result, span)?; + sequence(vec![adapter, ValueConversion::EraseReference]) + } else { + let result = self.recover_payload(payload, span)?; + let adapter = + self.physical_callable_plan(protocol, concrete, identity_arguments, result, span)?; + sequence(vec![ + ValueConversion::RecoverReference { + destination: protocol_shape, + evidence: RecoveryEvidence::TypeInstantiation, + }, + adapter, + ]) + }; + Ok(Some(plan)) + } +} diff --git a/crates/psrs-backend/src/cc/lower/dictionary/tests.rs b/crates/psrs-backend/src/cc/lower/dictionary/tests.rs index 3ed0f514..cc49f9e3 100644 --- a/crates/psrs-backend/src/cc/lower/dictionary/tests.rs +++ b/crates/psrs-backend/src/cc/lower/dictionary/tests.rs @@ -28,8 +28,12 @@ fn typed_thir_instance_and_superclass_evidence_lower_to_ordinary_products() { panic!("expected method selection to remain an ordinary Core projection"); }; assert_eq!(field, "isPositive"); - let psrs_core::ExprKind::FieldAccess { record, field } = &record.kind else { - panic!("expected superclass evidence to remain an ordinary Core projection"); + let psrs_core::ExprKind::Application(thunk, unit) = &record.kind else { + panic!("expected superclass selection to force its thunk"); + }; + assert!(matches!(unit.kind, psrs_core::ExprKind::Unit)); + let psrs_core::ExprKind::FieldAccess { record, field } = &thunk.kind else { + panic!("expected superclass selection to project its thunk"); }; assert_eq!(field, "super"); assert!(matches!( @@ -69,9 +73,18 @@ fn typed_thir_instance_and_superclass_evidence_lower_to_ordinary_products() { for (argument, field) in ord_arguments.iter().zip(fields) { assert_eq!(value_shape(make_ord, *argument), Some(*field)); } + let ValueShape::Reference(crate::cc::Reference { + heap: crate::cc::RefShape::Closure(thunk), + .. + }) = fields[2] + else { + panic!("superclass field must be a callable thunk"); + }; + let thunk = cc.representations.signature(thunk).unwrap(); + assert_eq!(thunk.parameters, vec![ValueShape::Integer]); assert_eq!( - value_shape(make_ord, ord_arguments[2]), - Some(make_ord.values[make_ord.parameters[0].0 as usize].ty) + thunk.result, + make_ord.values[make_ord.parameters[0].0 as usize].ty ); let main = cc @@ -166,12 +179,15 @@ fn dictionary_evidence_module() -> thir::Module { ]; let method = push_arrow(&mut types, integer, boolean); let eq_dictionary = push_record(&mut types, vec![("isPositive".into(), method)]); + let unit = thir::TypeId(types.len() as u32); + types.push(thir::Type::Constructor(thir::TypeConstructor::Unit)); + let superclass_thunk = push_arrow(&mut types, unit, eq_dictionary); let ord_dictionary = push_record( &mut types, vec![ ("compare".into(), method), ("rank".into(), method), - ("super".into(), eq_dictionary), + ("super".into(), superclass_thunk), ], ); let make_ord_type = push_arrow(&mut types, eq_dictionary, ord_dictionary); @@ -253,7 +269,18 @@ fn dictionary_evidence_module() -> thir::Module { ), ( "super".into(), - typed(thir::ExprKind::Local(LocalId(2)), eq_dictionary, span), + typed( + thir::ExprKind::Lambda { + binder: binder(4, "superclass_unit", unit, span), + body: Box::new(typed( + thir::ExprKind::Local(LocalId(2)), + eq_dictionary, + span, + )), + }, + superclass_thunk, + span, + ), ), ]), ord_dictionary, diff --git a/crates/psrs-backend/src/cc/lower/erased/conversion.rs b/crates/psrs-backend/src/cc/lower/erased/conversion.rs index b0693aff..2550cb00 100644 --- a/crates/psrs-backend/src/cc/lower/erased/conversion.rs +++ b/crates/psrs-backend/src/cc/lower/erased/conversion.rs @@ -14,6 +14,7 @@ impl FunctionLowerer<'_> { source: ValueShape, destination: ValueShape, span: TextRange, + instantiation: Option<&psrs_core::Instantiation<'_>>, ) -> Result> { let mut factory = self.child_lowerer(); let value = factory.fresh(source); @@ -24,6 +25,7 @@ impl FunctionLowerer<'_> { target_type, span, &mut assignments, + instantiation, )?; let symbol = self.generated_symbols.borrow_mut().fresh(self.owner); let function = Function { diff --git a/crates/psrs-backend/src/cc/lower/erased/curried.rs b/crates/psrs-backend/src/cc/lower/erased/curried.rs index 945aa3b0..10885a82 100644 --- a/crates/psrs-backend/src/cc/lower/erased/curried.rs +++ b/crates/psrs-backend/src/cc/lower/erased/curried.rs @@ -28,6 +28,7 @@ impl FunctionLowerer<'_> { target_shape: &Signature, span: psrs_span::TextRange, assignments: &mut Vec, + instantiation: Option<&psrs_core::Instantiation<'_>>, ) -> Result> { let prefix = target_shape.parameters.len(); let remaining_type = @@ -53,6 +54,7 @@ impl FunctionLowerer<'_> { self.enum_types, self.aggregate_types, self.newtype_ids, + self.nominal_types, self.array_types, self.record_types, self.function_types, @@ -99,12 +101,13 @@ impl FunctionLowerer<'_> { }); let mut inner_captures = vec![source_closure]; for index in 0..prefix { - let conversion = outer.typed_conversion( + let conversion = outer.typed_conversion_with_instantiation( target_parameters[index], source_parameters[index], target_shape.parameters[index], source_shape.parameters[index], span, + instantiation, )?; let converted = outer.emit_conversion( target_arguments[index], @@ -186,12 +189,13 @@ impl FunctionLowerer<'_> { }, span, }); - let conversion = outer.typed_conversion( + let conversion = outer.typed_conversion_with_instantiation( remaining_type, function_arrow_parameters(self.module, target_type).1, closure_value_type_for(remaining_signature_id), target_shape.result, span, + instantiation, )?; let result = outer.emit_conversion( inner_closure_value, diff --git a/crates/psrs-backend/src/cc/lower/erased/eta.rs b/crates/psrs-backend/src/cc/lower/erased/eta.rs index 757f87ff..5a97e895 100644 --- a/crates/psrs-backend/src/cc/lower/erased/eta.rs +++ b/crates/psrs-backend/src/cc/lower/erased/eta.rs @@ -30,6 +30,7 @@ impl FunctionLowerer<'_> { target_shape: &Signature, span: psrs_span::TextRange, assignments: &mut Vec, + instantiation: Option<&psrs_core::Instantiation<'_>>, ) -> Result> { let source_arity = source_shape.parameters.len(); let source_parameters = function_arrow_parameters(self.module, source_type).0; @@ -79,12 +80,13 @@ impl FunctionLowerer<'_> { // signature consumes, converting each from the target representation. let mut concrete_arguments = Vec::with_capacity(source_arity); for index in 0..source_arity { - let conversion = adapter.typed_conversion( + let conversion = adapter.typed_conversion_with_instantiation( target_parameters[index], source_parameters[index], target_shape.parameters[index], source_shape.parameters[index], span, + instantiation, )?; let converted = adapter.emit_conversion( target_arguments[index], @@ -154,12 +156,13 @@ impl FunctionLowerer<'_> { }; let source_result_type = function_arrow_parameters(self.module, source_type).1; let remaining_closure = closure_value_type_for(remaining_signature_id); - let recover = adapter.typed_conversion( + let recover = adapter.typed_conversion_with_instantiation( source_result_type, remaining_type, source_shape.result, remaining_closure, span, + instantiation, )?; let recovered = adapter.emit_conversion( concrete_result, diff --git a/crates/psrs-backend/src/cc/lower/erased/mod.rs b/crates/psrs-backend/src/cc/lower/erased/mod.rs index 64b728ab..733b2c9f 100644 --- a/crates/psrs-backend/src/cc/lower/erased/mod.rs +++ b/crates/psrs-backend/src/cc/lower/erased/mod.rs @@ -46,7 +46,15 @@ impl FunctionLowerer<'_> { if source_type == target_type || !is_function_type(self.module, target_type) { return Ok(value); } - self.adapt_erased_function_value(value, source_type, target_type, span, assignments) + let evidence = self.boundary.instantiation_at(source_type, target_type); + self.adapt_erased_function_value( + value, + source_type, + target_type, + span, + assignments, + evidence.as_ref(), + ) } fn value_shape_of(&self, value: ValueId) -> Option { @@ -63,6 +71,7 @@ impl FunctionLowerer<'_> { target_type: TypeId, span: psrs_span::TextRange, assignments: &mut Vec, + instantiation: Option<&psrs_core::Instantiation<'_>>, ) -> Result> { // A value that is not a function at its source type is an erased (or // concrete) value recovered at the target function type, such as a type @@ -74,8 +83,14 @@ impl FunctionLowerer<'_> { if source_shape == target_shape { return Ok(value); } - let conversion = - self.typed_conversion(source_type, target_type, source_shape, target_shape, span)?; + let conversion = self.typed_conversion_with_instantiation( + source_type, + target_type, + source_shape, + target_shape, + span, + instantiation, + )?; return Ok(self.emit_conversion( value, source_shape, @@ -91,6 +106,7 @@ impl FunctionLowerer<'_> { self.enum_types, self.aggregate_types, self.newtype_ids, + self.nominal_types, self.array_types, self.record_types, self.function_types, @@ -101,6 +117,7 @@ impl FunctionLowerer<'_> { self.enum_types, self.aggregate_types, self.newtype_ids, + self.nominal_types, self.array_types, self.record_types, self.function_types, @@ -147,6 +164,7 @@ impl FunctionLowerer<'_> { &target_shape, span, assignments, + instantiation, ); } // A concrete curried function (the `ado` block's `\x -> \y -> ...`) @@ -168,6 +186,7 @@ impl FunctionLowerer<'_> { &target_shape, span, assignments, + instantiation, ); } return Err(vec![BackendError::new( @@ -221,12 +240,13 @@ impl FunctionLowerer<'_> { for (index, argument) in adapter_arguments.into_iter().enumerate() { let source_parameter = source_shape.parameters[index]; let target_parameter = target_shape.parameters[index]; - let conversion = adapter.typed_conversion( + let conversion = adapter.typed_conversion_with_instantiation( target_parameters[index], source_parameters[index], target_parameter, source_parameter, span, + instantiation, )?; let converted = adapter.emit_conversion( argument, @@ -275,12 +295,13 @@ impl FunctionLowerer<'_> { }); let source_result_type = function_arrow_parameters(self.module, source_type).1; let target_result_type = function_arrow_parameters(self.module, target_type).1; - let conversion = adapter.typed_conversion( + let conversion = adapter.typed_conversion_with_instantiation( source_result_type, target_result_type, source_shape.result, target_shape.result, span, + instantiation, )?; let result = adapter.emit_conversion( concrete_result, diff --git a/crates/psrs-backend/src/cc/lower/global.rs b/crates/psrs-backend/src/cc/lower/global.rs index 1b2c13ed..07a67795 100644 --- a/crates/psrs-backend/src/cc/lower/global.rs +++ b/crates/psrs-backend/src/cc/lower/global.rs @@ -31,6 +31,21 @@ impl GlobalLowering for FunctionLowerer<'_> { )); }; if !signature.parameters.is_empty() && is_function_type(self.module, expression.ty) { + if self + .module + .external_types + .iter() + .any(|external| external.symbol == function) + { + let signature = signature.clone(); + return self.lower_external_function_value( + expression, + function, + &signature, + result_type, + assignments, + ); + } let Some(source_type) = self .module .declarations @@ -81,12 +96,27 @@ impl GlobalLowering for FunctionLowerer<'_> { if source_type == expression.ty { Ok(destination) } else { + let declaration = self + .module + .declarations + .iter() + .find(|declaration| declaration.symbol == function) + .expect("the source declaration was found above"); + // Evidence is required only if the adaptation reaches an + // abstract constructor boundary; `constructor_transport` + // reports the missing binding where the boundary applies. + let evidence = self.boundary.checked_instantiation( + source_type, + &declaration.quantified, + expression.ty, + ); self.adapt_erased_function_value( destination, source_type, expression.ty, expression.span, assignments, + evidence.as_ref(), ) } } else { @@ -101,6 +131,7 @@ impl GlobalLowering for FunctionLowerer<'_> { assignments.push(Assignment { destination, kind: AssignmentKind::DirectCall { + contract: None, function, arguments: Vec::new(), }, @@ -109,21 +140,40 @@ impl GlobalLowering for FunctionLowerer<'_> { if source_shape == result_type { return Ok(destination); } - let source_type = self + let (source_type, quantified) = if let Some(declaration) = self .module .declarations .iter() .find(|declaration| declaration.symbol == function) - .map(|declaration| declaration.ty) - .ok_or_else(|| { - global_error(expression, "global value has no source declaration type") - })?; - let conversion = self.typed_conversion( + { + (declaration.ty, declaration.quantified.clone()) + } else if let Some(external) = self + .module + .external_types + .iter() + .find(|external| external.symbol == function) + { + let (quantified, _) = psrs_core::scheme_parts(&self.module.types, external.ty) + .ok_or_else(|| { + global_error(expression, "external value has an invalid source scheme") + })?; + (external.ty, quantified) + } else { + return Err(global_error( + expression, + "global value has no source declaration type", + )); + }; + let evidence = + self.boundary + .checked_instantiation(source_type, &quantified, expression.ty); + let conversion = self.typed_conversion_with_instantiation( source_type, expression.ty, source_shape, result_type, expression.span, + evidence.as_ref(), )?; Ok(self.emit_conversion( destination, diff --git a/crates/psrs-backend/src/cc/lower/intrinsic.rs b/crates/psrs-backend/src/cc/lower/intrinsic.rs index c92dcd70..2b3b48f5 100644 --- a/crates/psrs-backend/src/cc/lower/intrinsic.rs +++ b/crates/psrs-backend/src/cc/lower/intrinsic.rs @@ -6,10 +6,12 @@ use super::super::{Assignment, AssignmentKind, ValueId, ValueShape}; use super::FunctionLowerer; -use super::scalar::{lower_binary_op, lower_unary_op}; use crate::BackendError; +use crate::target_intrinsics::{ + GeneratedOperation as G, Implementation, ScalarOperation, implementation, +}; use psrs_core::Expr; -use psrs_hir::{Intrinsic, IntrinsicCategory}; +use psrs_hir::Intrinsic; impl FunctionLowerer<'_> { pub(super) fn lower_intrinsic( @@ -20,8 +22,24 @@ impl FunctionLowerer<'_> { ty: ValueShape, assignments: &mut Vec, ) -> Result> { - match intrinsic { - Intrinsic::ArrayIndex => { + match implementation(intrinsic) { + Implementation::Artifact(_) => { + let values = arguments + .iter() + .map(|argument| self.lower_value(argument, assignments)) + .collect::, _>>()?; + let destination = self.fresh(ty); + assignments.push(Assignment { + destination, + kind: AssignmentKind::RuntimeCall { + intrinsic, + arguments: values, + }, + span: expression.span, + }); + Ok(destination) + } + Implementation::Generated(G::ArrayIndex) => { let array = &arguments[0]; let Some(representation) = self.array_types.get(&array.ty).copied() else { return Err(vec![BackendError::new( @@ -32,7 +50,8 @@ impl FunctionLowerer<'_> { }; let array = self.lower_value(array, assignments)?; let index = self.lower_value(&arguments[1], assignments)?; - let destination = self.fresh(ty); + let stored_shape = crate::cc::payload::erased_shape(); + let destination = self.fresh(stored_shape); assignments.push(Assignment { destination, kind: AssignmentKind::ArrayGet { @@ -43,16 +62,24 @@ impl FunctionLowerer<'_> { }, span: expression.span, }); - Ok(destination) + let plan = self.recover_payload(ty, expression.span)?; + Ok(self.emit_conversion( + destination, + stored_shape, + ty, + plan, + expression.span, + assignments, + )) } - Intrinsic::ArrayUpdate => self.lower_array_update( + Implementation::Generated(G::ArrayUpdate) => self.lower_array_update( expression, &arguments[0], &arguments[1], &arguments[2], assignments, ), - Intrinsic::ArrayLength => { + Implementation::Generated(G::ArrayLength) => { let value = self.lower_value(&arguments[0], assignments)?; let destination = self.fresh(ty); assignments.push(Assignment { @@ -62,50 +89,76 @@ impl FunctionLowerer<'_> { }); Ok(destination) } - Intrinsic::ArrayAppend => { + Implementation::Generated(G::ArrayFill) => { + self.lower_array_fill(expression, &arguments[0], &arguments[1], ty, assignments) + } + Implementation::Generated(G::ArrayWrite) => self.lower_array_write( + expression, + &arguments[0], + &arguments[1], + &arguments[2], + assignments, + ), + Implementation::Generated(G::ArrayAppend) => { self.lower_array_append(expression, &arguments[0], &arguments[1], ty, assignments) } - Intrinsic::StringToBytes => { + Implementation::Generated(G::StringToBytes) => { self.lower_string_to_bytes(expression, &arguments[0], ty, assignments) } - Intrinsic::BytesToString => { + Implementation::Generated(G::BytesToString) => { self.lower_bytes_to_string(expression, &arguments[0], ty, assignments) } - _ => match intrinsic.descriptor().category { - IntrinsicCategory::BinaryScalar => { - let left = self.lower_value(&arguments[0], assignments)?; - let right = self.lower_value(&arguments[1], assignments)?; - let destination = self.fresh(ty); - assignments.push(Assignment { - destination, - kind: AssignmentKind::Primitive { - op: lower_binary_op(intrinsic), - left, - right, - }, - span: expression.span, - }); - Ok(destination) - } - IntrinsicCategory::UnaryScalar => { - let value = self.lower_value(&arguments[0], assignments)?; - let destination = self.fresh(ty); - assignments.push(Assignment { - destination, - kind: AssignmentKind::Unary { - op: lower_unary_op(intrinsic), - value, - }, - span: expression.span, - }); - Ok(destination) - } - _ => Err(vec![BackendError::new( + Implementation::Generated(G::UnsafeCoerce) => { + let argument = &arguments[0]; + let source_type = argument.ty; + let source_shape = self.value_shape(source_type, expression.span)?; + let value = self.lower_value(argument, assignments)?; + let conversion = self.typed_conversion( + source_type, + expression.ty, + source_shape, + ty, + expression.span, + )?; + Ok(self.emit_conversion( + value, + source_shape, + ty, + conversion, + expression.span, + assignments, + )) + } + Implementation::Direct(operation) => { + let destination = self.fresh(ty); + let kind = match operation { + ScalarOperation::Unary(op) => AssignmentKind::Unary { + op, + value: self.lower_value(&arguments[0], assignments)?, + }, + ScalarOperation::Binary(op) => AssignmentKind::Primitive { + op, + left: self.lower_value(&arguments[0], assignments)?, + right: self.lower_value(&arguments[1], assignments)?, + }, + }; + assignments.push(Assignment { + destination, + kind, + span: expression.span, + }); + Ok(destination) + } + Implementation::Control => { + self.lower_control(expression, intrinsic, arguments, ty, assignments) + } + Implementation::Elaborated | Implementation::Unsupported => { + Err(vec![BackendError::invalid_ir( "P8 closure conversion", expression.span, - "intrinsic has no closure-conversion lowering", - )]), - }, + "intrinsic requires elaboration or has no runtime implementation", + )]) + } } } } diff --git a/crates/psrs-backend/src/cc/lower/lambda/captures.rs b/crates/psrs-backend/src/cc/lower/lambda/captures.rs index fee4554e..83a32b1e 100644 --- a/crates/psrs-backend/src/cc/lower/lambda/captures.rs +++ b/crates/psrs-backend/src/cc/lower/lambda/captures.rs @@ -13,6 +13,14 @@ pub(in crate::cc::lower) fn collect_captures( expression: &Expr, bound: &mut HashSet, captures: &mut Vec, +) { + psrs_span::with_sufficient_stack(|| collect_captures_inner(expression, bound, captures)) +} + +fn collect_captures_inner( + expression: &Expr, + bound: &mut HashSet, + captures: &mut Vec, ) { match &expression.kind { ExprKind::Local(local) => { diff --git a/crates/psrs-backend/src/cc/lower/lambda/eta.rs b/crates/psrs-backend/src/cc/lower/lambda/eta.rs new file mode 100644 index 00000000..aa277d83 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/lambda/eta.rs @@ -0,0 +1,72 @@ +//! Shared adaptation from a closure-valued body to a flattened callable. +use super::super::FunctionLowerer; +use crate::BackendError; +use crate::cc::{ + Assignment, AssignmentKind, RefShape, Reference, SignatureId, ValueId, ValueShape, +}; +use psrs_span::TextRange; + +pub(in crate::cc::lower) struct EtaApplication { + pub value: ValueId, + pub shape: ValueShape, + pub signature: SignatureId, + pub arguments: Vec, + pub result_shape: ValueShape, + pub span: TextRange, +} + +impl FunctionLowerer<'_> { + pub(in crate::cc::lower) fn apply_eta_result( + &mut self, + application: EtaApplication, + assignments: &mut Vec, + ) -> Result> { + let EtaApplication { + value, + shape, + signature, + arguments, + result_shape, + span, + } = application; + let closure_shape = ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Closure(signature), + }); + let callable = if super::is_erased_reference(shape) { + let closure = self.fresh(closure_shape); + assignments.push(Assignment { + destination: closure, + kind: AssignmentKind::RepresentationCast { + destination: closure, + value, + reference: Reference { + nullable: false, + heap: RefShape::Closure(signature), + }, + }, + span, + }); + closure + } else if shape == closure_shape { + value + } else { + return Err(vec![BackendError::new( + "P8 closure conversion", + span, + "eta body has an incompatible closure representation", + )]); + }; + let result = self.fresh(result_shape); + assignments.push(Assignment { + destination: result, + kind: AssignmentKind::IndirectCall { + function: callable, + signature, + arguments, + }, + span, + }); + Ok(result) + } +} diff --git a/crates/psrs-backend/src/cc/lower/lambda/mod.rs b/crates/psrs-backend/src/cc/lower/lambda/mod.rs index a5cda1c6..4e8ddba1 100644 --- a/crates/psrs-backend/src/cc/lower/lambda/mod.rs +++ b/crates/psrs-backend/src/cc/lower/lambda/mod.rs @@ -1,4 +1,6 @@ -use super::super::layout::{function_type_signature, scalar_type, unquantified_type}; +use super::super::layout::{ + function_arrow_parameters, function_type_signature, scalar_type, unquantified_type, +}; use super::super::{ Assignment, AssignmentKind, Function, RefShape, Reference, ValueId, ValueShape, }; @@ -8,10 +10,12 @@ use psrs_core::{Expr, ExprKind}; use std::collections::HashMap; mod captures; +mod eta; mod wrapper; pub(super) use captures::collect_captures; use captures::lambda_captures; +pub(super) use eta::EtaApplication; pub(super) use wrapper::make_wrapper; pub(super) trait LambdaLowering { @@ -80,6 +84,7 @@ impl LambdaLowering for FunctionLowerer<'_> { self.enum_types, self.aggregate_types, self.newtype_ids, + self.nominal_types, self.array_types, self.record_types, self.function_types, @@ -103,21 +108,42 @@ impl LambdaLowering for FunctionLowerer<'_> { let mut nested = self.child_lowerer(); let closure_parameter = nested.fresh(closure_value_type()); let mut parameters = vec![closure_parameter]; - for binder in &binders { - let parameter_type = scalar_type( + let source_parameter_types = function_arrow_parameters(self.module, expression.ty).0; + let mut nested_assignments = Vec::new(); + let mut binder_adaptations = Vec::with_capacity(binders.len()); + for (index, binder) in binders.iter().enumerate() { + let binder_shape = scalar_type( self.module, binder.ty, binder.span, self.enum_types, self.aggregate_types, self.newtype_ids, + self.nominal_types, self.array_types, self.record_types, self.function_types, )?; - let parameter = nested.fresh(parameter_type); - nested.locals.insert(binder.id, parameter); + let parameter_shape = signature_definition + .as_ref() + .and_then(|signature| signature.parameters.get(index)) + .copied() + .unwrap_or(binder_shape); + let parameter = nested.fresh(parameter_shape); nested.local_types.insert(binder.id, binder.ty); + let source_type = source_parameter_types + .get(index) + .copied() + .unwrap_or(binder.ty); + binder_adaptations.push(( + binder.id, + parameter, + source_type, + binder.ty, + parameter_shape, + binder_shape, + binder.span, + )); parameters.push(parameter); } let mut extra_parameters = Vec::new(); @@ -131,7 +157,26 @@ impl LambdaLowering for FunctionLowerer<'_> { extra_parameters.push(parameter); } } - let mut nested_assignments = Vec::new(); + for (local, parameter, source_type, binder_type, parameter_shape, binder_shape, span) in + binder_adaptations + { + let conversion = nested.typed_conversion( + source_type, + binder_type, + parameter_shape, + binder_shape, + span, + )?; + let bound_value = nested.emit_conversion( + parameter, + parameter_shape, + binder_shape, + conversion, + span, + &mut nested_assignments, + ); + nested.locals.insert(local, bound_value); + } for (index, capture) in captures.into_iter().enumerate() { let Some(outer_value) = self.locals.get(&capture).copied() else { return Err(capture_error(expression)); @@ -170,36 +215,17 @@ impl LambdaLowering for FunctionLowerer<'_> { .expect("an eta-expanded lambda has an interned signature"); let body_signature = body_signature.expect("an eta-expanded lambda has a body signature"); - // Recover the erased function value into its closure shape so the - // indirect call can name the body signature. - let callable = if is_erased_reference(nested_result_type) { - let closure = nested.fresh(closure_value_type_for(body_signature)); - nested_assignments.push(Assignment { - destination: closure, - kind: AssignmentKind::RepresentationCast { - destination: closure, - value: result, - reference: Reference { - nullable: false, - heap: RefShape::Closure(body_signature), - }, - }, - span: expression.span, - }); - closure - } else { - result - }; - let final_result = nested.fresh(signature_definition.result); - nested_assignments.push(Assignment { - destination: final_result, - kind: AssignmentKind::IndirectCall { - function: callable, + let final_result = nested.apply_eta_result( + EtaApplication { + value: result, + shape: nested_result_type, signature: body_signature, arguments: extra_parameters, + result_shape: signature_definition.result, + span: expression.span, }, - span: expression.span, - }); + &mut nested_assignments, + )?; (final_result, signature_definition.result) } else { (result, nested_result_type) @@ -215,11 +241,7 @@ impl LambdaLowering for FunctionLowerer<'_> { result_type: final_result_type, span: expression.span, }; - super::super::verify::verify_function( - &nested_function, - self.signatures, - self.representations, - )?; + self.verify_synthesized(&nested_function)?; let FunctionLowerer { generated: nested_generated, warnings: nested_warnings, @@ -274,11 +296,15 @@ impl LambdaLowering for FunctionLowerer<'_> { signatures: self.signatures, representations: self.representations, module: self.module, + boundary: self.boundary, enum_types: self.enum_types, aggregate_types: self.aggregate_types, newtype_ids: self.newtype_ids, + nominal_types: self.nominal_types, boxed_integer_type: self.boxed_integer_type, boxed_number_type: self.boxed_number_type, + control_layout: self.control_layout, + control_functions: std::rc::Rc::clone(&self.control_functions), array_types: self.array_types, record_types: self.record_types, constructor_tags: self.constructor_tags, @@ -287,6 +313,7 @@ impl LambdaLowering for FunctionLowerer<'_> { function_types: self.function_types, function_wrappers: self.function_wrappers, generated_symbols: std::rc::Rc::clone(&self.generated_symbols), + generated_signatures: std::rc::Rc::clone(&self.generated_signatures), owner: self.owner, warnings: Vec::new(), local_types: HashMap::new(), diff --git a/crates/psrs-backend/src/cc/lower/lambda/wrapper.rs b/crates/psrs-backend/src/cc/lower/lambda/wrapper.rs index be714e46..a207d979 100644 --- a/crates/psrs-backend/src/cc/lower/lambda/wrapper.rs +++ b/crates/psrs-backend/src/cc/lower/lambda/wrapper.rs @@ -45,6 +45,7 @@ pub(in crate::cc::lower) fn make_wrapper( assignments: vec![Assignment { destination: result, kind: AssignmentKind::DirectCall { + contract: None, function: source.symbol, arguments, }, @@ -114,6 +115,7 @@ fn eta_expanded_wrapper( let mut assignments = vec![Assignment { destination: intermediate, kind: AssignmentKind::DirectCall { + contract: None, function: source.symbol, arguments: first_parameters, }, diff --git a/crates/psrs-backend/src/cc/lower/letrec/mod.rs b/crates/psrs-backend/src/cc/lower/letrec/mod.rs index 9e30ab23..8866930b 100644 --- a/crates/psrs-backend/src/cc/lower/letrec/mod.rs +++ b/crates/psrs-backend/src/cc/lower/letrec/mod.rs @@ -4,7 +4,7 @@ use super::super::{ ValueShape, }; use super::FunctionLowerer; -use super::lambda::{LambdaLowering, collect_captures}; +use super::lambda::{EtaApplication, LambdaLowering, collect_captures}; use crate::BackendError; use psrs_core::{Binder, Binding, Expr, ExprKind}; use psrs_hir::{LocalId, SymbolId}; @@ -144,13 +144,13 @@ impl FunctionLowerer<'_> { "recursive local function has an unknown CC signature", )); }; - let (parameters, body) = peel_parameters(&binding.value, signature.parameters.len()) - .ok_or_else(|| { - lowering_error( - binding.span, - "recursive local function must bind every parameter in its CC signature", - ) - })?; + let (parameters, body) = peel_parameters(&binding.value, signature.parameters.len()); + if parameters.is_empty() && !psrs_core::opt::evaluation_is_inert(&binding.value) { + return Err(lowering_error( + binding.span, + "observable recursive initializer requires eager recursive value support", + )); + } let symbol = self.generated_symbols.borrow_mut().fresh(self.owner); functions.push(RecursiveFunction { binding, @@ -192,6 +192,15 @@ impl FunctionLowerer<'_> { parameters.push(parameter); } + // Like ordinary lambdas, recursive closures expose their complete + // flattened signature even when the written body returns a function. + let mut extra_parameters = Vec::new(); + for shape in &function.signature.parameters[function.parameters.len()..] { + let parameter = nested.fresh(*shape); + parameters.push(parameter); + extra_parameters.push(parameter); + } + let mut nested_assignments = Vec::new(); for (index, (local, shape)) in captures.iter().zip(capture_shapes).enumerate() { let destination = nested.fresh(*shape); @@ -237,7 +246,31 @@ impl FunctionLowerer<'_> { } let result = nested.lower_value(function.body, &mut nested_assignments)?; - let result_type = scalar_shape(&nested, function.body.ty, function.body.span)?; + let body_shape = scalar_shape(&nested, function.body.ty, function.body.span)?; + let (result, result_type) = if extra_parameters.is_empty() { + (result, body_shape) + } else { + let body_signature = + function_type_signature(self.module, self.function_types, function.body.ty) + .ok_or_else(|| { + lowering_error( + function.body.span, + "recursive eta body has no runtime function type", + ) + })?; + let result = nested.apply_eta_result( + EtaApplication { + value: result, + shape: body_shape, + signature: body_signature, + arguments: extra_parameters, + result_shape: function.signature.result, + span: function.body.span, + }, + &mut nested_assignments, + )?; + (result, function.signature.result) + }; if result_type != function.signature.result { return Err(lowering_error( function.body.span, @@ -301,17 +334,17 @@ fn recursive_captures( captures } -fn peel_parameters(expression: &Expr, count: usize) -> Option<(Vec<&Binder>, &Expr)> { +fn peel_parameters(expression: &Expr, count: usize) -> (Vec<&Binder>, &Expr) { let mut current = expression; let mut parameters = Vec::with_capacity(count); for _ in 0..count { let ExprKind::Lambda { binder, body } = ¤t.kind else { - return None; + break; }; parameters.push(binder); current = body; } - Some((parameters, current)) + (parameters, current) } fn emit_function_ref( @@ -372,6 +405,7 @@ fn scalar_shape( lowerer.enum_types, lowerer.aggregate_types, lowerer.newtype_ids, + lowerer.nominal_types, lowerer.array_types, lowerer.record_types, lowerer.function_types, diff --git a/crates/psrs-backend/src/cc/lower/letrec/tests/initialization.rs b/crates/psrs-backend/src/cc/lower/letrec/tests/initialization.rs new file mode 100644 index 00000000..c895905f --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/letrec/tests/initialization.rs @@ -0,0 +1,24 @@ +use super::*; + +#[test] +fn observable_recursive_initializer_is_not_delayed_until_a_call() { + let mut module = mutual_recursion_module(); + let ExprKind::Let { bindings, .. } = &mut module.declarations[0].value.kind else { + panic!("fixture contains a recursive group"); + }; + let original = bindings[0].value.clone(); + let ty = original.ty; + bindings[0].value.kind = ExprKind::If { + condition: Box::new(expression(ExprKind::Boolean(true), TypeId(1), 20, 21)), + then_branch: Box::new(expression(ExprKind::Trap, ty, 21, 22)), + else_branch: Box::new(original), + }; + let errors = crate::cc::lower_module(module) + .expect_err("an observable recursive initializer requires eager evaluation"); + assert!( + errors + .iter() + .any(|error| error.message.contains("eager recursive value support")), + "{errors:?}" + ); +} diff --git a/crates/psrs-backend/src/cc/lower/letrec/tests.rs b/crates/psrs-backend/src/cc/lower/letrec/tests/mod.rs similarity index 99% rename from crates/psrs-backend/src/cc/lower/letrec/tests.rs rename to crates/psrs-backend/src/cc/lower/letrec/tests/mod.rs index 582528ef..bfee48b0 100644 --- a/crates/psrs-backend/src/cc/lower/letrec/tests.rs +++ b/crates/psrs-backend/src/cc/lower/letrec/tests/mod.rs @@ -360,7 +360,7 @@ fn call_recursive( ) -> Expr { let decremented = expression( ExprKind::IntrinsicCall { - intrinsic: Intrinsic::I32Sub, + intrinsic: Intrinsic::IntSub, arguments: vec![local(parameter, int, start + 1), integer(1, int, start + 2)], }, int, @@ -381,7 +381,7 @@ fn call_recursive( fn eq_zero(local_id: LocalId, int: TypeId, boolean: TypeId, start: u32) -> Expr { expression( ExprKind::IntrinsicCall { - intrinsic: Intrinsic::I32Eq, + intrinsic: Intrinsic::IntEq, arguments: vec![local(local_id, int, start), integer(0, int, start + 1)], }, boolean, @@ -496,3 +496,5 @@ fn value_shape(function: &Function, value: ValueId) -> Option { .find(|declaration| declaration.id == value) .map(|declaration| declaration.ty) } + +mod initialization; diff --git a/crates/psrs-backend/src/cc/lower/mod.rs b/crates/psrs-backend/src/cc/lower/mod.rs index 9b564ddc..027402c7 100644 --- a/crates/psrs-backend/src/cc/lower/mod.rs +++ b/crates/psrs-backend/src/cc/lower/mod.rs @@ -3,6 +3,7 @@ use super::{ Assignment, AssignmentKind, Function, ReprId, RepresentationTable, Signature, SignatureId, ValueDecl, ValueId, ValueShape, }; +use crate::boundary::BoundaryEvidence; use crate::{BackendError, BackendWarning}; use psrs_core::{Expr, ExprKind, Module as CoreModule, dictionary::ClassLayout}; use psrs_hir::{LocalId, ModuleId, SymbolId, TypeId as HirTypeId}; @@ -13,6 +14,7 @@ use std::rc::Rc; mod array; mod call; mod constructor; +mod control; mod conversion; mod dictionary; mod erased; @@ -22,10 +24,11 @@ mod lambda; mod letrec; mod literals; mod record; -mod scalar; mod string_bytes; mod symbols; use call::ApplicationLowering; +pub(super) use control::plan_control_helpers; +pub(crate) use control::{Family, Shape, plan_external_targets, plan_shape_for, verify_boundary}; pub(in crate::cc) use conversion::VariantFieldConversion; use global::GlobalLowering; use lambda::LambdaLowering; @@ -34,13 +37,20 @@ pub(in crate::cc) use symbols::GeneratedSymbolAllocator; pub(super) struct LoweringContext<'a> { pub(super) module: &'a CoreModule, + /// The Core-to-CC boundary side table: checked instantiation evidence and + /// the registered representation policies. Replaces ambient reads of the + /// Core arena and any HIR-keyed protocol table. + pub(super) boundary: &'a BoundaryEvidence<'a>, pub(super) signatures: &'a HashMap, pub(super) representations: &'a RepresentationTable, pub(super) enum_types: &'a HashSet, pub(super) aggregate_types: &'a HashSet, pub(super) newtype_ids: &'a HashSet, + pub(super) nominal_types: &'a HashMap, pub(super) boxed_integer_type: Option, pub(super) boxed_number_type: Option, + pub(super) control_layout: Option, + pub(super) control_functions: Rc>>, pub(super) array_types: &'a HashMap, pub(super) record_types: &'a HashMap, pub(super) constructor_tags: &'a HashMap, @@ -49,6 +59,10 @@ pub(super) struct LoweringContext<'a> { pub(super) function_types: &'a HashMap, pub(super) function_wrappers: &'a HashMap, pub(super) generated_symbols: Rc>, + /// Signatures of functions generated during lowering (for example the + /// control executor). The per-function verifier resolves calls against these + /// in addition to the declared signatures. + pub(super) generated_signatures: Rc>>, } pub(super) fn lower_function( @@ -64,11 +78,15 @@ pub(super) fn lower_function( signatures: context.signatures, representations: context.representations, module, + boundary: context.boundary, enum_types: context.enum_types, aggregate_types: context.aggregate_types, newtype_ids: context.newtype_ids, + nominal_types: context.nominal_types, boxed_integer_type: context.boxed_integer_type, boxed_number_type: context.boxed_number_type, + control_layout: context.control_layout, + control_functions: Rc::clone(&context.control_functions), array_types: context.array_types, record_types: context.record_types, constructor_tags: context.constructor_tags, @@ -77,6 +95,7 @@ pub(super) fn lower_function( function_types: context.function_types, function_wrappers: context.function_wrappers, generated_symbols: Rc::clone(&context.generated_symbols), + generated_signatures: Rc::clone(&context.generated_signatures), owner: declaration.symbol.module, warnings: Vec::new(), generated: Vec::new(), @@ -102,6 +121,7 @@ pub(super) fn lower_function( context.enum_types, context.aggregate_types, context.newtype_ids, + context.nominal_types, context.array_types, context.record_types, context.function_types, @@ -140,6 +160,7 @@ pub(super) fn lower_function( context.enum_types, context.aggregate_types, context.newtype_ids, + context.nominal_types, context.array_types, context.record_types, context.function_types, @@ -166,6 +187,7 @@ pub(super) fn lower_function( context.enum_types, context.aggregate_types, context.newtype_ids, + context.nominal_types, context.array_types, context.record_types, context.function_types, @@ -180,7 +202,11 @@ pub(super) fn lower_function( result_type, span: declaration.span, }; - super::verify::verify_function(&function, context.signatures, context.representations)?; + let mut all_signatures = context.signatures.clone(); + for (symbol, signature) in state.generated_signatures.borrow().iter() { + all_signatures.insert(*symbol, signature.clone()); + } + super::verify::verify_function(&function, &all_signatures, context.representations)?; let mut generated = state.generated; generated.push(lambda::make_wrapper(&function, declaration, context)); let warnings = state @@ -203,11 +229,17 @@ pub(super) struct FunctionLowerer<'a> { pub(super) signatures: &'a HashMap, pub(super) representations: &'a RepresentationTable, pub(super) module: &'a CoreModule, + /// Checked instantiation evidence and registered representation policies, + /// supplied by the Core-to-CC boundary. + pub(super) boundary: &'a BoundaryEvidence<'a>, pub(super) enum_types: &'a HashSet, pub(super) aggregate_types: &'a HashSet, pub(super) newtype_ids: &'a HashSet, + pub(super) nominal_types: &'a HashMap, pub(super) boxed_integer_type: Option, pub(super) boxed_number_type: Option, + pub(super) control_layout: Option, + pub(super) control_functions: Rc>>, pub(super) array_types: &'a HashMap, pub(super) record_types: &'a HashMap, pub(super) constructor_tags: &'a HashMap, @@ -216,16 +248,43 @@ pub(super) struct FunctionLowerer<'a> { pub(super) function_types: &'a HashMap, pub(super) function_wrappers: &'a HashMap, pub(super) generated_symbols: Rc>, + /// Signatures of functions generated during lowering (for example the + /// control executor). The per-function verifier resolves calls against these + /// in addition to the declared signatures. + pub(super) generated_signatures: Rc>>, pub(super) owner: ModuleId, pub(super) warnings: Vec, pub(super) generated: Vec, } impl FunctionLowerer<'_> { + pub(in crate::cc::lower) fn verify_synthesized( + &self, + function: &Function, + ) -> Result<(), Vec> { + let mut signatures: std::collections::HashMap = + self.signatures.clone(); + signatures.extend( + self.generated_signatures + .borrow() + .iter() + .map(|(symbol, signature)| (*symbol, signature.clone())), + ); + crate::cc::verify::verify_function(function, &signatures, self.representations) + } + pub(super) fn lower_value( &mut self, expression: &Expr, assignments: &mut Vec, + ) -> Result> { + psrs_span::with_sufficient_stack(|| self.lower_value_inner_entry(expression, assignments)) + } + + fn lower_value_inner_entry( + &mut self, + expression: &Expr, + assignments: &mut Vec, ) -> Result> { let ty = scalar_type( self.module, @@ -234,6 +293,7 @@ impl FunctionLowerer<'_> { self.enum_types, self.aggregate_types, self.newtype_ids, + self.nominal_types, self.array_types, self.record_types, self.function_types, diff --git a/crates/psrs-backend/src/cc/lower/record/mod.rs b/crates/psrs-backend/src/cc/lower/record/mod.rs index e34f24bf..3056f0f9 100644 --- a/crates/psrs-backend/src/cc/lower/record/mod.rs +++ b/crates/psrs-backend/src/cc/lower/record/mod.rs @@ -212,7 +212,16 @@ impl FunctionLowerer<'_> { ty: ValueShape, assignments: &mut Vec, ) -> Result> { - let Some(representation) = self.record_types.get(&record.ty).copied() else { + // A local may be used at an instantiated type while its runtime value + // still has the layout fixed by its binder (notably a class dictionary + // passed through a higher-kinded method). Project using that stored + // layout, then convert the selected field to the use-site type below. + let source_type = match &record.kind { + psrs_core::ExprKind::Local(local) => self.local_types.get(local).copied(), + _ => None, + } + .unwrap_or(record.ty); + let Some(representation) = self.record_types.get(&source_type).copied() else { return Err(vec![BackendError::new( "P8 closure conversion", expression.span, @@ -260,12 +269,18 @@ impl FunctionLowerer<'_> { }); let target_type = expression.ty; let target_shape = self.value_shape(target_type, expression.span)?; - let conversion = self.typed_conversion( + // The stored field keeps the dictionary method's scheme. Its use type + // is the checked instance, including a constructor such as Effect. + // The source module still has that relation after representation + // rewriting replaces applications at the same type ids. + let evidence = self.boundary.instantiation_at(field_layout.ty, target_type); + let conversion = self.typed_conversion_with_instantiation( field_layout.ty, target_type, stored, target_shape, expression.span, + evidence.as_ref(), )?; Ok(self.emit_conversion( projected, diff --git a/crates/psrs-backend/src/cc/lower/scalar.rs b/crates/psrs-backend/src/cc/lower/scalar.rs deleted file mode 100644 index 76c4c7f7..00000000 --- a/crates/psrs-backend/src/cc/lower/scalar.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! P8 conversion from Core intrinsic identities to CC-owned scalar operations. - -use crate::cc::{BinaryOp, UnaryOp}; -use psrs_hir::Intrinsic; - -pub(super) fn lower_binary_op(value: Intrinsic) -> BinaryOp { - match value { - Intrinsic::I32Add => BinaryOp::IntAdd, - Intrinsic::I32Sub => BinaryOp::IntSub, - Intrinsic::I32Mul => BinaryOp::IntMul, - Intrinsic::I32DivS => BinaryOp::IntQuot, - Intrinsic::I32RemS => BinaryOp::IntRem, - Intrinsic::IntDiv => BinaryOp::IntDiv, - Intrinsic::IntMod => BinaryOp::IntMod, - Intrinsic::IntAnd => BinaryOp::IntAnd, - Intrinsic::IntOr => BinaryOp::IntOr, - Intrinsic::IntXor => BinaryOp::IntXor, - Intrinsic::IntShl => BinaryOp::IntShl, - Intrinsic::IntShr => BinaryOp::IntShr, - Intrinsic::IntZshr => BinaryOp::IntZshr, - Intrinsic::I32Eq => BinaryOp::IntEq, - Intrinsic::I32Ne => BinaryOp::IntNe, - Intrinsic::I32LtS => BinaryOp::IntLt, - Intrinsic::I32LeS => BinaryOp::IntLe, - Intrinsic::I32GtS => BinaryOp::IntGt, - Intrinsic::I32GeS => BinaryOp::IntGe, - Intrinsic::NumberAdd => BinaryOp::NumberAdd, - Intrinsic::NumberSub => BinaryOp::NumberSub, - Intrinsic::NumberMul => BinaryOp::NumberMul, - Intrinsic::NumberDiv => BinaryOp::NumberDiv, - Intrinsic::NumberEq => BinaryOp::NumberEq, - Intrinsic::NumberNe => BinaryOp::NumberNe, - Intrinsic::NumberLt => BinaryOp::NumberLt, - Intrinsic::NumberLe => BinaryOp::NumberLe, - Intrinsic::NumberGt => BinaryOp::NumberGt, - Intrinsic::NumberGe => BinaryOp::NumberGe, - Intrinsic::BooleanAnd => BinaryOp::BooleanAnd, - Intrinsic::BooleanOr => BinaryOp::BooleanOr, - Intrinsic::BooleanEq => BinaryOp::BooleanEq, - Intrinsic::BooleanNe => BinaryOp::BooleanNe, - Intrinsic::CharEq => BinaryOp::CharEq, - Intrinsic::CharNe => BinaryOp::CharNe, - Intrinsic::CharLt => BinaryOp::CharLt, - Intrinsic::CharLe => BinaryOp::CharLe, - Intrinsic::CharGt => BinaryOp::CharGt, - Intrinsic::CharGe => BinaryOp::CharGe, - _ => unreachable!( - "lower_binary_op is only called for a binary scalar intrinsic, got {value:?}" - ), - } -} - -pub(super) fn lower_unary_op(value: Intrinsic) -> UnaryOp { - match value { - Intrinsic::IntNeg => UnaryOp::IntNeg, - Intrinsic::IntComplement => UnaryOp::IntComplement, - Intrinsic::NumberNeg => UnaryOp::NumberNeg, - Intrinsic::BooleanNot => UnaryOp::BooleanNot, - Intrinsic::IntToNumber => UnaryOp::IntToNumber, - Intrinsic::NumberToInt => UnaryOp::NumberToInt, - Intrinsic::BooleanToInt => UnaryOp::BooleanToInt, - Intrinsic::IntToBoolean => UnaryOp::IntToBoolean, - Intrinsic::CharToInt => UnaryOp::CharToInt, - Intrinsic::IntToChar => UnaryOp::IntToChar, - _ => unreachable!( - "lower_unary_op is only called for a unary scalar intrinsic, got {value:?}" - ), - } -} diff --git a/crates/psrs-backend/src/cc/lower/string_bytes.rs b/crates/psrs-backend/src/cc/lower/string_bytes.rs index 1b6750b2..6d4328f0 100644 --- a/crates/psrs-backend/src/cc/lower/string_bytes.rs +++ b/crates/psrs-backend/src/cc/lower/string_bytes.rs @@ -1,4 +1,7 @@ -use super::super::{Assignment, AssignmentKind, ValueId, ValueShape}; +use super::super::{ + Assignment, AssignmentKind, RefShape, Reference, ReprId, Representation, ValueConversion, + ValueId, ValueShape, +}; use super::FunctionLowerer; use crate::BackendError; use psrs_core::Expr; @@ -19,14 +22,19 @@ impl FunctionLowerer<'_> { ty: ValueShape, assignments: &mut Vec, ) -> Result> { - let Some(representation) = self.array_types.get(&expression.ty).copied() else { + let Some(source_representation) = self.array_types.get(&expression.ty).copied() else { return Err(missing_representation( expression.span, "stringToBytes result has no representation requirement", )); }; + let representation = self.byte_buffer_representation(expression.span)?; + let buffer_shape = ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Repr(representation), + }); let value = self.lower_value(value, assignments)?; - let destination = self.fresh(ty); + let destination = self.fresh(buffer_shape); assignments.push(Assignment { destination, kind: AssignmentKind::StringToBytes { @@ -36,7 +44,20 @@ impl FunctionLowerer<'_> { }, span: expression.span, }); - Ok(destination) + let element = self.erase_payload(ValueShape::Integer, expression.span)?; + let plan = ValueConversion::ArrayMap { + source: representation, + target: source_representation, + element: Box::new(element), + }; + Ok(self.emit_conversion( + destination, + buffer_shape, + ty, + plan, + expression.span, + assignments, + )) } pub(super) fn lower_bytes_to_string( @@ -46,13 +67,33 @@ impl FunctionLowerer<'_> { ty: ValueShape, assignments: &mut Vec, ) -> Result> { - let Some(representation) = self.array_types.get(&value.ty).copied() else { + let Some(source_representation) = self.array_types.get(&value.ty).copied() else { return Err(missing_representation( expression.span, "bytesToString argument has no representation requirement", )); }; + let representation = self.byte_buffer_representation(expression.span)?; + let source_shape = self.value_shape(value.ty, value.span)?; + let buffer_shape = ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Repr(representation), + }); let value = self.lower_value(value, assignments)?; + let element = self.recover_payload(ValueShape::Integer, expression.span)?; + let plan = ValueConversion::ArrayMap { + source: source_representation, + target: representation, + element: Box::new(element), + }; + let value = self.emit_conversion( + value, + source_shape, + buffer_shape, + plan, + expression.span, + assignments, + ); let destination = self.fresh(ty); assignments.push(Assignment { destination, @@ -65,6 +106,13 @@ impl FunctionLowerer<'_> { }); Ok(destination) } + + fn byte_buffer_representation(&self, span: TextRange) -> Result> { + self.representations.representations.iter().position(|representation| + matches!(representation, Representation::Array { element } if *element == ValueShape::Integer)) + .map(|index| ReprId(index as u32)) + .ok_or_else(|| missing_representation(span, "text codec has no private integer buffer representation")) + } } fn missing_representation(span: TextRange, message: &'static str) -> Vec { diff --git a/crates/psrs-backend/src/cc/mod.rs b/crates/psrs-backend/src/cc/mod.rs index 2081c710..c68e1b54 100644 --- a/crates/psrs-backend/src/cc/mod.rs +++ b/crates/psrs-backend/src/cc/mod.rs @@ -1,3 +1,4 @@ +use crate::boundary::{BoundaryEvidence, RepresentationRegistry}; use crate::{BackendError, BackendInput, ExternalBindings, annotate_errors}; use psrs_core::Module as CoreModule; use psrs_hir::{SymbolId, TypeId as HirTypeId}; @@ -6,19 +7,33 @@ use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; +pub(crate) mod branch_facts; mod case; +mod control; +pub(crate) mod value_identity; +pub(crate) use control::ControlSchema; +pub use control::FrozenControlPlan; +pub use control::{ + ControlContract, ControlContractId, ControlDomain, ControlHelperContract, ControlHelperId, + ControlHelperRole, ControlResumptionContract, SourceTypeIdentityId, TypeConstructorIdentity, + TypeIdentity, +}; +pub(crate) use lower::{Family, Shape, plan_external_targets, plan_shape_for}; mod convert; mod layout; mod lower; +pub(crate) mod payload; mod projection; mod representation; mod scalar; mod source_abi; mod verify; +pub use verify::verify_module; pub(crate) use source_abi::abstract_signature; use layout::{aggregate_type_ids, declaration_shape, enum_type_ids, type_layout}; +pub(crate) use layout::{checked_function_arrow_parameters, is_callable_type}; use lower::{GeneratedSymbolAllocator, LoweringContext, lower_function}; pub use crate::types::ValueId; @@ -79,6 +94,14 @@ pub struct Assignment { #[derive(Clone, Debug, PartialEq, Eq)] pub enum AssignmentKind { + /// Retains the checked source operation at its explicit P8 erasure boundary. + /// The runtime result is the existing value; Wasm encoding erases this witness. + ControlEvidence { + value: ValueId, + implementation: ValueId, + arguments: Vec, + contract: ControlContractId, + }, Constant(i32), NumberConstant(String), StringConstant(String), @@ -92,6 +115,8 @@ pub enum AssignmentKind { value: ValueId, }, DirectCall { + /// Required for predeclared machine helpers; absent for ordinary calls. + contract: Option, function: SymbolId, arguments: Vec, }, @@ -105,6 +130,7 @@ pub enum AssignmentKind { signature: SignatureId, arguments: Vec, }, + /// A checked source runner supplies and discharges one logical root. ClosureGetCapture { closure: ValueId, index: u32, @@ -158,6 +184,12 @@ pub enum AssignmentKind { representation: ReprId, elements: Vec, }, + ArrayFill { + destination: ValueId, + representation: ReprId, + length: ValueId, + value: ValueId, + }, ArrayLen { destination: ValueId, value: ValueId, @@ -178,6 +210,11 @@ pub enum AssignmentKind { representation: ReprId, value: ValueId, }, + /// Artifact-backed operation with checked language arguments before ABI erasure. + RuntimeCall { + intrinsic: psrs_hir::Intrinsic, + arguments: Vec, + }, /// An `Array Int` read as a source `String`. Every element must be a /// canonical byte and the bytes must be well-formed UTF-8. BytesToString { @@ -248,8 +285,33 @@ pub fn lower_module_with_bindings( module: CoreModule, bindings: ExternalBindings, ) -> Result> { + lower_module_with_relations(module, bindings, None, RepresentationRegistry::new()) +} + +/// Lowers Core using the Core-to-CC boundary side table: an immutable source +/// program for checked instantiation evidence and the registered representation +/// policies. `source` is the pre-lowering module when effect applications have +/// been rewritten; otherwise it is absent and relations are read from `module`. +pub(crate) fn lower_module_with_relations( + mut module: CoreModule, + bindings: ExternalBindings, + source: Option<&CoreModule>, + mut registry: RepresentationRegistry, +) -> Result> { + crate::bindings::lower_primitives(&mut module, source)?; bindings.validate_core(&module)?; - if let Err(errors) = module.verify() { + module = psrs_core::instantiate_local_rows(module, source).map_err(|errors| { + errors + .into_iter() + .map(|error| { + BackendError::new("P8 local row instantiation", error.span, error.message) + .with_module(error.module) + }) + .collect::>() + })?; + let relations = source.unwrap_or(&module); + registry.register_newtypes(relations); + if let Err(errors) = module.verify_with_source(relations) { return Err(annotate_errors( errors .into_iter() @@ -261,7 +323,7 @@ pub fn lower_module_with_bindings( let newtype_ids = module.newtype_ids.iter().copied().collect(); let enum_types = enum_type_ids(&module, &newtype_ids); let aggregate_types = aggregate_type_ids(&module, &newtype_ids); - let layout = type_layout(&module, &enum_types, &aggregate_types, &newtype_ids)?; + let mut layout = type_layout(&module, &enum_types, &aggregate_types, &newtype_ids)?; let mut constructor_tags = HashMap::new(); let mut constructors_by_type: HashMap> = HashMap::new(); for constructor in &module.constructors { @@ -279,6 +341,7 @@ pub fn lower_module_with_bindings( &enum_types, &aggregate_types, &newtype_ids, + &layout.nominal_types, &layout.array_types, &layout.record_types, &layout.function_types, @@ -323,8 +386,47 @@ pub fn lower_module_with_bindings( projection, }); } + for binding in &bindings.runtime { + let type_id = binding.type_id.ok_or_else(|| { + vec![ + BackendError::invalid_ir( + "P8 closure conversion", + binding.span, + "runtime binding has no checked source scheme", + ) + .with_module(binding.source_module), + ] + })?; + let signature = layout::function_signature( + &module, + type_id, + &enum_types, + &aggregate_types, + &newtype_ids, + &layout.nominal_types, + &layout.array_types, + &layout.record_types, + &layout.function_types, + ) + .map_err(|errors| { + errors + .into_iter() + .map(|error| error.with_module(binding.source_module)) + .collect::>() + })?; + let signature = + crate::bindings::runtime_cc_signature(binding, &signature, &layout.representations)?; + signatures.insert(binding.symbol, signature.clone()); + externals.push(External { + symbol: binding.symbol, + signature: Some(signature), + projection: None, + }); + } let mut functions = Vec::with_capacity(module.declarations.len()); let generated_symbols = Rc::new(RefCell::new(GeneratedSymbolAllocator::new(&module))); + let generated_signatures: Rc>> = + Rc::new(RefCell::new(HashMap::new())); let function_wrappers = module .declarations .iter() @@ -335,15 +437,27 @@ pub fn lower_module_with_bindings( (declaration.symbol, wrapper) }) .collect::>(); + let control_functions = lower::plan_control_helpers(&mut layout, &generated_symbols); + let boundary = BoundaryEvidence::new( + relations, + &module, + registry, + layout.protocols, + Some(layout.function_slot), + ); let context = LoweringContext { module: &module, + boundary: &boundary, signatures: &signatures, representations: &layout.representations, enum_types: &enum_types, aggregate_types: &aggregate_types, newtype_ids: &newtype_ids, + nominal_types: &layout.nominal_types, boxed_integer_type: layout.boxed_integer_type, boxed_number_type: layout.boxed_number_type, + control_layout: layout.control_layout, + control_functions: Rc::new(RefCell::new(control_functions)), array_types: &layout.array_types, record_types: &layout.record_types, constructor_tags: &constructor_tags, @@ -352,6 +466,7 @@ pub fn lower_module_with_bindings( function_types: &layout.function_types, function_wrappers: &function_wrappers, generated_symbols, + generated_signatures, }; let mut warnings = Vec::new(); for declaration in &module.declarations { diff --git a/crates/psrs-backend/src/cc/payload.rs b/crates/psrs-backend/src/cc/payload.rs new file mode 100644 index 00000000..f10ce210 --- /dev/null +++ b/crates/psrs-backend/src/cc/payload.rs @@ -0,0 +1,363 @@ +//! Shared conversion planning for bare polymorphic storage slots. +//! CC and canonical ABI adapters use the same recursive owner protocols. + +use super::{ + BoxKind, RecoveryEvidence, RefShape, Reference, ReprId, RepresentationTable, SignatureId, + ValueConversion, ValueShape, +}; +use crate::BackendError; +use psrs_span::TextRange; + +/// Project a checked field through the same owner protocol as payload erasure. +/// Retain semantic aggregate structure when its physical slot is only eqref. +pub(crate) fn stored_guest_layout( + field: &super::Field, + table: &RepresentationTable, +) -> Option { + use super::{Field, GuestLayout}; + if field.stored != erased_shape() { + return super::guest_layout(field.stored, table); + } + Some(match &field.value { + GuestLayout::Scalar { .. } | GuestLayout::Boxed { .. } => GuestLayout::Scalar { + shape: erased_shape(), + }, + GuestLayout::Product { labels, fields, .. } => GuestLayout::Product { + repr: super::layout::protocols::record(table, labels)?, + labels: labels.clone(), + fields: fields + .iter() + .map(|field| Field { + value: field.value.clone(), + stored: erased_shape(), + }) + .collect(), + }, + GuestLayout::Array { repr, element } => { + if !matches!(table.representation(*repr), Some(super::Representation::Array { element }) if *element == erased_shape()) + { + return None; + } + GuestLayout::Array { + repr: *repr, + element: element.clone(), + } + } + GuestLayout::Variant { .. } => field.value.clone(), + }) +} + +pub(crate) trait PayloadPlanner { + fn payload_table(&self) -> &RepresentationTable; + fn payload_box(&self, kind: BoxKind) -> Option; + fn payload_callable( + &mut self, + signature: SignatureId, + entering: bool, + span: TextRange, + ) -> Result>; + fn payload_error(&self, span: TextRange, message: &'static str) -> Vec; + + fn erase_payload( + &mut self, + shape: ValueShape, + span: TextRange, + ) -> Result> { + if shape == erased_shape() { + return Ok(ValueConversion::Identity); + } + if let Some(plan) = self.array_payload(shape, true, span)? { + return Ok(plan); + } + if let Some(plan) = self.record_payload(shape, true, span)? { + return Ok(plan); + } + if let ValueShape::Reference(Reference { + heap: RefShape::Closure(signature), + .. + }) = shape + { + let adapter = self.payload_callable(signature, true, span)?; + return Ok(sequence(vec![adapter, ValueConversion::EraseReference])); + } + let boxed = match shape { + ValueShape::Integer | ValueShape::Boolean => { + self.box_plan(BoxKind::Integer, self.payload_box(BoxKind::Integer), span)? + } + ValueShape::Number => { + self.box_plan(BoxKind::Number, self.payload_box(BoxKind::Number), span)? + } + ValueShape::String | ValueShape::Reference(_) => { + return Ok(ValueConversion::EraseReference); + } + }; + Ok(sequence(vec![boxed, ValueConversion::EraseReference])) + } + + fn recover_payload( + &mut self, + shape: ValueShape, + span: TextRange, + ) -> Result> { + if shape == erased_shape() { + return Ok(ValueConversion::Identity); + } + if let Some(plan) = self.array_payload(shape, false, span)? { + return Ok(plan); + } + if let Some(plan) = self.record_payload(shape, false, span)? { + return Ok(plan); + } + if let ValueShape::Reference(Reference { + heap: RefShape::Closure(signature), + .. + }) = shape + { + return self.payload_callable(signature, false, span); + } + match shape { + ValueShape::Integer | ValueShape::Boolean => self.unbox_plan( + BoxKind::Integer, + self.payload_box(BoxKind::Integer), + shape, + span, + ), + ValueShape::Number => self.unbox_plan( + BoxKind::Number, + self.payload_box(BoxKind::Number), + shape, + span, + ), + ValueShape::String | ValueShape::Reference(_) => { + Ok(ValueConversion::RecoverReference { + destination: shape, + evidence: RecoveryEvidence::TypeInstantiation, + }) + } + } + } + + fn array_payload( + &mut self, + shape: ValueShape, + entering: bool, + span: TextRange, + ) -> Result, Vec> { + let ValueShape::Reference(Reference { + heap: RefShape::Repr(concrete), + .. + }) = shape + else { + return Ok(None); + }; + let Some(crate::cc::Representation::Array { element }) = + self.payload_table().representation(concrete) + else { + return Ok(None); + }; + let element_shape = *element; + let protocol = self.payload_table().representations.iter().position(|representation| + matches!(representation, crate::cc::Representation::Array { element } if *element == erased_shape())) + .map(|index| ReprId(index as u32)) + .ok_or_else(|| self.payload_error(span, "Array owner has no erased storage protocol"))?; + let protocol_shape = ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Repr(protocol), + }); + if concrete == protocol { + return Ok(Some(if entering { + ValueConversion::EraseReference + } else { + ValueConversion::RecoverReference { + destination: shape, + evidence: RecoveryEvidence::TypeInstantiation, + } + })); + } + if entering { + let element = self.erase_payload(element_shape, span)?; + Ok(Some(sequence(vec![ + ValueConversion::ArrayMap { + source: concrete, + target: protocol, + element: Box::new(element), + }, + ValueConversion::EraseReference, + ]))) + } else { + let element = self.recover_payload(element_shape, span)?; + Ok(Some(sequence(vec![ + ValueConversion::RecoverReference { + destination: protocol_shape, + evidence: RecoveryEvidence::TypeInstantiation, + }, + ValueConversion::ArrayMap { + source: protocol, + target: concrete, + element: Box::new(element), + }, + ]))) + } + } + + fn record_payload( + &mut self, + shape: ValueShape, + entering: bool, + span: TextRange, + ) -> Result, Vec> { + let ValueShape::Reference(Reference { + heap: RefShape::Repr(concrete), + .. + }) = shape + else { + return Ok(None); + }; + let Some(labels) = self + .payload_table() + .product_labels(concrete) + .map(<[String]>::to_vec) + else { + return Ok(None); + }; + let Some(crate::cc::Representation::Product { fields }) = + self.payload_table().representation(concrete) + else { + return Err(self.payload_error(span, "record owner labels do not name a product")); + }; + let fields = fields.clone(); + let protocol = super::layout::protocols::record(self.payload_table(), &labels) + .ok_or_else(|| self.payload_error(span, "record owner has no erased field protocol"))?; + if concrete == protocol { + return Ok(Some(if entering { + ValueConversion::EraseReference + } else { + ValueConversion::RecoverReference { + destination: shape, + evidence: RecoveryEvidence::TypeInstantiation, + } + })); + } + let plans = fields + .into_iter() + .map(|field| { + if entering { + self.erase_payload(field, span) + } else { + self.recover_payload(field, span) + } + }) + .collect::, _>>()?; + Ok(Some(if entering { + sequence(vec![ + ValueConversion::ProductMap { + source: concrete, + target: protocol, + labels, + fields: plans, + }, + ValueConversion::EraseReference, + ]) + } else { + let protocol_shape = ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Repr(protocol), + }); + sequence(vec![ + ValueConversion::RecoverReference { + destination: protocol_shape, + evidence: RecoveryEvidence::TypeInstantiation, + }, + ValueConversion::ProductMap { + source: protocol, + target: concrete, + labels, + fields: plans, + }, + ]) + })) + } + + fn box_plan( + &self, + kind: BoxKind, + representation: Option, + span: TextRange, + ) -> Result> { + representation + .map(|representation| ValueConversion::BoxScalar { + kind, + representation, + }) + .ok_or_else(|| self.payload_error(span, "erased scalar has no box representation")) + } + + fn unbox_plan( + &self, + kind: BoxKind, + representation: Option, + destination: ValueShape, + span: TextRange, + ) -> Result> { + representation + .map(|representation| ValueConversion::UnboxScalar { + kind, + representation, + destination, + }) + .ok_or_else(|| self.payload_error(span, "erased scalar has no box representation")) + } +} + +pub(crate) fn erased_shape() -> ValueShape { + ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Erased, + }) +} + +fn sequence(steps: Vec) -> ValueConversion { + match steps.len() { + 0 => ValueConversion::Identity, + 1 => steps.into_iter().next().expect("one conversion step"), + _ => ValueConversion::Sequence(steps), + } +} + +/// The canonical ABI admits no callable values; aggregate storage still uses +/// the same owner plans as CC. Reachability and MIR lowering share this planner. +pub(crate) struct StoragePayloadPlanner<'a>(pub(crate) &'a RepresentationTable); + +impl PayloadPlanner for StoragePayloadPlanner<'_> { + fn payload_table(&self) -> &RepresentationTable { + self.0 + } + fn payload_box(&self, kind: crate::cc::BoxKind) -> Option { + let shape = match kind { + crate::cc::BoxKind::Integer => crate::cc::ValueShape::Integer, + crate::cc::BoxKind::Number => crate::cc::ValueShape::Number, + }; + self.0 + .representations + .iter() + .position( + |repr| matches!(repr, crate::cc::Representation::Box { value } if *value == shape), + ) + .map(|index| ReprId(index as u32)) + } + fn payload_callable( + &mut self, + _signature: crate::cc::SignatureId, + _entering: bool, + span: TextRange, + ) -> Result> { + Err(vec![BackendError::new( + "P9 MIR lowering", + span, + "callable values have no canonical ABI payload conversion", + )]) + } + fn payload_error(&self, span: TextRange, message: &'static str) -> Vec { + vec![BackendError::new("P9 MIR lowering", span, message)] + } +} diff --git a/crates/psrs-backend/src/cc/projection.rs b/crates/psrs-backend/src/cc/projection.rs index 83bd96c7..919c3a66 100644 --- a/crates/psrs-backend/src/cc/projection.rs +++ b/crates/psrs-backend/src/cc/projection.rs @@ -417,8 +417,8 @@ mod tests { nullable: false, heap: RefShape::Erased, }); - let representations = RepresentationTable { - representations: vec![ + let representations = RepresentationTable::from_parts( + vec![ Representation::Product { fields: vec![ValueShape::String], }, @@ -435,11 +435,11 @@ mod tests { ], }, ], - signatures: Vec::new(), - product_labels: [(ReprId(0), vec!["name".to_string()])] + Vec::new(), + [(ReprId(0), vec!["name".to_string()])] .into_iter() .collect(), - }; + ); let record_types = [(record, ReprId(0))].into_iter().collect(); let array_types = HashMap::new(); let constructor_types = [(just, ReprId(1)), (nothing, ReprId(1))] diff --git a/crates/psrs-backend/src/cc/representation.rs b/crates/psrs-backend/src/cc/representation.rs index 059eccb5..c7644e2d 100644 --- a/crates/psrs-backend/src/cc/representation.rs +++ b/crates/psrs-backend/src/cc/representation.rs @@ -225,6 +225,8 @@ fn storage_case( #[derive(Clone, Debug, PartialEq, Eq, Default)] pub struct RepresentationTable { + /// Source planning publishes immutable machine metadata before CC lowering. + pub(in crate::cc) control: super::control::ControlPlanning, pub representations: Vec, pub signatures: Vec, /// Canonical logical labels for record products; positional products do @@ -233,6 +235,20 @@ pub struct RepresentationTable { } impl RepresentationTable { + /// Construct ordinary IR layouts without publishing control-machine metadata. + pub fn from_parts( + representations: Vec, + signatures: Vec, + product_labels: std::collections::HashMap>, + ) -> Self { + Self { + representations, + signatures, + product_labels, + ..Self::default() + } + } + pub fn reserve(&mut self) -> ReprId { let id = ReprId(self.representations.len() as u32); // A temporary product is replaced before the table leaves P8. Keeping diff --git a/crates/psrs-backend/src/cc/scalar.rs b/crates/psrs-backend/src/cc/scalar.rs index 023fce67..c0e19713 100644 --- a/crates/psrs-backend/src/cc/scalar.rs +++ b/crates/psrs-backend/src/cc/scalar.rs @@ -5,6 +5,11 @@ pub enum UnaryOp { IntNeg, IntComplement, NumberNeg, + NumberAbs, + NumberSqrt, + NumberTrunc, + NumberFloor, + NumberCeil, BooleanNot, IntToNumber, NumberToInt, @@ -21,8 +26,6 @@ pub enum BinaryOp { IntMul, IntQuot, IntRem, - IntDiv, - IntMod, IntAnd, IntOr, IntXor, diff --git a/crates/psrs-backend/src/cc/value_identity.rs b/crates/psrs-backend/src/cc/value_identity.rs new file mode 100644 index 00000000..3f97eba6 --- /dev/null +++ b/crates/psrs-backend/src/cc/value_identity.rs @@ -0,0 +1,144 @@ +#![allow(dead_code)] +//! Identity-preserving CC operations, shared by semantic evidence consumers. +use super::{AssignmentKind, ValueConversion, ValueId}; +use std::collections::HashSet; + +/// A join preserves identity only when every normal alternative has the same +/// origin. A trapping alternative contributes no returned value. +pub(crate) fn reference_origin<'a>( + value: ValueId, + mut definition: impl FnMut(ValueId) -> Option<&'a AssignmentKind>, +) -> ValueId { + fn resolve<'a>( + value: ValueId, + definition: &mut impl FnMut(ValueId) -> Option<&'a AssignmentKind>, + active: &mut HashSet, + ) -> Option { + if !active.insert(value) { + return Some(value); + } + let result = match definition(value) { + Some(AssignmentKind::Unreachable) => None, + Some( + AssignmentKind::RepresentationCast { value, .. } + | AssignmentKind::ArraySet { value, .. }, + ) => resolve(*value, definition, active), + Some(AssignmentKind::AggregateConvert { + value, conversion, .. + }) if transparent(&conversion.plan) => resolve(*value, definition, active), + Some(AssignmentKind::If { + then_value, + else_value, + .. + }) => join(value, [*then_value, *else_value], definition, active), + Some(AssignmentKind::TagSwitch { + cases, + default_value, + .. + }) => join( + value, + cases.iter().map(|case| case.value).chain([*default_value]), + definition, + active, + ), + _ => Some(value), + }; + active.remove(&value); + result + } + fn join<'a>( + value: ValueId, + inputs: impl IntoIterator, + definition: &mut impl FnMut(ValueId) -> Option<&'a AssignmentKind>, + active: &mut HashSet, + ) -> Option { + let mut common = None; + for input in inputs { + if let Some(origin) = resolve(input, definition, active) { + if common.is_some_and(|previous| previous != origin) { + return Some(value); + } + common = Some(origin); + } + } + common + } + resolve(value, &mut definition, &mut HashSet::new()).unwrap_or(value) +} + +pub(crate) fn transparent(conversion: &ValueConversion) -> bool { + match conversion { + ValueConversion::Identity + | ValueConversion::EraseReference + | ValueConversion::RecoverReference { .. } => true, + ValueConversion::Sequence(steps) => steps.iter().all(transparent), + _ => false, + } +} + +/// Index definitions across every nested assignment sequence, without guessing +/// identities from spans or names. +pub(crate) fn definitions( + body: &[super::Assignment], +) -> std::collections::HashMap { + let mut definitions = std::collections::HashMap::new(); + let mut pending = vec![body]; + while let Some(body) = pending.pop() { + for assignment in body { + // An in-place store changes contents, not the carrier's definition. + // Preserve the allocation/projection that established its identity. + if !matches!(&assignment.kind, AssignmentKind::ArraySet { value, .. } if *value == assignment.destination) + { + definitions.insert(assignment.destination, &assignment.kind); + } + match &assignment.kind { + AssignmentKind::If { + then_assignments, + else_assignments, + .. + } => { + pending.extend([then_assignments.as_slice(), else_assignments.as_slice()]); + } + AssignmentKind::TagSwitch { + cases, + default_assignments, + .. + } => { + pending.extend(cases.iter().map(|case| case.assignments.as_slice())); + pending.push(default_assignments); + } + _ => {} + } + } + } + definitions +} + +pub(crate) fn path_origin( + mut value: ValueId, + definitions: &std::collections::HashMap, + aliases: &std::collections::HashMap, +) -> ValueId { + let mut seen = HashSet::new(); + while seen.insert(value) { + let next = + aliases + .get(&value) + .copied() + .or_else(|| match definitions.get(&value).copied() { + Some( + AssignmentKind::RepresentationCast { value, .. } + | AssignmentKind::ArraySet { value, .. }, + ) => Some(*value), + Some(AssignmentKind::AggregateConvert { + value, conversion, .. + }) if transparent(&conversion.plan) => Some(*value), + _ => None, + }); + match next { + Some(next) => value = next, + None => break, + } + } + value +} diff --git a/crates/psrs-backend/src/cc/verify/helpers.rs b/crates/psrs-backend/src/cc/verify/helpers.rs index 7252dda8..915c9581 100644 --- a/crates/psrs-backend/src/cc/verify/helpers.rs +++ b/crates/psrs-backend/src/cc/verify/helpers.rs @@ -68,7 +68,7 @@ pub(super) fn verify_capture_layout(function: &Function) -> Result<(), Vec Result, Vec> { +pub(crate) fn capture_shapes(function: &Function) -> Result, Vec> { let declared = function .values .iter() @@ -150,6 +150,9 @@ pub(super) fn verify_call_shape( declared: &HashMap, signature: &Signature, arguments: &[ValueId], + callee: &str, + function_name: &str, + function_span: TextRange, ) -> Result<(), Vec> { if arguments.len() != signature.parameters.len() || arguments @@ -157,17 +160,40 @@ pub(super) fn verify_call_shape( .zip(&signature.parameters) .any(|(argument, expected)| declared.get(argument).copied() != Some(*expected)) { + let expected = signature + .parameters + .iter() + .map(|shape| format!("{shape:?}")) + .collect::>() + .join(", "); + let actual = arguments + .iter() + .map(|value| match declared.get(value) { + Some(shape) => format!("{value:?}: {shape:?}"), + None => format!("{value:?}: "), + }) + .collect::>() + .join(", "); return Err(assignment_error( assignment, - "call arguments do not match its signature", + format!( + "call shape mismatch: callee `{callee}` in function `{function_name}` at {function_span:?}; expected {} argument(s) [{expected}], got {} [{actual}]", + signature.parameters.len(), + arguments.len(), + ), )); } - require_destination( - declared, - assignment, - signature.result, - "call result shape does not match its signature", - ) + let actual_result = declared.get(&assignment.destination).copied(); + if actual_result != Some(signature.result) { + return Err(assignment_error( + assignment, + format!( + "call result shape mismatch: callee `{callee}` in function `{function_name}` at {function_span:?}; expected {:?}, got {actual_result:?}", + signature.result, + ), + )); + } + Ok(()) } pub(super) fn verify_product_value( diff --git a/crates/psrs-backend/src/cc/verify/mod.rs b/crates/psrs-backend/src/cc/verify/mod.rs index 290e611d..c377be34 100644 --- a/crates/psrs-backend/src/cc/verify/mod.rs +++ b/crates/psrs-backend/src/cc/verify/mod.rs @@ -12,7 +12,9 @@ mod variant; use helpers::{verify_capture_layout, verify_value_shape}; use ops::{verify_assignments, verify_table}; -pub(super) fn verify_module(module: &Module) -> Result<(), Vec> { +/// Checks target-neutral value, representation and callable contracts. Source +/// typing and complete semantic control provenance remain producer obligations. +pub fn verify_module(module: &Module) -> Result<(), Vec> { verify_table(&module.representations, module.span)?; let mut signatures = HashMap::new(); let mut functions_by_symbol = HashMap::new(); @@ -194,7 +196,15 @@ fn verify_function_inner( representations, functions, function.span, + &function.name, )?; + if let Err(message) = crate::cc::lower::verify_boundary(function, representations) { + return Err(vec![BackendError::invalid_ir( + "P8 CC verification", + function.span, + message, + )]); + } if !available.contains(&function.result) { return Err(vec![BackendError::invalid_ir( "P8 CC verification", diff --git a/crates/psrs-backend/src/cc/verify/ops/aggregate/mod.rs b/crates/psrs-backend/src/cc/verify/ops/aggregate/mod.rs index 0f833623..6b38f07a 100644 --- a/crates/psrs-backend/src/cc/verify/ops/aggregate/mod.rs +++ b/crates/psrs-backend/src/cc/verify/ops/aggregate/mod.rs @@ -38,7 +38,7 @@ fn verify_plan( table: &RepresentationTable, ) -> Result> { match plan { - ValueConversion::Identity => Ok(source), + ValueConversion::Identity => Ok(plan.output_shape(source)), ValueConversion::BoxScalar { kind, representation, @@ -62,7 +62,7 @@ fn verify_plan( "scalar box plan has incompatible shapes", )); } - Ok(repr_shape(*representation)) + Ok(plan.output_shape(source)) } ValueConversion::UnboxScalar { kind, @@ -86,12 +86,12 @@ fn verify_plan( if *value == ValueShape::Integer && matches!(destination, ValueShape::Integer | ValueShape::Boolean) => { - Ok(*destination) + Ok(plan.output_shape(source)) } BoxKind::Number if *value == ValueShape::Number && *destination == ValueShape::Number => { - Ok(*destination) + Ok(plan.output_shape(source)) } _ => Err(assignment_error( assignment, @@ -110,7 +110,7 @@ fn verify_plan( "reference erasure requires a typed reference", )); } - Ok(erased_shape()) + Ok(plan.output_shape(source)) } ValueConversion::RecoverReference { destination, @@ -177,7 +177,7 @@ fn verify_plan( )); } } - Ok(*destination) + Ok(plan.output_shape(source)) } ValueConversion::FunctionAdapter { source: expected, @@ -201,7 +201,7 @@ fn verify_plan( } super::super::helpers::verify_value_shape(&source, table, assignment.span)?; super::super::helpers::verify_value_shape(destination, table, assignment.span)?; - Ok(*destination) + Ok(plan.output_shape(source)) } ValueConversion::Sequence(plans) => { let mut shape = source; @@ -241,7 +241,7 @@ fn verify_plan( "array map element conversion is invalid", )); } - Ok(repr_shape(*target)) + Ok(plan.output_shape(source)) } ValueConversion::ProductMap { source: source_id, @@ -289,7 +289,7 @@ fn verify_plan( )); } } - Ok(repr_shape(*target)) + Ok(plan.output_shape(source)) } } } diff --git a/crates/psrs-backend/src/cc/verify/ops/arrays.rs b/crates/psrs-backend/src/cc/verify/ops/arrays.rs index a7a3ab29..000a47f7 100644 --- a/crates/psrs-backend/src/cc/verify/ops/arrays.rs +++ b/crates/psrs-backend/src/cc/verify/ops/arrays.rs @@ -48,6 +48,24 @@ pub(super) fn verify_array_assignment( )?; uses.extend(elements.iter().copied()); } + AssignmentKind::ArrayFill { + destination, + representation, + length, + value, + } => { + verify_embedded_destination(assignment, *destination)?; + let element = verify_array_representation(table, *representation, assignment)?; + require_value_shape(declared, *length, ValueShape::Integer, assignment)?; + require_value_shape(declared, *value, element, assignment)?; + require_destination( + declared, + assignment, + repr_shape(*representation), + "arrayFill has an incompatible result shape", + )?; + uses.extend([*length, *value]); + } AssignmentKind::ArrayLen { destination, value } => { verify_embedded_destination(assignment, *destination)?; verify_array_value(declared, *value, table, None, assignment)?; diff --git a/crates/psrs-backend/src/cc/verify/ops/control.rs b/crates/psrs-backend/src/cc/verify/ops/control.rs new file mode 100644 index 00000000..8bb93fb9 --- /dev/null +++ b/crates/psrs-backend/src/cc/verify/ops/control.rs @@ -0,0 +1,159 @@ +use super::super::helpers::{assignment_error, declared_shape}; +use crate::BackendError; +use crate::cc::Signature; +use crate::cc::control::ControlImplementation; +use crate::cc::{Assignment, AssignmentKind, RepresentationTable, ValueId, ValueShape}; +use psrs_hir::SymbolId; +use std::collections::HashMap; + +pub(super) fn verify( + assignment: &Assignment, + declared: &HashMap, + table: &RepresentationTable, + assignments: &[Assignment], + signatures: &HashMap, +) -> Result, Vec> { + let AssignmentKind::ControlEvidence { + value, + implementation, + arguments, + contract, + } = &assignment.kind + else { + unreachable!() + }; + let proof = table + .control_contracts() + .get(contract.0 as usize) + .ok_or_else(|| { + assignment_error( + assignment, + "control evidence has no checked source contract", + ) + })?; + if proof.arguments().len() != arguments.len() { + return Err(assignment_error( + assignment, + "control evidence argument count disagrees with its checked source contract", + )); + } + if declared_shape(declared, *value, assignment)? + != declared_shape(declared, assignment.destination, assignment)? + { + return Err(assignment_error( + assignment, + "control evidence changes its runtime result shape", + )); + } + let carrier_shape = match proof.implementation { + ControlImplementation::Frame { representation, .. } => { + Some(super::super::helpers::repr_shape(representation)) + } + ControlImplementation::Call { + signature, + role: crate::cc::ControlHelperRole::Once, + } => table.signature(signature).map(|signature| signature.result), + _ => None, + }; + if carrier_shape.is_some_and(|shape| declared.get(value).copied() != Some(shape)) { + return Err(assignment_error( + assignment, + "control carrier result must retain its nominal representation", + )); + } + let definition = assignments + .iter() + .find(|candidate| candidate.destination == *implementation) + .ok_or_else(|| { + assignment_error(assignment, "control implementation has no local definition") + })?; + if matches!(proof.implementation, ControlImplementation::Frame { .. }) + && value != implementation + { + return Err(assignment_error( + assignment, + "control evidence result is not its checked frame", + )); + } + let operands = match (&proof.implementation, &definition.kind) { + ( + ControlImplementation::Frame { + representation, + case, + }, + AssignmentKind::VariantNew { + representation: actual, + case: tag, + fields, + .. + }, + ) if representation == actual && case == tag => fields, + ( + ControlImplementation::Call { signature, role }, + AssignmentKind::DirectCall { + function, + arguments, + contract: Some(contract), + }, + ) if signatures.get(function) == table.signature(*signature) + && table + .control_helpers() + .get(contract.0 as usize) + .is_some_and(|helper| helper.role() == *role) => + { + arguments + } + _ => { + return Err(assignment_error( + assignment, + "control implementation disagrees with its checked frame or helper plan", + )); + } + }; + if operands.len() < arguments.len() + || !arguments + .iter() + .zip(operands) + .all(|(source, actual)| origin(*source, assignments) == origin(*actual, assignments)) + { + return Err(assignment_error( + assignment, + "control implementation does not preserve its checked source operands", + )); + } + for (argument, expected) in arguments.iter().zip(proof.argument_identities()) { + let origin = origin(*argument, assignments); + if let Some(Assignment { + kind: AssignmentKind::ControlEvidence { contract, .. }, + .. + }) = assignments + .iter() + .find(|candidate| candidate.destination == origin) + { + let producer = table + .control_contracts() + .get(contract.0 as usize) + .ok_or_else(|| { + assignment_error(assignment, "control operand has no checked source contract") + })?; + if producer.result_identity() != *expected { + return Err(assignment_error( + assignment, + "control operand source type disagrees with its checked domain", + )); + } + } + } + let mut uses = arguments.clone(); + uses.extend([*value, *implementation]); + Ok(uses) +} + +fn origin(value: ValueId, assignments: &[Assignment]) -> ValueId { + crate::cc::value_identity::reference_origin(value, |value| { + assignments + .iter() + .find(|assignment| assignment.destination == value) + .map(|assignment| &assignment.kind) + }) +} diff --git a/crates/psrs-backend/src/cc/verify/ops/mod.rs b/crates/psrs-backend/src/cc/verify/ops/mod.rs index b411b1da..1ae4dfae 100644 --- a/crates/psrs-backend/src/cc/verify/ops/mod.rs +++ b/crates/psrs-backend/src/cc/verify/ops/mod.rs @@ -15,12 +15,14 @@ use tag_switch::verify_tag_switch; mod aggregate; mod arrays; +mod control; mod string_bytes; mod table; mod tag_switch; pub(super) use table::verify_table; +#[allow(clippy::too_many_arguments)] pub(super) fn verify_assignments( assignments: &[Assignment], available: &mut HashSet, @@ -29,10 +31,14 @@ pub(super) fn verify_assignments( table: &RepresentationTable, functions: Option<&HashMap>, function_span: TextRange, + function_name: &str, ) -> Result<(), Vec> { for assignment in assignments { let mut uses = Vec::new(); match &assignment.kind { + AssignmentKind::ControlEvidence { .. } => { + uses = control::verify(assignment, declared, table, assignments, signatures)?; + } AssignmentKind::AggregateConvert { destination, value, @@ -83,21 +89,72 @@ pub(super) fn verify_assignments( verify_binary_operation(*op, *left, *right, assignment, declared)?; uses.extend([*left, *right]); } + AssignmentKind::RuntimeCall { + intrinsic, + arguments, + } => { + let crate::target_intrinsics::Implementation::Artifact(provider) = + crate::target_intrinsics::implementation(*intrinsic) + else { + return Err(assignment_error( + assignment, + "runtime call has no artifact implementation", + )); + }; + provider + .validate_protocol() + .map_err(|error| assignment_error(assignment, error))?; + let (parameters, result) = provider + .language_signature() + .map_err(|error| assignment_error(assignment, error))?; + if arguments.len() != parameters.len() { + return Err(assignment_error( + assignment, + "runtime call has an incompatible argument count", + )); + } + for (value, expected) in arguments.iter().zip(parameters) { + require_value_shape(declared, *value, expected, assignment)?; + } + require_destination( + declared, + assignment, + result, + "runtime call has an incompatible result shape", + )?; + uses.extend(arguments.iter().copied()); + } AssignmentKind::Unary { op, value } => { verify_unary_operation(*op, *value, assignment, declared)?; uses.push(*value); } AssignmentKind::DirectCall { + contract, function, arguments, } => { + table + .verify_control_call(*function, *contract) + .map_err(|message| assignment_error(assignment, message))?; let Some(signature) = signatures.get(function) else { return Err(assignment_error( assignment, "direct call references an unknown function", )); }; - verify_call_shape(assignment, declared, signature, arguments)?; + let callee = functions + .and_then(|functions| functions.get(function)) + .map(|function| function.name.clone()) + .unwrap_or_else(|| format!("symbol {function:?}")); + verify_call_shape( + assignment, + declared, + signature, + arguments, + &callee, + function_name, + function_span, + )?; uses.extend(arguments.iter().copied()); } AssignmentKind::FunctionRef { @@ -137,7 +194,15 @@ pub(super) fn verify_assignments( let signature_id = *signature; let signature = table_signature(table, signature_id, assignment)?; require_value_shape(declared, *function, closure_shape(signature_id), assignment)?; - verify_call_shape(assignment, declared, signature, arguments)?; + verify_call_shape( + assignment, + declared, + signature, + arguments, + &format!("indirect call with signature {signature_id:?}"), + function_name, + function_span, + )?; uses.push(*function); uses.extend(arguments.iter().copied()); } @@ -312,7 +377,8 @@ pub(super) fn verify_assignments( | AssignmentKind::ArrayGet { .. } | AssignmentKind::ArrayClone { .. } | AssignmentKind::ArraySet { .. } - | AssignmentKind::ArrayAppend { .. } => { + | AssignmentKind::ArrayAppend { .. } + | AssignmentKind::ArrayFill { .. } => { arrays::verify_array_assignment(assignment, declared, table, &mut uses)?; } AssignmentKind::StringToBytes { @@ -354,6 +420,7 @@ pub(super) fn verify_assignments( table, functions, function_span, + function_name, )?; let mut else_available = available.clone(); verify_assignments( @@ -364,6 +431,7 @@ pub(super) fn verify_assignments( table, functions, function_span, + function_name, )?; if !then_available.contains(then_value) || !else_available.contains(else_value) { return Err(undef_error(assignment.span, function_span)); @@ -397,6 +465,7 @@ pub(super) fn verify_assignments( table, functions, function_span, + function_name, )?; uses.push(*value); } diff --git a/crates/psrs-backend/src/cc/verify/ops/table.rs b/crates/psrs-backend/src/cc/verify/ops/table.rs index 78d0c089..5f609209 100644 --- a/crates/psrs-backend/src/cc/verify/ops/table.rs +++ b/crates/psrs-backend/src/cc/verify/ops/table.rs @@ -8,6 +8,72 @@ pub(crate) fn verify_table( table: &RepresentationTable, span: TextRange, ) -> Result<(), Vec> { + let mut roles = HashSet::new(); + let mut helpers = HashSet::new(); + for helper in table.control_helpers() { + if !roles.insert(helper.role()) + || !helpers.insert(helper.function()) + || table.signature(helper.signature()).is_none() + { + return Err(table_error( + span, + "control helper contract has a duplicate identity or missing signature", + )); + } + } + if !table.control_helpers().is_empty() + && crate::cc::ControlHelperRole::ALL + .iter() + .any(|role| !roles.contains(role)) + { + return Err(table_error( + span, + "control machine is missing a required semantic helper", + )); + } + if let Err(message) = table.verify_control_schema() { + return Err(table_error(span, message)); + } + if !table.control_helpers().is_empty() { + let valid = table.control_resumption().is_some_and(|resume| { + !helpers.contains(&resume.function()) + && table.signature(resume.signature()).is_some() + && table.control_checkpoint_schema().is_some() + && table + .control_helpers() + .iter() + .find(|helper| helper.role() == crate::cc::ControlHelperRole::Once) + .and_then(|helper| table.signature(helper.signature())) + .and_then(|signature| signature.parameters.first()) + == Some(&crate::cc::ValueShape::Reference(crate::cc::Reference { + nullable: false, + heap: crate::cc::RefShape::Closure(resume.signature()), + })) + }); + if !valid { + return Err(table_error( + span, + "control machine has no valid raw resumption binding", + )); + } + } else if table.control_resumption().is_some() { + return Err(table_error( + span, + "raw resumption binding has no control helper schema", + )); + } + for proof in table.control_contracts() { + if proof.argument_identities().len() != proof.arguments().len() + || std::iter::once(&proof.result_identity()) + .chain(proof.argument_identities()) + .any(|id| table.source_type_identities().get(id.0 as usize).is_none()) + { + return Err(table_error( + span, + "control contract has no canonical source type identity", + )); + } + } for (index, representation) in table.representations.iter().enumerate() { match representation { Representation::Box { value } | Representation::Array { element: value } => { diff --git a/crates/psrs-backend/src/cc/verify/ops/tag_switch.rs b/crates/psrs-backend/src/cc/verify/ops/tag_switch.rs index d10d451c..160dfc0b 100644 --- a/crates/psrs-backend/src/cc/verify/ops/tag_switch.rs +++ b/crates/psrs-backend/src/cc/verify/ops/tag_switch.rs @@ -20,6 +20,7 @@ pub(super) fn verify_tag_switch( table: &crate::cc::RepresentationTable, functions: Option<&HashMap>, function_span: TextRange, + function_name: &str, ) -> Result<(), Vec> { require_value_shape(declared, value, ValueShape::Integer, assignment)?; if cases.is_empty() @@ -52,6 +53,7 @@ pub(super) fn verify_tag_switch( table, functions, function_span, + function_name, )?; if !default_available.contains(&default_value) { return Err(undef_error(assignment.span, function_span)); @@ -67,6 +69,7 @@ pub(super) fn verify_tag_switch( table, functions, function_span, + function_name, )?; if !case_available.contains(&case.value) || declared_shape(declared, case.value, assignment)? != expected diff --git a/crates/psrs-backend/src/cc/verify/scalar.rs b/crates/psrs-backend/src/cc/verify/scalar.rs index 2ae57ef3..4bfef14e 100644 --- a/crates/psrs-backend/src/cc/verify/scalar.rs +++ b/crates/psrs-backend/src/cc/verify/scalar.rs @@ -14,8 +14,8 @@ pub(super) fn verify_binary_operation( use BinaryOp::*; let (operand, result) = match op { - IntAdd | IntSub | IntMul | IntQuot | IntRem | IntDiv | IntMod | IntAnd | IntOr | IntXor - | IntShl | IntShr | IntZshr => (ValueShape::Integer, ValueShape::Integer), + IntAdd | IntSub | IntMul | IntQuot | IntRem | IntAnd | IntOr | IntXor | IntShl | IntShr + | IntZshr => (ValueShape::Integer, ValueShape::Integer), IntEq | IntNe | IntLt | IntLe | IntGt | IntGe | CharEq | CharNe | CharLt | CharLe | CharGt | CharGe => (ValueShape::Integer, ValueShape::Boolean), NumberAdd | NumberSub | NumberMul | NumberDiv => (ValueShape::Number, ValueShape::Number), @@ -49,7 +49,9 @@ pub(super) fn verify_unary_operation( IntNeg | IntComplement | CharToInt | IntToChar => { (ValueShape::Integer, ValueShape::Integer) } - NumberNeg => (ValueShape::Number, ValueShape::Number), + NumberAbs | NumberSqrt | NumberNeg | NumberTrunc | NumberFloor | NumberCeil => { + (ValueShape::Number, ValueShape::Number) + } BooleanNot => (ValueShape::Boolean, ValueShape::Boolean), IntToNumber => (ValueShape::Integer, ValueShape::Number), NumberToInt => (ValueShape::Number, ValueShape::Integer), diff --git a/crates/psrs-backend/src/cc/verify/tests/control_calls.rs b/crates/psrs-backend/src/cc/verify/tests/control_calls.rs new file mode 100644 index 00000000..7988cdd6 --- /dev/null +++ b/crates/psrs-backend/src/cc/verify/tests/control_calls.rs @@ -0,0 +1,399 @@ +//! Nominal machine identities must survive helper calls and closure captures. + +use super::super::{verify_function, verify_module}; +use super::symbol; +use crate::cc::layout::control; +use crate::cc::payload::erased_shape; +use crate::cc::{ + Assignment, AssignmentKind, Function, Module, RefShape, Reference, RepresentationTable, + Signature, ValueDecl, ValueId, ValueShape, +}; +use psrs_span::TextRange; +use std::collections::HashMap; + +fn reference(heap: RefShape) -> ValueShape { + ValueShape::Reference(Reference { + nullable: false, + heap, + }) +} + +fn call_fixture(signature: &Signature) -> Function { + let mut values = signature + .parameters + .iter() + .enumerate() + .map(|(index, ty)| ValueDecl { + id: ValueId(index as u32), + ty: *ty, + }) + .collect::>(); + let parameters = values.iter().map(|value| value.id).collect::>(); + let result = ValueId(values.len() as u32); + values.push(ValueDecl { + id: result, + ty: signature.result, + }); + let span = TextRange::new(0, 1); + Function { + symbol: symbol(0), + name: "control_call".into(), + parameters: parameters.clone(), + values, + assignments: vec![Assignment { + destination: result, + kind: AssignmentKind::DirectCall { + contract: None, + function: symbol(1), + arguments: parameters, + }, + span, + }], + result, + result_type: signature.result, + span, + } +} + +#[test] +fn control_helper_calls_reject_erased_and_cross_wired_machine_arguments() { + let mut table = RepresentationTable::default(); + let continuation = table.add_signature(Signature { + parameters: vec![erased_shape()], + result: erased_shape(), + }); + let nominal_types = control::reserve(&mut table); + let layout = control::append(&mut table, continuation, &nominal_types); + for (id, argument, wrong) in [ + ( + layout.executor, + 3, + reference(RefShape::Repr(layout.boundary)), + ), + ( + layout.clone_sequence, + 0, + reference(RefShape::Repr(layout.boundary)), + ), + ( + layout.rebuild_sequence, + 2, + reference(RefShape::Repr(layout.sequence)), + ), + ( + layout.boundary_check, + 0, + reference(RefShape::Repr(layout.owner)), + ), + (layout.once, 0, reference(RefShape::Repr(layout.owner))), + (layout.cancel, 0, reference(RefShape::Repr(layout.boundary))), + ] { + let signature = table.signature(id).unwrap(); + let signatures = HashMap::from([(symbol(1), signature.clone())]); + let valid = call_fixture(signature); + verify_function(&valid, &signatures, &table).expect("matching machine call"); + for wrong in [wrong, erased_shape()] { + let mut invalid = valid.clone(); + invalid.values[argument].ty = wrong; + let errors = verify_function(&invalid, &signatures, &table).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("call shape mismatch")), + "{errors:?}" + ); + } + } +} + +#[test] +fn control_replay_result_cannot_be_declared_as_an_erased_user_pair() { + let mut table = RepresentationTable::default(); + let continuation = table.add_signature(Signature { + parameters: vec![erased_shape()], + result: erased_shape(), + }); + let nominal_types = control::reserve(&mut table); + let layout = control::append(&mut table, continuation, &nominal_types); + let signature = table.signature(layout.clone_sequence).unwrap(); + let signatures = HashMap::from([(symbol(1), signature.clone())]); + let mut function = call_fixture(signature); + function.values.last_mut().unwrap().ty = erased_shape(); + function.result_type = erased_shape(); + let errors = verify_function(&function, &signatures, &table).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("call result shape mismatch")), + "{errors:?}" + ); +} + +#[test] +fn mir_rejects_machine_call_arguments_with_swapped_queue_and_extent() { + let mut table = RepresentationTable::default(); + let continuation = table.add_signature(Signature { + parameters: vec![erased_shape()], + result: erased_shape(), + }); + let nominal_types = control::reserve(&mut table); + let layout = control::append(&mut table, continuation, &nominal_types); + for (id, left, right) in [ + (layout.executor, 3, 4), + (layout.clone_sequence, 0, 3), + (layout.rebuild_sequence, 1, 2), + ] { + let caller = call_fixture(table.signature(id).unwrap()); + // The callee body is irrelevant to this call-contract verifier fixture. + let mut callee = caller.clone(); + callee.symbol = symbol(1); + callee.name = "machine_callee".into(); + callee.assignments[0].kind = AssignmentKind::Unreachable; + let module = Module { + name: "MachineCalls".into(), + functions: vec![caller, callee], + externals: vec![], + representations: table.clone(), + entry: None, + span: TextRange::new(0, 1), + }; + verify_module(&module).expect("valid CC helper call"); + let (mut mir, _) = crate::mir::lower_module(module).expect("valid MIR helper call"); + let terminator = mir.functions[0] + .blocks + .iter_mut() + .find_map(|block| { + block.terminator.as_mut().filter(|terminator| { + matches!(terminator, crate::mir::Terminator::ReturnCall { .. }) + }) + }) + .expect("typed helper tail call"); + let crate::mir::Terminator::ReturnCall { arguments, .. } = terminator else { + unreachable!() + }; + arguments.swap(left, right); + let errors = crate::mir::verify_module(&mir).expect_err("wrong machine argument types"); + assert!( + errors + .iter() + .any(|error| error.message.contains("argument")), + "{errors:?}" + ); + } +} + +fn raw_capture_module() -> Module { + let mut table = RepresentationTable::default(); + let continuation = table.add_signature(Signature { + parameters: vec![erased_shape()], + result: erased_shape(), + }); + let nominal_types = control::reserve(&mut table); + let layout = control::append(&mut table, continuation, &nominal_types); + let span = TextRange::new(0, 1); + let queue = reference(RefShape::Repr(layout.sequence)); + let boundary = reference(RefShape::Repr(layout.boundary)); + let receiver = reference(RefShape::Aggregate); + let closure = reference(RefShape::Closure(continuation)); + let target = Function { + symbol: symbol(1), + name: "raw_capture_target".into(), + parameters: vec![ValueId(0), ValueId(1)], + values: [receiver, erased_shape(), queue, boundary] + .into_iter() + .enumerate() + .map(|(index, ty)| ValueDecl { + id: ValueId(index as u32), + ty, + }) + .collect(), + assignments: (0..2) + .map(|index| Assignment { + destination: ValueId(index + 2), + kind: AssignmentKind::ClosureGetCapture { + closure: ValueId(0), + index, + }, + span, + }) + .collect(), + result: ValueId(1), + result_type: erased_shape(), + span, + }; + let caller = Function { + symbol: symbol(0), + name: "raw_capture".into(), + parameters: vec![ValueId(0), ValueId(1)], + values: [queue, boundary, closure] + .into_iter() + .enumerate() + .map(|(index, ty)| ValueDecl { + id: ValueId(index as u32), + ty, + }) + .collect(), + assignments: vec![Assignment { + destination: ValueId(2), + kind: AssignmentKind::FunctionRef { + function: symbol(1), + signature: continuation, + captures: vec![ValueId(0), ValueId(1)], + }, + span, + }], + result: ValueId(2), + result_type: closure, + span, + }; + Module { + name: "RawCapture".into(), + functions: vec![caller, target], + externals: vec![], + representations: table, + entry: None, + span, + } +} + +#[test] +fn raw_resumption_capture_preserves_queue_and_checkpoint_shapes() { + let module = raw_capture_module(); + verify_module(&module).expect("typed checkpoint capture"); + for captures in [vec![ValueId(1), ValueId(0)], vec![ValueId(0), ValueId(0)]] { + let mut invalid = module.clone(); + let AssignmentKind::FunctionRef { + captures: actual, .. + } = &mut invalid.functions[0].assignments[0].kind + else { + unreachable!() + }; + *actual = captures; + let errors = verify_module(&invalid).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("captures do not match")), + "{errors:?}" + ); + } +} + +#[test] +fn mir_raw_resumption_rejects_swapped_duplicate_and_missing_captures() { + let module = raw_capture_module(); + let (mir, _) = crate::mir::lower_module(module).expect("typed raw captures lower"); + crate::mir::verify_module(&mir).expect("typed MIR captures"); + for slots in [vec![1, 0], vec![0, 0], vec![0]] { + let mut invalid = mir.clone(); + let instruction = &mut invalid.functions[0].blocks[0].instructions[0]; + let crate::mir::Instruction::ClosureNew { captures, .. } = instruction else { + panic!("expected raw closure construction"); + }; + *captures = slots.into_iter().map(crate::types::ValueId).collect(); + let errors = crate::mir::verify_module(&invalid).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("captures do not match")), + "{errors:?}" + ); + } +} + +#[test] +fn mir_capture_contract_survives_sparse_projections_after_optimization() { + let (mut mir, _) = crate::mir::lower_module(raw_capture_module()).unwrap(); + // Dead projection elimination may leave slot 1 without slot 0. Stored + // captures are unchanged; MIR must check the surviving slot by its index. + mir.functions[1].blocks[0].instructions.remove(0); + crate::mir::verify_module(&mir).expect("sparse live projection contract"); + let crate::mir::Instruction::ClosureNew { captures, .. } = + &mut mir.functions[0].blocks[0].instructions[0] + else { + unreachable!() + }; + captures.swap(0, 1); + assert!( + crate::mir::verify_module(&mir) + .unwrap_err() + .iter() + .any(|error| error.message.contains("captures do not match")) + ); +} + +#[test] +fn mir_capture_contract_rejects_inconsistent_reads_of_one_slot() { + let (mut mir, _) = crate::mir::lower_module(raw_capture_module()).unwrap(); + let crate::mir::Instruction::ClosureGetCapture { index, .. } = + &mut mir.functions[1].blocks[0].instructions[1] + else { + unreachable!() + }; + *index = 0; + let errors = crate::mir::verify_module(&mir).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("incompatible projection types")), + "{errors:?}" + ); +} + +#[test] +fn mir_capture_contract_tracks_receiver_copies_and_reference_casts() { + let (mir, _) = crate::mir::lower_module(raw_capture_module()).unwrap(); + for cast in [false, true] { + let mut module = mir.clone(); + let target = &mut module.functions[1]; + let receiver = target.parameters[0]; + let ty = target + .values + .iter() + .find(|value| value.id == receiver) + .unwrap() + .ty; + let alias = crate::types::ValueId(4); + target + .values + .push(crate::types::ValueDecl { id: alias, ty }); + for instruction in &mut target.blocks[0].instructions { + if let crate::mir::Instruction::ClosureGetCapture { closure, .. } = instruction { + *closure = alias; + } + } + let span = target.span; + let copy = if cast { + let crate::types::ValueType::Ref(reference) = ty else { + unreachable!() + }; + crate::mir::Instruction::RefCast { + destination: alias, + value: receiver, + reference, + span, + } + } else { + crate::mir::Instruction::Copy { + destination: alias, + value: receiver, + span, + } + }; + target.blocks[0].instructions.insert(0, copy); + crate::mir::verify_module(&module).expect("receiver aliases retain the contract"); + let crate::mir::Instruction::ClosureNew { captures, .. } = + &mut module.functions[0].blocks[0].instructions[0] + else { + unreachable!() + }; + captures.swap(0, 1); + let errors = crate::mir::verify_module(&module).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("captures do not match")), + "{errors:?}" + ); + } +} diff --git a/crates/psrs-backend/src/cc/verify/tests/control_frames.rs b/crates/psrs-backend/src/cc/verify/tests/control_frames.rs new file mode 100644 index 00000000..9721c305 --- /dev/null +++ b/crates/psrs-backend/src/cc/verify/tests/control_frames.rs @@ -0,0 +1,182 @@ +//! Machine object identities must survive variant storage and MIR lowering. + +use super::super::verify_function; +use super::symbol; +use crate::cc::layout::control; +use crate::cc::payload::erased_shape; +use crate::cc::{ + Assignment, AssignmentKind, Function, Module, RefShape, Reference, ReprId, Representation, + RepresentationTable, Signature, ValueDecl, ValueId, ValueShape, +}; +use psrs_span::TextRange; +use std::collections::HashMap; + +fn shape(id: ReprId) -> ValueShape { + ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Repr(id), + }) +} + +fn constructor(table: &RepresentationTable, representation: ReprId, case: u32) -> Function { + let Some(Representation::Variant { cases }) = table.representation(representation) else { + panic!("expected a machine variant"); + }; + let fields = &cases + .iter() + .find(|candidate| candidate.tag == case) + .unwrap() + .fields; + let mut values = fields + .iter() + .enumerate() + .map(|(index, ty)| ValueDecl { + id: ValueId(index as u32), + ty: *ty, + }) + .collect::>(); + let parameters = values.iter().map(|value| value.id).collect::>(); + let result = ValueId(values.len() as u32); + values.push(ValueDecl { + id: result, + ty: shape(representation), + }); + let span = TextRange::new(0, 1); + Function { + symbol: symbol(0), + name: "machine_frame".into(), + parameters: parameters.clone(), + values, + assignments: vec![Assignment { + destination: result, + kind: AssignmentKind::VariantNew { + destination: result, + representation, + case, + fields: parameters, + }, + span, + }], + result, + result_type: shape(representation), + span, + } +} + +#[test] +fn control_frames_reject_erased_or_wrong_machine_fields_in_cc() { + let mut table = RepresentationTable::default(); + let continuation = table.add_signature(Signature { + parameters: vec![erased_shape()], + result: erased_shape(), + }); + let nominal_types = control::reserve(&mut table); + let layout = control::append(&mut table, continuation, &nominal_types); + for (representation, case, field, wrong) in [ + (layout.computation, 3, 1, shape(layout.boundary)), + (layout.computation, 4, 2, shape(layout.sequence)), + (layout.sequence, 2, 0, shape(layout.context)), + (layout.sequence, 3, 0, shape(layout.sequence)), + (layout.context, 1, 4, shape(layout.boundary)), + (layout.owner, 0, 1, shape(layout.callback)), + (layout.boundary, 1, 1, shape(layout.owner)), + ] { + let valid = constructor(&table, representation, case); + verify_function(&valid, &HashMap::new(), &table).expect("valid machine fields"); + for wrong in [wrong, erased_shape()] { + let mut invalid = valid.clone(); + invalid.values[field].ty = wrong; + let errors = verify_function(&invalid, &HashMap::new(), &table).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("variant fields")), + "{errors:?}" + ); + } + } +} + +#[test] +fn control_frame_projection_cannot_silently_erase_checkpoint_identity() { + let mut table = RepresentationTable::default(); + let continuation = table.add_signature(Signature { + parameters: vec![erased_shape()], + result: erased_shape(), + }); + let nominal_types = control::reserve(&mut table); + let layout = control::append(&mut table, continuation, &nominal_types); + let mut function = constructor(&table, layout.sequence, 3); + let result = ValueId(function.values.len() as u32); + function.values.push(ValueDecl { + id: result, + ty: erased_shape(), + }); + function.assignments.push(Assignment { + destination: result, + kind: AssignmentKind::VariantGet { + destination: result, + representation: layout.sequence, + case: 3, + field: 0, + value: function.result, + }, + span: function.span, + }); + function.result = result; + function.result_type = erased_shape(); + let errors = verify_function(&function, &HashMap::new(), &table).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("variant field has")), + "{errors:?}" + ); +} + +#[test] +fn mir_preserves_control_frame_fields_and_rejects_cross_wired_storage() { + let mut table = RepresentationTable::default(); + let continuation = table.add_signature(Signature { + parameters: vec![erased_shape()], + result: erased_shape(), + }); + let nominal_types = control::reserve(&mut table); + let layout = control::append(&mut table, continuation, &nominal_types); + for (representation, case, left, right) in [ + (layout.computation, 3, 1, 2), + (layout.sequence, 3, 0, 1), + (layout.context, 1, 2, 3), + (layout.owner, 0, 0, 1), + (layout.boundary, 1, 0, 1), + ] { + let function = constructor(&table, representation, case); + let module = Module { + name: "ControlFrames".into(), + functions: vec![function.clone()], + externals: vec![], + representations: table.clone(), + entry: None, + span: function.span, + }; + let (mut mir, _) = crate::mir::lower_module(module).expect("valid typed machine frame"); + crate::mir::verify_module(&mir).expect("valid frame in MIR"); + let instruction = mir.functions[0].blocks.iter_mut() + .flat_map(|block| &mut block.instructions) + .find(|instruction| matches!(instruction, crate::mir::Instruction::StructNew { arguments, .. } if arguments.len() == function.parameters.len() + 1)) + .expect("frame constructor"); + let crate::mir::Instruction::StructNew { arguments, .. } = instruction else { + unreachable!() + }; + // Slot zero is the variant tag; mutate only its stored payload fields. + arguments.swap(left + 1, right + 1); + let errors = + crate::mir::verify_module(&mir).expect_err("MIR must reject cross-wired frame storage"); + assert!( + errors.iter().any(|error| error + .message + .contains("struct.new argument has the wrong type")), + "{errors:?}" + ); + } +} diff --git a/crates/psrs-backend/src/cc/verify/tests/effects.rs b/crates/psrs-backend/src/cc/verify/tests/effects.rs index 0b77acc5..d1cb02f0 100644 --- a/crates/psrs-backend/src/cc/verify/tests/effects.rs +++ b/crates/psrs-backend/src/cc/verify/tests/effects.rs @@ -241,6 +241,7 @@ fn rejects_a_direct_call_to_an_unbound_runner_external() { assignments: vec![Assignment { destination, kind: AssignmentKind::DirectCall { + contract: None, function: runner, arguments: Vec::new(), }, diff --git a/crates/psrs-backend/src/cc/verify/tests/mod.rs b/crates/psrs-backend/src/cc/verify/tests/mod.rs index 8e3256ba..4b4ae9a9 100644 --- a/crates/psrs-backend/src/cc/verify/tests/mod.rs +++ b/crates/psrs-backend/src/cc/verify/tests/mod.rs @@ -4,8 +4,11 @@ use psrs_hir::ModuleId; use psrs_span::TextRange; mod adaptation; +mod control_calls; +mod control_frames; mod dictionary; mod effects; +mod number_decimal; mod string_eq; mod structure; mod variant; @@ -49,6 +52,7 @@ fn rejects_a_direct_call_with_the_wrong_result_shape() { assignments: vec![Assignment { destination, kind: AssignmentKind::DirectCall { + contract: None, function: callee, arguments: Vec::new(), }, @@ -244,7 +248,18 @@ fn rejects_a_unary_operation_with_the_wrong_operand_shape() { span: TextRange::new(0, 1), }; - assert!(verify_function(&function, &HashMap::new(), &table()).is_err()); + for op in [ + super::super::UnaryOp::NumberAbs, + super::super::UnaryOp::NumberSqrt, + super::super::UnaryOp::NumberNeg, + super::super::UnaryOp::NumberTrunc, + super::super::UnaryOp::NumberFloor, + super::super::UnaryOp::NumberCeil, + ] { + let mut function = function.clone(); + function.assignments[0].kind = AssignmentKind::Unary { op, value: input }; + assert!(verify_function(&function, &HashMap::new(), &table()).is_err()); + } } #[test] diff --git a/crates/psrs-backend/src/cc/verify/tests/number_decimal.rs b/crates/psrs-backend/src/cc/verify/tests/number_decimal.rs new file mode 100644 index 00000000..a9389f4f --- /dev/null +++ b/crates/psrs-backend/src/cc/verify/tests/number_decimal.rs @@ -0,0 +1,85 @@ +use super::*; + +#[test] +fn decimal_conversion_checks_both_shapes_before_abi_erasure() { + let input = super::super::super::ValueId(0); + let output = super::super::super::ValueId(1); + for (operand, result, valid) in [ + (ValueShape::String, ValueShape::Number, true), + (ValueShape::Integer, ValueShape::Number, false), + (ValueShape::Number, ValueShape::Number, false), + (ValueShape::String, ValueShape::Integer, false), + ] { + let function = Function { + symbol: symbol(0), + name: "decimal".into(), + parameters: vec![input], + values: vec![ + ValueDecl { + id: input, + ty: operand, + }, + ValueDecl { + id: output, + ty: result, + }, + ], + assignments: vec![Assignment { + destination: output, + kind: AssignmentKind::RuntimeCall { + intrinsic: psrs_hir::Intrinsic::NumberFromDecimal, + arguments: vec![input], + }, + span: TextRange::new(0, 1), + }], + result: output, + result_type: result, + span: TextRange::new(0, 1), + }; + assert_eq!( + verify_function(&function, &HashMap::new(), &table()).is_ok(), + valid + ); + } +} + +#[test] +fn runtime_calls_reject_wrong_arity_and_non_artifact_identities() { + use psrs_hir::Intrinsic; + let input = super::super::super::ValueId(0); + let output = super::super::super::ValueId(1); + for (intrinsic, arguments) in [ + (Intrinsic::NumberFromDecimal, vec![]), + (Intrinsic::NumberFromDecimal, vec![input, input]), + (Intrinsic::NumberAbs, vec![input]), + (Intrinsic::Undefined, vec![]), + ] { + let function = Function { + symbol: symbol(0), + name: "invalid_runtime_call".into(), + parameters: vec![input], + values: vec![ + ValueDecl { + id: input, + ty: ValueShape::String, + }, + ValueDecl { + id: output, + ty: ValueShape::Number, + }, + ], + assignments: vec![Assignment { + destination: output, + kind: AssignmentKind::RuntimeCall { + intrinsic, + arguments, + }, + span: TextRange::new(0, 1), + }], + result: output, + result_type: ValueShape::Number, + span: TextRange::new(0, 1), + }; + assert!(verify_function(&function, &HashMap::new(), &table()).is_err()); + } +} diff --git a/crates/psrs-backend/src/cc/verify/tests/structure.rs b/crates/psrs-backend/src/cc/verify/tests/structure.rs index bee14368..5b509f49 100644 --- a/crates/psrs-backend/src/cc/verify/tests/structure.rs +++ b/crates/psrs-backend/src/cc/verify/tests/structure.rs @@ -141,6 +141,7 @@ fn rejects_a_direct_call_with_the_wrong_arity() { assignments: vec![Assignment { destination, kind: AssignmentKind::DirectCall { + contract: None, function: callee, arguments: Vec::new(), }, @@ -157,7 +158,13 @@ fn rejects_a_direct_call_with_the_wrong_arity() { result: ValueShape::Integer, }, )]); - assert!(verify_function(&function, &signatures, &table()).is_err()); + let errors = verify_function(&function, &signatures, &table()) + .expect_err("the callee expects one integer argument"); + let message = &errors[0].message; + assert!(message.contains("call shape mismatch")); + assert!(message.contains("bad_arity")); + assert!(message.contains("expected 1 argument(s) [Integer]")); + assert!(message.contains("got 0 []")); } #[test] diff --git a/crates/psrs-backend/src/component.rs b/crates/psrs-backend/src/component.rs deleted file mode 100644 index 93ee886d..00000000 --- a/crates/psrs-backend/src/component.rs +++ /dev/null @@ -1,327 +0,0 @@ -//! Component Model encoding. A core module produced by the Wasm lowering is -//! componentized with `wit-component`, using WIT metadata that describes the -//! world the module implements. See -//! `docs/decision/DEC-06-runtime-interface-via-wit.md`. - -use wit_component::{ComponentEncoder, StringEncoding, embed_component_metadata}; -use wit_parser::{Resolve, WorldId}; - -/// Vendored WASI 0.2.12 WIT, matching the `wasi:cli/run@0.2.12` export that the -/// pinned `wasmtime` baseline expects. Pushed into the `Resolve` in dependency -/// order before the application world. -pub(crate) const WASI_DEPS: &[(&str, &str)] = &[ - ("wasi/io.wit", include_str!("../wit/deps/io.wit")), - ("wasi/clocks.wit", include_str!("../wit/deps/clocks.wit")), - ("wasi/random.wit", include_str!("../wit/deps/random.wit")), - ( - "wasi/filesystem.wit", - include_str!("../wit/deps/filesystem.wit"), - ), - ("wasi/sockets.wit", include_str!("../wit/deps/sockets.wit")), - ("wasi/cli.wit", include_str!("../wit/deps/cli.wit")), -]; - -/// The application world: a WASI command that only exports `wasi:cli/run`. -const APP_WIT: &str = include_str!("../wit/psrs-app.wit"); - -/// Interfaces that the current component world imports and the backend can -/// lower through its canonical ABI adapter. Keep this list next to the world -/// declaration so ABI discovery and component encoding share one contract. -pub(crate) const COMPONENT_INTERFACES: &[&str] = &[ - "wasi:io/error@0.2.12", - "wasi:io/poll@0.2.12", - "wasi:io/streams@0.2.12", - "wasi:clocks/monotonic-clock@0.2.12", - "wasi:clocks/wall-clock@0.2.12", - "wasi:random/random@0.2.12", - "wasi:random/insecure@0.2.12", - "wasi:random/insecure-seed@0.2.12", - "wasi:cli/environment@0.2.12", - "wasi:cli/exit@0.2.12", - "wasi:cli/stdin@0.2.12", - "wasi:cli/stdout@0.2.12", - "wasi:cli/stderr@0.2.12", - "wasi:cli/terminal-input@0.2.12", - "wasi:cli/terminal-output@0.2.12", - "wasi:cli/terminal-stdin@0.2.12", - "wasi:cli/terminal-stdout@0.2.12", - "wasi:cli/terminal-stderr@0.2.12", - "wasi:filesystem/types@0.2.12", - "wasi:filesystem/preopens@0.2.12", - "wasi:sockets/network@0.2.12", - "wasi:sockets/instance-network@0.2.12", - "wasi:sockets/udp@0.2.12", - "wasi:sockets/udp-create-socket@0.2.12", - "wasi:sockets/tcp@0.2.12", - "wasi:sockets/tcp-create-socket@0.2.12", - "wasi:sockets/ip-name-lookup@0.2.12", -]; - -pub(crate) fn component_interface_supported(module: &str) -> bool { - COMPONENT_INTERFACES.contains(&module) -} - -/// Pushes the vendored WASI WIT into `resolve` in dependency order. -pub(crate) fn load_vendored_wasi(resolve: &mut Resolve) -> Result<(), String> { - for (path, contents) in WASI_DEPS { - resolve - .push_str(path, contents) - .map_err(|error| format!("invalid vendored WIT `{path}`: {error}"))?; - } - Ok(()) -} - -/// Resolves the `psrs:app` command world against the vendored WASI WIT. -pub fn command_world() -> Result<(Resolve, WorldId), String> { - let mut resolve = Resolve::default(); - load_vendored_wasi(&mut resolve)?; - let package = resolve - .push_str("psrs-app.wit", APP_WIT) - .map_err(|error| format!("invalid application WIT: {error}"))?; - let world = resolve.packages[package] - .worlds - .get("command") - .copied() - .ok_or_else(|| "application WIT is missing the `command` world".to_string())?; - Ok((resolve, world)) -} - -/// Lifts a core module into a component for `world`. The core module must -/// implement the world at the canonical ABI level. -pub fn componentize(core: &[u8], resolve: &Resolve, world: WorldId) -> Result, String> { - let mut bytes = core.to_vec(); - embed_component_metadata(&mut bytes, resolve, world, StringEncoding::UTF8) - .map_err(|error| format!("failed to embed component metadata: {error}"))?; - ComponentEncoder::default() - .module(&bytes) - .map_err(|error| format!("failed to read the core module: {error}"))? - .validate(true) - .encode() - .map_err(|error| format!("failed to encode the component: {error:#}")) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::wasm::{ - DataIndex, DataSegment, Export, ExportIndex, ExportKind, FuncType, Function, FunctionIndex, - Import, Memory, MemoryIndex, Module, Op, TypeIndex, - }; - use psrs_hir::{ModuleId, SymbolId}; - use psrs_span::TextRange; - use wasm_encoder::{Instruction, ValType}; - use wit_parser::{WorldItem, WorldKey}; - - fn core_module_exporting_run() -> Vec { - let span = TextRange::new(0, 1); - let module = Module { - name: "App".into(), - imports: Vec::new(), - types: vec![FuncType { - parameters: Vec::new(), - results: vec![ValType::I32], - }], - type_defs: Vec::new(), - functions: vec![Function { - symbol: SymbolId::new(ModuleId(0), 0), - name: crate::abi::RUN_CORE_EXPORT.into(), - type_index: TypeIndex(0), - parameters: Vec::new(), - locals: Vec::new(), - body: vec![Op::Leaf(Instruction::I32Const(0))], - span, - }], - memories: Vec::new(), - data: Vec::new(), - exports: vec![Export { - name: crate::abi::RUN_CORE_EXPORT.into(), - kind: ExportKind::Function, - index: ExportIndex::Function(FunctionIndex(0)), - }], - entry: None, - realloc: None, - globals: Vec::new(), - helpers: Vec::new(), - span, - }; - crate::wasm::encode_module(&module).expect("encoding the core module") - } - - #[test] - fn componentizes_a_command_exporting_run() { - let (resolve, world) = command_world().expect("WASI and application WIT should load"); - let core = core_module_exporting_run(); - let component = componentize(&core, &resolve, world).expect("componentizing"); - crate::validator() - .validate_all(&component) - .expect("the component should validate"); - let text = wasmprinter::print_bytes(&component).expect("printing the component"); - assert!( - text.contains("(component") && text.contains("wasi:cli/run@0.2.12"), - "the component should export the WASI run interface: {text}" - ); - } - - #[test] - fn component_world_matches_the_capability_profile() { - let (resolve, world) = command_world().expect("WASI and application WIT should load"); - let mut actual = resolve.worlds[world] - .imports - .values() - .filter_map(|item| match item { - WorldItem::Interface { id, .. } => resolve.id_of(*id), - WorldItem::Function(_) | WorldItem::Type { .. } => None, - }) - .collect::>(); - let mut expected = COMPONENT_INTERFACES.to_vec(); - actual.sort_unstable(); - expected.sort_unstable(); - assert_eq!(actual, expected); - for key in resolve.worlds[world].imports.keys() { - assert!( - matches!(key, WorldKey::Interface(_)), - "the command world should import only named interfaces" - ); - } - } - - #[test] - fn runs_the_command_when_wasmtime_is_available() { - if std::process::Command::new("wasmtime") - .arg("--version") - .output() - .is_err() - { - eprintln!("skipping: wasmtime is not installed"); - return; - } - let (resolve, world) = command_world().expect("WASI and application WIT should load"); - let core = core_module_exporting_run(); - let component = componentize(&core, &resolve, world).expect("componentizing"); - let path = std::env::temp_dir().join(format!("psrs-command-{}.wasm", std::process::id())); - std::fs::write(&path, &component).unwrap(); - let output = std::process::Command::new("wasmtime") - .arg("run") - .arg(&path) - .output() - .unwrap(); - let _ = std::fs::remove_file(&path); - assert!( - output.status.success(), - "wasmtime failed to run the component: {output:?}" - ); - } - - fn core_module_printing() -> Vec { - let span = TextRange::new(0, 1); - let module = Module { - name: "Print".into(), - imports: vec![ - Import { - module: "wasi:cli/stdout@0.2.12".into(), - name: "get-stdout".into(), - type_index: TypeIndex(0), - }, - Import { - module: "wasi:io/streams@0.2.12".into(), - name: "[method]output-stream.blocking-write-and-flush".into(), - type_index: TypeIndex(1), - }, - ], - types: vec![ - FuncType { - parameters: Vec::new(), - results: vec![ValType::I32], - }, - FuncType { - parameters: vec![ValType::I32; 4], - results: Vec::new(), - }, - FuncType { - parameters: Vec::new(), - results: vec![ValType::I32], - }, - ], - type_defs: Vec::new(), - functions: vec![Function { - symbol: SymbolId::new(ModuleId(0), 0), - name: crate::abi::RUN_CORE_EXPORT.into(), - type_index: TypeIndex(2), - parameters: Vec::new(), - locals: vec![ValType::I32], - body: vec![ - Op::Leaf(Instruction::Call(0)), - Op::Leaf(Instruction::LocalSet(0)), - Op::Leaf(Instruction::LocalGet(0)), - Op::Leaf(Instruction::I32Const(100)), - Op::Leaf(Instruction::I32Const(6)), - Op::Leaf(Instruction::I32Const(0)), - Op::Leaf(Instruction::Call(1)), - Op::Leaf(Instruction::I32Const(0)), - ], - span, - }], - memories: vec![Memory { - id: crate::types::MemoryId(0), - index: MemoryIndex(0), - minimum: 1, - maximum: None, - }], - data: vec![DataSegment { - id: crate::types::DataId(0), - index: DataIndex(0), - mode: crate::wasm::DataMode::Active { offset: 100 }, - bytes: b"hello\n".to_vec(), - }], - exports: vec![ - Export { - name: crate::abi::RUN_CORE_EXPORT.into(), - kind: ExportKind::Function, - index: ExportIndex::Function(FunctionIndex(2)), - }, - Export { - name: "memory".into(), - kind: ExportKind::Memory, - index: ExportIndex::Memory(MemoryIndex(0)), - }, - ], - entry: None, - realloc: None, - globals: Vec::new(), - helpers: Vec::new(), - span, - }; - crate::wasm::encode_module(&module).expect("encoding the core module") - } - - #[test] - fn prints_via_wasi_stdout_when_wasmtime_is_available() { - if std::process::Command::new("wasmtime") - .arg("--version") - .output() - .is_err() - { - eprintln!("skipping: wasmtime is not installed"); - return; - } - let (resolve, world) = command_world().expect("WASI and application WIT should load"); - let core = core_module_printing(); - let component = componentize(&core, &resolve, world).expect("componentizing"); - crate::validator() - .validate_all(&component) - .expect("the component should validate"); - let path = std::env::temp_dir().join(format!("psrs-print-{}.wasm", std::process::id())); - std::fs::write(&path, &component).unwrap(); - let output = std::process::Command::new("wasmtime") - .arg("run") - .arg(&path) - .output() - .unwrap(); - let _ = std::fs::remove_file(&path); - assert!( - output.status.success(), - "wasmtime failed to run the component: {output:?}" - ); - assert_eq!(output.stdout, b"hello\n"); - } -} diff --git a/crates/psrs-backend/src/control/execution/mod.rs b/crates/psrs-backend/src/control/execution/mod.rs new file mode 100644 index 00000000..d237b304 --- /dev/null +++ b/crates/psrs-backend/src/control/execution/mod.rs @@ -0,0 +1,243 @@ +//! Complete action inventory from the operational semantics, independent of CC. +use super::schema::{ComputationCase as C, ContextCase as X, ExtentCase as E, SequenceCase as Q}; +use super::schema::{ControlHelperRole as R, FieldKind}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(crate) enum Program { + Executor, + Run, + CloneSequence, + RebuildSequence, + Check, + Close, + Start, + Once, + Cancel, + RawResume, +} + +impl Program { + pub(crate) fn helper(self) -> Option { + Some(match self { + Self::Executor => R::Executor, + Self::Run => R::Run, + Self::CloneSequence => R::CloneSequence, + Self::RebuildSequence => R::RebuildSequence, + Self::Check => R::Check, + Self::Close => R::Close, + Self::Start => R::Start, + Self::Once => R::Once, + Self::Cancel => R::Cancel, + Self::RawResume => return None, + }) + } + + pub(crate) fn parameters(self) -> &'static [FieldKind] { + self.helper() + .map_or(&[FieldKind::Callable, FieldKind::Payload], R::parameters) + } + + pub(crate) fn result(self) -> FieldKind { + self.helper().map_or(FieldKind::Payload, R::result) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum Outcome { + Advance, + Publish, + Return, + Tail(R), + Reject, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(crate) enum Selection { + Computation(C), + Sequence(Q), + Context(X), + Extent(E), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(crate) enum ActionId { + ReturnEmptyRoot, + ReturnEmptyScoped, + ReturnCons, + ReturnFinish, + AppendEmpty, + AppendCons, + AppendAppend, + AppendFinish, + PerformRoot, + PerformScoped, + Bind, + Resume, + HandleEmpty, + HandleCons, + HandleFinish, + HandleAppendEmpty, + HandleAppendCons, + HandleAppendAppend, + HandleAppendFinish, + OwnedResume, + CloneEmptyDone, + CloneEmptyNext, + CloneEmptyRejectCons, + CloneEmptyRejectFinish, + CloneCons, + CloneAppend, + CloneFinish, + RebuildEmpty, + RebuildCons, + RebuildRejectAppend, + RebuildFinish, + CheckRoot, + CheckScoped, + CloseRoot, + CloseScoped, + Start, + Once, + Cancel, + RawResume, + Run, +} + +impl ActionId { + pub(crate) fn outcome(self) -> Outcome { + match self { + Self::ReturnEmptyRoot => Outcome::Publish, + Self::CloneEmptyDone => Outcome::Tail(R::RebuildSequence), + Self::CloneEmptyNext | Self::CloneCons | Self::CloneAppend | Self::CloneFinish => { + Outcome::Tail(R::CloneSequence) + } + Self::CloneEmptyRejectCons + | Self::CloneEmptyRejectFinish + | Self::RebuildRejectAppend => Outcome::Reject, + Self::RebuildCons | Self::RebuildFinish => Outcome::Tail(R::RebuildSequence), + Self::CheckScoped => Outcome::Tail(R::Check), + Self::RebuildEmpty + | Self::CheckRoot + | Self::CloseRoot + | Self::CloseScoped + | Self::Start + | Self::Once + | Self::Cancel + | Self::RawResume + | Self::Run => Outcome::Return, + _ => Outcome::Advance, + } + } + pub(crate) const ALL: [Self; 40] = [ + Self::ReturnEmptyRoot, + Self::ReturnEmptyScoped, + Self::ReturnCons, + Self::ReturnFinish, + Self::AppendEmpty, + Self::AppendCons, + Self::AppendAppend, + Self::AppendFinish, + Self::PerformRoot, + Self::PerformScoped, + Self::Bind, + Self::Resume, + Self::HandleEmpty, + Self::HandleCons, + Self::HandleFinish, + Self::HandleAppendEmpty, + Self::HandleAppendCons, + Self::HandleAppendAppend, + Self::HandleAppendFinish, + Self::OwnedResume, + Self::CloneEmptyDone, + Self::CloneEmptyNext, + Self::CloneEmptyRejectCons, + Self::CloneEmptyRejectFinish, + Self::CloneCons, + Self::CloneAppend, + Self::CloneFinish, + Self::RebuildEmpty, + Self::RebuildCons, + Self::RebuildRejectAppend, + Self::RebuildFinish, + Self::CheckRoot, + Self::CheckScoped, + Self::CloseRoot, + Self::CloseScoped, + Self::Start, + Self::Once, + Self::Cancel, + Self::RawResume, + Self::Run, + ]; + + /// Every required action has one exact selection path in one program. + pub(crate) fn selection(self) -> (Program, &'static [Selection]) { + use Selection::{Computation as c, Context as x, Extent as e, Sequence as q}; + match self { + Self::ReturnEmptyRoot => (Program::Executor, &[c(C::Return), q(Q::Empty), x(X::Root)]), + Self::ReturnEmptyScoped => ( + Program::Executor, + &[c(C::Return), q(Q::Empty), x(X::Scoped)], + ), + Self::ReturnCons => (Program::Executor, &[c(C::Return), q(Q::Cons)]), + Self::ReturnFinish => (Program::Executor, &[c(C::Return), q(Q::Finish)]), + Self::AppendEmpty => ( + Program::Executor, + &[c(C::Return), q(Q::Append), q(Q::Empty)], + ), + Self::AppendCons => (Program::Executor, &[c(C::Return), q(Q::Append), q(Q::Cons)]), + Self::AppendAppend => ( + Program::Executor, + &[c(C::Return), q(Q::Append), q(Q::Append)], + ), + Self::AppendFinish => ( + Program::Executor, + &[c(C::Return), q(Q::Append), q(Q::Finish)], + ), + Self::PerformRoot => (Program::Executor, &[c(C::Perform), x(X::Root)]), + Self::PerformScoped => (Program::Executor, &[c(C::Perform), x(X::Scoped)]), + Self::Bind => (Program::Executor, &[c(C::Bind)]), + Self::Resume => (Program::Executor, &[c(C::Resume)]), + Self::HandleEmpty => (Program::Executor, &[c(C::Handle), q(Q::Empty)]), + Self::HandleCons => (Program::Executor, &[c(C::Handle), q(Q::Cons)]), + Self::HandleFinish => (Program::Executor, &[c(C::Handle), q(Q::Finish)]), + Self::HandleAppendEmpty => ( + Program::Executor, + &[c(C::Handle), q(Q::Append), q(Q::Empty)], + ), + Self::HandleAppendCons => { + (Program::Executor, &[c(C::Handle), q(Q::Append), q(Q::Cons)]) + } + Self::HandleAppendAppend => ( + Program::Executor, + &[c(C::Handle), q(Q::Append), q(Q::Append)], + ), + Self::HandleAppendFinish => ( + Program::Executor, + &[c(C::Handle), q(Q::Append), q(Q::Finish)], + ), + Self::OwnedResume => (Program::Executor, &[c(C::OwnedResume)]), + Self::CloneEmptyDone => (Program::CloneSequence, &[q(Q::Empty), q(Q::Empty)]), + Self::CloneEmptyNext => (Program::CloneSequence, &[q(Q::Empty), q(Q::Append)]), + Self::CloneEmptyRejectCons => (Program::CloneSequence, &[q(Q::Empty), q(Q::Cons)]), + Self::CloneEmptyRejectFinish => (Program::CloneSequence, &[q(Q::Empty), q(Q::Finish)]), + Self::CloneCons => (Program::CloneSequence, &[q(Q::Cons)]), + Self::CloneAppend => (Program::CloneSequence, &[q(Q::Append)]), + Self::CloneFinish => (Program::CloneSequence, &[q(Q::Finish)]), + Self::RebuildEmpty => (Program::RebuildSequence, &[q(Q::Empty)]), + Self::RebuildCons => (Program::RebuildSequence, &[q(Q::Cons)]), + Self::RebuildRejectAppend => (Program::RebuildSequence, &[q(Q::Append)]), + Self::RebuildFinish => (Program::RebuildSequence, &[q(Q::Finish)]), + Self::CheckRoot => (Program::Check, &[e(E::Root)]), + Self::CheckScoped => (Program::Check, &[e(E::Scoped)]), + Self::CloseRoot => (Program::Close, &[e(E::Root)]), + Self::CloseScoped => (Program::Close, &[e(E::Scoped)]), + Self::Start => (Program::Start, &[]), + Self::Once => (Program::Once, &[]), + Self::Cancel => (Program::Cancel, &[]), + Self::RawResume => (Program::RawResume, &[]), + Self::Run => (Program::Run, &[]), + } + } +} diff --git a/crates/psrs-backend/src/control/mod.rs b/crates/psrs-backend/src/control/mod.rs new file mode 100644 index 00000000..347caf8c --- /dev/null +++ b/crates/psrs-backend/src/control/mod.rs @@ -0,0 +1,7 @@ +//! Stage-independent contracts for the control machine. +//! +//! The machine semantics live at the checked construction plan +//! (`cc/lower/control/plan`). This module keeps the named schema the plan, +//! layout and verification consume, plus the flat-driver action inventory. +pub(crate) mod execution; +pub(crate) mod schema; diff --git a/crates/psrs-backend/src/control/schema.rs b/crates/psrs-backend/src/control/schema.rs new file mode 100644 index 00000000..edce17ac --- /dev/null +++ b/crates/psrs-backend/src/control/schema.rs @@ -0,0 +1,291 @@ +//! The authoritative named schema of the control machine. +//! +//! This module names the machine's node cases, their fields and the fixed +//! helper roles without importing any IR representation. Physical layout +//! planning, P8 construction and every verifier consume these names instead of +//! positional offsets, so a case field or a role is stated once. The schema is +//! effect-agnostic: it never mentions a concrete operation or handler. +//! +//! A named case is always identified by its stable tag. Field index constants +//! are the machine's own, not an inference from an IR variant's field order; +//! layout planning checks that the physical frame it emits has exactly the +//! declared arity. + +/// Logical storage roles, before any CC representation or physical MIR layout. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum FieldKind { + Payload, + Callable, + Computation, + Sequence, + Context, + Extent, + Status, + Owner, + Callback, + ResultPair, + ReplayResult, +} + +/// Direct-call helper roles. Raw resume uses the closure receiver protocol. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ControlHelperRole { + Executor, + Run, + CloneSequence, + RebuildSequence, + Start, + Check, + Close, + Once, + Cancel, +} + +impl ControlHelperRole { + pub(crate) const ALL: [Self; 9] = [ + Self::Executor, + Self::Run, + Self::CloneSequence, + Self::RebuildSequence, + Self::Start, + Self::Check, + Self::Close, + Self::Once, + Self::Cancel, + ]; + + pub(crate) fn parameters(self) -> &'static [FieldKind] { + use FieldKind::*; + match self { + Self::Run => &[Payload, Callable, Computation], + Self::Executor => &[Payload, Callable, Computation, Sequence, Extent, Context], + Self::CloneSequence => &[Sequence, Sequence, Sequence, Extent, Extent], + Self::RebuildSequence => &[Sequence, Sequence, Extent, Extent], + Self::Start => &[], + Self::Check | Self::Close => &[Extent], + Self::Once => &[Callable], + Self::Cancel => &[Owner], + } + } + + pub(crate) fn result(self) -> FieldKind { + use FieldKind::*; + match self { + Self::Executor | Self::Run => Payload, + Self::CloneSequence | Self::RebuildSequence => ReplayResult, + Self::Start | Self::Check | Self::Close => Extent, + Self::Once | Self::Cancel => Owner, + } + } +} + +/// The six cases of a `Control f a` computation frame. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ComputationCase { + Return, + Perform, + Bind, + Resume, + Handle, + OwnedResume, +} + +impl ComputationCase { + pub(crate) fn fields(self) -> &'static [FieldKind] { + use FieldKind::*; + match self { + Self::Return | Self::Perform => &[Payload], + Self::Bind => &[Computation, Callable], + Self::Resume => &[Payload, Sequence, Extent], + Self::Handle => &[Callable, Callable, Computation], + Self::OwnedResume => &[Owner, Payload], + } + } + pub const ALL: [Self; 6] = [ + Self::Return, + Self::Perform, + Self::Bind, + Self::Resume, + Self::Handle, + Self::OwnedResume, + ]; + + pub const fn tag(self) -> u32 { + self as u32 + } +} + +/// Fields of the computation cases whose payload is not a single erased value. +pub mod computation { + /// `Bind`: the inner computation and the callback it feeds. + pub const BIND_INNER: u32 = 0; + pub const BIND_CONTINUATION: u32 = 1; + /// `Return`/`Perform`: the erased source payload. + pub const PAYLOAD: u32 = 0; + /// `Resume`: the erased answer, the saved source queue, the checkpoint. + pub const RESUME_VALUE: u32 = 0; + pub const RESUME_SAVED: u32 = 1; + pub const RESUME_CHECKPOINT: u32 = 2; + /// `Handle`: the return clause, the operation clause, the source body. + pub const HANDLE_RETURN: u32 = 0; + pub const HANDLE_OPERATION: u32 = 1; + pub const HANDLE_SOURCE: u32 = 2; + /// `OwnedResume`: the one-shot owner and the erased answer. + pub const OWNED_OWNER: u32 = 0; + pub const OWNED_VALUE: u32 = 1; +} + +/// The four cases of the persistent sequencing spine. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum SequenceCase { + Empty, + Cons, + Append, + Finish, +} + +impl SequenceCase { + pub(crate) fn fields(self) -> &'static [FieldKind] { + use FieldKind::*; + match self { + Self::Empty => &[], + Self::Cons => &[Callable, Sequence], + Self::Append => &[Sequence, Sequence], + Self::Finish => &[Extent, Sequence, Extent], + } + } + pub const ALL: [Self; 4] = [Self::Empty, Self::Cons, Self::Append, Self::Finish]; + + pub const fn tag(self) -> u32 { + self as u32 + } +} + +/// Fields of the sequencing spine. +pub mod sequence { + pub const CONS_HEAD: u32 = 0; + pub const CONS_TAIL: u32 = 1; + pub const APPEND_LEFT: u32 = 0; + pub const APPEND_RIGHT: u32 = 1; + pub const FINISH_EXTENT: u32 = 0; + pub const FINISH_REST: u32 = 1; + pub const FINISH_PARENT: u32 = 2; +} + +/// The two cases of a dispatch context. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ContextCase { + Root, + Scoped, +} + +impl ContextCase { + pub(crate) fn fields(self) -> &'static [FieldKind] { + use FieldKind::*; + match self { + Self::Root => &[Extent], + Self::Scoped => &[Callable, Callable, Sequence, Extent, Context, Extent], + } + } + pub const ALL: [Self; 2] = [Self::Root, Self::Scoped]; + + pub const fn tag(self) -> u32 { + self as u32 + } +} + +/// Fields of a dispatch context. +pub mod context { + pub const ROOT_EXTENT: u32 = 0; + pub const SCOPED_RETURN: u32 = 0; + pub const SCOPED_OPERATION: u32 = 1; + pub const SCOPED_SAVED: u32 = 2; + pub const SCOPED_PARENT: u32 = 3; + pub const SCOPED_CALLER: u32 = 4; + pub const SCOPED_EXTENT: u32 = 5; +} + +/// The two cases of a lifetime extent. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ExtentCase { + Root, + Scoped, +} + +impl ExtentCase { + pub(crate) fn fields(self) -> &'static [FieldKind] { + use FieldKind::*; + match self { + Self::Root => &[Status], + Self::Scoped => &[Status, Extent], + } + } + pub const ALL: [Self; 2] = [Self::Root, Self::Scoped]; + + pub const fn tag(self) -> u32 { + self as u32 + } +} + +/// Fields of a lifetime extent. +pub mod extent { + pub const STATUS: u32 = 0; + pub const PARENT: u32 = 1; +} + +/// The single case of a one-shot owner object. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum OwnerCase { + OneShot, +} + +impl OwnerCase { + pub(crate) fn fields(self) -> &'static [FieldKind] { + &[FieldKind::Callback, FieldKind::Status] + } + pub const ALL: [Self; 1] = [Self::OneShot]; + + pub const fn tag(self) -> u32 { + self as u32 + } +} + +/// Fields of a one-shot owner object. +pub mod owner { + pub const CALLBACK: u32 = 0; + pub const STATUS: u32 = 1; +} + +/// Fields of the closed-run result pair. +pub mod result_pair { + pub const STATE: u32 = 0; + pub const VALUE: u32 = 1; +} + +/// Fields of a replay result. +pub mod replay { + pub const QUEUE: u32 = 0; + pub const ACTIVE: u32 = 1; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn case_tags_are_stable_and_unique() { + for (index, case) in ComputationCase::ALL.iter().enumerate() { + assert_eq!(case.tag(), index as u32); + } + for (index, case) in SequenceCase::ALL.iter().enumerate() { + assert_eq!(case.tag(), index as u32); + } + for (index, case) in ContextCase::ALL.iter().enumerate() { + assert_eq!(case.tag(), index as u32); + } + for (index, case) in ExtentCase::ALL.iter().enumerate() { + assert_eq!(case.tag(), index as u32); + } + assert_eq!(OwnerCase::OneShot.tag(), 0); + } +} diff --git a/crates/psrs-backend/src/effects/entry.rs b/crates/psrs-backend/src/effects/entry.rs deleted file mode 100644 index 8f142d1e..00000000 --- a/crates/psrs-backend/src/effects/entry.rs +++ /dev/null @@ -1,226 +0,0 @@ -use super::verify::effect_error; -use crate::BackendError; -use psrs_core::{ - Binder, Binding, Declaration, Expr, ExprKind, Module as CoreModule, Type, TypeConstructor, - TypeId, -}; -use psrs_hir::SymbolId; -use psrs_span::TextRange; - -pub(super) fn validate_source( - module: &CoreModule, - trusted: &psrs_core::effect::TrustedEffect, - entry: psrs_core::effect::EffectCommandEntry, -) -> Result<(), Vec> { - let Some(main) = module - .declarations - .iter() - .find(|declaration| declaration.symbol == entry.symbol) - else { - return Err(vec![effect_error( - module, - module.span, - "Effect command entry has no source declaration", - )]); - }; - if !main.quantified.is_empty() - || psrs_core::effect::effect_application(module, main.ty, trusted.effect_type) - .is_none_or(|(_, payload)| !is_unit(module, payload)) - { - return Err(vec![effect_error( - module, - main.name_span, - "Effect command entry must have type Effect Unit", - )]); - } - Ok(()) -} - -pub(super) fn install( - module: &mut CoreModule, - trusted: &psrs_core::effect::TrustedEffect, - entry: psrs_core::effect::EffectCommandEntry, -) -> Result<(), Vec> { - let Some(main) = module - .declarations - .iter() - .find(|declaration| declaration.symbol == entry.symbol) - .cloned() - else { - return Err(vec![effect_error( - module, - module.span, - "Effect command entry has no source declaration", - )]); - }; - let Some((token_parameters, unit)) = psrs_core::closure_parts(&module.types, main.ty) else { - return Err(vec![effect_error( - module, - main.name_span, - "lowered Effect command entry is not an ordinary token closure", - )]); - }; - if token_parameters.len() != 1 || !is_unit(module, unit) { - return Err(vec![effect_error( - module, - main.name_span, - "lowered Effect command entry must take one token and return Unit", - )]); - } - let Some(run) = trusted - .operations - .iter() - .find(|operation| operation.operation == psrs_core::effect::EffectOperation::Run) - .map(|operation| operation.symbol) - else { - return Err(vec![effect_error( - module, - main.name_span, - "trusted runEffect operation is missing from the Effect contract", - )]); - }; - if !module - .declarations - .iter() - .any(|declaration| declaration.symbol == run) - { - return Err(vec![effect_error( - module, - main.name_span, - "trusted runEffect operation was not lowered", - )]); - } - let int = intern(module, Type::Constructor(TypeConstructor::Int)); - let run_type = psrs_core::effect::function_type(module, &[main.ty], unit); - let result_local = psrs_core::effect::fresh_local(module); - let binder = Binder { - id: result_local, - name: "effectResult".to_string(), - ty: unit, - span: main.name_span, - }; - let run_action = expr( - ExprKind::Application( - Box::new(expr(ExprKind::Global(run), run_type, main.name_span)), - Box::new(expr( - ExprKind::Global(entry.symbol), - main.ty, - main.name_span, - )), - ), - unit, - main.name_span, - ); - let value = expr( - ExprKind::Let { - bindings: vec![Binding { - binder, - quantified: Vec::new(), - value: run_action, - span: main.name_span, - }], - body: Box::new(expr(ExprKind::Integer(0), int, main.name_span)), - }, - int, - main.name_span, - ); - let symbol = fresh_entry_symbol(module, main.symbol.module); - module.declarations.push(Declaration { - symbol, - name: "psrs.command-entry".to_string(), - name_span: main.name_span, - quantified: Vec::new(), - ty: int, - value, - span: main.span, - }); - module.entry = Some(symbol); - if !valid_wrapper(module, symbol, entry.symbol, run, unit, int) { - return Err(vec![effect_error( - module, - main.name_span, - "generated Effect command entry does not run the selected action exactly once", - )]); - } - Ok(()) -} - -fn valid_wrapper( - module: &CoreModule, - wrapper_symbol: SymbolId, - source: SymbolId, - run: SymbolId, - unit: TypeId, - int: TypeId, -) -> bool { - let Some(wrapper) = module - .declarations - .iter() - .find(|declaration| declaration.symbol == wrapper_symbol) - else { - return false; - }; - if wrapper.ty != int || !wrapper.quantified.is_empty() { - return false; - } - let ExprKind::Let { bindings, body } = &wrapper.value.kind else { - return false; - }; - if bindings.len() != 1 || bindings[0].binder.ty != unit || bindings[0].value.ty != unit { - return false; - } - let ExprKind::Application(function, action) = &bindings[0].value.kind else { - return false; - }; - if !matches!(function.kind, ExprKind::Global(symbol) if symbol == run) - || !matches!(action.kind, ExprKind::Global(symbol) if symbol == source) - { - return false; - } - matches!(body.kind, ExprKind::Integer(0)) && body.ty == int -} - -fn is_unit(module: &CoreModule, ty: TypeId) -> bool { - matches!( - module.types.get(ty.0 as usize), - Some(Type::Constructor(TypeConstructor::Unit)) - ) -} - -fn intern(module: &mut CoreModule, ty: Type) -> TypeId { - if let Some(index) = module.types.iter().position(|current| current == &ty) { - TypeId(index as u32) - } else { - module.types.push(ty); - TypeId((module.types.len() - 1) as u32) - } -} - -fn fresh_entry_symbol(module: &CoreModule, owner: psrs_hir::ModuleId) -> SymbolId { - let next = module - .declarations - .iter() - .filter(|declaration| declaration.symbol.module == owner) - .map(|declaration| declaration.symbol.index) - .chain( - module - .externals - .iter() - .filter(|external| external.symbol.module == owner) - .map(|external| external.symbol.index), - ) - .chain( - module - .constructors - .iter() - .filter(|constructor| constructor.symbol.module == owner) - .map(|constructor| constructor.symbol.index), - ) - .max() - .map_or(0, |index| index.saturating_add(1)); - SymbolId::new(owner, next) -} - -fn expr(kind: ExprKind, ty: TypeId, span: TextRange) -> Expr { - Expr { kind, ty, span } -} diff --git a/crates/psrs-backend/src/effects/mod.rs b/crates/psrs-backend/src/effects/mod.rs deleted file mode 100644 index 720c09a1..00000000 --- a/crates/psrs-backend/src/effects/mod.rs +++ /dev/null @@ -1,87 +0,0 @@ -//! Effect lowering coordinator. Abstract identities and the import plan are -//! checked before the Core type table erases `Effect` applications. - -mod entry; -mod suspension; -mod verify; - -use crate::BackendError; -use crate::bindings::ExternalBindings; -use psrs_core::{Module as CoreModule, effect::EffectCompilation}; - -pub(crate) fn lower_effects( - module: &mut CoreModule, - bindings: &mut ExternalBindings, - context: &EffectCompilation, -) -> Result<(), Vec> { - verify::trusted_contract(module, bindings, &context.trusted)?; - validate_entry_context(module, context)?; - let suspensions = suspension::plan(module, bindings, &context.trusted)?; - let lowering = psrs_core::effect::lower_effects(module, &context.trusted) - .map_err(|errors| verify::verification_errors(&errors))?; - - let applied = suspension::apply(module, bindings, &suspensions)?; - if let Some(entry) = context.command_entry { - entry::install(module, &context.trusted, entry)?; - } - - lowering - .verify(module) - .map_err(|errors| verify::verification_errors(&errors))?; - if let Err(errors) = module.verify() { - return Err(verify::verification_errors(&errors)); - } - suspension::verify(module, bindings, &suspensions, &applied)?; - let synthesized = lowering.synthesized.to_vec(); - bindings - .imports - .retain(|binding| !synthesized.contains(&binding.symbol)); - Ok(()) -} - -fn validate_entry_context( - module: &CoreModule, - context: &EffectCompilation, -) -> Result<(), Vec> { - let selected = module.entry; - match (selected, context.command_entry) { - (Some(selected), Some(entry)) if selected != entry.symbol => { - Err(vec![verify::effect_error( - module, - module.span, - "Effect command entry does not match the selected source entry", - )]) - } - (Some(_), Some(entry)) => entry::validate_source(module, &context.trusted, entry), - (Some(selected), None) => { - let Some(declaration) = module - .declarations - .iter() - .find(|declaration| declaration.symbol == selected) - else { - return Ok(()); - }; - if psrs_core::effect::effect_application( - module, - declaration.ty, - context.trusted.effect_type, - ) - .is_some() - { - Err(vec![verify::effect_error( - module, - declaration.name_span, - "missing Effect command entry metadata for the selected source entry", - )]) - } else { - Ok(()) - } - } - (None, Some(_)) => Err(vec![verify::effect_error( - module, - module.span, - "Effect command entry metadata has no selected source entry", - )]), - (None, None) => Ok(()), - } -} diff --git a/crates/psrs-backend/src/effects/suspension.rs b/crates/psrs-backend/src/effects/suspension.rs deleted file mode 100644 index 339d312a..00000000 --- a/crates/psrs-backend/src/effects/suspension.rs +++ /dev/null @@ -1,462 +0,0 @@ -use super::verify::{effect_error, source_effect_error}; -use crate::BackendError; -use crate::bindings::ExternalBindings; -use psrs_core::{ - Binder, Declaration, Expr, ExprKind, Module as CoreModule, Type, TypeId, closure_parts, -}; -use psrs_hir::{LocalId, ModuleId, SymbolId, TypeVariableId}; -use psrs_span::TextRange; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub(super) struct SuspensionPlan { - index: usize, - original: SymbolId, - source_module: ModuleId, - name: String, - span: TextRange, - source: TypeId, - source_body: TypeId, - quantified: Vec, - parameters: Vec, - application: TypeId, - payload: TypeId, -} - -#[derive(Clone, Debug)] -pub(super) struct AppliedSuspension { - plan: SuspensionPlan, - host: SymbolId, - host_type: TypeId, -} - -fn import_error(module: &CoreModule, plan: &SuspensionPlan, message: &str) -> BackendError { - source_effect_error(module, plan.source_module, plan.span, message) -} - -pub(super) fn plan( - module: &CoreModule, - bindings: &ExternalBindings, - trusted: &psrs_core::effect::TrustedEffect, -) -> Result, Vec> { - let mut plans = Vec::new(); - let operation_symbols = trusted - .operations - .iter() - .map(|operation| operation.symbol) - .collect::>(); - for (index, binding) in bindings.imports.iter().enumerate() { - if operation_symbols.contains(&binding.symbol) { - continue; - } - let Some(source) = binding.type_id else { - continue; - }; - let shape = psrs_core::effect::classify_effect_import(module, source, trusted.effect_type) - .map_err(|message| { - vec![source_effect_error( - module, - binding.source_module, - binding.span, - message, - )] - })?; - let Some(shape) = shape else { - continue; - }; - let Some(external) = module - .externals - .iter() - .find(|external| external.symbol == binding.symbol) - else { - return Err(vec![source_effect_error( - module, - binding.source_module, - binding.span, - "effectful import has no external symbol", - )]); - }; - let source_body = strip_foralls(module, source).ok_or_else(|| { - vec![source_effect_error( - module, - binding.source_module, - binding.span, - "effectful import has an invalid quantified type", - )] - })?; - plans.push(SuspensionPlan { - index, - original: binding.symbol, - source_module: binding.source_module, - name: external.name.clone(), - span: binding.span, - source, - source_body, - quantified: shape.quantified, - parameters: shape.parameters, - application: shape.application, - payload: shape.payload, - }); - } - Ok(plans) -} - -pub(super) fn apply( - module: &mut CoreModule, - bindings: &mut ExternalBindings, - plans: &[SuspensionPlan], -) -> Result, Vec> { - let mut applied = Vec::with_capacity(plans.len()); - for plan in plans { - let Some(binding) = bindings.imports.get(plan.index) else { - return Err(vec![import_error( - module, - plan, - "effect import plan no longer names an external binding", - )]); - }; - if binding.symbol != plan.original - || binding.source_module != plan.source_module - || binding.type_id != Some(plan.source) - { - return Err(vec![import_error( - module, - plan, - "effect import plan no longer matches its source binding", - )]); - } - let Some((parameters, payload)) = closure_parts(&module.types, plan.application) else { - return Err(vec![import_error( - module, - plan, - "planned Effect result was not lowered to a closure", - )]); - }; - if parameters.len() != 1 || payload != plan.payload { - return Err(vec![import_error( - module, - plan, - "planned Effect result has an invalid token closure shape", - )]); - } - let token = parameters[0]; - let host = psrs_core::effect::fresh_foreign_symbol(module); - let host_body = psrs_core::effect::function_type(module, &plan.parameters, plan.payload); - let host_type = quantify(module, &plan.quantified, host_body); - let Some(external) = module - .externals - .iter_mut() - .find(|external| external.symbol == plan.original) - else { - return Err(vec![import_error( - module, - plan, - "effectful import has no external symbol", - )]); - }; - external.symbol = host; - let Some(external_type) = module - .external_types - .iter_mut() - .find(|external| external.symbol == plan.original) - else { - return Err(vec![import_error( - module, - plan, - "effectful import has no checked source signature", - )]); - }; - external_type.symbol = host; - external_type.ty = host_type; - bindings.imports[plan.index].symbol = host; - bindings.imports[plan.index].type_id = Some(host_type); - let wrapper = build_wrapper(module, plan, host, host_body, token)?; - module.declarations.push(wrapper); - applied.push(AppliedSuspension { - plan: plan.clone(), - host, - host_type, - }); - } - Ok(applied) -} - -pub(super) fn verify( - module: &CoreModule, - bindings: &ExternalBindings, - plans: &[SuspensionPlan], - applied: &[AppliedSuspension], -) -> Result<(), Vec> { - if plans.len() != applied.len() { - return Err(vec![effect_error( - module, - module.span, - "effect import plan and wrapper counts differ", - )]); - } - for (plan, applied) in plans.iter().zip(applied) { - if &applied.plan != plan { - return Err(vec![import_error( - module, - plan, - "effect import wrapper was built from different lowering evidence", - )]); - } - let Some(binding) = bindings.imports.get(plan.index) else { - return Err(vec![import_error( - module, - plan, - "effect wrapper has no host binding", - )]); - }; - if binding.symbol != applied.host - || binding.source_module != plan.source_module - || binding.type_id != Some(applied.host_type) - { - return Err(vec![import_error( - module, - plan, - "effect wrapper and host binding signatures differ", - )]); - } - let Some(host_external) = module - .externals - .iter() - .find(|external| external.symbol == applied.host) - else { - return Err(vec![import_error( - module, - plan, - "effect wrapper host import is missing", - )]); - }; - if host_external.signature.is_none() { - return Err(vec![import_error( - module, - plan, - "effect wrapper host import has no source signature", - )]); - } - let Some(wrapper) = module - .declarations - .iter() - .find(|declaration| declaration.symbol == plan.original) - else { - return Err(vec![import_error( - module, - plan, - "effect import wrapper declaration is missing", - )]); - }; - if wrapper.ty != plan.source_body - || wrapper.quantified != plan.quantified - || !wrapper_matches(module, wrapper, plan, applied.host) - { - return Err(vec![import_error( - module, - plan, - "effect import wrapper does not preserve its planned signature and suspension", - )]); - } - let Some((token_parameters, result)) = closure_parts(&module.types, plan.application) - else { - return Err(vec![import_error( - module, - plan, - "planned Effect result is no longer a closure", - )]); - }; - if token_parameters.len() != 1 || result != plan.payload { - return Err(vec![import_error( - module, - plan, - "planned Effect result no longer matches its recorded payload", - )]); - } - } - Ok(()) -} - -fn build_wrapper( - module: &CoreModule, - plan: &SuspensionPlan, - host: SymbolId, - host_body: TypeId, - token: TypeId, -) -> Result> { - let mut next_local = psrs_core::effect::fresh_local(module).0; - let mut fresh = || { - let id = LocalId(next_local); - next_local += 1; - id - }; - let binders = plan - .parameters - .iter() - .copied() - .map(|ty| (fresh(), ty)) - .collect::>(); - let mut call = expr(ExprKind::Global(host), host_body, plan.span); - let mut remaining = host_body; - for (local, ty) in &binders { - let Some((_, result)) = psrs_core::arrow_parts(&module.types, remaining) else { - return Err(vec![import_error( - module, - plan, - "planned host import type has fewer arguments than its source binding", - )]); - }; - call = expr( - ExprKind::Application( - Box::new(call), - Box::new(expr(ExprKind::Local(*local), *ty, plan.span)), - ), - result, - plan.span, - ); - remaining = result; - } - if remaining != plan.payload { - return Err(vec![import_error( - module, - plan, - "planned host import result differs from its Effect payload", - )]); - } - let mut arrow_types = Vec::new(); - let mut current = plan.source_body; - for _ in &plan.parameters { - arrow_types.push(current); - let Some((_, result)) = psrs_core::arrow_parts(&module.types, current) else { - return Err(vec![import_error( - module, - plan, - "effectful import type is not a function of its planned payload", - )]); - }; - current = result; - } - if current != plan.application { - return Err(vec![import_error( - module, - plan, - "effect import source signature changed after planning", - )]); - } - let token_local = fresh(); - let mut value = lambda( - token_local, - "token", - token, - call, - plan.application, - plan.span, - ); - for (index, (local, ty)) in binders.iter().enumerate().rev() { - value = lambda(*local, "value", *ty, value, arrow_types[index], plan.span); - } - Ok(Declaration { - symbol: plan.original, - name: plan.name.clone(), - name_span: plan.span, - quantified: plan.quantified.clone(), - ty: plan.source_body, - value, - span: plan.span, - }) -} - -fn wrapper_matches( - module: &CoreModule, - wrapper: &Declaration, - plan: &SuspensionPlan, - host: SymbolId, -) -> bool { - let mut expression = &wrapper.value; - let mut locals = Vec::with_capacity(plan.parameters.len() + 1); - for parameter in &plan.parameters { - let ExprKind::Lambda { binder, body } = &expression.kind else { - return false; - }; - if binder.ty != *parameter { - return false; - } - locals.push(binder.id); - expression = body; - } - let ExprKind::Lambda { binder, body } = &expression.kind else { - return false; - }; - let Some((tokens, result)) = closure_parts(&module.types, plan.application) else { - return false; - }; - if tokens != [binder.ty] || result != plan.payload || expression.ty != plan.application { - return false; - } - locals.push(binder.id); - let mut call = body.as_ref(); - for local in locals.iter().take(plan.parameters.len()).rev() { - let ExprKind::Application(function, argument) = &call.kind else { - return false; - }; - if !matches!(argument.kind, ExprKind::Local(id) if id == *local) { - return false; - } - call = function; - } - matches!(call.kind, ExprKind::Global(symbol) if symbol == host) -} - -fn strip_foralls(module: &CoreModule, mut ty: TypeId) -> Option { - let mut remaining = module.types.len(); - while let Some(Type::ForAll { body, .. }) = module.types.get(ty.0 as usize) { - if remaining == 0 { - return None; - } - remaining -= 1; - ty = *body; - } - Some(ty) -} - -fn quantify(module: &mut CoreModule, variables: &[TypeVariableId], body: TypeId) -> TypeId { - if variables.is_empty() { - return body; - } - let ty = Type::ForAll { - variables: variables.to_vec(), - body, - }; - if let Some(index) = module.types.iter().position(|candidate| candidate == &ty) { - TypeId(index as u32) - } else { - module.types.push(ty); - TypeId((module.types.len() - 1) as u32) - } -} - -fn lambda( - id: LocalId, - name: &str, - ty: TypeId, - body: Expr, - function_ty: TypeId, - span: TextRange, -) -> Expr { - expr( - ExprKind::Lambda { - binder: Binder { - id, - name: name.to_string(), - ty, - span, - }, - body: Box::new(body), - }, - function_ty, - span, - ) -} - -fn expr(kind: ExprKind, ty: TypeId, span: TextRange) -> Expr { - Expr { kind, ty, span } -} diff --git a/crates/psrs-backend/src/effects/verify.rs b/crates/psrs-backend/src/effects/verify.rs deleted file mode 100644 index af89e06d..00000000 --- a/crates/psrs-backend/src/effects/verify.rs +++ /dev/null @@ -1,277 +0,0 @@ -use crate::BackendError; -use crate::bindings::ExternalBindings; -use psrs_core::{Module as CoreModule, Type, TypeConstructor}; -use psrs_hir::{ExternalKind, TypeId as HirTypeId, TypeVariableId}; -use psrs_span::TextRange; -use std::collections::HashSet; - -const EFFECT_INTERFACE: &str = "psrs:effect"; - -pub(super) fn trusted_contract( - module: &CoreModule, - bindings: &ExternalBindings, - trusted: &psrs_core::effect::TrustedEffect, -) -> Result<(), Vec> { - if !module.opaque_ids.contains(&trusted.effect_type) - || !module.types.iter().any(|ty| { - matches!(ty, Type::Constructor(TypeConstructor::User(id)) if *id == trusted.effect_type) - }) - { - return Err(vec![effect_error( - module, - module.span, - "trusted Effect identity is missing or is not an opaque type", - )]); - } - let mut symbols = HashSet::new(); - let mut operations = HashSet::new(); - for operation in &trusted.operations { - if !symbols.insert(operation.symbol) || !operations.insert(operation.operation) { - return Err(vec![effect_error( - module, - module.span, - "trusted Effect operation bindings are duplicated", - )]); - } - let Some(external) = module - .externals - .iter() - .find(|external| external.symbol == operation.symbol) - else { - return Err(vec![effect_error( - module, - module.span, - "trusted Effect operation binding has no external declaration", - )]); - }; - let expected = operation.operation.wit_function(); - if !matches!( - &external.kind, - ExternalKind::Wit { interface, function } - if interface == EFFECT_INTERFACE && function == expected - ) { - return Err(vec![source_effect_error( - module, - external_source_module(module, operation.symbol), - external - .signature - .as_ref() - .map_or(module.span, |ty| ty.span), - "trusted Effect operation identity does not match its WIT binding", - )]); - } - let Some(binding) = bindings - .imports - .iter() - .find(|binding| binding.symbol == operation.symbol) - else { - return Err(vec![source_effect_error( - module, - external_source_module(module, operation.symbol), - external - .signature - .as_ref() - .map_or(module.span, |ty| ty.span), - "trusted Effect operation has no external binding metadata", - )]); - }; - let Some(signature) = binding.type_id else { - return Err(vec![source_effect_error( - module, - binding.source_module, - binding.span, - "trusted Effect operation has no checked source signature", - )]); - }; - if !valid_core_operation_signature( - module, - signature, - trusted.effect_type, - operation.operation, - ) { - return Err(vec![source_effect_error( - module, - binding.source_module, - binding.span, - &format!( - "trusted Effect operation `{expected}` has an invalid checked source signature" - ), - )]); - } - } - if [ - psrs_core::effect::EffectOperation::Pure, - psrs_core::effect::EffectOperation::Bind, - psrs_core::effect::EffectOperation::Run, - psrs_core::effect::EffectOperation::Trap, - ] - .into_iter() - .any(|required| !operations.contains(&required)) - { - return Err(vec![effect_error( - module, - module.span, - "trusted Effect contract is missing a required operation binding", - )]); - } - for external in &module.externals { - if matches!( - &external.kind, - ExternalKind::Wit { interface, .. } if interface == EFFECT_INTERFACE - ) && !symbols.contains(&external.symbol) - { - return Err(vec![source_effect_error( - module, - external_source_module(module, external.symbol), - external - .signature - .as_ref() - .map_or(module.span, |ty| ty.span), - "Effect WIT import is missing from the trusted operation bindings", - )]); - } - } - Ok(()) -} - -fn valid_core_operation_signature( - module: &CoreModule, - ty: psrs_core::TypeId, - effect: HirTypeId, - operation: psrs_core::effect::EffectOperation, -) -> bool { - let Some((variables, parameters, result)) = core_function_signature(module, ty) else { - return false; - }; - match operation { - psrs_core::effect::EffectOperation::Pure => { - variables.len() == 1 - && parameters.len() == 1 - && core_variable(module, parameters[0]) == Some(variables[0]) - && core_effect_payload(module, result, effect) - .is_some_and(|payload| core_variable(module, payload) == Some(variables[0])) - } - psrs_core::effect::EffectOperation::Bind => { - if variables.len() != 2 || parameters.len() != 2 { - return false; - } - let first = core_effect_payload(module, parameters[0], effect); - let continuation = psrs_core::arrow_parts(&module.types, parameters[1]); - let result = core_effect_payload(module, result, effect); - matches!((first, continuation, result), - (Some(first), Some((input, output)), Some(output_result)) - if core_variable(module, first) == Some(variables[0]) - && core_variable(module, input) == Some(variables[0]) - && core_effect_payload(module, output, effect) - .is_some_and(|payload| core_variable(module, payload) == Some(variables[1])) - && core_variable(module, output_result) == Some(variables[1])) - } - psrs_core::effect::EffectOperation::Run => { - variables.len() == 1 - && parameters.len() == 1 - && core_effect_payload(module, parameters[0], effect) - .is_some_and(|payload| core_variable(module, payload) == Some(variables[0])) - && core_variable(module, result) == Some(variables[0]) - } - psrs_core::effect::EffectOperation::Trap => { - parameters.is_empty() - && variables.is_empty() - && core_effect_payload(module, result, effect).is_some_and(|payload| { - matches!( - module.types.get(payload.0 as usize), - Some(Type::Constructor(TypeConstructor::Unit)) - ) - }) - } - } -} - -fn core_function_signature( - module: &CoreModule, - ty: psrs_core::TypeId, -) -> Option<( - Vec, - Vec, - psrs_core::TypeId, -)> { - let mut quantified = Vec::new(); - let mut body = ty; - let mut seen = HashSet::new(); - while seen.insert(body) { - let Some((variables, inner)) = psrs_core::forall_parts(&module.types, body) else { - break; - }; - quantified.extend_from_slice(variables); - body = inner; - } - if !seen.insert(body) && psrs_core::forall_parts(&module.types, body).is_some() { - return None; - } - let mut parameters = Vec::new(); - let mut arrow_seen = HashSet::new(); - loop { - if !arrow_seen.insert(body) { - return None; - } - let Some((parameter, result)) = psrs_core::arrow_parts(&module.types, body) else { - break; - }; - parameters.push(parameter); - body = result; - } - Some((quantified, parameters, body)) -} - -fn core_variable(module: &CoreModule, ty: psrs_core::TypeId) -> Option { - match module.types.get(ty.0 as usize) { - Some(Type::Variable(variable)) => Some(*variable), - _ => None, - } -} - -fn core_effect_payload( - module: &CoreModule, - ty: psrs_core::TypeId, - effect: HirTypeId, -) -> Option { - psrs_core::effect::effect_application(module, ty, effect).map(|(_, payload)| payload) -} - -pub(super) fn verification_errors(errors: &[psrs_core::VerifyError]) -> Vec { - errors - .iter() - .map(|error| { - BackendError::invalid_ir("P8 effect lowering", error.span, error.message.to_string()) - .with_module(error.module) - }) - .collect() -} - -pub(super) fn effect_error(module: &CoreModule, span: TextRange, message: &str) -> BackendError { - let error = BackendError::invalid_ir("P8 effect lowering", span, message.to_string()); - match module.entry { - Some(entry) => error.with_module(entry.module), - None => error, - } -} - -pub(super) fn source_effect_error( - _module: &CoreModule, - source_module: psrs_hir::ModuleId, - span: TextRange, - message: &str, -) -> BackendError { - BackendError::invalid_ir("P8 effect lowering", span, message.to_string()) - .with_module(source_module) -} - -pub(super) fn external_source_module( - module: &CoreModule, - symbol: psrs_hir::SymbolId, -) -> psrs_hir::ModuleId { - module - .external_types - .iter() - .find(|external| external.symbol == symbol) - .map_or(module.id, |external| external.source_module) -} diff --git a/crates/psrs-backend/src/lib.rs b/crates/psrs-backend/src/lib.rs index 40629f36..aab67ffc 100644 --- a/crates/psrs-backend/src/lib.rs +++ b/crates/psrs-backend/src/lib.rs @@ -1,16 +1,22 @@ pub mod abi; mod bindings; +mod boundary; pub mod capability; pub mod cc; -pub mod component; -mod effects; +mod control; +mod linking; pub mod mir; +mod pipeline; +mod target_intrinsics; +mod target_runtime; +pub mod trace; pub mod types; pub mod wasm; -pub use bindings::{BackendInput, ExternalBinding, ExternalBindings}; +pub use bindings::{BackendInput, ExternalBinding, ExternalBindings, RuntimeBinding}; pub use capability::TargetCapabilities; +pub use trace::*; use psrs_hir::ModuleId; use psrs_span::TextRange; @@ -126,30 +132,11 @@ pub fn compile(module: psrs_core::Module) -> Result> Ok(compile_with_stages(module)?.artifact) } -/// Compiles linked Core using the trusted Effect identities issued by the -/// driver. Core-only callers that do not provide this contract receive no -/// library-specific Effect lowering. -pub fn compile_with_effect_context( - module: psrs_core::Module, - effect_context: psrs_core::effect::EffectCompilation, -) -> Result> { - Ok(compile_with_context(module, Some(effect_context), TargetCapabilities::default())?.artifact) -} - -/// Lowers Core to CC after applying an explicit Effect contract. -/// -/// Callers that already removed trusted Effect imports, or that have no -/// trusted contract, pass `None` and get the same binding extraction as -/// [`cc::lower_module`]. -pub fn lower_cc_with_context( - mut module: psrs_core::Module, - effect_context: Option<&psrs_core::effect::EffectCompilation>, -) -> Result> { - let mut external_bindings = ExternalBindings::from_core(&module); - if let Some(context) = effect_context { - effects::lower_effects(&mut module, &mut external_bindings, context)?; - } - cc::lower_module_with_bindings(module, external_bindings) +/// Emits an artifact from a P8 input using the default target profile. Source +/// typing and binding conformance belong to P8; this entry rechecks CC and all +/// subsequent representation contracts, including modified inspection inputs. +pub fn compile_cc(input: BackendInput) -> Result> { + pipeline::compile_cc(input) } /// The default-profile validator, retained for backend unit tests. @@ -170,14 +157,41 @@ pub(crate) fn validator_for(target: TargetCapabilities) -> wasmparser::Validator pub struct Stages { /// Verified Typed Core after P7 and before P8. pub core: psrs_core::Module, - /// The explicit trusted Effect context needed to compile `core` again. - pub effect_context: Option, pub cc: cc::Module, pub mir: mir::Module, pub wasm: wasm::Module, pub artifact: Artifact, } +/// IR values captured by the diagnostic compile path. Each field is populated +/// only after that representation has been produced successfully by its pass. +#[derive(Clone, Debug, Default)] +pub struct PartialStages { + /// Optimized Core after P7. + pub core: Option, + /// CC after closure conversion and its verifier both succeed. + pub cc: Option, + /// Latest MIR produced by lowering or optimization. + pub mir: Option, + /// The pass that most recently produced `mir`. + pub mir_stage: Option<&'static str>, +} + +/// Backend diagnostics together with the last successful IR values. +#[derive(Clone, Debug)] +pub struct CompileFailure { + pub errors: Vec, + pub partial: Box, +} + +/// Normal backend result together with the pass/artifact events observed while +/// compiling it. Partial IR values are retained only when requested. +#[derive(Clone, Debug)] +pub struct TracedCompile { + pub result: Result, + pub trace: CompileTrace, +} + pub fn compile_with_stages(module: psrs_core::Module) -> Result> { compile_with_target(module, TargetCapabilities::default()) } @@ -187,104 +201,40 @@ pub fn compile_with_target( module: psrs_core::Module, target: TargetCapabilities, ) -> Result> { - compile_with_context(module, None, target) + compile_with_context(module, target) } pub fn compile_with_context( module: psrs_core::Module, - effect_context: Option, target: TargetCapabilities, ) -> Result> { - let owner = module.entry.map(|entry| entry.module); - let mut module = - psrs_core::opt::optimize(module, psrs_core::opt::Budget::default()).map_err(|errors| { - annotate_errors( - errors - .into_iter() - .map(|error| { - BackendError::new("P7 Core optimization", error.span, error.message) - .with_module(error.module) - }) - .collect(), - owner, - ) - })?; - let optimized_core = module.clone(); - let mut external_bindings = ExternalBindings::from_core(&module); - if let Some(context) = effect_context.as_ref() { - effects::lower_effects(&mut module, &mut external_bindings, context)?; - } - external_bindings.validate_conformance(&module, target)?; - let lowered_cc = cc::lower_module_with_bindings(module, external_bindings)?; - let cc = lowered_cc.cc; - let (mir, mut wasi) = - mir::lower_module_with_bindings(cc.clone(), lowered_cc.externals, target)?; - let mir = mir::opt::optimize(mir, target)?; - let owner = mir.entry.map(|entry| entry.module); - if !target.component_model - || !target.wasi_p2 - || !target.wasi_cli - || !target.wasi_io - || !target.wasi_clocks - || !target.wasi_random - { - return Err(annotate_errors( - vec![BackendError::new( - "P11 target capabilities", - mir.span, - "the current artifact pipeline requires Component Model and WASI 0.2 capabilities", - )], - owner, - )); - } - let wasm = wasm::lower_module_with_capabilities(&mir, &mut wasi, target) - .map_err(|errors| annotate_errors(errors, owner))?; - let core = wasm::encode_module(&wasm).map_err(|errors| annotate_errors(errors, owner))?; - let (resolve, world) = component::command_world().map_err(|message| { - annotate_errors( - vec![BackendError::new("P11 component", mir.span, message)], - owner, - ) - })?; - let binary = component::componentize(&core, &resolve, world).map_err(|message| { - annotate_errors( - vec![BackendError::new("P11 component", mir.span, message)], - owner, - ) - })?; - validator_for(target) - .validate_all(&binary) - .map_err(|error| { - annotate_errors( - vec![BackendError::new( - "P11 Wasm validation", - mir.span, - format!("generated WebAssembly failed validation: {error}"), - )], - owner, - ) - })?; - let warnings = lowered_cc.warnings; - let text = wasmprinter::print_bytes(&binary).map_err(|error| { - annotate_errors( - vec![BackendError::new( - "P11 WAT printing", - mir.span, - format!("generated WebAssembly could not be printed as WAT: {error}"), - )], - owner, - ) - })?; - Ok(Stages { - core: optimized_core, - effect_context, - cc, - mir, - wasm, - artifact: Artifact { - wasm: binary, - wat: text, - warnings, + pipeline::compile_with_context_inner(module, target, None, None) +} + +/// Compiles with actual top-level pass/artifact tracing. Setting +/// `capture_partial` retains IR snapshots for diagnostics; the trace itself is +/// lightweight and does not clone or format IR values. +pub fn compile_with_context_traced( + module: psrs_core::Module, + target: TargetCapabilities, + capture_partial: bool, +) -> TracedCompile { + let (result, trace) = pipeline::compile_with_context_traced(module, target, capture_partial); + TracedCompile { result, trace } +} + +/// Compiles using the normal backend pipeline and returns only representations +/// whose producing pass completed. In particular, a CC verifier failure leaves +/// the verified Core available but does not publish the unverified CC candidate. +pub fn compile_with_context_capturing( + module: psrs_core::Module, + target: TargetCapabilities, +) -> Result { + let mut partial = PartialStages::default(); + pipeline::compile_with_context_inner(module, target, Some(&mut partial), None).map_err( + |errors| CompileFailure { + errors, + partial: Box::new(partial), }, - }) + ) } diff --git a/crates/psrs-backend/src/linking/mod.rs b/crates/psrs-backend/src/linking/mod.rs new file mode 100644 index 00000000..ccf3d4b9 --- /dev/null +++ b/crates/psrs-backend/src/linking/mod.rs @@ -0,0 +1,383 @@ +//! Checked IR-to-linker requests and diagnostic mapping. +//! +//! The backend converts its optimized MIR imports and target capability +//! profile into linker-owned target records, runs the independent planner, and +//! attaches language diagnostics to any structured link failure. The resulting +//! plan is consumed by both Wasm emission and component assembly. + +use crate::abi::{self, WasiRegistry, names}; +use crate::mir; +use crate::target_runtime; +use crate::{BackendError, TargetCapabilities}; +use psrs_hir::{ModuleId, SymbolId}; +use psrs_linker::{ + BindingRequirement, Boundary, CheckedLinkPlan, CoreSignature, CoreType, MemoryDemand, Provider, + RequirementId, ResolvedWorldContext, TargetLinkInput, TargetPolicy, +}; +use psrs_span::TextRange; +use std::collections::HashMap; +use std::sync::{Arc, OnceLock}; + +/// A checked plan together with the backend's requirement identity mappings. +pub(crate) struct LinkPlan { + pub context: Arc, + pub plan: CheckedLinkPlan, + /// The planned core import `(module, field)` for each MIR import symbol. + pub imports: HashMap, +} + +/// The default resolved-world context, parsed once per process. +pub(crate) fn default_context() -> Result, Vec> { + static CONTEXT: OnceLock, String>> = OnceLock::new(); + match CONTEXT.get_or_init(|| { + psrs_linker::resolve_default_definitions() + .map(Arc::new) + .map_err(|error| error.to_string()) + }) { + Ok(context) => Ok(Arc::clone(context)), + Err(message) => Err(definitions_error(message)), + } +} + +fn definitions_error(message: &str) -> Vec { + vec![BackendError::new( + "P9 target definitions", + TextRange::new(0, 0), + message, + )] +} + +/// Builds and checks the target link plan for an optimized MIR module. +pub(crate) fn plan_for_module( + context: &Arc, + module: &mir::Module, + wasi: &mut WasiRegistry, + target: TargetCapabilities, +) -> Result> { + let mut requirements = Vec::new(); + let mut symbols: Vec<(SymbolId, RequirementId)> = Vec::new(); + let mut spans: HashMap)> = HashMap::new(); + let mut next = 0_u32; + let owner = module.entry.map(|entry| entry.module); + + for import in &module.imports { + let id = RequirementId(next); + next += 1; + if let Some(provider) = &import.runtime { + crate::bindings::verify_runtime_import(import, &module.types).map_err(|message| { + vec![BackendError::invalid_ir( + "P9 target linking", + module.span, + message, + )] + })?; + let operation = (psrs_runtime::STORAGE_UNIT.operations)() + .into_iter() + .find(|operation| operation.export == provider.function) + .ok_or_else(|| { + vec![BackendError::invalid_ir( + "P9 target linking", + module.span, + "runtime import has no catalog operation", + )] + })?; + let expected = + crate::wasm::raw_signature(import, &module.types).map_err(|message| { + vec![BackendError::invalid_ir( + "P9 target linking", + module.span, + message, + )] + })?; + let requirement = BindingRequirement { + id, + origin: operation.name.clone(), + boundary: Boundary::RawCore { + module: provider.module.clone(), + field: provider.function.clone(), + }, + expected: Some(expected), + provider: Provider::RuntimeOperation { + name: operation.name, + version: operation.version.into(), + }, + }; + spans.insert(requirement.origin.clone(), (module.span, owner)); + requirements.push(requirement); + symbols.push((import.symbol, id)); + continue; + } + if import.symbol == abi::REALLOC_SYMBOL { + crate::target_intrinsics::generated::verify(module, import).map_err(|message| { + vec![BackendError::invalid_ir( + "P9 target linking", + module.span, + message, + )] + })?; + let requirement = BindingRequirement { + id, + origin: psrs_runtime::ALLOCATOR_REALLOC_OP.name.into(), + boundary: Boundary::RawCore { + module: psrs_runtime::ALLOCATOR_MODULE.into(), + field: psrs_runtime::REALLOC_EXPORT.into(), + }, + expected: Some(core_signature(import, module.span)?), + provider: Provider::RuntimeOperation { + name: psrs_runtime::ALLOCATOR_REALLOC_OP.name.into(), + version: psrs_runtime::ALLOCATOR_REALLOC_OP.version.into(), + }, + }; + spans.insert(requirement.origin.clone(), (module.span, owner)); + requirements.push(requirement); + symbols.push((import.symbol, id)); + continue; + } + // Generated helpers are roots even though no source foreign declaration + // names them; they are lowered locally and need no external provider. + if let Some(name) = crate::target_intrinsics::generated::name(import.symbol) { + crate::target_intrinsics::generated::verify(module, import).map_err(|message| { + vec![BackendError::invalid_ir( + "P9 target linking", + module.span, + message, + )] + })?; + let requirement = BindingRequirement { + id, + origin: format!("generated.{name}"), + boundary: Boundary::RawCore { + module: "generated".into(), + field: name.into(), + }, + expected: None, + provider: Provider::Generated, + }; + spans.insert(requirement.origin.clone(), (module.span, owner)); + requirements.push(requirement); + continue; + } + if let Some(implementation) = target_runtime::for_symbol(import.symbol) { + implementation.validate_protocol().map_err(|message| { + vec![BackendError::invalid_ir( + "P9 target linking", + module.span, + message, + )] + })?; + let requirement = implementation.requirement(id, core_signature(import, module.span)?); + spans.insert(requirement.origin.clone(), (module.span, owner)); + requirements.push(requirement); + symbols.push((import.symbol, id)); + } else if let Some((interface, field)) = wasi.symbol_name(import.symbol) { + let interface = interface.to_string(); + let field = field.to_string(); + let signature = core_signature(import, module.span)?; + let requirement = BindingRequirement { + id, + origin: format!("{interface}.{field}"), + boundary: Boundary::ResolvedWit { + interface: interface.clone(), + function: field, + }, + expected: Some(signature), + provider: Provider::HostInterface { interface }, + }; + spans.insert(requirement.origin.clone(), (module.span, owner)); + requirements.push(requirement); + symbols.push((import.symbol, id)); + } else { + return Err(vec![BackendError::new( + "P9 target linking", + module.span, + "a MIR import symbol has no selected provider", + )]); + } + } + + // The synthesized command entry exits through WASI; the plan owns that + // capability even though no source foreign declaration names it. + if target.wasi_cli && module.entry.is_some() { + let exit = wasi + .import(names::EXIT, names::EXIT_WITH_CODE) + .map_err(|message| { + vec![BackendError::new("P9 target linking", module.span, message)] + })?; + let id = RequirementId(next); + let requirement = BindingRequirement { + id, + origin: format!("{}.{}", exit.module, exit.name), + boundary: Boundary::ResolvedWit { + interface: exit.module.clone(), + function: exit.name.clone(), + }, + expected: Some(CoreSignature { + parameters: exit + .parameters + .iter() + .copied() + .map(core_value_type) + .collect(), + result: exit.result.map(core_value_type), + }), + provider: Provider::HostInterface { + interface: exit.module.clone(), + }, + }; + spans.insert(requirement.origin.clone(), (module.span, owner)); + requirements.push(requirement); + symbols.push((exit.symbol, id)); + } + + let input = TargetLinkInput { + requirements, + units: psrs_linker::runtime::package_offers(&psrs_runtime::PSRS_RUNTIME) + .map_err(|errors| map_link_errors(&errors, &spans))?, + policy: TargetPolicy { + permitted_host_interfaces: permitted_host_interfaces(context, target), + }, + memory: memory_demand(), + }; + let plan = + psrs_linker::plan(context, input).map_err(|errors| map_link_errors(&errors, &spans))?; + + let imports = symbols + .into_iter() + .filter_map(|(symbol, id)| { + plan.import(id) + .map(|binding| (symbol, (binding.module.clone(), binding.field.clone()))) + }) + .collect(); + + Ok(LinkPlan { + context: Arc::clone(context), + plan, + imports, + }) +} + +/// Assembles the component from a checked plan. +pub(crate) fn compose( + link: &LinkPlan, + application: &[u8], + span: TextRange, + owner: Option, +) -> Result, Vec> { + psrs_linker::compose(&link.context, &link.plan, application) + .map(|artifact| artifact.bytes) + .map_err(|errors| { + errors + .0 + .into_iter() + .map(|error| { + attach( + BackendError::new("P11 component", span, error.to_string()), + owner, + ) + }) + .collect() + }) +} + +fn core_signature( + import: &mir::Import, + span: TextRange, +) -> Result> { + let mut parameters = Vec::with_capacity(import.parameters.len()); + for ty in &import.parameters { + let Some(ty) = core_value_type_opt(*ty) else { + return Err(vec![BackendError::invalid_ir( + "P9 target linking", + span, + "a raw import has a non-scalar canonical parameter", + )]); + }; + parameters.push(ty); + } + let result = match import.result { + Some(ty) => Some(core_value_type_opt(ty).ok_or_else(|| { + vec![BackendError::invalid_ir( + "P9 target linking", + span, + "a raw import has a non-scalar canonical result", + )] + })?), + None => None, + }; + Ok(CoreSignature { parameters, result }) +} + +fn core_value_type(ty: crate::types::ValueType) -> CoreType { + core_value_type_opt(ty).expect("a canonical ABI import is scalar") +} + +fn core_value_type_opt(ty: crate::types::ValueType) -> Option { + match ty { + crate::types::ValueType::I32 | crate::types::ValueType::Boolean => Some(CoreType::I32), + crate::types::ValueType::I64 => Some(CoreType::I64), + crate::types::ValueType::F32 => Some(CoreType::F32), + crate::types::ValueType::F64 => Some(CoreType::F64), + crate::types::ValueType::Ref(_) => None, + } +} + +fn memory_demand() -> MemoryDemand { + MemoryDemand { + canonical_scratch: (abi::PRINT_SCRATCH as u32, abi::SCRATCH_END), + allocator_state: (abi::HEAP_STATE, abi::HEAP_STATE + abi::HEAP_STATE_SIZE), + base_heap_start: abi::HEAP_START, + heap_alignment: abi::MIN_BLOCK, + growth_owner: psrs_runtime::ALLOCATOR_UNIT.id.into(), + maximum_pages: None, + } +} + +/// The canonical ids the selected target profile permits: the world's import +/// interfaces, with the capability-family gate applied to WASI services only. +fn permitted_host_interfaces( + context: &ResolvedWorldContext, + target: TargetCapabilities, +) -> Vec { + context + .world_imports() + .iter() + .filter(|interface| { + !interface.starts_with("wasi:") || abi::wasi_interface_enabled(target, interface) + }) + .cloned() + .collect() +} + +fn map_link_errors( + errors: &psrs_linker::LinkErrors, + spans: &HashMap)>, +) -> Vec { + errors + .0 + .iter() + .map(|error| { + let (span, owner) = error + .subject + .as_ref() + .and_then(|subject| spans.get(subject)) + .copied() + .unwrap_or((TextRange::new(0, 0), None)); + attach( + BackendError::new("P11 target linking", span, error.to_string()), + owner, + ) + }) + .collect() +} + +fn attach(error: BackendError, owner: Option) -> BackendError { + match owner { + Some(module) => error.with_module(module), + None => error, + } +} + +#[cfg(test)] +mod runtime_tests; +#[cfg(test)] +mod tests; diff --git a/crates/psrs-backend/src/linking/runtime_tests.rs b/crates/psrs-backend/src/linking/runtime_tests.rs new file mode 100644 index 00000000..138b2c9b --- /dev/null +++ b/crates/psrs-backend/src/linking/runtime_tests.rs @@ -0,0 +1,384 @@ +use super::*; +use crate::mir::{ + BasicBlock, BlockId, Function, Import, Instruction as I, RuntimeImport, Terminator, +}; +use crate::types::{ + CompositeType, DefinedType, DefinedTypeId, FieldType, FunctionId, HeapType, RecGroup, RefType, + StorageType, ValueDecl, ValueId, ValueType, +}; + +fn reference(id: u32) -> ValueType { + ValueType::Ref(RefType { + nullable: false, + heap: HeapType::Index(DefinedTypeId(id)), + }) +} + +fn fixture() -> mir::Module { + let span = TextRange::new(0, 1); + let owner = ModuleId(7); + let symbol = |id| SymbolId::new(owner, id); + let value = |id| ValueId(id); + let eq = ValueType::Ref(RefType { + nullable: true, + heap: HeapType::Eq, + }); + let import = |id, function: &str, parameters, result| Import { + symbol: symbol(id), + runtime: Some(RuntimeImport { + module: psrs_runtime::STORAGE_MODULE.into(), + function: function.into(), + }), + parameters, + result, + }; + let types = [ + CompositeType::Struct(vec![FieldType { + mutable: false, + storage: StorageType::I64, + }]), + CompositeType::Array(FieldType { + mutable: true, + storage: StorageType::Ref(RefType { + nullable: true, + heap: HeapType::Eq, + }), + }), + CompositeType::Struct(vec![FieldType { + mutable: false, + storage: StorageType::I32, + }]), + ] + .into_iter() + .map(|composite| { + RecGroup(vec![DefinedType { + final_type: true, + supertype: None, + composite, + }]) + }) + .collect(); + let shapes = [ + ValueType::I32, + ValueType::I32, + reference(2), + reference(1), + ValueType::I32, + ValueType::I32, + reference(2), + reference(1), + eq, + reference(2), + ValueType::I32, + eq, + eq, + ]; + let instructions = vec![ + I::Constant { + destination: value(0), + value: 1, + span, + }, + I::Constant { + destination: value(1), + value: 40, + span, + }, + I::StructNew { + destination: value(2), + type_index: DefinedTypeId(2), + arguments: vec![value(1)], + span, + }, + I::RefCast { + destination: value(11), + value: value(2), + reference: RefType { + nullable: true, + heap: HeapType::Eq, + }, + span, + }, + I::Call { + contract: None, + destination: value(3), + function: symbol(11), + arguments: vec![value(0), value(11)], + span, + }, + I::Constant { + destination: value(4), + value: 0, + span, + }, + I::Constant { + destination: value(5), + value: 99, + span, + }, + I::StructNew { + destination: value(6), + type_index: DefinedTypeId(2), + arguments: vec![value(5)], + span, + }, + I::Copy { + destination: value(7), + value: value(3), + span, + }, + I::RefCast { + destination: value(12), + value: value(6), + reference: RefType { + nullable: true, + heap: HeapType::Eq, + }, + span, + }, + I::CallVoid { + function: symbol(12), + arguments: vec![value(3), value(4), value(12)], + span, + }, + I::Call { + contract: None, + destination: value(8), + function: symbol(13), + arguments: vec![value(7), value(4)], + span, + }, + I::RefCast { + destination: value(9), + value: value(8), + reference: RefType { + nullable: false, + heap: HeapType::Index(DefinedTypeId(2)), + }, + span, + }, + I::StructGet { + destination: value(10), + type_index: DefinedTypeId(2), + field: 0, + value: value(9), + span, + }, + ]; + mir::Module { + name: "RuntimeStorageConsumer".into(), + types, + strings: vec![], + imports: vec![ + import( + 11, + "array_fill", + vec![ValueType::I32, eq], + Some(reference(1)), + ), + import( + 12, + "array_write", + vec![reference(1), ValueType::I32, eq], + None, + ), + import( + 13, + "array_read", + vec![reference(1), ValueType::I32], + Some(eq), + ), + ], + functions: vec![Function { + id: FunctionId(0), + symbol: symbol(20), + name: "main".into(), + parameters: vec![], + values: shapes + .into_iter() + .enumerate() + .map(|(id, ty)| ValueDecl { + id: value(id as u32), + ty, + }) + .collect(), + entry: BlockId(0), + blocks: vec![BasicBlock { + id: BlockId(0), + parameters: vec![], + instructions, + terminator: Some(Terminator::Return { + value: value(10), + span, + }), + }], + result: value(10), + result_type: ValueType::I32, + span, + }], + entry: Some(symbol(20)), + layout: None, + span, + } +} + +#[test] +fn runtime_import_contracts_are_checked_by_binding_and_linking_owners() { + let module = fixture(); + mir::verify_module(&module).unwrap(); + let context = default_context().unwrap(); + let target = TargetCapabilities::default(); + let mut wasi = WasiRegistry::load().unwrap(); + let plan = plan_for_module(&context, &module, &mut wasi, target).unwrap(); + assert_eq!(plan.plan.artifacts().len(), 1); + assert_eq!( + plan.imports[&module.imports[0].symbol], + (psrs_runtime::STORAGE_MODULE.into(), "array_fill".into()) + ); + for change in 0..6 { + let mut wrong = module.clone(); + let import = &mut wrong.imports[0]; + match change { + 0 => import.runtime.as_mut().unwrap().module = "wrong-provider".into(), + 1 => import.runtime.as_mut().unwrap().function = "array_read".into(), + 2 => import.parameters[0] = ValueType::I64, + 3 => import.result = Some(ValueType::I32), + 4 => { + import.result = Some(ValueType::Ref(RefType { + nullable: true, + heap: HeapType::Index(DefinedTypeId(1)), + })) + } + _ => import.symbol = crate::abi::REALLOC_SYMBOL, + } + // Provider identity is opaque to MIR. Ordinary call typing remains + // checked there, while provider selection belongs to the link boundary. + if change == 0 || change == 1 { + mir::verify_module(&wrong).unwrap(); + } + assert!( + crate::bindings::verify_runtime_import(&wrong.imports[0], &wrong.types).is_err(), + "change {change}" + ); + assert!( + plan_for_module(&context, &wrong, &mut wasi, target).is_err(), + "change {change}" + ); + } + let mut wrong = module.clone(); + wrong.types[1].0[0].final_type = false; + mir::verify_module(&wrong).unwrap(); + assert!( + plan_for_module(&context, &wrong, &mut wasi, target).is_err(), + "linking must compare the actual GC definition with the runtime catalog" + ); +} + +#[test] +fn backend_runtime_storage_imports_compose_and_execute_an_alias_write() { + let mut module = fixture(); + let function = &mut module.functions[0]; + function.values.extend([ + ValueDecl { + id: ValueId(13), + ty: ValueType::I32, + }, + ValueDecl { + id: ValueId(14), + ty: ValueType::Boolean, + }, + ValueDecl { + id: ValueId(15), + ty: ValueType::I32, + }, + ]); + function.blocks[0].instructions.extend([ + I::Constant { + destination: ValueId(13), + value: 99, + span: module.span, + }, + I::Primitive { + destination: ValueId(14), + op: mir::NumericOp::I32Ne, + left: ValueId(10), + right: ValueId(13), + span: module.span, + }, + I::UnaryPrimitive { + destination: ValueId(15), + op: mir::UnaryOp::BoolToI32, + value: ValueId(14), + span: module.span, + }, + ]); + function.result = ValueId(15); + function.blocks[0].terminator = Some(Terminator::Return { + value: ValueId(15), + span: module.span, + }); + if std::process::Command::new("wasmtime") + .arg("--version") + .output() + .is_err() + { + assert!( + std::env::var_os("PSRS_REQUIRE_WASMTIME").is_none(), + "Wasmtime is required" + ); + return; + } + let target = TargetCapabilities::default(); + let context = default_context().unwrap(); + for expected in [99, 100] { + let instruction = module.functions[0].blocks[0] + .instructions + .iter_mut() + .find(|instruction| { + matches!( + instruction, + I::Constant { + destination: ValueId(13), + .. + } + ) + }) + .unwrap(); + let I::Constant { value, .. } = instruction else { + unreachable!() + }; + *value = expected; + mir::verify_module(&module).unwrap(); + let mut wasi = WasiRegistry::load().unwrap(); + let plan = plan_for_module(&context, &module, &mut wasi, target).unwrap(); + let wasm = crate::wasm::lower_module_with_plan(&module, &mut wasi, target, &plan).unwrap(); + let core = crate::wasm::encode_module(&wasm).unwrap(); + let component = compose( + &plan, + &core, + module.span, + module.entry.map(|entry| entry.module), + ) + .unwrap(); + crate::validator_for(target) + .validate_all(&component) + .unwrap(); + let path = std::env::temp_dir().join(format!( + "psrs-backend-storage-{}-{expected}.wasm", + std::process::id() + )); + std::fs::write(&path, component).unwrap(); + let output = std::process::Command::new("wasmtime") + .arg("run") + .arg(&path) + .output() + .unwrap(); + std::fs::remove_file(path).unwrap(); + assert_eq!( + output.status.code(), + Some(i32::from(expected != 99)), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + } +} diff --git a/crates/psrs-backend/src/linking/tests.rs b/crates/psrs-backend/src/linking/tests.rs new file mode 100644 index 00000000..ea523bc4 --- /dev/null +++ b/crates/psrs-backend/src/linking/tests.rs @@ -0,0 +1,131 @@ +use super::*; +use crate::types::{ + CompositeType, DefinedType, DefinedTypeId, FieldType, HeapType, RecGroup, RefType, StorageType, + ValueType, +}; + +fn module(import: mir::Import) -> mir::Module { + mir::Module { + name: "ConsumerAbi".into(), + entry: None, + types: vec![], + strings: vec![], + imports: vec![import], + functions: vec![], + layout: None, + span: TextRange::new(0, 1), + } +} + +fn plans(module: &mir::Module) -> bool { + plan_for_module( + &default_context().unwrap(), + module, + &mut WasiRegistry::load().unwrap(), + TargetCapabilities::default(), + ) + .is_ok() +} + +#[test] +fn actual_artifact_consumer_signatures_are_checked_before_emission() { + for provider in crate::target_intrinsics::artifacts() { + assert!(plans(&module(provider.import()))); + let good = provider.import(); + let mut invalid = Vec::new(); + if good.parameters.is_empty() { + // A numeric constant has no parameter to retarget. An added argument is + // the arity mismatch that a unary export expresses by dropping one. + let mut wrong = good.clone(); + wrong.parameters.push(ValueType::F64); + invalid.push(wrong); + } else { + let mut wrong = good.clone(); + wrong.parameters[0] = if wrong.parameters[0] == ValueType::I32 { + ValueType::F64 + } else { + ValueType::I32 + }; + invalid.push(wrong); + let mut wrong = good.clone(); + wrong.parameters.pop(); + invalid.push(wrong); + let mut wrong = good.clone(); + wrong.parameters[0] = ValueType::Ref(RefType { + nullable: true, + heap: HeapType::Any, + }); + invalid.push(wrong); + } + let mut wrong = good.clone(); + wrong.result = None; + invalid.push(wrong); + let mut wrong = good; + wrong.result = Some(ValueType::I64); + invalid.push(wrong); + for import in invalid { + let module = module(import); + // MIR's local call contract can be consistent while its provider ABI is wrong. + crate::mir::verify_module(&module).unwrap(); + assert!(!plans(&module)); + assert!( + crate::wasm::lower_module(&module, &mut WasiRegistry::load().unwrap()).is_err() + ); + } + } +} + +#[test] +fn generated_bindings_match_the_shared_body_signature_and_string_layout() { + let string = Some(DefinedTypeId(0)); + for symbol in [ + abi::REALLOC_SYMBOL, + abi::STRING_TO_BYTES_SYMBOL, + abi::BYTES_TO_STRING_SYMBOL, + abi::VALIDATE_STEP_SYMBOL, + ] { + let mut good = + module(crate::target_intrinsics::generated::signature(symbol, string).unwrap()); + good.types.push(RecGroup(vec![DefinedType { + final_type: true, + supertype: None, + composite: CompositeType::Array(FieldType { + storage: StorageType::I8, + mutable: true, + }), + }])); + // validate_step is synthesized together with codecs and requires their string type. + if symbol == abi::VALIDATE_STEP_SYMBOL { + good.imports.push( + crate::target_intrinsics::generated::signature(abi::STRING_TO_BYTES_SYMBOL, string) + .unwrap(), + ); + } + assert!(plans(&good), "{symbol:?}"); + let mut wrong = good.clone(); + wrong.imports[0].result = None; + assert!(!plans(&wrong)); + let mut wrong = good.clone(); + wrong.imports[0].parameters.push(ValueType::I32); + assert!(!plans(&wrong)); + if symbol == abi::STRING_TO_BYTES_SYMBOL || symbol == abi::BYTES_TO_STRING_SYMBOL { + let mut wrong = good.clone(); + wrong.types[0].0[0].composite = CompositeType::Array(FieldType { + storage: StorageType::I32, + mutable: true, + }); + assert!(!plans(&wrong)); + let mut wrong = good; + let ty = if symbol == abi::STRING_TO_BYTES_SYMBOL { + &mut wrong.imports[0].parameters[0] + } else { + wrong.imports[0].result.as_mut().unwrap() + }; + *ty = ValueType::Ref(RefType { + nullable: true, + heap: HeapType::Index(DefinedTypeId(0)), + }); + assert!(!plans(&wrong)); + } + } +} diff --git a/crates/psrs-backend/src/mir/binding_tests.rs b/crates/psrs-backend/src/mir/binding_tests.rs index 7e0ae893..99ac7061 100644 --- a/crates/psrs-backend/src/mir/binding_tests.rs +++ b/crates/psrs-backend/src/mir/binding_tests.rs @@ -14,6 +14,7 @@ fn input(call: bool) -> (cc::Module, ExternalBindings) { Assignment { destination: crate::types::ValueId(0), kind: AssignmentKind::DirectCall { + contract: None, function: external, arguments: Vec::new(), }, @@ -54,6 +55,7 @@ fn input(call: bool) -> (cc::Module, ExternalBindings) { span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external, source_module: ModuleId(0), @@ -144,6 +146,7 @@ fn p9_exposes_an_owned_handle_without_dropping_it() { Assignment { destination: crate::types::ValueId(0), kind: AssignmentKind::DirectCall { + contract: None, function: external, arguments: Vec::new(), }, @@ -163,6 +166,7 @@ fn p9_exposes_an_owned_handle_without_dropping_it() { span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external, source_module: ModuleId(0), diff --git a/crates/psrs-backend/src/mir/cfg.rs b/crates/psrs-backend/src/mir/cfg.rs index d4c44736..9f9a22c9 100644 --- a/crates/psrs-backend/src/mir/cfg.rs +++ b/crates/psrs-backend/src/mir/cfg.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] //! Shared CFG edge analysis for MIR. //! //! MIR terminators carry no structuring hint: `Branch` has no `merge_block`, @@ -8,10 +9,118 @@ use super::{BasicBlock, BlockId, Function, Terminator}; use std::collections::{HashMap, HashSet, VecDeque}; +/// Every normal path after `start` must cross a required instruction before a +/// normal exit, checked publication, or another execution of `start`. Trapping +/// paths are excluded; this does not establish caught-host-trap cleanup. +pub(crate) fn normal_paths_cross( + function: &Function, + start: (BlockId, usize), + required: impl Fn(&super::Instruction) -> bool, + publication: impl Fn(&super::Instruction) -> bool, +) -> bool { + let blocks = function + .blocks + .iter() + .map(|block| (block.id, block)) + .collect::>(); + if blocks + .get(&start.0) + .is_none_or(|block| start.1 >= block.instructions.len()) + { + return false; + } + let mut pending = vec![(start.0, start.1 + 1)]; + let mut seen = HashSet::new(); + while let Some((id, index)) = pending.pop() { + if !seen.insert((id, index)) { + continue; + } + let Some(block) = blocks.get(&id) else { + return false; + }; + let mut crossed = false; + for (offset, instruction) in block.instructions[index..].iter().enumerate() { + if required(instruction) + || matches!(instruction, super::Instruction::Unreachable { .. }) + { + crossed = true; + break; + } + if (id, index + offset) == start || publication(instruction) { + return false; + } + } + if crossed { + continue; + } + match &block.terminator { + Some(Terminator::Trap { .. }) => {} + Some( + Terminator::Jump { .. } | Terminator::Branch { .. } | Terminator::Switch { .. }, + ) => { + pending.extend( + successors(block.terminator.as_ref().unwrap()) + .into_iter() + .map(|id| (id, 0)), + ); + } + _ => return false, + } + } + true +} + +/// Can a later instruction be reached before the producer executes again? +/// `None` denotes malformed point/edge metadata, not an empty path set. +pub(crate) fn reachable_without( + function: &Function, + source: (BlockId, usize), + target: (BlockId, usize), + producer: (BlockId, usize), +) -> Option { + let blocks = function + .blocks + .iter() + .map(|block| (block.id, block)) + .collect::>(); + for point in [source, target, producer] { + if point.1 >= blocks.get(&point.0)?.instructions.len() { + return None; + } + } + let mut pending = vec![(source.0, source.1 + 1)]; + let mut seen = HashSet::new(); + while let Some((id, index)) = pending.pop() { + if !seen.insert((id, index)) { + continue; + } + let block = blocks.get(&id)?; + let mut stopped = false; + for (offset, instruction) in block.instructions[index..].iter().enumerate() { + let point = (id, index + offset); + if point == producer || matches!(instruction, super::Instruction::Unreachable { .. }) { + stopped = true; + break; + } + if point == target { + return Some(true); + } + } + if !stopped { + pending.extend( + successors(block.terminator.as_ref()?) + .into_iter() + .map(|id| (id, 0)), + ); + } + } + Some(false) +} + /// The successor blocks named by a terminator. pub(crate) fn successors(terminator: &Terminator) -> Vec { match terminator { - Terminator::Return { .. } => Vec::new(), + Terminator::Return { .. } | Terminator::Trap { .. } => Vec::new(), Terminator::Jump { target, .. } => vec![*target], Terminator::Branch { then_block, @@ -87,7 +196,8 @@ pub(crate) fn join_blocks(function: &Function) -> HashSet { .map(|(_, target)| *target) .chain(std::iter::once(*default)) .collect(), - Terminator::Return { .. } + Terminator::Trap { .. } + | Terminator::Return { .. } | Terminator::Jump { .. } | Terminator::ReturnCall { .. } | Terminator::ReturnCallRef { .. } => continue, @@ -120,6 +230,108 @@ fn block_distances(by_id: &HashMap, entry: BlockId) -> Has distances } +/// Block dominators shared by SSA and lifecycle verification. +/// +/// Dominance is only defined along paths from the entry. Blocks with no such +/// path are excluded from the result so that an unreachable predecessor cannot +/// shrink a reachable block's dominator set to nothing. +pub(crate) fn dominators(function: &Function) -> HashMap> { + dominators_from(function, function.entry, None) +} + +/// Reachability within a semantic region. `stop` belongs to the next region. +pub(crate) fn reachable_from( + function: &Function, + entry: BlockId, + stop: Option, +) -> HashSet { + let blocks = function + .blocks + .iter() + .map(|block| (block.id, block)) + .collect::>(); + let mut reachable = HashSet::new(); + let mut pending = vec![entry]; + while let Some(id) = pending.pop() { + if Some(id) == stop || !reachable.insert(id) { + continue; + } + if let Some(terminator) = blocks.get(&id).and_then(|block| block.terminator.as_ref()) { + pending.extend(successors(terminator)); + } + } + reachable +} + +/// Dominance in the same CFG region used to resolve returned join values. +pub(crate) fn dominators_from( + function: &Function, + entry: BlockId, + stop: Option, +) -> HashMap> { + let blocks = function + .blocks + .iter() + .map(|block| (block.id, block)) + .collect::>(); + let reachable = reachable_from(function, entry, stop); + let all = reachable.iter().copied().collect::>(); + let mut predecessors = reachable + .iter() + .copied() + .map(|id| (id, HashSet::new())) + .collect::>>(); + for id in &reachable { + let Some(terminator) = blocks.get(id).and_then(|block| block.terminator.as_ref()) else { + continue; + }; + for successor in successors(terminator) { + if let Some(preds) = predecessors.get_mut(&successor) { + preds.insert(*id); + } + } + } + + let mut dominators = reachable + .iter() + .copied() + .map(|id| { + let initial = if id == entry { + HashSet::from([entry]) + } else { + all.clone() + }; + (id, initial) + }) + .collect::>(); + loop { + let mut changed = false; + for id in reachable.iter().copied().filter(|id| *id != entry) { + let mut next = all.clone(); + if let Some(preds) = predecessors.get(&id) { + if preds.is_empty() { + next.clear(); + } else { + for pred in preds { + if let Some(pred_dominators) = dominators.get(pred) { + next.retain(|candidate| pred_dominators.contains(candidate)); + } + } + } + } + next.insert(id); + if dominators.get(&id) != Some(&next) { + dominators.insert(id, next); + changed = true; + } + } + if !changed { + break; + } + } + dominators +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/psrs-backend/src/mir/control/boundary.rs b/crates/psrs-backend/src/mir/control/boundary.rs new file mode 100644 index 00000000..33bb6740 --- /dev/null +++ b/crates/psrs-backend/src/mir/control/boundary.rs @@ -0,0 +1,117 @@ +//! The MIR boundary checker: the replacement for the retired recognizers. +//! +//! It reconstructs the set of machine dispatch arms an emitted MIR function +//! performs and compares it with the checked construction plan's dispatch arms. +//! Construction owns the machine semantics ([DEC-21]); this re-checks that the +//! lowered function still dispatches exactly the planned cases. +//! +//! Terminal outcomes are deliberately not compared here. Self tail calls are +//! loopified into loop back-edges, so the plan's `Advance`/`Tail` terminals do +//! not survive as MIR `ReturnCall`s; the CC boundary checker owns terminal +//! comparison before loopification. Generic MIR verification still checks +//! types, layout, casts and CFG. + +use crate::BackendError; +use crate::cc::{Family, Shape, plan_shape_for}; +use crate::mir::value_identity::{definitions, reference_origin}; +use crate::mir::{Function, Instruction as I, Module, Terminator as T}; +use crate::types::DefinedTypeId; +use psrs_hir::SymbolId; +use std::collections::{BTreeSet, HashMap, HashSet}; + +pub(super) fn verify(module: &Module, errors: &mut Vec) { + let Some(layout) = module.layout.as_ref() else { + return; + }; + let table = layout.representation_table(); + let Some(schema) = table.control_schema() else { + return; + }; + let mut families = HashMap::new(); + for (repr, family) in [ + (schema.computation, Family::Computation), + (schema.sequence, Family::Sequence), + (schema.context, Family::Context), + (schema.extent, Family::Extent), + (schema.owner, Family::Owner), + ] { + if let Ok(index) = layout.repr_index(repr) { + families.insert(index, family); + } + } + for function in &module.functions { + let Some(expected) = plan_shape_for(function.symbol, table) else { + continue; + }; + let Some(expected_targets) = crate::cc::plan_external_targets(function.symbol, table) + else { + continue; + }; + let dispatched = arms(function, &families) == flatten(&expected); + // Non-self tail calls survive loopification as `ReturnCall`s; self tail + // calls become loop back-edges and are excluded from the plan targets. + let terminated = external_targets(function) == expected_targets; + if !dispatched || !terminated { + errors.push(BackendError::invalid_ir( + "P9 MIR verification", + function.span, + "emitted machine dispatch or outcome disagrees with the checked plan", + )); + } + } +} + +fn flatten(shape: &Shape) -> BTreeSet<(Family, u32)> { + let mut set = BTreeSet::new(); + collect(shape, &mut set); + set +} + +fn collect(shape: &Shape, set: &mut BTreeSet<(Family, u32)>) { + if let Shape::Switch(family, arms) = shape { + for (tag, arm) in arms { + set.insert((*family, *tag)); + collect(arm, set); + } + } +} + +/// The non-self `ReturnCall` targets in this function. +fn external_targets(function: &Function) -> HashSet { + function + .blocks + .iter() + .filter_map(|block| match &block.terminator { + Some(T::ReturnCall { + function: target, .. + }) if *target != function.symbol => Some(*target), + _ => None, + }) + .collect() +} + +/// The machine dispatch arms reachable in this function, across all blocks. +fn arms(function: &Function, families: &HashMap) -> BTreeSet<(Family, u32)> { + let definitions = definitions(function); + let mut set = BTreeSet::new(); + for block in &function.blocks { + let Some(T::Switch { value, cases, .. }) = &block.terminator else { + continue; + }; + let selector = reference_origin(*value, &definitions); + let Some(I::StructGet { + type_index, + field: 0, + .. + }) = definitions.get(&selector).copied() + else { + continue; + }; + if let Some(family) = families.get(type_index) { + for (tag, _) in cases { + set.insert((*family, *tag as u32)); + } + } + } + set +} diff --git a/crates/psrs-backend/src/mir/control/contracts.rs b/crates/psrs-backend/src/mir/control/contracts.rs new file mode 100644 index 00000000..6c30b036 --- /dev/null +++ b/crates/psrs-backend/src/mir/control/contracts.rs @@ -0,0 +1,141 @@ +#![allow(dead_code)] +use crate::BackendError; +use crate::mir::{Instruction, Module}; + +impl Module { + pub fn control_helpers(&self) -> &[crate::cc::ControlHelperContract] { + self.layout.as_ref().map_or(&[], |layout| { + layout.representation_table().control_helpers() + }) + } + + /// Source contracts remain immutable and inspectable after physical layout. + pub fn control_contracts(&self) -> &[crate::cc::ControlContract] { + self.layout.as_ref().map_or(&[], |layout| { + layout.representation_table().control_contracts() + }) + } +} + +pub(super) fn verify(module: &Module, errors: &mut Vec) { + for function in &module.functions { + if let Err(mut failures) = verify_helper_calls( + function, + module + .layout + .as_ref() + .map(|layout| layout.representation_table()), + ) { + errors.append(&mut failures); + } + for instruction in function.blocks.iter().flat_map(|block| &block.instructions) { + let Instruction::ControlEvidence { + arguments, + contract, + span, + .. + } = instruction + else { + continue; + }; + let mut message = match module.control_contracts().get(contract.0 as usize) { + None => Some("MIR control evidence has no checked source contract"), + Some(proof) if arguments.len() != proof.arguments().len() => Some( + "MIR control evidence argument count disagrees with its checked source contract", + ), + _ => None, + }; + if message.is_none() { + let proof = &module.control_contracts()[contract.0 as usize]; + for (argument, expected) in arguments.iter().zip(proof.argument_identities()) { + let mut value = *argument; + let definitions = function + .blocks + .iter() + .flat_map(|block| &block.instructions) + .collect::>(); + for _ in 0..definitions.len() { + match definitions + .iter() + .find(|instruction| instruction.destination() == Some(value)) + { + Some( + Instruction::Copy { value: source, .. } + | Instruction::RefCast { value: source, .. }, + ) => value = *source, + Some(Instruction::ControlEvidence { contract, .. }) => { + if module + .control_contracts() + .get(contract.0 as usize) + .is_none_or(|producer| producer.result_identity() != *expected) + { + message = Some( + "MIR control operand source type disagrees with its checked domain", + ); + } + break; + } + _ => break, + } + } + } + } + if let Some(message) = message { + errors.push( + BackendError::invalid_ir("P9 MIR verification", *span, message) + .with_module(function.symbol.module), + ); + } + } + } +} + +pub(in crate::mir) fn verify_helper_calls( + function: &crate::mir::Function, + table: Option<&crate::cc::RepresentationTable>, +) -> Result<(), Vec> { + for block in &function.blocks { + let calls = block + .instructions + .iter() + .filter_map(|instruction| match instruction { + Instruction::Call { + function, + contract, + span, + .. + } => Some((*function, *contract, *span)), + _ => None, + }) + .chain( + block + .terminator + .iter() + .filter_map(|terminator| match terminator { + crate::mir::Terminator::ReturnCall { + function, + contract, + span, + .. + } => Some((*function, *contract, *span)), + _ => None, + }), + ); + for (callee, contract, span) in calls { + let check = match table { + Some(table) => table.verify_control_call(callee, contract), + None if contract.is_some() => { + Err("control call has no predeclared helper contract") + } + None => Ok(()), + }; + check.map_err(|message| { + vec![ + BackendError::invalid_ir("P9 MIR control verification", span, message) + .with_module(function.symbol.module), + ] + })?; + } + } + Ok(()) +} diff --git a/crates/psrs-backend/src/mir/control/mod.rs b/crates/psrs-backend/src/mir/control/mod.rs new file mode 100644 index 00000000..a5a165fa --- /dev/null +++ b/crates/psrs-backend/src/mir/control/mod.rs @@ -0,0 +1,17 @@ +//! Control helper call contracts after physical layout. +//! +//! The machine recognizers are removed: construction now owns the machine +//! semantics (see DEC-21 and `cc/lower/control/plan`). Only the helper call +//! binding contract remains, so a lowered call still names a checked helper. + +use crate::BackendError; +use crate::mir::Module; + +mod boundary; +mod contracts; + +pub(super) use contracts::verify_helper_calls; + +pub(super) fn verify(module: &Module, errors: &mut Vec) { + boundary::verify(module, errors); +} diff --git a/crates/psrs-backend/src/mir/opt/effects.rs b/crates/psrs-backend/src/mir/effects.rs similarity index 55% rename from crates/psrs-backend/src/mir/opt/effects.rs rename to crates/psrs-backend/src/mir/effects.rs index bb1065c3..f0ad48c3 100644 --- a/crates/psrs-backend/src/mir/opt/effects.rs +++ b/crates/psrs-backend/src/mir/effects.rs @@ -1,4 +1,5 @@ -//! Conservative instruction effect summaries used by dead-code elimination. +#![allow(dead_code)] +//! Conservative instruction effect summaries shared by optimization and semantic verification. use crate::mir::Instruction; @@ -44,6 +45,7 @@ pub(super) fn classify(instruction: &Instruction) -> InstructionEffects { | I::ArrayNewData { .. } | I::ArrayNewDefault { .. } | I::ArrayNewSized { .. } + | I::ArrayNewFilled { .. } | I::ArrayGet { .. } | I::ArrayGetU { .. } | I::ArrayClone { .. } @@ -66,6 +68,7 @@ pub(super) fn classify(instruction: &Instruction) -> InstructionEffects { | I::ArrayNewData { .. } | I::ArrayNewDefault { .. } | I::ArrayNewSized { .. } + | I::ArrayNewFilled { .. } | I::ArrayClone { .. } ), ..InstructionEffects::default() @@ -109,6 +112,7 @@ pub(super) fn classify(instruction: &Instruction) -> InstructionEffects { ..InstructionEffects::default() }, I::Copy { .. } + | I::ControlEvidence { .. } | I::Constant { .. } | I::NumberConstant { .. } | I::Primitive { .. } @@ -126,3 +130,99 @@ pub(super) fn classify(instruction: &Instruction) -> InstructionEffects { pub(super) fn is_pure_and_total(instruction: &Instruction) -> bool { classify(instruction).is_pure_and_total() } + +/// Share the conservative call boundary with semantic IR verification. +pub(in crate::mir) fn may_invoke(instruction: &Instruction) -> bool { + classify(instruction).may_call +} + +/// Total local bodies: exclude fallible instructions, CFG cycles and recursive calls. +pub(in crate::mir) fn total_functions( + module: &crate::mir::Module, +) -> std::collections::HashSet { + use crate::mir::Terminator; + use std::collections::{HashMap, HashSet}; + let mut dependencies = HashMap::>::new(); + for function in &module.functions { + let mut calls = HashSet::new(); + let mut safe = acyclic(function); + for block in &function.blocks { + for instruction in &block.instructions { + match instruction { + Instruction::Call { function, .. } | Instruction::CallVoid { function, .. } => { + calls.insert(*function); + } + _ if !is_pure_and_total(instruction) => { + safe = false; + break; + } + _ => {} + } + } + match &block.terminator { + Some(Terminator::ReturnCall { function, .. }) => { + calls.insert(*function); + } + Some(Terminator::ReturnCallRef { .. } | Terminator::Trap { .. }) => safe = false, + _ => {} + } + } + if safe { + dependencies.insert(function.symbol, calls); + } + } + let mut safe = HashSet::new(); + loop { + let previous = safe.len(); + for (symbol, calls) in &dependencies { + if calls.iter().all(|callee| safe.contains(callee)) { + safe.insert(*symbol); + } + } + if safe.len() == previous { + return safe; + } + } +} + +fn acyclic(function: &crate::mir::Function) -> bool { + fn visit( + id: crate::mir::BlockId, + blocks: &std::collections::HashMap, + active: &mut std::collections::HashSet, + done: &mut std::collections::HashSet, + ) -> bool { + if done.contains(&id) { + return true; + } + if !active.insert(id) { + return false; + } + let Some(block) = blocks.get(&id) else { + return false; + }; + let Some(terminator) = &block.terminator else { + return false; + }; + if !crate::mir::cfg::successors(terminator) + .into_iter() + .all(|next| visit(next, blocks, active, done)) + { + return false; + } + active.remove(&id); + done.insert(id); + true + } + let blocks = function + .blocks + .iter() + .map(|block| (block.id, block)) + .collect(); + visit( + function.entry, + &blocks, + &mut Default::default(), + &mut Default::default(), + ) +} diff --git a/crates/psrs-backend/src/mir/gc_tests/binary_matrix/mod.rs b/crates/psrs-backend/src/mir/gc_tests/binary_matrix/mod.rs index 2525ed3c..9faea64e 100644 --- a/crates/psrs-backend/src/mir/gc_tests/binary_matrix/mod.rs +++ b/crates/psrs-backend/src/mir/gc_tests/binary_matrix/mod.rs @@ -90,20 +90,6 @@ fn verifies_and_executes_every_cc_binary_scalar_variant_on_both_targets() { ValueShape::Integer, Expected::Integer(1), ), - ( - BinaryOp::IntDiv, - 0, - 1, - ValueShape::Integer, - Expected::Integer(3), - ), - ( - BinaryOp::IntMod, - 0, - 1, - ValueShape::Integer, - Expected::Integer(1), - ), ( BinaryOp::IntAnd, 0, @@ -482,6 +468,6 @@ fn verifies_and_executes_every_cc_binary_scalar_variant_on_both_targets() { crate::TargetCapabilities::default(), ) .expect("the complete binary scalar module should lower for GC"); - assert_eq!(gc_mir.functions.len(), 3); + assert_eq!(gc_mir.functions.len(), 1); run_gc(&gc_mir, 0); } diff --git a/crates/psrs-backend/src/mir/gc_tests/div_mod/mod.rs b/crates/psrs-backend/src/mir/gc_tests/div_mod/mod.rs deleted file mode 100644 index f20f256c..00000000 --- a/crates/psrs-backend/src/mir/gc_tests/div_mod/mod.rs +++ /dev/null @@ -1,277 +0,0 @@ -use super::*; - -mod switch; - -#[test] -fn lowers_euclidean_integer_division_and_modulo_on_both_targets() { - use crate::cc::BinaryOp; - - let symbol = SymbolId::new(ModuleId(0), 0); - let values = (0..42) - .map(|id| crate::cc::ValueDecl { - id: ValueId(id), - ty: if (24..=38).contains(&id) { - ValueShape::Boolean - } else { - ValueShape::Integer - }, - }) - .collect(); - let constant = |destination, value| Assignment { - destination: ValueId(destination), - kind: AssignmentKind::Constant(value), - span: span(), - }; - let binary = |destination, op, left, right| Assignment { - destination: ValueId(destination), - kind: AssignmentKind::Primitive { - op, - left: ValueId(left), - right: ValueId(right), - }, - span: span(), - }; - let module = CcModule { - name: "EuclideanScalars".into(), - externals: Vec::new(), - representations: RepresentationTable::default(), - functions: vec![CcFunction { - symbol, - name: "main".into(), - parameters: Vec::new(), - values, - assignments: vec![ - constant(0, -5), - constant(1, 3), - binary(2, BinaryOp::IntDiv, 0, 1), - binary(3, BinaryOp::IntMod, 0, 1), - constant(4, 5), - constant(5, -3), - binary(6, BinaryOp::IntDiv, 4, 5), - binary(7, BinaryOp::IntMod, 4, 5), - constant(8, -5), - constant(9, -3), - binary(10, BinaryOp::IntDiv, 8, 9), - binary(11, BinaryOp::IntMod, 8, 9), - constant(12, 6), - constant(13, -3), - binary(14, BinaryOp::IntDiv, 12, 13), - binary(15, BinaryOp::IntMod, 12, 13), - constant(16, -2), - constant(17, 1), - constant(18, -2), - constant(19, -1), - constant(20, 1), - constant(21, -2), - constant(22, -2), - constant(23, 0), - binary(24, BinaryOp::IntEq, 2, 16), - binary(25, BinaryOp::IntEq, 3, 17), - binary(26, BinaryOp::IntEq, 6, 18), - binary(27, BinaryOp::IntEq, 7, 19), - binary(28, BinaryOp::IntEq, 10, 20), - binary(29, BinaryOp::IntEq, 11, 21), - binary(30, BinaryOp::IntEq, 14, 22), - binary(31, BinaryOp::IntEq, 15, 23), - binary(32, BinaryOp::BooleanAnd, 24, 25), - binary(33, BinaryOp::BooleanAnd, 32, 26), - binary(34, BinaryOp::BooleanAnd, 33, 27), - binary(35, BinaryOp::BooleanAnd, 34, 28), - binary(36, BinaryOp::BooleanAnd, 35, 29), - binary(37, BinaryOp::BooleanAnd, 36, 30), - binary(38, BinaryOp::BooleanAnd, 37, 31), - Assignment { - destination: ValueId(39), - kind: AssignmentKind::If { - condition: ValueId(38), - then_assignments: vec![constant(40, 3)], - then_value: ValueId(40), - else_assignments: vec![constant(41, -1)], - else_value: ValueId(41), - }, - span: span(), - }, - ], - result: ValueId(39), - result_type: ValueShape::Integer, - span: span(), - }], - entry: Some(symbol), - span: span(), - }; - - let (gc_mir, _) = crate::mir::lower_module_with_capabilities( - module.clone(), - crate::TargetCapabilities::default(), - ) - .expect("Euclidean scalar CC should lower for the GC target"); - assert_eq!(gc_mir.functions.len(), 3); - assert_eq!(gc_mir.functions[1].name, "__psrs_euclidean_int_div"); - assert_eq!(gc_mir.functions[2].name, "__psrs_euclidean_int_mod"); - assert!( - gc_mir.functions[0] - .blocks - .iter() - .flat_map(|block| &block.instructions) - .filter(|instruction| matches!(instruction, Instruction::Call { .. })) - .count() - >= 8 - ); - run_gc(&gc_mir, 3); -} - -#[test] -fn does_not_emit_helpers_without_division_or_modulo() { - use crate::cc::BinaryOp; - - let symbol = SymbolId::new(ModuleId(0), 0); - let values = (0..=3) - .map(|id| crate::cc::ValueDecl { - id: ValueId(id), - ty: ValueShape::Integer, - }) - .collect(); - let module = CcModule { - name: "NoEuclideanScalars".into(), - externals: Vec::new(), - representations: RepresentationTable::default(), - functions: vec![CcFunction { - symbol, - name: "main".into(), - parameters: Vec::new(), - values, - assignments: vec![ - Assignment { - destination: ValueId(0), - kind: AssignmentKind::Constant(7), - span: span(), - }, - Assignment { - destination: ValueId(1), - kind: AssignmentKind::Constant(2), - span: span(), - }, - Assignment { - destination: ValueId(2), - kind: AssignmentKind::Primitive { - op: BinaryOp::IntAdd, - left: ValueId(0), - right: ValueId(1), - }, - span: span(), - }, - Assignment { - destination: ValueId(3), - kind: AssignmentKind::Primitive { - op: BinaryOp::IntQuot, - left: ValueId(0), - right: ValueId(1), - }, - span: span(), - }, - ], - result: ValueId(2), - result_type: ValueShape::Integer, - span: span(), - }], - entry: Some(symbol), - span: span(), - }; - - let (gc_mir, _) = - crate::mir::lower_module_with_capabilities(module, crate::TargetCapabilities::default()) - .expect("truncated quotient must not require a floor helper"); - assert_eq!(gc_mir.functions.len(), 1); - run_gc(&gc_mir, 9); -} - -#[test] -fn skips_helper_symbols_used_by_module_functions() { - use crate::cc::BinaryOp; - - let colliding = SymbolId::new(ModuleId(0), u32::MAX); - let values = (0..=4) - .map(|id| crate::cc::ValueDecl { - id: ValueId(id), - ty: ValueShape::Integer, - }) - .collect(); - let module = CcModule { - name: "CollidingScalarSymbols".into(), - externals: Vec::new(), - representations: RepresentationTable::default(), - functions: vec![CcFunction { - symbol: colliding, - name: "main".into(), - parameters: Vec::new(), - values, - assignments: vec![ - Assignment { - destination: ValueId(0), - kind: AssignmentKind::Constant(7), - span: span(), - }, - Assignment { - destination: ValueId(1), - kind: AssignmentKind::Constant(2), - span: span(), - }, - Assignment { - destination: ValueId(2), - kind: AssignmentKind::Primitive { - op: BinaryOp::IntDiv, - left: ValueId(0), - right: ValueId(1), - }, - span: span(), - }, - Assignment { - destination: ValueId(3), - kind: AssignmentKind::Primitive { - op: BinaryOp::IntMod, - left: ValueId(0), - right: ValueId(1), - }, - span: span(), - }, - Assignment { - destination: ValueId(4), - kind: AssignmentKind::Primitive { - op: BinaryOp::IntAdd, - left: ValueId(2), - right: ValueId(3), - }, - span: span(), - }, - ], - result: ValueId(4), - result_type: ValueShape::Integer, - span: span(), - }], - entry: Some(colliding), - span: span(), - }; - - let (gc_mir, _) = - crate::mir::lower_module_with_capabilities(module, crate::TargetCapabilities::default()) - .expect("the helper allocator must skip the colliding module symbol"); - assert_eq!(gc_mir.functions.len(), 3); - let helpers = gc_mir - .functions - .iter() - .filter(|function| function.name.starts_with("__psrs_euclidean")) - .collect::>(); - assert_eq!(helpers.len(), 2); - let mut symbols = std::collections::HashSet::new(); - for helper in helpers { - assert_ne!( - helper.symbol, colliding, - "a generated helper reused a module symbol" - ); - assert!( - symbols.insert(helper.symbol), - "generated helpers share a symbol" - ); - } - run_gc(&gc_mir, 4); -} diff --git a/crates/psrs-backend/src/mir/gc_tests/div_mod/switch.rs b/crates/psrs-backend/src/mir/gc_tests/div_mod/switch.rs deleted file mode 100644 index 3f0d5514..00000000 --- a/crates/psrs-backend/src/mir/gc_tests/div_mod/switch.rs +++ /dev/null @@ -1,260 +0,0 @@ -use super::*; - -#[test] -fn detects_division_and_modulo_nested_in_tag_switch_cases() { - use crate::cc::{BinaryOp, TagCase}; - - let symbol = SymbolId::new(ModuleId(0), 0); - let values = (0..=7) - .map(|id| crate::cc::ValueDecl { - id: ValueId(id), - ty: ValueShape::Integer, - }) - .collect(); - let constant = |destination, value| Assignment { - destination: ValueId(destination), - kind: AssignmentKind::Constant(value), - span: span(), - }; - let binary = |destination, op, left, right| Assignment { - destination: ValueId(destination), - kind: AssignmentKind::Primitive { - op, - left: ValueId(left), - right: ValueId(right), - }, - span: span(), - }; - let module = CcModule { - name: "NestedEuclideanScalars".into(), - externals: Vec::new(), - representations: RepresentationTable::default(), - functions: vec![CcFunction { - symbol, - name: "main".into(), - parameters: Vec::new(), - values, - assignments: vec![ - constant(0, 0), - Assignment { - destination: ValueId(7), - kind: AssignmentKind::TagSwitch { - value: ValueId(0), - cases: vec![TagCase { - tag: 0, - assignments: vec![ - constant(1, 7), - constant(2, 2), - binary(3, BinaryOp::IntDiv, 1, 2), - binary(4, BinaryOp::IntMod, 1, 2), - binary(5, BinaryOp::IntAdd, 3, 4), - ], - value: ValueId(5), - }], - default_assignments: vec![constant(6, 0)], - default_value: ValueId(6), - }, - span: span(), - }, - ], - result: ValueId(7), - result_type: ValueShape::Integer, - span: span(), - }], - entry: Some(symbol), - span: span(), - }; - - let (gc_mir, _) = - crate::mir::lower_module_with_capabilities(module, crate::TargetCapabilities::default()) - .expect("division nested in a tag switch must still generate its helper"); - assert_eq!(gc_mir.functions.len(), 3); - assert!( - gc_mir - .functions - .iter() - .any(|function| function.name == "__psrs_euclidean_int_div"), - "the nested divide must allocate the div helper" - ); - assert!( - gc_mir - .functions - .iter() - .any(|function| function.name == "__psrs_euclidean_int_mod"), - "the nested modulo must allocate the mod helper" - ); - run_gc(&gc_mir, 4); -} - -#[test] -fn detects_modulo_nested_in_a_tag_switch_default_arm() { - use crate::cc::{BinaryOp, TagCase}; - - let symbol = SymbolId::new(ModuleId(0), 0); - let values = (0..=8) - .map(|id| crate::cc::ValueDecl { - id: ValueId(id), - ty: ValueShape::Integer, - }) - .collect(); - let constant = |destination, value| Assignment { - destination: ValueId(destination), - kind: AssignmentKind::Constant(value), - span: span(), - }; - let binary = |destination, op, left, right| Assignment { - destination: ValueId(destination), - kind: AssignmentKind::Primitive { - op, - left: ValueId(left), - right: ValueId(right), - }, - span: span(), - }; - let module = CcModule { - name: "DefaultEuclideanScalars".into(), - externals: Vec::new(), - representations: RepresentationTable::default(), - functions: vec![CcFunction { - symbol, - name: "main".into(), - parameters: Vec::new(), - values, - assignments: vec![ - constant(0, 1), - Assignment { - destination: ValueId(8), - kind: AssignmentKind::TagSwitch { - value: ValueId(0), - cases: vec![TagCase { - tag: 0, - assignments: vec![ - constant(1, 7), - constant(2, 2), - binary(3, BinaryOp::IntDiv, 1, 2), - ], - value: ValueId(3), - }], - default_assignments: vec![ - constant(4, 7), - constant(5, 2), - binary(6, BinaryOp::IntMod, 4, 5), - ], - default_value: ValueId(6), - }, - span: span(), - }, - ], - result: ValueId(8), - result_type: ValueShape::Integer, - span: span(), - }], - entry: Some(symbol), - span: span(), - }; - - let (gc_mir, _) = - crate::mir::lower_module_with_capabilities(module, crate::TargetCapabilities::default()) - .expect("modulo in a tag switch default arm must still generate its helper"); - assert_eq!(gc_mir.functions.len(), 3); - assert!( - gc_mir - .functions - .iter() - .any(|function| function.name == "__psrs_euclidean_int_div") - ); - assert!( - gc_mir - .functions - .iter() - .any(|function| function.name == "__psrs_euclidean_int_mod") - ); - run_gc(&gc_mir, 1); -} - -#[test] -fn generates_helpers_for_division_and_modulo_inside_tag_switch_arms() { - use crate::cc::{BinaryOp, TagCase}; - - let symbol = SymbolId::new(ModuleId(0), 0); - let values = (0..=8) - .map(|id| crate::cc::ValueDecl { - id: ValueId(id), - ty: ValueShape::Integer, - }) - .collect(); - let constant = |destination, value| Assignment { - destination: ValueId(destination), - kind: AssignmentKind::Constant(value), - span: span(), - }; - let binary = |destination, op, left, right| Assignment { - destination: ValueId(destination), - kind: AssignmentKind::Primitive { - op, - left: ValueId(left), - right: ValueId(right), - }, - span: span(), - }; - let module = CcModule { - name: "TagSwitchScalars".into(), - externals: Vec::new(), - representations: RepresentationTable::default(), - functions: vec![CcFunction { - symbol, - name: "main".into(), - parameters: Vec::new(), - values, - assignments: vec![ - constant(0, 0), - Assignment { - destination: ValueId(5), - kind: AssignmentKind::TagSwitch { - value: ValueId(0), - cases: vec![TagCase { - tag: 0, - assignments: vec![ - constant(2, 7), - constant(3, 3), - binary(4, BinaryOp::IntDiv, 2, 3), - ], - value: ValueId(4), - }], - default_assignments: vec![ - constant(6, 7), - constant(7, 3), - binary(8, BinaryOp::IntMod, 6, 7), - ], - default_value: ValueId(8), - }, - span: span(), - }, - ], - result: ValueId(5), - result_type: ValueShape::Integer, - span: span(), - }], - entry: Some(symbol), - span: span(), - }; - - let (gc_mir, _) = - crate::mir::lower_module_with_capabilities(module, crate::TargetCapabilities::default()) - .expect("division and modulo inside a tag switch should lower"); - assert!( - gc_mir - .functions - .iter() - .any(|function| function.name == "__psrs_euclidean_int_div"), - "the IntDiv helper must be generated for a case arm" - ); - assert!( - gc_mir - .functions - .iter() - .any(|function| function.name == "__psrs_euclidean_int_mod"), - "the IntMod helper must be generated for the default arm" - ); - run_gc(&gc_mir, 2); -} diff --git a/crates/psrs-backend/src/mir/gc_tests/erased.rs b/crates/psrs-backend/src/mir/gc_tests/erased.rs index 27bd4c0d..d9870c43 100644 --- a/crates/psrs-backend/src/mir/gc_tests/erased.rs +++ b/crates/psrs-backend/src/mir/gc_tests/erased.rs @@ -87,6 +87,7 @@ fn executes_erased_identity_for_scalars_and_concrete_references_on_both_targets( let call_identity = |destination, value| Assignment { destination: ValueId(destination), kind: AssignmentKind::DirectCall { + contract: None, function: identity_symbol, arguments: vec![ValueId(value)], }, diff --git a/crates/psrs-backend/src/mir/gc_tests/mod.rs b/crates/psrs-backend/src/mir/gc_tests/mod.rs index 4754f8f7..e325fcb5 100644 --- a/crates/psrs-backend/src/mir/gc_tests/mod.rs +++ b/crates/psrs-backend/src/mir/gc_tests/mod.rs @@ -8,8 +8,8 @@ use std::sync::atomic::{AtomicU32, Ordering}; mod array; mod binary_matrix; -mod div_mod; mod erased; +mod number_trunc; mod rank_n; mod scalar; mod unary; @@ -39,7 +39,10 @@ fn run_gc(mir: &crate::mir::Module, expected_code: i32) { fn run_gc_output(mir: &crate::mir::Module) -> Option { let target = crate::TargetCapabilities::default(); let mut wasi = wasi(); - let wasm = crate::wasm::lower_module_with_capabilities(mir, &mut wasi, target) + let context = crate::linking::default_context().expect("default target context"); + let link = crate::linking::plan_for_module(&context, mir, &mut wasi, target) + .expect("checked GC target plan"); + let wasm = crate::wasm::lower_module_with_plan(mir, &mut wasi, target, &link) .expect("the GC MIR should lower to Wasm"); let core = crate::wasm::encode_module(&wasm).expect("encoding GC Wasm"); crate::validator_for(target) @@ -63,9 +66,9 @@ fn run_gc_output(mir: &crate::mir::Module) -> Option { eprintln!("skipping: wasmtime is unusable"); return None; } - let (resolve, world) = crate::component::command_world().expect("WASI WIT should load"); - let component = crate::component::componentize(&core, &resolve, world) - .expect("componentizing the GC module"); + let component = + crate::linking::compose(&link, &core, mir.span, mir.entry.map(|entry| entry.module)) + .expect("componentizing the GC module"); let path = std::env::temp_dir().join(format!( "psrs-gc-{}-{}.wasm", std::process::id(), diff --git a/crates/psrs-backend/src/mir/gc_tests/number_trunc.rs b/crates/psrs-backend/src/mir/gc_tests/number_trunc.rs new file mode 100644 index 00000000..c0a2e918 --- /dev/null +++ b/crates/psrs-backend/src/mir/gc_tests/number_trunc.rs @@ -0,0 +1,144 @@ +use super::*; + +#[test] +fn optimized_and_unoptimized_number_trunc_preserve_zero_sign_and_range() { + check_number_unary( + crate::cc::UnaryOp::NumberTrunc, + -0.9, + 4294967296.5, + 4294967296.0, + true, + ); +} + +#[test] +fn optimized_and_unoptimized_number_floor_and_ceil_preserve_sign_and_range() { + check_number_unary( + crate::cc::UnaryOp::NumberFloor, + -0.0, + 4294967296.5, + 4294967296.0, + true, + ); + check_number_unary( + crate::cc::UnaryOp::NumberCeil, + -0.9, + 4294967296.5, + 4294967297.0, + true, + ); +} + +#[test] +fn optimized_and_unoptimized_number_sqrt_preserves_negative_zero_and_exact_squares() { + check_number_unary( + crate::cc::UnaryOp::NumberSqrt, + -0.0, + 4294967296.0, + 65536.0, + true, + ); +} + +#[test] +fn optimized_and_unoptimized_number_abs_clear_zero_sign_and_preserve_magnitude() { + check_number_unary( + crate::cc::UnaryOp::NumberAbs, + -0.0, + -4294967296.5, + 4294967296.5, + false, + ); +} + +fn check_number_unary( + operation: crate::cc::UnaryOp, + input: f64, + large: f64, + expected: f64, + negative: bool, +) { + use crate::cc::{BinaryOp, UnaryOp, ValueDecl}; + use ValueShape::{Boolean as B, Integer as I, Number as N}; + let symbol = SymbolId::new(ModuleId(0), 0); + let unary = |destination, op, value| Assignment { + destination: ValueId(destination), + kind: AssignmentKind::Unary { + op, + value: ValueId(value), + }, + span: span(), + }; + let number = |destination, value: f64| Assignment { + destination: ValueId(destination), + kind: AssignmentKind::NumberConstant(value.to_string()), + span: span(), + }; + let binary = |destination, op, left, right| Assignment { + destination: ValueId(destination), + kind: AssignmentKind::Primitive { + op, + left: ValueId(left), + right: ValueId(right), + }, + span: span(), + }; + let module = CcModule { + name: "NumberUnaryDifferential".into(), + externals: Vec::new(), + representations: RepresentationTable::default(), + functions: vec![CcFunction { + symbol, + name: "main".into(), + parameters: Vec::new(), + values: [N, N, N, N, N, B, N, N, N, B, B, I, I, I] + .into_iter() + .enumerate() + .map(|(id, ty)| ValueDecl { + id: ValueId(id as u32), + ty, + }) + .collect(), + assignments: vec![ + number(0, input), + unary(1, operation, 0), + number(2, 1.0), + binary(3, BinaryOp::NumberDiv, 2, 1), + number(4, 0.0), + binary( + 5, + if negative { + BinaryOp::NumberLt + } else { + BinaryOp::NumberGt + }, + 3, + 4, + ), + number(6, large), + unary(7, operation, 6), + number(8, expected), + binary(9, BinaryOp::NumberEq, 7, 8), + binary(10, BinaryOp::BooleanAnd, 5, 9), + unary(11, UnaryOp::BooleanToInt, 10), + Assignment { + destination: ValueId(12), + kind: AssignmentKind::Constant(41), + span: span(), + }, + binary(13, BinaryOp::IntAdd, 11, 12), + ], + result: ValueId(13), + result_type: I, + span: span(), + }], + entry: Some(symbol), + span: span(), + }; + let target = crate::TargetCapabilities::default(); + let (mir, _) = crate::mir::lower_module_with_capabilities(module, target) + .expect("Number unary operation should lower to MIR"); + run_gc(&mir, 42); + let optimized = crate::mir::opt::optimize(mir, target).expect("valid optimization"); + run_gc(&optimized, 42); +} diff --git a/crates/psrs-backend/src/mir/gc_tests/unary.rs b/crates/psrs-backend/src/mir/gc_tests/unary.rs index 04699681..7f4af630 100644 --- a/crates/psrs-backend/src/mir/gc_tests/unary.rs +++ b/crates/psrs-backend/src/mir/gc_tests/unary.rs @@ -75,7 +75,7 @@ fn lowers_unary_and_conversion_operations_on_both_targets() { span: span(), }; let module = CcModule { - name: "UnaryScalars".into(), + name: "Unarys".into(), externals: Vec::new(), representations: RepresentationTable::default(), functions: vec![CcFunction { diff --git a/crates/psrs-backend/src/mir/indirect_tests/aggregate/collections.rs b/crates/psrs-backend/src/mir/indirect_tests/aggregate/collections.rs index 991932dd..cca17385 100644 --- a/crates/psrs-backend/src/mir/indirect_tests/aggregate/collections.rs +++ b/crates/psrs-backend/src/mir/indirect_tests/aggregate/collections.rs @@ -46,6 +46,7 @@ fn base( Assignment { destination: ValueId(0), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: Vec::new(), }, @@ -67,8 +68,8 @@ fn base( result: reference(0), }), }], - representations: cc::RepresentationTable { - representations: vec![ + representations: cc::RepresentationTable::from_parts( + vec![ Representation::Variant { cases: vec![ VariantCase { @@ -83,9 +84,9 @@ fn base( }, inner, ], - signatures: Vec::new(), - product_labels: Default::default(), - }, + Vec::new(), + Default::default(), + ), functions: vec![cc::Function { symbol: main_symbol, name: "main".into(), @@ -100,6 +101,7 @@ fn base( span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), diff --git a/crates/psrs-backend/src/mir/indirect_tests/aggregate/erased_storage.rs b/crates/psrs-backend/src/mir/indirect_tests/aggregate/erased_storage.rs new file mode 100644 index 00000000..4f46a56e --- /dev/null +++ b/crates/psrs-backend/src/mir/indirect_tests/aggregate/erased_storage.rs @@ -0,0 +1,128 @@ +//! ABI arrays and records cross bare slots in both directions. +use super::*; +use crate::cc::{ + Assignment, AssignmentKind, External, ExternalProjection, GuestLayout, Signature, ValueDecl, +}; +use crate::types::ValueId; +use psrs_hir::SymbolId; + +fn roundtrip(array: bool, include_protocol: bool) -> (cc::Module, ExternalBindings, Resolve) { + let (mut module, mut bindings, _) = if array { + option_list_record_fixture() + } else { + nested_record_fixture() + }; + let result_shape = module.externals[0].signature.as_ref().unwrap().result; + let mut guest = cc::guest_layout(result_shape, &module.representations).unwrap(); + let GuestLayout::Variant { repr, cases } = &mut guest else { + panic!("variant fixture") + }; + cases[1].fields[0].stored = erased(); + let Representation::Variant { cases: stored } = + &mut module.representations.representations[repr.0 as usize] + else { + panic!("variant storage") + }; + stored[1].fields[0] = erased(); + // The canonical protocol is registered by CC's layout owner. It is only + // reachable through ABI storage conversion, never through a source value. + if include_protocol { + let products = module.representations.product_labels.clone(); + for (_, labels) in products { + let id = module.representations.reserve(); + module.representations.set( + id, + Representation::Product { + fields: vec![erased(); labels.len()], + }, + ); + module.representations.set_product_labels(id, labels); + } + if array { + let id = module.representations.reserve(); + module + .representations + .set(id, Representation::Array { element: erased() }); + } + } + let id = module.representations.reserve(); + module.representations.set( + id, + Representation::Box { + value: ValueShape::Integer, + }, + ); + module.externals[0].projection = Some(ExternalProjection { + parameters: vec![], + result: Some(guest.clone()), + }); + let get = module.externals[0].symbol; + let take = SymbolId::new(get.module, get.index + 1); + module.externals.push(External { + symbol: take, + signature: Some(Signature { + parameters: vec![result_shape], + result: ValueShape::Integer, + }), + projection: Some(ExternalProjection { + parameters: vec![guest], + result: None, + }), + }); + let mut binding = bindings.imports[0].clone(); + binding.symbol = take; + binding.function = "take".into(); + bindings.imports.push(binding); + let main = &mut module.functions[0]; + main.values.push(ValueDecl { + id: ValueId(3), + ty: ValueShape::Integer, + }); + main.assignments.push(Assignment { + destination: ValueId(3), + kind: AssignmentKind::DirectCall { + contract: None, + function: take, + arguments: vec![ValueId(0)], + }, + span: module.span, + }); + main.result = ValueId(3); + let payload = if array { "list" } else { "pair" }; + let mut resolve = Resolve::default(); + resolve.push_str("storage.wit", &format!( + "package wasi:io@0.2.12; interface streams {{ record pair {{ x: s32, y: bool }} get: func() -> option<{payload}>; take: func(value: option<{payload}>) -> s32; }}" + )).unwrap(); + (module, bindings, resolve) +} + +#[test] +fn erased_record_array_payload_roundtrips_through_canonical_storage() { + let (module, bindings, resolve) = roundtrip(true, true); + lower_and_validate(module, bindings, resolve); +} + +#[test] +fn erased_record_payload_roundtrips_through_canonical_storage() { + let (module, bindings, resolve) = roundtrip(false, true); + lower_and_validate(module, bindings, resolve); +} + +#[test] +fn erased_payload_without_an_owner_protocol_is_rejected() { + let (module, bindings, resolve) = roundtrip(true, false); + let target = TargetCapabilities { + wasi_cli: false, + ..TargetCapabilities::default() + }; + let registry = abi::WasiRegistry::from_resolve(resolve, target); + let Err(errors) = lower_module_with_registry(module, bindings, target, registry) else { + panic!("a missing storage protocol must be rejected"); + }; + assert!( + errors + .iter() + .any(|error| error.message.contains("storage protocol")), + "{errors:?}" + ); +} diff --git a/crates/psrs-backend/src/mir/indirect_tests/aggregate/fixtures.rs b/crates/psrs-backend/src/mir/indirect_tests/aggregate/fixtures.rs index 51302b74..628744d6 100644 --- a/crates/psrs-backend/src/mir/indirect_tests/aggregate/fixtures.rs +++ b/crates/psrs-backend/src/mir/indirect_tests/aggregate/fixtures.rs @@ -62,11 +62,8 @@ pub(super) fn fixture_with_representations( Representation::Variant { cases }, ]; representations.extend(extra); - let representations = cc::RepresentationTable { - representations, - signatures: Vec::new(), - product_labels: Default::default(), - }; + let representations = + cc::RepresentationTable::from_parts(representations, Vec::new(), Default::default()); let external_symbol = SymbolId::new(ModuleId::INTRINSICS, FOREIGN_SYMBOL_BASE); let main_symbol = SymbolId::new(ModuleId(0), 0); @@ -95,6 +92,7 @@ pub(super) fn fixture_with_representations( Assignment { destination: ValueId(0), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: Vec::new(), }, @@ -132,6 +130,7 @@ pub(super) fn fixture_with_representations( span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), @@ -153,8 +152,8 @@ pub(super) fn parameter_fixture() -> (cc::Module, ExternalBindings, Resolve) { ) .expect("the parameter WIT fixture should resolve"); - let representations = cc::RepresentationTable { - representations: vec![ + let representations = cc::RepresentationTable::from_parts( + vec![ Representation::Box { value: ValueShape::Integer, }, @@ -171,9 +170,9 @@ pub(super) fn parameter_fixture() -> (cc::Module, ExternalBindings, Resolve) { ], }, ], - signatures: Vec::new(), - product_labels: Default::default(), - }; + Vec::new(), + Default::default(), + ); let external_symbol = SymbolId::new(ModuleId::INTRINSICS, FOREIGN_SYMBOL_BASE); let main_symbol = SymbolId::new(ModuleId(0), 0); @@ -237,6 +236,7 @@ pub(super) fn parameter_fixture() -> (cc::Module, ExternalBindings, Resolve) { Assignment { destination: ValueId(3), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: vec![ValueId(2)], }, @@ -269,6 +269,7 @@ pub(super) fn parameter_fixture() -> (cc::Module, ExternalBindings, Resolve) { span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), diff --git a/crates/psrs-backend/src/mir/indirect_tests/aggregate/indirect.rs b/crates/psrs-backend/src/mir/indirect_tests/aggregate/indirect.rs index be513667..f3418434 100644 --- a/crates/psrs-backend/src/mir/indirect_tests/aggregate/indirect.rs +++ b/crates/psrs-backend/src/mir/indirect_tests/aggregate/indirect.rs @@ -25,8 +25,8 @@ pub(super) fn indirect_aggregate_fixture() -> (cc::Module, ExternalBindings, Res .push_str("aggregate-indirect.wit", &wit) .expect("the indirect aggregate WIT fixture should resolve"); - let representations = cc::RepresentationTable { - representations: vec![ + let representations = cc::RepresentationTable::from_parts( + vec![ Representation::Box { value: ValueShape::Integer, }, @@ -43,9 +43,9 @@ pub(super) fn indirect_aggregate_fixture() -> (cc::Module, ExternalBindings, Res ], }, ], - signatures: Vec::new(), - product_labels: Default::default(), - }; + Vec::new(), + Default::default(), + ); let external_symbol = SymbolId::new(ModuleId::INTRINSICS, FOREIGN_SYMBOL_BASE); let main_symbol = SymbolId::new(ModuleId(0), 0); @@ -130,6 +130,7 @@ pub(super) fn indirect_aggregate_fixture() -> (cc::Module, ExternalBindings, Res assignments.push(Assignment { destination: result, kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments, }, @@ -161,6 +162,7 @@ pub(super) fn indirect_aggregate_fixture() -> (cc::Module, ExternalBindings, Res span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), diff --git a/crates/psrs-backend/src/mir/indirect_tests/aggregate/large.rs b/crates/psrs-backend/src/mir/indirect_tests/aggregate/large.rs index 4cae5c7c..31bc2cb6 100644 --- a/crates/psrs-backend/src/mir/indirect_tests/aggregate/large.rs +++ b/crates/psrs-backend/src/mir/indirect_tests/aggregate/large.rs @@ -22,8 +22,8 @@ pub(super) fn large_record_fixture() -> (cc::Module, ExternalBindings, Resolve) ) .expect("the large record WIT fixture should resolve"); - let representations = cc::RepresentationTable { - representations: vec![ + let representations = cc::RepresentationTable::from_parts( + vec![ Representation::Box { value: ValueShape::Integer, }, @@ -43,14 +43,14 @@ pub(super) fn large_record_fixture() -> (cc::Module, ExternalBindings, Resolve) ], }, ], - signatures: Vec::new(), - product_labels: [( + Vec::new(), + [( ReprId(1), vec!["a".to_string(), "b".to_string(), "c".to_string()], )] .into_iter() .collect(), - }; + ); let external_symbol = SymbolId::new(ModuleId::INTRINSICS, FOREIGN_SYMBOL_BASE); let main_symbol = SymbolId::new(ModuleId(0), 0); @@ -72,6 +72,7 @@ pub(super) fn large_record_fixture() -> (cc::Module, ExternalBindings, Resolve) Assignment { destination: ValueId(0), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: Vec::new(), }, @@ -108,6 +109,7 @@ pub(super) fn large_record_fixture() -> (cc::Module, ExternalBindings, Resolve) span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), @@ -132,8 +134,8 @@ pub(super) fn large_unit_result_fixture() -> (cc::Module, ExternalBindings, Reso ) .expect("the large unit result WIT fixture should resolve"); - let representations = cc::RepresentationTable { - representations: vec![ + let representations = cc::RepresentationTable::from_parts( + vec![ Representation::Box { value: ValueShape::Integer, }, @@ -155,14 +157,14 @@ pub(super) fn large_unit_result_fixture() -> (cc::Module, ExternalBindings, Reso ], }, ], - signatures: Vec::new(), - product_labels: [( + Vec::new(), + [( ReprId(1), vec!["a".to_string(), "b".to_string(), "c".to_string()], )] .into_iter() .collect(), - }; + ); let external_symbol = SymbolId::new(ModuleId::INTRINSICS, FOREIGN_SYMBOL_BASE); let main_symbol = SymbolId::new(ModuleId(0), 0); @@ -195,6 +197,7 @@ pub(super) fn large_unit_result_fixture() -> (cc::Module, ExternalBindings, Reso Assignment { destination: ValueId(0), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: Vec::new(), }, @@ -214,6 +217,7 @@ pub(super) fn large_unit_result_fixture() -> (cc::Module, ExternalBindings, Reso span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), diff --git a/crates/psrs-backend/src/mir/indirect_tests/aggregate/list_record.rs b/crates/psrs-backend/src/mir/indirect_tests/aggregate/list_record.rs index 0f4c1557..b7b1d0c7 100644 --- a/crates/psrs-backend/src/mir/indirect_tests/aggregate/list_record.rs +++ b/crates/psrs-backend/src/mir/indirect_tests/aggregate/list_record.rs @@ -39,11 +39,11 @@ fn finish( result, }), }], - representations: cc::RepresentationTable { + representations: cc::RepresentationTable::from_parts( representations, - signatures: Vec::new(), - product_labels: product_labels.into_iter().collect(), - }, + Vec::new(), + product_labels.into_iter().collect(), + ), functions: vec![cc::Function { symbol: main_symbol, name: "main".into(), @@ -66,6 +66,7 @@ fn finish( Assignment { destination: ValueId(0), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: Vec::new(), }, @@ -85,6 +86,7 @@ fn finish( span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), diff --git a/crates/psrs-backend/src/mir/indirect_tests/aggregate/mod.rs b/crates/psrs-backend/src/mir/indirect_tests/aggregate/mod.rs index 95e7409a..b9c9e799 100644 --- a/crates/psrs-backend/src/mir/indirect_tests/aggregate/mod.rs +++ b/crates/psrs-backend/src/mir/indirect_tests/aggregate/mod.rs @@ -2,6 +2,7 @@ //! record and variant payloads. mod collections; +mod erased_storage; mod fixtures; mod indirect; mod large; @@ -10,7 +11,6 @@ mod nested; mod record_fields; mod resource_result; mod scalars; - use super::lower_module_with_registry; use crate::ExternalBindings; use crate::TargetCapabilities; diff --git a/crates/psrs-backend/src/mir/indirect_tests/aggregate/nested.rs b/crates/psrs-backend/src/mir/indirect_tests/aggregate/nested.rs index cd719ede..ff2a6226 100644 --- a/crates/psrs-backend/src/mir/indirect_tests/aggregate/nested.rs +++ b/crates/psrs-backend/src/mir/indirect_tests/aggregate/nested.rs @@ -19,8 +19,8 @@ pub(super) fn nested_variant_fixture() -> (cc::Module, ExternalBindings, Resolve ) .expect("the nested variant WIT fixture should resolve"); - let representations = cc::RepresentationTable { - representations: vec![ + let representations = cc::RepresentationTable::from_parts( + vec![ Representation::Box { value: ValueShape::Integer, }, @@ -51,9 +51,9 @@ pub(super) fn nested_variant_fixture() -> (cc::Module, ExternalBindings, Resolve ], }, ], - signatures: Vec::new(), - product_labels: Default::default(), - }; + Vec::new(), + Default::default(), + ); let external_symbol = SymbolId::new(ModuleId::INTRINSICS, FOREIGN_SYMBOL_BASE); let main_symbol = SymbolId::new(ModuleId(0), 0); @@ -79,6 +79,7 @@ pub(super) fn nested_variant_fixture() -> (cc::Module, ExternalBindings, Resolve Assignment { destination: ValueId(0), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: Vec::new(), }, @@ -115,6 +116,7 @@ pub(super) fn nested_variant_fixture() -> (cc::Module, ExternalBindings, Resolve span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), @@ -136,8 +138,8 @@ pub(super) fn nested_record_fixture() -> (cc::Module, ExternalBindings, Resolve) ) .expect("the nested record WIT fixture should resolve"); - let representations = cc::RepresentationTable { - representations: vec![ + let representations = cc::RepresentationTable::from_parts( + vec![ Representation::Box { value: ValueShape::Integer, }, @@ -159,11 +161,11 @@ pub(super) fn nested_record_fixture() -> (cc::Module, ExternalBindings, Resolve) fields: vec![ValueShape::Integer, ValueShape::Boolean], }, ], - signatures: Vec::new(), - product_labels: [(ReprId(2), vec!["x".to_string(), "y".to_string()])] + Vec::new(), + [(ReprId(2), vec!["x".to_string(), "y".to_string()])] .into_iter() .collect(), - }; + ); let external_symbol = SymbolId::new(ModuleId::INTRINSICS, FOREIGN_SYMBOL_BASE); let main_symbol = SymbolId::new(ModuleId(0), 0); @@ -185,6 +187,7 @@ pub(super) fn nested_record_fixture() -> (cc::Module, ExternalBindings, Resolve) Assignment { destination: ValueId(0), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: Vec::new(), }, @@ -221,6 +224,7 @@ pub(super) fn nested_record_fixture() -> (cc::Module, ExternalBindings, Resolve) span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), @@ -244,8 +248,8 @@ pub(super) fn nested_record_parameter_fixture() -> (cc::Module, ExternalBindings ) .expect("the nested record parameter WIT fixture should resolve"); - let representations = cc::RepresentationTable { - representations: vec![ + let representations = cc::RepresentationTable::from_parts( + vec![ Representation::Box { value: ValueShape::Integer, }, @@ -265,11 +269,11 @@ pub(super) fn nested_record_parameter_fixture() -> (cc::Module, ExternalBindings fields: vec![ValueShape::Integer, ValueShape::Boolean], }, ], - signatures: Vec::new(), - product_labels: [(ReprId(2), vec!["x".to_string(), "y".to_string()])] + Vec::new(), + [(ReprId(2), vec!["x".to_string(), "y".to_string()])] .into_iter() .collect(), - }; + ); let external_symbol = SymbolId::new(ModuleId::INTRINSICS, FOREIGN_SYMBOL_BASE); let main_symbol = SymbolId::new(ModuleId(0), 0); @@ -349,6 +353,7 @@ pub(super) fn nested_record_parameter_fixture() -> (cc::Module, ExternalBindings Assignment { destination: ValueId(5), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: vec![ValueId(4)], }, @@ -380,6 +385,7 @@ pub(super) fn nested_record_parameter_fixture() -> (cc::Module, ExternalBindings span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), diff --git a/crates/psrs-backend/src/mir/indirect_tests/aggregate/record_fields.rs b/crates/psrs-backend/src/mir/indirect_tests/aggregate/record_fields.rs index cb7ab113..61311e9c 100644 --- a/crates/psrs-backend/src/mir/indirect_tests/aggregate/record_fields.rs +++ b/crates/psrs-backend/src/mir/indirect_tests/aggregate/record_fields.rs @@ -21,8 +21,8 @@ pub(super) fn record_with_aggregate_field_fixture() -> (cc::Module, ExternalBind ) .expect("the record field WIT fixture should resolve"); - let representations = cc::RepresentationTable { - representations: vec![ + let representations = cc::RepresentationTable::from_parts( + vec![ Representation::Box { value: ValueShape::Integer, }, @@ -57,11 +57,11 @@ pub(super) fn record_with_aggregate_field_fixture() -> (cc::Module, ExternalBind ], }, ], - signatures: Vec::new(), - product_labels: [(ReprId(2), vec!["inner".to_string()])] + Vec::new(), + [(ReprId(2), vec!["inner".to_string()])] .into_iter() .collect(), - }; + ); let external_symbol = SymbolId::new(ModuleId::INTRINSICS, FOREIGN_SYMBOL_BASE); let main_symbol = SymbolId::new(ModuleId(0), 0); @@ -87,6 +87,7 @@ pub(super) fn record_with_aggregate_field_fixture() -> (cc::Module, ExternalBind Assignment { destination: ValueId(0), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: Vec::new(), }, @@ -123,6 +124,7 @@ pub(super) fn record_with_aggregate_field_fixture() -> (cc::Module, ExternalBind span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), @@ -148,8 +150,8 @@ pub(super) fn record_with_aggregate_field_parameter_fixture() ) .expect("the record field parameter WIT fixture should resolve"); - let representations = cc::RepresentationTable { - representations: vec![ + let representations = cc::RepresentationTable::from_parts( + vec![ Representation::Box { value: ValueShape::Integer, }, @@ -184,11 +186,11 @@ pub(super) fn record_with_aggregate_field_parameter_fixture() ], }, ], - signatures: Vec::new(), - product_labels: [(ReprId(2), vec!["inner".to_string()])] + Vec::new(), + [(ReprId(2), vec!["inner".to_string()])] .into_iter() .collect(), - }; + ); let external_symbol = SymbolId::new(ModuleId::INTRINSICS, FOREIGN_SYMBOL_BASE); let main_symbol = SymbolId::new(ModuleId(0), 0); @@ -316,6 +318,7 @@ pub(super) fn record_with_aggregate_field_parameter_fixture() Assignment { destination: ValueId(7), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: vec![ValueId(6)], }, @@ -347,6 +350,7 @@ pub(super) fn record_with_aggregate_field_parameter_fixture() span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), diff --git a/crates/psrs-backend/src/mir/indirect_tests/aggregate/resource_result.rs b/crates/psrs-backend/src/mir/indirect_tests/aggregate/resource_result.rs index 4ccfb193..2633ba9e 100644 --- a/crates/psrs-backend/src/mir/indirect_tests/aggregate/resource_result.rs +++ b/crates/psrs-backend/src/mir/indirect_tests/aggregate/resource_result.rs @@ -22,8 +22,8 @@ pub(super) fn resource_result_fixture() -> (cc::Module, ExternalBindings, Resolv ) .expect("the resource result WIT fixture should resolve"); - let representations = cc::RepresentationTable { - representations: vec![ + let representations = cc::RepresentationTable::from_parts( + vec![ Representation::Box { value: ValueShape::Integer, }, @@ -40,9 +40,9 @@ pub(super) fn resource_result_fixture() -> (cc::Module, ExternalBindings, Resolv ], }, ], - signatures: Vec::new(), - product_labels: Default::default(), - }; + Vec::new(), + Default::default(), + ); let external_symbol = SymbolId::new(ModuleId::INTRINSICS, FOREIGN_SYMBOL_BASE); let main_symbol = SymbolId::new(ModuleId(0), 0); @@ -60,6 +60,7 @@ pub(super) fn resource_result_fixture() -> (cc::Module, ExternalBindings, Resolv Assignment { destination: ValueId(0), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: Vec::new(), }, @@ -96,6 +97,7 @@ pub(super) fn resource_result_fixture() -> (cc::Module, ExternalBindings, Resolv span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), diff --git a/crates/psrs-backend/src/mir/indirect_tests/aggregate/scalars.rs b/crates/psrs-backend/src/mir/indirect_tests/aggregate/scalars.rs index ab0a6711..e1293188 100644 --- a/crates/psrs-backend/src/mir/indirect_tests/aggregate/scalars.rs +++ b/crates/psrs-backend/src/mir/indirect_tests/aggregate/scalars.rs @@ -21,8 +21,8 @@ pub(super) fn wide_scalar_fixture() -> (cc::Module, ExternalBindings, Resolve) { ) .expect("the wide scalar WIT fixture should resolve"); - let representations = cc::RepresentationTable { - representations: vec![ + let representations = cc::RepresentationTable::from_parts( + vec![ Representation::Box { value: ValueShape::Integer, }, @@ -43,9 +43,9 @@ pub(super) fn wide_scalar_fixture() -> (cc::Module, ExternalBindings, Resolve) { ], }, ], - signatures: Vec::new(), - product_labels: Default::default(), - }; + Vec::new(), + Default::default(), + ); let external_symbol = SymbolId::new(ModuleId::INTRINSICS, FOREIGN_SYMBOL_BASE); let main_symbol = SymbolId::new(ModuleId(0), 0); @@ -71,6 +71,7 @@ pub(super) fn wide_scalar_fixture() -> (cc::Module, ExternalBindings, Resolve) { Assignment { destination: ValueId(0), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: Vec::new(), }, @@ -107,6 +108,7 @@ pub(super) fn wide_scalar_fixture() -> (cc::Module, ExternalBindings, Resolve) { span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), @@ -130,8 +132,8 @@ pub(super) fn wide_scalar_parameter_fixture() -> (cc::Module, ExternalBindings, ) .expect("the wide scalar parameter WIT fixture should resolve"); - let representations = cc::RepresentationTable { - representations: vec![ + let representations = cc::RepresentationTable::from_parts( + vec![ Representation::Box { value: ValueShape::Number, }, @@ -148,9 +150,9 @@ pub(super) fn wide_scalar_parameter_fixture() -> (cc::Module, ExternalBindings, ], }, ], - signatures: Vec::new(), - product_labels: Default::default(), - }; + Vec::new(), + Default::default(), + ); let external_symbol = SymbolId::new(ModuleId::INTRINSICS, FOREIGN_SYMBOL_BASE); let main_symbol = SymbolId::new(ModuleId(0), 0); @@ -214,6 +216,7 @@ pub(super) fn wide_scalar_parameter_fixture() -> (cc::Module, ExternalBindings, Assignment { destination: ValueId(3), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: vec![ValueId(2)], }, @@ -245,6 +248,7 @@ pub(super) fn wide_scalar_parameter_fixture() -> (cc::Module, ExternalBindings, span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), diff --git a/crates/psrs-backend/src/mir/indirect_tests/composite/fixture.rs b/crates/psrs-backend/src/mir/indirect_tests/composite/fixture.rs index 8369eca2..2c208833 100644 --- a/crates/psrs-backend/src/mir/indirect_tests/composite/fixture.rs +++ b/crates/psrs-backend/src/mir/indirect_tests/composite/fixture.rs @@ -37,8 +37,8 @@ pub(super) fn composite_indirect_fixture() -> (cc::Module, ExternalBindings, Res let flags_shape = product_shape(0); let details_shape = product_shape(1); let payload_shape = product_shape(2); - let representations = cc::RepresentationTable { - representations: vec![ + let representations = cc::RepresentationTable::from_parts( + vec![ cc::Representation::Product { fields: vec![ValueShape::Boolean; 5], }, @@ -55,8 +55,8 @@ pub(super) fn composite_indirect_fixture() -> (cc::Module, ExternalBindings, Res element: ValueShape::Integer, }, ], - signatures: Vec::new(), - product_labels: [ + Vec::new(), + [ ( cc::ReprId(0), vec!["audit", "debug", "execute", "read", "write"], @@ -67,7 +67,7 @@ pub(super) fn composite_indirect_fixture() -> (cc::Module, ExternalBindings, Res .into_iter() .map(|(id, labels)| (id, labels.into_iter().map(String::from).collect())) .collect(), - }; + ); let external_symbol = SymbolId::new(ModuleId::INTRINSICS, FOREIGN_SYMBOL_BASE); let main_symbol = SymbolId::new(ModuleId(0), 0); @@ -213,6 +213,7 @@ pub(super) fn composite_indirect_fixture() -> (cc::Module, ExternalBindings, Res assignments.push(Assignment { destination: call_result, kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: call_arguments, }, @@ -244,6 +245,7 @@ pub(super) fn composite_indirect_fixture() -> (cc::Module, ExternalBindings, Res span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), diff --git a/crates/psrs-backend/src/mir/indirect_tests/mod.rs b/crates/psrs-backend/src/mir/indirect_tests/mod.rs index e5d61c59..ccf2f72b 100644 --- a/crates/psrs-backend/src/mir/indirect_tests/mod.rs +++ b/crates/psrs-backend/src/mir/indirect_tests/mod.rs @@ -58,6 +58,7 @@ fn indirect_fixture() -> (cc::Module, ExternalBindings, Resolve) { assignments.push(Assignment { destination: call_result, kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments, }, @@ -89,6 +90,7 @@ fn indirect_fixture() -> (cc::Module, ExternalBindings, Resolve) { span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), diff --git a/crates/psrs-backend/src/mir/indirect_tests/record_list/fixtures/aggregate.rs b/crates/psrs-backend/src/mir/indirect_tests/record_list/fixtures/aggregate.rs index a91166a7..8462aae3 100644 --- a/crates/psrs-backend/src/mir/indirect_tests/record_list/fixtures/aggregate.rs +++ b/crates/psrs-backend/src/mir/indirect_tests/record_list/fixtures/aggregate.rs @@ -16,8 +16,8 @@ pub(crate) fn flags_fixture() -> (cc::Module, ExternalBindings, Resolve) { // Source flag fields are sorted alphabetically: `read` then `write`. WIT // declares `write` before `read`, so `write` is bit 0 and `read` is bit 1. - let representations = cc::RepresentationTable { - representations: vec![ + let representations = cc::RepresentationTable::from_parts( + vec![ cc::Representation::Product { fields: vec![ValueShape::Boolean, ValueShape::Boolean], }, @@ -25,11 +25,11 @@ pub(crate) fn flags_fixture() -> (cc::Module, ExternalBindings, Resolve) { element: reference(0), }, ], - signatures: Vec::new(), - product_labels: [(cc::ReprId(0), vec!["read".to_string(), "write".to_string()])] + Vec::new(), + [(cc::ReprId(0), vec!["read".to_string(), "write".to_string()])] .into_iter() .collect(), - }; + ); let external_symbol = SymbolId::new(ModuleId::INTRINSICS, FOREIGN_SYMBOL_BASE); let main_symbol = SymbolId::new(ModuleId(0), 0); @@ -87,6 +87,7 @@ pub(crate) fn flags_fixture() -> (cc::Module, ExternalBindings, Resolve) { Assignment { destination: ValueId(4), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: vec![ValueId(3)], }, @@ -119,6 +120,7 @@ pub(crate) fn flags_fixture() -> (cc::Module, ExternalBindings, Resolve) { span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), @@ -140,13 +142,13 @@ pub(crate) fn handle_fixture() -> (cc::Module, ExternalBindings, Resolve) { ) .expect("the handle list WIT fixture should resolve"); - let representations = cc::RepresentationTable { - representations: vec![cc::Representation::Array { + let representations = cc::RepresentationTable::from_parts( + vec![cc::Representation::Array { element: ValueShape::Integer, }], - signatures: Vec::new(), - product_labels: Default::default(), - }; + Vec::new(), + Default::default(), + ); let external_symbol = SymbolId::new(ModuleId::INTRINSICS, FOREIGN_SYMBOL_BASE); let main_symbol = SymbolId::new(ModuleId(0), 0); @@ -182,6 +184,7 @@ pub(crate) fn handle_fixture() -> (cc::Module, ExternalBindings, Resolve) { Assignment { destination: ValueId(2), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: vec![ValueId(1)], }, @@ -214,6 +217,7 @@ pub(crate) fn handle_fixture() -> (cc::Module, ExternalBindings, Resolve) { span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), @@ -236,8 +240,8 @@ pub(crate) fn tuple_fixture() -> (cc::Module, ExternalBindings, Resolve) { .expect("the tuple list WIT fixture should resolve"); // DEC-13 maps `tuple` to the closed record `{ _1, _2 }`. - let representations = cc::RepresentationTable { - representations: vec![ + let representations = cc::RepresentationTable::from_parts( + vec![ cc::Representation::Product { fields: vec![ValueShape::String, ValueShape::String], }, @@ -245,11 +249,11 @@ pub(crate) fn tuple_fixture() -> (cc::Module, ExternalBindings, Resolve) { element: reference(0), }, ], - signatures: Vec::new(), - product_labels: [(cc::ReprId(0), vec!["_1".to_string(), "_2".to_string()])] + Vec::new(), + [(cc::ReprId(0), vec!["_1".to_string(), "_2".to_string()])] .into_iter() .collect(), - }; + ); let external_symbol = SymbolId::new(ModuleId::INTRINSICS, FOREIGN_SYMBOL_BASE); let main_symbol = SymbolId::new(ModuleId(0), 0); @@ -307,6 +311,7 @@ pub(crate) fn tuple_fixture() -> (cc::Module, ExternalBindings, Resolve) { Assignment { destination: ValueId(4), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: vec![ValueId(3)], }, @@ -339,6 +344,7 @@ pub(crate) fn tuple_fixture() -> (cc::Module, ExternalBindings, Resolve) { span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), @@ -368,13 +374,13 @@ fn handle_result(result: &str) -> (cc::Module, ExternalBindings, Resolve) { ) .expect("the handle list result WIT fixture should resolve"); - let representations = cc::RepresentationTable { - representations: vec![cc::Representation::Array { + let representations = cc::RepresentationTable::from_parts( + vec![cc::Representation::Array { element: ValueShape::Integer, }], - signatures: Vec::new(), - product_labels: Default::default(), - }; + Vec::new(), + Default::default(), + ); let external_symbol = SymbolId::new(ModuleId::INTRINSICS, FOREIGN_SYMBOL_BASE); let main_symbol = SymbolId::new(ModuleId(0), 0); @@ -407,6 +413,7 @@ fn handle_result(result: &str) -> (cc::Module, ExternalBindings, Resolve) { Assignment { destination: ValueId(0), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: Vec::new(), }, @@ -426,6 +433,7 @@ fn handle_result(result: &str) -> (cc::Module, ExternalBindings, Resolve) { span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), diff --git a/crates/psrs-backend/src/mir/indirect_tests/record_list/fixtures/fixed.rs b/crates/psrs-backend/src/mir/indirect_tests/record_list/fixtures/fixed.rs index cb36b0b4..1b76949e 100644 --- a/crates/psrs-backend/src/mir/indirect_tests/record_list/fixtures/fixed.rs +++ b/crates/psrs-backend/src/mir/indirect_tests/record_list/fixtures/fixed.rs @@ -71,6 +71,7 @@ pub(crate) fn fixed_list_fixture() -> (cc::Module, ExternalBindings, Resolve) { Assignment { destination: ValueId(4), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: vec![ValueId(3)], }, @@ -87,13 +88,13 @@ pub(crate) fn fixed_list_fixture() -> (cc::Module, ExternalBindings, Resolve) { result: ValueShape::Integer, }), }], - representations: cc::RepresentationTable { - representations: vec![Representation::Array { + representations: cc::RepresentationTable::from_parts( + vec![Representation::Array { element: ValueShape::Integer, }], - signatures: Vec::new(), - product_labels: Default::default(), - }, + Vec::new(), + Default::default(), + ), functions: vec![cc::Function { symbol: main_symbol, name: "main".into(), @@ -108,6 +109,7 @@ pub(crate) fn fixed_list_fixture() -> (cc::Module, ExternalBindings, Resolve) { span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), @@ -141,13 +143,13 @@ pub(crate) fn fixed_list_result_fixture() -> (cc::Module, ExternalBindings, Reso result: reference(0), }), }], - representations: cc::RepresentationTable { - representations: vec![Representation::Array { + representations: cc::RepresentationTable::from_parts( + vec![Representation::Array { element: ValueShape::Integer, }], - signatures: Vec::new(), - product_labels: Default::default(), - }, + Vec::new(), + Default::default(), + ), functions: vec![cc::Function { symbol: main_symbol, name: "main".into(), @@ -166,6 +168,7 @@ pub(crate) fn fixed_list_result_fixture() -> (cc::Module, ExternalBindings, Reso Assignment { destination: ValueId(0), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: Vec::new(), }, @@ -185,6 +188,7 @@ pub(crate) fn fixed_list_result_fixture() -> (cc::Module, ExternalBindings, Reso span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), diff --git a/crates/psrs-backend/src/mir/indirect_tests/record_list/fixtures/mod.rs b/crates/psrs-backend/src/mir/indirect_tests/record_list/fixtures/mod.rs index 63794799..19e3403e 100644 --- a/crates/psrs-backend/src/mir/indirect_tests/record_list/fixtures/mod.rs +++ b/crates/psrs-backend/src/mir/indirect_tests/record_list/fixtures/mod.rs @@ -40,8 +40,8 @@ pub(super) fn fixture() -> (cc::Module, ExternalBindings, Resolve) { ) .expect("the record list WIT fixture should resolve"); - let representations = cc::RepresentationTable { - representations: vec![ + let representations = cc::RepresentationTable::from_parts( + vec![ cc::Representation::Product { fields: vec![ValueShape::Integer, ValueShape::Number], }, @@ -49,11 +49,11 @@ pub(super) fn fixture() -> (cc::Module, ExternalBindings, Resolve) { element: reference(0), }, ], - signatures: Vec::new(), - product_labels: [(cc::ReprId(0), vec!["x".to_string(), "y".to_string()])] + Vec::new(), + [(cc::ReprId(0), vec!["x".to_string(), "y".to_string()])] .into_iter() .collect(), - }; + ); let external_symbol = SymbolId::new(ModuleId::INTRINSICS, FOREIGN_SYMBOL_BASE); let main_symbol = SymbolId::new(ModuleId(0), 0); @@ -111,6 +111,7 @@ pub(super) fn fixture() -> (cc::Module, ExternalBindings, Resolve) { Assignment { destination: ValueId(4), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: vec![ValueId(3)], }, @@ -143,6 +144,7 @@ pub(super) fn fixture() -> (cc::Module, ExternalBindings, Resolve) { span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), @@ -164,8 +166,8 @@ pub(super) fn result_fixture() -> (cc::Module, ExternalBindings, Resolve) { ) .expect("the record list WIT fixture should resolve"); - let representations = cc::RepresentationTable { - representations: vec![ + let representations = cc::RepresentationTable::from_parts( + vec![ cc::Representation::Product { fields: vec![ValueShape::Integer, ValueShape::Number], }, @@ -173,11 +175,11 @@ pub(super) fn result_fixture() -> (cc::Module, ExternalBindings, Resolve) { element: reference(0), }, ], - signatures: Vec::new(), - product_labels: [(cc::ReprId(0), vec!["x".to_string(), "y".to_string()])] + Vec::new(), + [(cc::ReprId(0), vec!["x".to_string(), "y".to_string()])] .into_iter() .collect(), - }; + ); let external_symbol = SymbolId::new(ModuleId::INTRINSICS, FOREIGN_SYMBOL_BASE); let main_symbol = SymbolId::new(ModuleId(0), 0); @@ -210,6 +212,7 @@ pub(super) fn result_fixture() -> (cc::Module, ExternalBindings, Resolve) { Assignment { destination: ValueId(0), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: Vec::new(), }, @@ -229,6 +232,7 @@ pub(super) fn result_fixture() -> (cc::Module, ExternalBindings, Resolve) { span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), @@ -253,8 +257,8 @@ pub(super) fn string_fixture() -> (cc::Module, ExternalBindings, Resolve) { // Source record fields are sorted alphabetically: `code` then `text`. WIT // declares `text` before `code`, so the canonical layout and the GC struct // order differ and the plan must project by name. - let representations = cc::RepresentationTable { - representations: vec![ + let representations = cc::RepresentationTable::from_parts( + vec![ cc::Representation::Product { fields: vec![ValueShape::Integer, ValueShape::String], }, @@ -262,11 +266,11 @@ pub(super) fn string_fixture() -> (cc::Module, ExternalBindings, Resolve) { element: reference(0), }, ], - signatures: Vec::new(), - product_labels: [(cc::ReprId(0), vec!["code".to_string(), "text".to_string()])] + Vec::new(), + [(cc::ReprId(0), vec!["code".to_string(), "text".to_string()])] .into_iter() .collect(), - }; + ); let external_symbol = SymbolId::new(ModuleId::INTRINSICS, FOREIGN_SYMBOL_BASE); let main_symbol = SymbolId::new(ModuleId(0), 0); @@ -324,6 +328,7 @@ pub(super) fn string_fixture() -> (cc::Module, ExternalBindings, Resolve) { Assignment { destination: ValueId(4), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: vec![ValueId(3)], }, @@ -356,6 +361,7 @@ pub(super) fn string_fixture() -> (cc::Module, ExternalBindings, Resolve) { span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), diff --git a/crates/psrs-backend/src/mir/indirect_tests/record_list/fixtures/variants.rs b/crates/psrs-backend/src/mir/indirect_tests/record_list/fixtures/variants.rs index 237f1ce6..f50b715a 100644 --- a/crates/psrs-backend/src/mir/indirect_tests/record_list/fixtures/variants.rs +++ b/crates/psrs-backend/src/mir/indirect_tests/record_list/fixtures/variants.rs @@ -115,6 +115,7 @@ fn parameter_fixture( assignments.push(Assignment { destination: ValueId(4), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: vec![ValueId(3)], }, @@ -130,11 +131,11 @@ fn parameter_fixture( result: ValueShape::Integer, }), }], - representations: cc::RepresentationTable { + representations: cc::RepresentationTable::from_parts( representations, - signatures: Vec::new(), - product_labels: Default::default(), - }, + Vec::new(), + Default::default(), + ), functions: vec![cc::Function { symbol: main_symbol, name: "main".into(), @@ -149,6 +150,7 @@ fn parameter_fixture( span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), @@ -272,16 +274,16 @@ pub(crate) fn option_string_list_result_fixture() -> (cc::Module, ExternalBindin result: reference(1), }), }], - representations: cc::RepresentationTable { - representations: vec![ + representations: cc::RepresentationTable::from_parts( + vec![ maybe_string(), Representation::Array { element: reference(0), }, ], - signatures: Vec::new(), - product_labels: Default::default(), - }, + Vec::new(), + Default::default(), + ), functions: vec![cc::Function { symbol: main_symbol, name: "main".into(), @@ -300,6 +302,7 @@ pub(crate) fn option_string_list_result_fixture() -> (cc::Module, ExternalBindin Assignment { destination: ValueId(0), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: Vec::new(), }, @@ -319,6 +322,7 @@ pub(crate) fn option_string_list_result_fixture() -> (cc::Module, ExternalBindin span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), @@ -415,6 +419,7 @@ pub(crate) fn option_list_list_fixture() -> (cc::Module, ExternalBindings, Resol Assignment { destination: ValueId(5), kind: AssignmentKind::DirectCall { + contract: None, function: external_symbol, arguments: vec![ValueId(4)], }, @@ -431,8 +436,8 @@ pub(crate) fn option_list_list_fixture() -> (cc::Module, ExternalBindings, Resol result: ValueShape::Integer, }), }], - representations: cc::RepresentationTable { - representations: vec![ + representations: cc::RepresentationTable::from_parts( + vec![ Representation::Array { element: ValueShape::Integer, }, @@ -452,9 +457,9 @@ pub(crate) fn option_list_list_fixture() -> (cc::Module, ExternalBindings, Resol element: reference(1), }, ], - signatures: Vec::new(), - product_labels: Default::default(), - }, + Vec::new(), + Default::default(), + ), functions: vec![cc::Function { symbol: main_symbol, name: "main".into(), @@ -469,6 +474,7 @@ pub(crate) fn option_list_list_fixture() -> (cc::Module, ExternalBindings, Resol span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), diff --git a/crates/psrs-backend/src/mir/indirect_tests/record_list/fixtures/wide_flags.rs b/crates/psrs-backend/src/mir/indirect_tests/record_list/fixtures/wide_flags.rs index 48cd2ab3..1ed497f1 100644 --- a/crates/psrs-backend/src/mir/indirect_tests/record_list/fixtures/wide_flags.rs +++ b/crates/psrs-backend/src/mir/indirect_tests/record_list/fixtures/wide_flags.rs @@ -97,6 +97,7 @@ pub(crate) fn wide_flags_list_fixture() -> (cc::Module, ExternalBindings, Resolv assignments.push(Assignment { destination: result, kind: AssignmentKind::DirectCall { + contract: None, function: SymbolId::new(ModuleId::INTRINSICS, FOREIGN_SYMBOL_BASE), arguments: vec![array], }, @@ -194,6 +195,7 @@ pub(crate) fn wide_flags_record_fixture() -> (cc::Module, ExternalBindings, Reso assignments.push(Assignment { destination: result, kind: AssignmentKind::DirectCall { + contract: None, function: SymbolId::new(ModuleId::INTRINSICS, FOREIGN_SYMBOL_BASE), arguments: vec![array], }, @@ -309,6 +311,7 @@ pub(crate) fn wide_flags_variant_fixture() -> (cc::Module, ExternalBindings, Res assignments.push(Assignment { destination: result, kind: AssignmentKind::DirectCall { + contract: None, function: SymbolId::new(ModuleId::INTRINSICS, FOREIGN_SYMBOL_BASE), arguments: vec![array], }, @@ -368,11 +371,11 @@ fn finish( result: ValueShape::Integer, }), }], - representations: cc::RepresentationTable { + representations: cc::RepresentationTable::from_parts( representations, - signatures: Vec::new(), - product_labels: labels.into_iter().collect(), - }, + Vec::new(), + labels.into_iter().collect(), + ), functions: vec![cc::Function { symbol: main_symbol, name: "main".into(), @@ -387,6 +390,7 @@ fn finish( span: span(), }; let bindings = ExternalBindings { + runtime: Vec::new(), imports: vec![ExternalBinding { symbol: external_symbol, source_module: ModuleId(0), diff --git a/crates/psrs-backend/src/mir/instruction/methods.rs b/crates/psrs-backend/src/mir/instruction/methods.rs index c96d5aab..52f9dace 100644 --- a/crates/psrs-backend/src/mir/instruction/methods.rs +++ b/crates/psrs-backend/src/mir/instruction/methods.rs @@ -7,6 +7,7 @@ impl Instruction { pub fn destination(&self) -> Option { match self { Self::Copy { destination, .. } + | Self::ControlEvidence { destination, .. } | Self::Constant { destination, .. } | Self::NumberConstant { destination, .. } | Self::ArrayNewData { destination, .. } @@ -29,6 +30,7 @@ impl Instruction { | Self::ArrayNew { destination, .. } | Self::ArrayNewDefault { destination, .. } | Self::ArrayNewSized { destination, .. } + | Self::ArrayNewFilled { destination, .. } | Self::ArrayGet { destination, .. } | Self::ArrayGetU { destination, .. } | Self::ArrayClone { destination, .. } @@ -69,6 +71,16 @@ impl Instruction { pub fn operands(&self) -> Vec { match self { Self::Copy { value, .. } => vec![*value], + Self::ControlEvidence { + value, + implementation, + arguments, + .. + } => { + let mut uses = arguments.clone(); + uses.extend([*value, *implementation]); + uses + } Self::Constant { .. } | Self::NumberConstant { .. } | Self::ArrayNewData { .. } => { Vec::new() } @@ -109,6 +121,7 @@ impl Instruction { } => arguments.clone(), Self::ArrayNewDefault { length, source, .. } => vec![*length, *source], Self::ArrayNewSized { length, .. } => vec![*length], + Self::ArrayNewFilled { length, value, .. } => vec![*length, *value], Self::StructSet { value, new_value, .. } => vec![*value, *new_value], @@ -159,6 +172,7 @@ impl Instruction { pub fn span(&self) -> TextRange { match self { Self::Copy { span, .. } + | Self::ControlEvidence { span, .. } | Self::Constant { span, .. } | Self::NumberConstant { span, .. } | Self::ArrayNewData { span, .. } @@ -183,6 +197,7 @@ impl Instruction { | Self::ArrayNew { span, .. } | Self::ArrayNewDefault { span, .. } | Self::ArrayNewSized { span, .. } + | Self::ArrayNewFilled { span, .. } | Self::ArrayGet { span, .. } | Self::ArrayGetU { span, .. } | Self::ArrayClone { span, .. } diff --git a/crates/psrs-backend/src/mir/instruction/mod.rs b/crates/psrs-backend/src/mir/instruction/mod.rs index 54430977..f0b372c6 100644 --- a/crates/psrs-backend/src/mir/instruction/mod.rs +++ b/crates/psrs-backend/src/mir/instruction/mod.rs @@ -19,6 +19,15 @@ pub enum ListDirection { #[derive(Clone, Debug, PartialEq, Eq)] pub enum Instruction { + /// Source proof retained beside an already represented runtime value. + ControlEvidence { + destination: ValueId, + value: ValueId, + implementation: ValueId, + arguments: Vec, + contract: crate::cc::ControlContractId, + span: TextRange, + }, Copy { destination: ValueId, value: ValueId, @@ -56,6 +65,8 @@ pub enum Instruction { span: TextRange, }, Call { + /// Retained until a checked call conversion discharges its callee binding. + contract: Option, destination: ValueId, function: SymbolId, arguments: Vec, @@ -190,6 +201,14 @@ pub enum Instruction { length: ValueId, span: TextRange, }, + /// Fresh fully initialized allocation; the element must fit the storage type. + ArrayNewFilled { + destination: ValueId, + type_index: DefinedTypeId, + length: ValueId, + value: ValueId, + span: TextRange, + }, ArrayGet { destination: ValueId, type_index: DefinedTypeId, diff --git a/crates/psrs-backend/src/mir/layout/accessors.rs b/crates/psrs-backend/src/mir/layout/accessors.rs index 4b6d05fb..5833416a 100644 --- a/crates/psrs-backend/src/mir/layout/accessors.rs +++ b/crates/psrs-backend/src/mir/layout/accessors.rs @@ -8,6 +8,10 @@ use crate::cc::{ use crate::types::{DefinedTypeId, HeapType, RefType, ValueType}; impl PlannedLayout { + pub(crate) fn types(&self) -> &[crate::types::RecGroup] { + &self.types + } + pub(crate) fn repr_index(&self, id: ReprId) -> Result { self.repr_indices .get(&id) @@ -43,6 +47,23 @@ impl PlannedLayout { .ok_or(LayoutError::UnknownField) } + /// Nominal case identity and its immutable physical tag initializer. + pub(crate) fn variant_tags(&self) -> impl Iterator + '_ { + self.variant_indices + .iter() + .map(|((_, tag), index)| (*index, *tag as i32)) + } + + pub(crate) fn variant_bases(&self) -> impl Iterator + '_ { + self.repr_indices.iter().filter_map(|(repr, index)| { + matches!( + self.representation_table().representation(*repr), + Some(crate::cc::Representation::Variant { .. }) + ) + .then_some(*index) + }) + } + /// The stored field shapes of a variant case, before the tag field. pub(crate) fn variant_field( &self, diff --git a/crates/psrs-backend/src/mir/layout/groups.rs b/crates/psrs-backend/src/mir/layout/groups.rs new file mode 100644 index 00000000..64be2f4a --- /dev/null +++ b/crates/psrs-backend/src/mir/layout/groups.rs @@ -0,0 +1,254 @@ +//! Dependency-minimal recursion groups and their complete type-ID remapping. +use super::*; + +pub(super) fn partition( + definitions: Vec, +) -> Result<(Vec, Vec), LayoutError> { + let edges = definitions.iter().map(dependencies).collect::>(); + if edges.iter().flatten().any(|id| *id >= definitions.len()) { + return Err(LayoutError::UnknownRepresentation); + } + let components = components(&edges); + let mut mapping = vec![DefinedTypeId(0); definitions.len()]; + let mut next = 0; + for component in &components { + for original in component { + mapping[*original] = DefinedTypeId(next); + next += 1; + } + } + let groups = components + .into_iter() + .map(|component| { + RecGroup( + component + .into_iter() + .map(|original| { + let mut definition = definitions[original].clone(); + remap(&mut definition, &mapping); + definition + }) + .collect(), + ) + }) + .collect(); + Ok((groups, mapping)) +} + +fn dependencies(definition: &DefinedType) -> Vec { + let mut result = definition + .supertype + .into_iter() + .map(|id| id.0 as usize) + .collect::>(); + let mut reference = |heap| { + if let HeapType::Index(id) = heap { + result.push(id.0 as usize); + } + }; + match &definition.composite { + CompositeType::Func { + parameters, + results, + } => { + for value in parameters.iter().chain(results) { + if let ValueType::Ref(ty) = value { + reference(ty.heap); + } + } + } + CompositeType::Struct(fields) => { + for field in fields { + if let StorageType::Ref(ty) = field.storage { + reference(ty.heap); + } + } + } + CompositeType::Array(field) => { + if let StorageType::Ref(ty) = field.storage { + reference(ty.heap); + } + } + } + result.sort_unstable(); + result.dedup(); + result +} + +/// Iterative Tarjan traversal avoids Rust stack growth for large type graphs. +/// Edges point from a definition to its dependencies, so completed components +/// already have the order required by Wasm: dependencies precede dependents. +fn components(edges: &[Vec]) -> Vec> { + let mut indices = vec![None; edges.len()]; + let mut low = vec![0; edges.len()]; + let mut active = vec![false; edges.len()]; + let mut stack = Vec::new(); + let mut frames = Vec::new(); + let mut result = Vec::new(); + let mut next = 0; + for root in 0..edges.len() { + if indices[root].is_some() { + continue; + } + indices[root] = Some(next); + low[root] = next; + next += 1; + active[root] = true; + stack.push(root); + frames.push((root, 0)); + while let Some((node, edge)) = frames.last_mut() { + let node = *node; + if let Some(child) = edges[node].get(*edge).copied() { + *edge += 1; + if let Some(index) = indices[child] { + if active[child] { + low[node] = low[node].min(index); + } + } else { + indices[child] = Some(next); + low[child] = next; + next += 1; + active[child] = true; + stack.push(child); + frames.push((child, 0)); + } + continue; + } + frames.pop(); + if low[node] == indices[node].unwrap() { + let mut component = Vec::new(); + loop { + let member = stack.pop().unwrap(); + active[member] = false; + component.push(member); + if member == node { + break; + } + } + component.sort_unstable(); + result.push(component); + } + if let Some((parent, _)) = frames.last() { + low[*parent] = low[*parent].min(low[node]); + } + } + } + result +} + +fn remap(definition: &mut DefinedType, mapping: &[DefinedTypeId]) { + let reference = |ty: &mut RefType| { + if let HeapType::Index(id) = &mut ty.heap { + *id = mapping[id.0 as usize]; + } + }; + if let Some(id) = &mut definition.supertype { + *id = mapping[id.0 as usize]; + } + match &mut definition.composite { + CompositeType::Func { + parameters, + results, + } => { + for value in parameters.iter_mut().chain(results) { + if let ValueType::Ref(ty) = value { + reference(ty); + } + } + } + CompositeType::Struct(fields) => { + for field in fields { + if let StorageType::Ref(ty) = &mut field.storage { + reference(ty); + } + } + } + CompositeType::Array(field) => { + if let StorageType::Ref(ty) = &mut field.storage { + reference(ty); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn structure(references: &[u32]) -> DefinedType { + DefinedType { + final_type: true, + supertype: None, + composite: CompositeType::Struct( + references + .iter() + .map(|id| FieldType { + storage: StorageType::Ref(RefType { + nullable: true, + heap: HeapType::Index(DefinedTypeId(*id)), + }), + mutable: false, + }) + .collect(), + ), + } + } + + #[test] + fn cycles_share_a_group_and_acyclic_dependencies_precede_users() { + let (groups, mapping) = partition(vec![ + structure(&[1, 3]), + structure(&[2]), + structure(&[1]), + structure(&[]), + structure(&[]), + ]) + .unwrap(); + assert_eq!( + groups.iter().map(|g| g.0.len()).collect::>(), + [2, 1, 1, 1] + ); + assert_eq!( + mapping, + [ + DefinedTypeId(3), + DefinedTypeId(0), + DefinedTypeId(1), + DefinedTypeId(2), + DefinedTypeId(4) + ] + ); + let CompositeType::Struct(fields) = &groups[2].0[0].composite else { + panic!("struct expected"); + }; + assert_eq!( + fields[0].storage, + StorageType::Ref(RefType { + nullable: true, + heap: HeapType::Index(mapping[1]) + }) + ); + assert_eq!( + fields[1].storage, + StorageType::Ref(RefType { + nullable: true, + heap: HeapType::Index(mapping[3]) + }) + ); + } + + #[test] + fn a_large_acyclic_type_chain_uses_no_recursive_rust_traversal() { + let mut definitions = (1..10_000).map(|id| structure(&[id])).collect::>(); + definitions.push(structure(&[])); + let (groups, mapping) = partition(definitions).unwrap(); + assert_eq!(groups.len(), 10_000); + assert_eq!(mapping[0], DefinedTypeId(9_999)); + assert_eq!(mapping[9_999], DefinedTypeId(0)); + } + + #[test] + fn dangling_type_edges_are_rejected_before_remapping() { + assert!(partition(vec![structure(&[1])]).is_err()); + } +} diff --git a/crates/psrs-backend/src/mir/layout/mod.rs b/crates/psrs-backend/src/mir/layout/mod.rs index 796736df..7b46010f 100644 --- a/crates/psrs-backend/src/mir/layout/mod.rs +++ b/crates/psrs-backend/src/mir/layout/mod.rs @@ -14,6 +14,7 @@ use crate::types::{ use std::collections::HashMap; mod accessors; +mod groups; mod validate; use validate::validate_selected; @@ -341,12 +342,29 @@ impl PlannedLayout { signature_indices.insert(*id, index); } + let (types, mapping) = groups::partition(definitions)?; + let remap = |id: DefinedTypeId| mapping[id.0 as usize]; + for id in repr_indices.values_mut() { + *id = remap(*id); + } + for id in variant_indices.values_mut() { + *id = remap(*id); + } + for id in signature_indices.values_mut() { + *id = remap(*id); + } + let product_fields = product_fields + .into_iter() + .map(|(id, fields)| (remap(id), fields)) + .collect(); + let closure_index = closure_index.map(remap); + let capture_array_index = capture_array_index.map(remap); + let boxed_integer_index = boxed_integer_index.map(remap); + let boxed_number_index = boxed_number_index.map(remap); + let string_index = string_index.map(remap); + Ok(Self { - types: if definitions.is_empty() { - Vec::new() - } else { - vec![RecGroup(definitions)] - }, + types, repr_indices, product_fields, array_elements, @@ -368,9 +386,10 @@ impl PlannedLayout { } } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] pub(crate) enum LayoutError { UnknownRepresentation, + InvalidPayloadConversion(String), UnknownSignature, UnknownField, MissingIntegerBox, @@ -382,6 +401,17 @@ pub(crate) enum LayoutError { UnsupportedValue, } +impl std::fmt::Display for LayoutError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidPayloadConversion(message) => { + write!(f, "invalid payload storage conversion: {message}") + } + other => write!(f, "{other:?}"), + } + } +} + /// The concrete Wasm value type behind a MIR value type. `Boolean` and `I32` /// share `i32`, so signatures that differ only there must share a function type. fn concrete_value_type(value: ValueType) -> ValueType { @@ -457,3 +487,6 @@ fn array_storage_type( #[cfg(test)] mod tests; + +#[cfg(test)] +mod storage_tests; diff --git a/crates/psrs-backend/src/mir/layout/storage_tests.rs b/crates/psrs-backend/src/mir/layout/storage_tests.rs new file mode 100644 index 00000000..b0dc9dfc --- /dev/null +++ b/crates/psrs-backend/src/mir/layout/storage_tests.rs @@ -0,0 +1,216 @@ +//! Cross-module ABI evidence using the actual planner and Wasm encoder. +use super::*; +use crate::cc::{Reference, Signature}; +use crate::wasm::{self, Export, ExportIndex, ExportKind, FuncType, FunctionIndex, Op, TypeIndex}; +use psrs_hir::{ModuleId, SymbolId}; +use psrs_span::TextRange; +use wasm_encoder::{HeapType as H, Instruction as I, RefType as R, ValType as V}; + +fn consumer(merge_groups: bool) -> Vec { + let mut table = RepresentationTable::default(); + let array = table.reserve(); + table.set( + array, + Representation::Array { + element: CcValueShape::Reference(Reference { + nullable: true, + heap: CcRefShape::Erased, + }), + }, + ); + let boxed = table.reserve(); + table.set( + boxed, + Representation::Box { + value: CcValueShape::Integer, + }, + ); + table.add_signature(Signature { + parameters: vec![CcValueShape::Integer], + result: CcValueShape::Integer, + }); + // Unrelated closure and box types must not become members of the array's + // canonical recursion group merely because this application uses them. + let layout = PlannedLayout::plan(&table, TargetCapabilities::default()).unwrap(); + let array = layout.repr_index(array).unwrap().0; + let boxed = layout.repr_index(boxed).unwrap().0; + let array_ref = |nullable| { + V::Ref(R { + nullable, + heap_type: H::Concrete(array), + }) + }; + let box_ref = V::Ref(R { + nullable: true, + heap_type: H::Concrete(boxed), + }); + let type_count = layout.types.iter().map(|g| g.0.len() as u32).sum::(); + let span = TextRange::new(0, 1); + let raw = |instruction| Op::Leaf(instruction); + let body = vec![ + raw(I::I32Const(7)), + raw(I::StructNew(boxed)), + raw(I::LocalSet(2)), + raw(I::I32Const(78)), + raw(I::StructNew(boxed)), + raw(I::LocalSet(3)), + raw(I::I32Const(2)), + raw(I::LocalGet(2)), + raw(I::Call(0)), + raw(I::LocalSet(0)), + raw(I::LocalGet(0)), + raw(I::LocalSet(1)), + raw(I::LocalGet(0)), + raw(I::RefAsNonNull), + raw(I::I32Const(0)), + raw(I::Call(1)), + raw(I::LocalGet(2)), + raw(I::RefEq), + raw(I::I32Eqz), + Op::If { + then_body: vec![raw(I::Unreachable)], + else_body: Vec::new(), + result: None, + span, + }, + raw(I::LocalGet(0)), + raw(I::RefAsNonNull), + raw(I::I32Const(1)), + raw(I::LocalGet(3)), + raw(I::Call(2)), + raw(I::LocalGet(1)), + raw(I::RefAsNonNull), + raw(I::I32Const(1)), + raw(I::Call(1)), + raw(I::LocalGet(3)), + raw(I::RefEq), + raw(I::I32Eqz), + Op::If { + then_body: vec![raw(I::Unreachable)], + else_body: Vec::new(), + result: None, + span, + }, + raw(I::LocalGet(1)), + raw(I::RefAsNonNull), + raw(I::I32Const(1)), + raw(I::Call(1)), + raw(I::RefCastNonNull(H::Concrete(boxed))), + raw(I::StructGet { + struct_type_index: boxed, + field_index: 0, + }), + ]; + let imports = ["array_fill", "array_read", "array_write"] + .into_iter() + .enumerate() + .map(|(index, name)| wasm::Import { + module: psrs_runtime::STORAGE_MODULE.into(), + name: name.into(), + type_index: TypeIndex(type_count + index as u32), + }) + .collect(); + let types = vec![ + FuncType { + parameters: vec![V::I32, V::Ref(R::EQREF)], + results: vec![array_ref(false)], + }, + FuncType { + parameters: vec![array_ref(false), V::I32], + results: vec![V::Ref(R::EQREF)], + }, + FuncType { + parameters: vec![array_ref(false), V::I32, V::Ref(R::EQREF)], + results: Vec::new(), + }, + FuncType { + parameters: Vec::new(), + results: vec![V::I32], + }, + ]; + let type_defs = if merge_groups { + vec![RecGroup( + layout.types.into_iter().flat_map(|group| group.0).collect(), + )] + } else { + layout.types + }; + let module = wasm::Module { + name: "storage_planned_consumer".into(), + imports, + types, + type_defs, + functions: vec![wasm::Function { + symbol: SymbolId::new(ModuleId(0), 0), + name: "test".into(), + type_index: TypeIndex(type_count + 3), + parameters: Vec::new(), + locals: vec![array_ref(true), array_ref(true), box_ref, box_ref], + body, + span, + }], + memories: Vec::new(), + data: Vec::new(), + globals: Vec::new(), + helpers: Vec::new(), + entry: None, + realloc: None, + exports: vec![Export { + name: "test".into(), + kind: ExportKind::Function, + index: ExportIndex::Function(FunctionIndex(3)), + }], + span, + }; + let bytes = wasm::encode_module(&module).unwrap(); + crate::validator().validate_all(&bytes).unwrap(); + bytes +} + +#[test] +fn planned_gc_array_matches_runtime_storage_and_preserves_aliases() { + use std::process::Command; + if !Command::new("wasmtime") + .arg("--version") + .output() + .is_ok_and(|version| version.status.success()) + { + assert_ne!( + std::env::var("PSRS_REQUIRE_WASMTIME").as_deref(), + Ok("1"), + "Wasmtime is required" + ); + return; + } + let directory = + std::env::temp_dir().join(format!("psrs-planned-storage-{}", std::process::id())); + std::fs::create_dir(&directory).unwrap(); + let provider = directory.join("storage.wasm"); + let guest = directory.join("guest.wasm"); + std::fs::write(&provider, psrs_runtime::storage::encode_module()).unwrap(); + let run = || { + Command::new("wasmtime") + .args(["run", "--preload"]) + .arg(format!( + "{}={}", + psrs_runtime::STORAGE_MODULE, + provider.display() + )) + .args(["--invoke", "test"]) + .arg(&guest) + .output() + .unwrap() + }; + std::fs::write(&guest, consumer(true)).unwrap(); + let rejected = run(); + assert!(!rejected.status.success()); + assert!( + String::from_utf8_lossy(&rejected.stderr).contains("incompatible import type"), + "{rejected:?}" + ); + std::fs::write(&guest, consumer(false)).unwrap(); + let output = run(); + std::fs::remove_dir_all(directory).unwrap(); + assert!(output.status.success(), "{output:?}"); + assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "78"); +} diff --git a/crates/psrs-backend/src/mir/layout/tests.rs b/crates/psrs-backend/src/mir/layout/tests.rs index b30519bb..ff14ccda 100644 --- a/crates/psrs-backend/src/mir/layout/tests.rs +++ b/crates/psrs-backend/src/mir/layout/tests.rs @@ -3,6 +3,10 @@ use crate::cc::{Function, Module as CcModule, Reference, Signature, ValueDecl, V use psrs_hir::{ModuleId, SymbolId}; use psrs_span::TextRange; +fn flattened_definitions(layout: &PlannedLayout) -> Vec<&DefinedType> { + layout.types.iter().flat_map(|group| &group.0).collect() +} + #[test] fn planner_owns_the_gc_closure_and_capture_layouts() { let mut table = RepresentationTable::default(); @@ -13,7 +17,7 @@ fn planner_owns_the_gc_closure_and_capture_layouts() { let layout = PlannedLayout::plan(&table, TargetCapabilities::default()) .expect("planning a closure signature"); - let definitions = &layout.types[0].0; + let definitions = flattened_definitions(&layout); assert!(matches!(definitions[0].composite, CompositeType::Array(_))); assert!(matches!(definitions[1].composite, CompositeType::Struct(_))); @@ -45,9 +49,10 @@ fn signatures_that_lower_to_the_same_wasm_type_share_one_definition() { layout.signature_index(SignatureId(1)).unwrap(), "Boolean and Integer both lower to i32 and must share a function type" ); - let function_types = layout.types[0] - .0 + let function_types = layout + .types .iter() + .flat_map(|group| &group.0) .filter(|definition| matches!(definition.composite, CompositeType::Func { .. })) .count(); assert_eq!( @@ -58,16 +63,16 @@ fn signatures_that_lower_to_the_same_wasm_type_share_one_definition() { #[test] fn planner_rejects_a_dangling_closure_signature() { - let table = RepresentationTable { - representations: vec![Representation::Product { + let table = RepresentationTable::from_parts( + vec![Representation::Product { fields: vec![CcValueShape::Reference(Reference { nullable: false, heap: CcRefShape::Closure(SignatureId(0)), })], }], - signatures: Vec::new(), - product_labels: Default::default(), - }; + Vec::new(), + Default::default(), + ); assert!(matches!( PlannedLayout::plan(&table, TargetCapabilities::default()), Err(LayoutError::UnknownSignature) @@ -75,11 +80,11 @@ fn planner_rejects_a_dangling_closure_signature() { } #[test] fn gc_planner_rejects_an_mvp_only_target() { - let table = RepresentationTable { - representations: vec![Representation::Product { fields: Vec::new() }], - signatures: Vec::new(), - product_labels: Default::default(), - }; + let table = RepresentationTable::from_parts( + vec![Representation::Product { fields: Vec::new() }], + Vec::new(), + Default::default(), + ); let mvp_only = TargetCapabilities { gc: false, @@ -155,7 +160,7 @@ fn maps_every_cc_value_shape_to_its_specified_mir_type() { let layout = PlannedLayout::plan(&table, TargetCapabilities::default()) .expect("planning every scalar and reference shape"); - let definitions = &layout.types[0].0; + let definitions = flattened_definitions(&layout); let product_index = layout.repr_index(product).unwrap(); let closure_index = layout.closure_layout().unwrap().0; @@ -253,7 +258,7 @@ fn maps_every_cc_value_shape_to_its_specified_mir_type() { } #[test] -fn plans_mutually_recursive_requirements_in_one_recursion_group() { +fn plans_dependency_order_for_recursive_variant_requirements() { let mut table = RepresentationTable::default(); let variant = table.reserve(); let product = table.reserve(); @@ -293,10 +298,10 @@ fn plans_mutually_recursive_requirements_in_one_recursion_group() { PlannedLayout::plan(&table, TargetCapabilities::default()).expect("planning recursion"); assert_eq!( layout.types.len(), - 1, - "all definitions share one recursion group" + 4, + "the tag base, product, array and case have acyclic concrete type dependencies" ); - let definitions = &layout.types[0].0; + let definitions = flattened_definitions(&layout); assert_eq!( definitions.len(), 4, @@ -326,7 +331,7 @@ fn plans_mutually_recursive_requirements_in_one_recursion_group() { Some(variant_index) ); - for definition in definitions { + for definition in &definitions { for field in match &definition.composite { CompositeType::Struct(fields) => fields.clone(), CompositeType::Array(field) => vec![*field], @@ -376,7 +381,7 @@ fn plans_field_bearing_variants_as_tag_carrying_supertypes() { ); let layout = PlannedLayout::plan(&table, TargetCapabilities::default()).expect("planning a mixed sum"); - let definitions = &layout.types[0].0; + let definitions = flattened_definitions(&layout); let supertype = layout.repr_index(variant).unwrap(); assert!( !definitions[supertype.0 as usize].final_type, @@ -423,7 +428,7 @@ fn plans_the_uniform_closure_and_capture_array_layout() { }); let layout = PlannedLayout::plan(&table, TargetCapabilities::default()).expect("planning a closure"); - let definitions = &layout.types[0].0; + let definitions = flattened_definitions(&layout); let (closure, capture) = layout.closure_layout().unwrap(); let CompositeType::Array(element) = &definitions[capture.0 as usize].composite else { diff --git a/crates/psrs-backend/src/mir/lower/aggregate/mod.rs b/crates/psrs-backend/src/mir/lower/aggregate/mod.rs index 3f86363a..1dffb401 100644 --- a/crates/psrs-backend/src/mir/lower/aggregate/mod.rs +++ b/crates/psrs-backend/src/mir/lower/aggregate/mod.rs @@ -33,7 +33,7 @@ impl FunctionLowerer<'_> { Ok(block) } - fn lower_value_conversion( + pub(in crate::mir) fn lower_value_conversion( &mut self, block: BlockId, value: ValueId, @@ -169,6 +169,7 @@ impl FunctionLowerer<'_> { self.append_instruction( block, Instruction::Call { + contract: None, destination: result, function: *function, arguments: vec![value], diff --git a/crates/psrs-backend/src/mir/lower/array_assignments.rs b/crates/psrs-backend/src/mir/lower/array_assignments.rs index 8700108a..169d4ff8 100644 --- a/crates/psrs-backend/src/mir/lower/array_assignments.rs +++ b/crates/psrs-backend/src/mir/lower/array_assignments.rs @@ -33,6 +33,59 @@ impl FunctionLowerer<'_> { }, assignment.span, )?, + AssignmentKind::ArrayFill { + destination, + representation, + length, + value, + } => { + // A negative source Int is an error, not an unsigned huge allocation. + let zero = self.fresh(crate::types::ValueType::I32); + self.append_instruction( + current, + Instruction::Constant { + destination: zero, + value: 0, + span: assignment.span, + }, + assignment.span, + )?; + let negative = self.fresh(crate::types::ValueType::Boolean); + self.append_instruction( + current, + Instruction::Primitive { + destination: negative, + op: crate::mir::NumericOp::I32LtS, + left: *length, + right: zero, + span: assignment.span, + }, + assignment.span, + )?; + self.append_instruction( + current, + Instruction::TrapIf { + condition: negative, + span: assignment.span, + }, + assignment.span, + )?; + let type_index = self + .layout + .repr_index(*representation) + .map_err(|error| layout_error(assignment.span, error))?; + self.append_instruction( + current, + Instruction::ArrayNewFilled { + destination: *destination, + type_index, + length: *length, + value: *value, + span: assignment.span, + }, + assignment.span, + )?; + } AssignmentKind::ArrayLen { destination, value } => self.append_instruction( current, Instruction::ArrayLen { diff --git a/crates/psrs-backend/src/mir/lower/assignment_string/mod.rs b/crates/psrs-backend/src/mir/lower/assignment_string/mod.rs index 5151214d..516d7aca 100644 --- a/crates/psrs-backend/src/mir/lower/assignment_string/mod.rs +++ b/crates/psrs-backend/src/mir/lower/assignment_string/mod.rs @@ -158,6 +158,7 @@ impl FunctionLowerer<'_> { self.append_instruction( current, Instruction::Call { + contract: None, destination: buffer, function: crate::abi::REALLOC_SYMBOL, arguments: vec![zero, zero, align, capacity], @@ -196,6 +197,7 @@ impl FunctionLowerer<'_> { self.append_instruction( current, Instruction::Call { + contract: None, destination: string, function: crate::abi::BYTES_TO_STRING_SYMBOL, arguments: vec![buffer, length], @@ -209,6 +211,7 @@ impl FunctionLowerer<'_> { self.append_instruction( current, Instruction::Call { + contract: None, destination: discarded, function: crate::abi::REALLOC_SYMBOL, arguments: vec![buffer, capacity, align, free_size], @@ -278,7 +281,7 @@ impl FunctionLowerer<'_> { Ok(capacity) } - fn constant( + pub(super) fn constant( &mut self, block: BlockId, value: i32, diff --git a/crates/psrs-backend/src/mir/lower/assignments.rs b/crates/psrs-backend/src/mir/lower/assignments.rs index 1a59d604..7d8750e4 100644 --- a/crates/psrs-backend/src/mir/lower/assignments.rs +++ b/crates/psrs-backend/src/mir/lower/assignments.rs @@ -8,6 +8,44 @@ impl FunctionLowerer<'_> { ) -> Result> { for assignment in assignments { match &assignment.kind { + AssignmentKind::ControlEvidence { + value, + implementation, + contract, + arguments, + } => { + let proof = self + .layout + .representation_table() + .control_contracts() + .get(contract.0 as usize) + .ok_or_else(|| { + vec![BackendError::invalid_ir( + "P9 MIR lowering", + assignment.span, + "control evidence has no checked source contract", + )] + })?; + if proof.arguments().len() != arguments.len() { + return Err(vec![BackendError::invalid_ir( + "P9 MIR lowering", + assignment.span, + "control evidence argument count disagrees with its checked source contract", + )]); + } + self.append_instruction( + current, + Instruction::ControlEvidence { + destination: assignment.destination, + value: *value, + implementation: *implementation, + arguments: arguments.clone(), + contract: *contract, + span: assignment.span, + }, + assignment.span, + )?; + } AssignmentKind::AggregateConvert { value, conversion, .. } => { @@ -19,6 +57,7 @@ impl FunctionLowerer<'_> { self.append_instruction( current, Instruction::Call { + contract: None, destination: assignment.destination, function: helper, arguments: vec![*value], @@ -91,14 +130,33 @@ impl FunctionLowerer<'_> { )?; continue; } - let instruction = self.scalar_helpers.binary_instruction( - *op, + let operation = super::super::NumericOp::try_from(*op).map_err(|op| { + vec![BackendError::invalid_ir( + "P9 MIR lowering", + assignment.span, + format!("unsupported scalar operation {op:?}"), + )] + })?; + let instruction = Instruction::Primitive { + destination: assignment.destination, + op: operation, + left: *left, + right: *right, + span: assignment.span, + }; + self.append_instruction(current, instruction, assignment.span)?; + } + AssignmentKind::RuntimeCall { + intrinsic, + arguments, + } => { + self.lower_runtime_call( + current, assignment.destination, - *left, - *right, + *intrinsic, + arguments, assignment.span, )?; - self.append_instruction(current, instruction, assignment.span)?; } AssignmentKind::Unary { op, value } => self.append_instruction( current, @@ -188,6 +246,7 @@ impl FunctionLowerer<'_> { AssignmentKind::ArrayNew { .. } | AssignmentKind::ArrayLen { .. } | AssignmentKind::ArrayAppend { .. } + | AssignmentKind::ArrayFill { .. } | AssignmentKind::StringToBytes { .. } | AssignmentKind::BytesToString { .. } | AssignmentKind::ArrayGet { .. } @@ -196,24 +255,35 @@ impl FunctionLowerer<'_> { current = self.lower_array_assignment(current, assignment)?; } AssignmentKind::DirectCall { + contract, function, arguments, } => { + self.layout + .representation_table() + .verify_control_call(*function, *contract) + .map_err(|message| { + vec![BackendError::invalid_ir( + "P9 MIR lowering", + assignment.span, + message, + )] + })?; if let Some(import) = self.wit_imports.get(function).cloned() { - current = wit::lower( - self, - &import.import, - &import.signature, - import.projection.as_ref(), + current = self.lower_wit_call(&import, assignment, current)?; + } else if let Some(import) = self.runtime_imports.get(function).cloned() { + self.lower_storage_call( + current, assignment.destination, + &import, arguments, assignment.span, - current, )?; } else { self.append_instruction( current, Instruction::Call { + contract: *contract, destination: assignment.destination, function: *function, arguments: arguments.clone(), @@ -306,7 +376,7 @@ impl FunctionLowerer<'_> { } => { let then_block = self.new_block(Vec::new()); let else_block = self.new_block(Vec::new()); - let merge = self.new_block(vec![assignment.destination]); + let merge = self.new_block(self.control_values(assignment.destination)); self.set_terminator( current, Terminator::Branch { @@ -322,7 +392,7 @@ impl FunctionLowerer<'_> { then_end, Terminator::Jump { target: merge, - arguments: vec![*then_value], + arguments: self.control_values(*then_value), span: assignment.span, }, assignment.span, @@ -332,7 +402,7 @@ impl FunctionLowerer<'_> { else_end, Terminator::Jump { target: merge, - arguments: vec![*else_value], + arguments: self.control_values(*else_value), span: assignment.span, }, assignment.span, @@ -350,7 +420,7 @@ impl FunctionLowerer<'_> { .map(|_| self.new_block(Vec::new())) .collect::>(); let default_block = self.new_block(Vec::new()); - let merge = self.new_block(vec![assignment.destination]); + let merge = self.new_block(self.control_values(assignment.destination)); self.set_terminator( current, Terminator::Switch { @@ -371,7 +441,7 @@ impl FunctionLowerer<'_> { end, Terminator::Jump { target: merge, - arguments: vec![case.value], + arguments: self.control_values(case.value), span: assignment.span, }, assignment.span, @@ -382,7 +452,7 @@ impl FunctionLowerer<'_> { end, Terminator::Jump { target: merge, - arguments: vec![*default_value], + arguments: self.control_values(*default_value), span: assignment.span, }, assignment.span, diff --git a/crates/psrs-backend/src/mir/lower/conversion_helpers.rs b/crates/psrs-backend/src/mir/lower/conversion_helpers.rs index 9ba12a19..8b452693 100644 --- a/crates/psrs-backend/src/mir/lower/conversion_helpers.rs +++ b/crates/psrs-backend/src/mir/lower/conversion_helpers.rs @@ -1,5 +1,4 @@ use crate::cc::{self, AggregateConvert, ValueConversion}; -use crate::mir::Function; use crate::types::ValueId; use psrs_hir::{ModuleId, SymbolId}; use psrs_span::TextRange; @@ -15,13 +14,12 @@ pub(in crate::mir) struct ConversionHelpers { } impl ConversionHelpers { - pub(in crate::mir) fn new(module: &cc::Module, scalar_helpers: &[Function]) -> Self { + pub(in crate::mir) fn new(module: &cc::Module) -> Self { let mut used: HashSet = module .functions .iter() .map(|function| function.symbol) .chain(module.externals.iter().map(|external| external.symbol)) - .chain(scalar_helpers.iter().map(|function| function.symbol)) .collect(); // Intrinsic symbols are allocated downward from `u32::MAX`; the // canonical ABI and codec reserve the top indices. @@ -134,7 +132,7 @@ mod tests { entry: None, span: TextRange::new(0, 1), }; - let mut helpers = ConversionHelpers::new(&module, &[]); + let mut helpers = ConversionHelpers::new(&module); let span = TextRange::new(0, 1); let first = array_conversion(ValueConversion::Identity); let different = array_conversion(ValueConversion::EraseReference); diff --git a/crates/psrs-backend/src/mir/lower/mod.rs b/crates/psrs-backend/src/mir/lower/mod.rs index 3d6dcf85..b43a18d4 100644 --- a/crates/psrs-backend/src/mir/lower/mod.rs +++ b/crates/psrs-backend/src/mir/lower/mod.rs @@ -6,7 +6,6 @@ use super::{BasicBlock, BlockId, Function, Terminator}; use crate::BackendError; use crate::cc::{self, AssignmentKind}; use crate::mir::instruction::Instruction; -use crate::mir::scalar_helpers::ScalarHelpers; use crate::types::{FunctionId, HeapType, ValueDecl, ValueId, ValueType}; use psrs_hir::SymbolId; use psrs_span::TextRange; @@ -18,8 +17,11 @@ mod assignment_array; mod assignment_string; mod assignments; mod conversion_helpers; -mod tail; +mod runtime_call; +mod storage_call; +pub(in crate::mir) mod tail; mod variant; +mod wit_call; pub(super) use conversion_helpers::ConversionHelpers; #[cfg(test)] mod wit_tests; @@ -28,7 +30,7 @@ fn layout_error(span: TextRange, error: LayoutError) -> Vec { vec![BackendError::new( "P9 MIR lowering", span, - format!("invalid concrete layout request: {error:?}"), + format!("invalid concrete layout request: {error}"), )] } @@ -45,7 +47,7 @@ pub(super) fn lower_function( source: &cc::Function, id: FunctionId, wit_imports: &HashMap, - scalar_helpers: &ScalarHelpers, + runtime_imports: &HashMap, layout: &PlannedLayout, conversion_helpers: Option<&mut ConversionHelpers>, literals: Option<&mut StringLiterals>, @@ -53,6 +55,8 @@ pub(super) fn lower_function( ) -> Result> { let entry = BlockId(0); let mut lowerer = FunctionLowerer { + owner: source.symbol, + wit_calls: Vec::new(), next_block: 1, blocks: vec![BasicBlock { id: entry, @@ -79,7 +83,7 @@ pub(super) fn lower_function( .max() .map_or(0, |max| max + 1), wit_imports, - scalar_helpers, + runtime_imports, layout, conversion_helpers, literals, @@ -107,23 +111,32 @@ pub(super) fn lower_function( .map_err(|error| layout_error(source.span, error))?, span: source.span, }; + for plan in &lowerer.wit_calls { + plan.verify(&function)?; + } + super::control::verify_helper_calls(&function, Some(layout.representation_table()))?; tail::mark_tail(&mut function, target)?; wit::verify_function(&function, wit_imports)?; Ok(function) } pub(super) struct FunctionLowerer<'a> { + owner: SymbolId, + wit_calls: Vec, next_block: u32, blocks: Vec, values: Vec, next_value: u32, wit_imports: &'a HashMap, - scalar_helpers: &'a ScalarHelpers, - layout: &'a PlannedLayout, + runtime_imports: &'a HashMap, + pub(in crate::mir) layout: &'a PlannedLayout, conversion_helpers: Option<&'a mut ConversionHelpers>, literals: Option<&'a mut StringLiterals>, } impl FunctionLowerer<'_> { + fn control_values(&self, value: ValueId) -> Vec { + vec![value] + } pub(super) fn value_type(&self, id: ValueId) -> Option { self.values .iter() @@ -268,13 +281,24 @@ impl FunctionLowerer<'_> { span: TextRange, ) -> Result<(), Vec> { let target = self.find_block_mut(block, span)?; - if target.terminator.replace(terminator).is_some() { + if target.terminator.is_some() { return Err(vec![BackendError::invalid_ir( "P9 MIR lowering", span, "basic block already has a terminator", )]); } + target.terminator = Some( + if matches!( + target.instructions.last(), + Some(Instruction::Unreachable { .. }) + ) { + // A static trap has no normal return or jump successor. + Terminator::Trap { span } + } else { + terminator + }, + ); Ok(()) } diff --git a/crates/psrs-backend/src/mir/lower/runtime_call.rs b/crates/psrs-backend/src/mir/lower/runtime_call.rs new file mode 100644 index 00000000..0212c03d --- /dev/null +++ b/crates/psrs-backend/src/mir/lower/runtime_call.rs @@ -0,0 +1,130 @@ +//! Language-value transport selected by the artifact's checked value protocol. +use super::*; +use crate::target_intrinsics::{Implementation, implementation}; +use psrs_runtime::RawCallProtocol; + +impl FunctionLowerer<'_> { + pub(super) fn lower_runtime_call( + &mut self, + block: BlockId, + destination: ValueId, + intrinsic: psrs_hir::Intrinsic, + arguments: &[ValueId], + span: TextRange, + ) -> Result<(), Vec> { + let Implementation::Artifact(provider) = implementation(intrinsic) else { + return Err(vec![BackendError::invalid_ir( + "P9 MIR lowering", + span, + "runtime call has no artifact provider", + )]); + }; + provider + .validate_protocol() + .map_err(|error| vec![BackendError::invalid_ir("P9 MIR lowering", span, error)])?; + match provider.abi.protocol { + RawCallProtocol::Scalars => { + self.emit_artifact_call(block, destination, provider, arguments.to_vec(), span) + } + RawCallProtocol::Utf8Input => { + let mut raw = Vec::new(); + let mut frees = Vec::new(); + crate::mir::wit::lower_string( + self, + arguments[0], + &mut raw, + &mut frees, + block, + span, + )?; + self.emit_artifact_call(block, destination, provider, raw, span)?; + for pending in frees { + crate::mir::wit::free_buffer( + self, + pending.pointer, + pending.length, + pending.align, + block, + span, + )?; + } + Ok(()) + } + RawCallProtocol::Utf8Output { capacity } => { + let zero = self.constant(block, 0, span)?; + let align = self.constant(block, 1, span)?; + let capacity = self.constant(block, capacity as i32, span)?; + let buffer = self.fresh(ValueType::I32); + self.append_instruction( + block, + Instruction::Call { + contract: None, + destination: buffer, + function: crate::abi::REALLOC_SYMBOL, + arguments: vec![zero, zero, align, capacity], + span, + }, + span, + )?; + let length = self.fresh(ValueType::I32); + let mut raw = arguments.to_vec(); + raw.extend([buffer, capacity]); + self.emit_artifact_call(block, length, provider, raw, span)?; + self.append_instruction( + block, + Instruction::Call { + contract: None, + destination, + function: crate::abi::BYTES_TO_STRING_SYMBOL, + arguments: vec![buffer, length], + span, + }, + span, + )?; + crate::mir::wit::free_buffer(self, buffer, capacity, 1, block, span) + } + } + } + + fn emit_artifact_call( + &mut self, + block: BlockId, + destination: ValueId, + provider: &crate::target_runtime::ArtifactImplementation, + arguments: Vec, + span: TextRange, + ) -> Result<(), Vec> { + if provider.abi.result.is_some() { + self.append_instruction( + block, + Instruction::Call { + contract: None, + destination, + function: provider.symbol, + arguments, + span, + }, + span, + ) + } else { + self.append_instruction( + block, + Instruction::CallVoid { + function: provider.symbol, + arguments, + span, + }, + span, + )?; + self.append_instruction( + block, + Instruction::Constant { + destination, + value: 0, + span, + }, + span, + ) + } + } +} diff --git a/crates/psrs-backend/src/mir/lower/storage_call.rs b/crates/psrs-backend/src/mir/lower/storage_call.rs new file mode 100644 index 00000000..50dea0b5 --- /dev/null +++ b/crates/psrs-backend/src/mir/lower/storage_call.rs @@ -0,0 +1,119 @@ +//! Ordinary storage calls: CC has already adapted payloads to erased storage. +use super::*; + +impl FunctionLowerer<'_> { + pub(super) fn lower_storage_call( + &mut self, + block: BlockId, + destination: ValueId, + import: &crate::mir::Import, + arguments: &[ValueId], + span: TextRange, + ) -> Result<(), Vec> { + if arguments.len() != import.parameters.len() { + return Err(vec![BackendError::invalid_ir( + "P9 runtime binding", + span, + "storage call has the wrong argument count", + )]); + } + let provider = import.runtime.as_ref().ok_or_else(|| { + vec![BackendError::invalid_ir( + "P9 runtime binding", + span, + "storage call has no provider", + )] + })?; + let operation = psrs_runtime::StorageOperation::from_export(&provider.function) + .ok_or_else(|| { + vec![BackendError::invalid_ir( + "P9 runtime binding", + span, + "storage call has no checked export", + )] + })?; + let mut raw_arguments = Vec::with_capacity(arguments.len()); + for (value, expected) in arguments.iter().copied().zip(&import.parameters) { + if self.value_type(value) == Some(*expected) { + raw_arguments.push(value); + } else if let ValueType::Ref(reference) = expected { + let raw = self.fresh(*expected); + self.append_instruction( + block, + Instruction::RefCast { + destination: raw, + value, + reference: *reference, + span, + }, + span, + )?; + raw_arguments.push(raw); + } else { + return Err(vec![BackendError::invalid_ir( + "P9 runtime binding", + span, + "storage operand disagrees with its raw type", + )]); + } + } + if let Some(result) = import.result { + let raw = if self.value_type(destination) == Some(result) { + destination + } else { + self.fresh(result) + }; + self.append_instruction( + block, + Instruction::Call { + contract: None, + destination: raw, + function: import.symbol, + arguments: raw_arguments, + span, + }, + span, + )?; + if raw != destination { + let Some(ValueType::Ref(reference)) = self.value_type(destination) else { + return Err(vec![BackendError::invalid_ir( + "P9 runtime binding", + span, + "storage result recovery requires a reference", + )]); + }; + self.append_instruction( + block, + Instruction::RefCast { + destination, + value: raw, + reference, + span, + }, + span, + )?; + } + } else { + self.append_instruction( + block, + Instruction::CallVoid { + function: import.symbol, + arguments: raw_arguments, + span, + }, + span, + )?; + let instruction = if operation.abi().returns_normally { + Instruction::Constant { + destination, + value: 0, + span, + } + } else { + Instruction::Unreachable { destination, span } + }; + self.append_instruction(block, instruction, span)?; + } + Ok(()) + } +} diff --git a/crates/psrs-backend/src/mir/lower/tail.rs b/crates/psrs-backend/src/mir/lower/tail.rs index f288628f..976a9d0d 100644 --- a/crates/psrs-backend/src/mir/lower/tail.rs +++ b/crates/psrs-backend/src/mir/lower/tail.rs @@ -21,6 +21,13 @@ use std::collections::{HashMap, HashSet}; pub(super) fn mark_tail( function: &mut Function, target: TargetCapabilities, +) -> Result<(), Vec> { + rewrite(function, target) +} + +pub(in crate::mir) fn rewrite( + function: &mut Function, + target: TargetCapabilities, ) -> Result<(), Vec> { let returned = return_forwarded_values(function); let uses = use_counts(function); @@ -56,13 +63,14 @@ pub(super) fn mark_tail( self_calls.push((block.id, arguments.clone(), *span)); } Instruction::Call { + contract, function: callee, arguments, span, .. } => non_self.push(( block.id, - NonSelfCall::Direct(*callee), + NonSelfCall::Direct(*callee, *contract), arguments.clone(), *span, )), @@ -99,7 +107,8 @@ pub(super) fn mark_tail( if target.tail_call { for (block_id, call, arguments, span) in non_self { let terminator = match call { - NonSelfCall::Direct(callee) => Terminator::ReturnCall { + NonSelfCall::Direct(callee, contract) => Terminator::ReturnCall { + contract, function: callee, arguments, span, @@ -122,7 +131,7 @@ pub(super) fn mark_tail( } enum NonSelfCall { - Direct(psrs_hir::SymbolId), + Direct(psrs_hir::SymbolId, Option), Reference(ValueId), } @@ -311,7 +320,10 @@ fn loopify_self_calls( block.instructions.pop(); block.terminator = Some(Terminator::Jump { target: header_id, - arguments: arguments.clone(), + arguments: arguments + .iter() + .map(|id| mapping.get(id).copied().unwrap_or(*id)) + .collect(), span: *span, }); } @@ -381,6 +393,7 @@ fn use_counts(function: &Function) -> HashMap { fn terminator_operands(terminator: &Terminator) -> Vec { match terminator { + Terminator::Trap { .. } => Vec::new(), Terminator::Return { value, .. } => vec![*value], Terminator::Jump { arguments, .. } => arguments.clone(), Terminator::Branch { condition, .. } => vec![*condition], diff --git a/crates/psrs-backend/src/mir/lower/variant.rs b/crates/psrs-backend/src/mir/lower/variant/mod.rs similarity index 74% rename from crates/psrs-backend/src/mir/lower/variant.rs rename to crates/psrs-backend/src/mir/lower/variant/mod.rs index bd282496..1be22c87 100644 --- a/crates/psrs-backend/src/mir/lower/variant.rs +++ b/crates/psrs-backend/src/mir/lower/variant/mod.rs @@ -4,6 +4,9 @@ use crate::cc::{Assignment, AssignmentKind}; use crate::mir::{BlockId, Instruction}; use crate::types::{HeapType, RefType, ValueType}; +#[cfg(test)] +mod tests; + impl FunctionLowerer<'_> { pub(in crate::mir) fn lower_variant( &mut self, @@ -22,6 +25,24 @@ impl FunctionLowerer<'_> { .layout .variant_index(*representation, *case) .map_err(|error| layout_error(span, error))?; + // CC permits a variant's common representation as well as an + // abstract aggregate result. Construct at the concrete case + // type before widening to a declared common representation. + let declared = self.value_type(*destination); + let widened = match declared { + Some(ValueType::Ref(reference)) if matches!(reference.heap, HeapType::Index(index) if index != case_type) => { + Some(reference) + } + _ => None, + }; + let constructed = if widened.is_some() { + self.fresh(ValueType::Ref(RefType { + nullable: false, + heap: HeapType::Index(case_type), + })) + } else { + *destination + }; let tag = self.fresh(ValueType::I32); self.append_instruction( current, @@ -36,13 +57,26 @@ impl FunctionLowerer<'_> { self.append_instruction( current, Instruction::StructNew { - destination: *destination, + destination: constructed, type_index: case_type, arguments, span, }, span, - ) + )?; + if let Some(reference) = widened { + self.append_instruction( + current, + Instruction::RefCast { + destination: *destination, + value: constructed, + reference, + span, + }, + span, + )?; + } + Ok(()) } AssignmentKind::VariantTag { destination, diff --git a/crates/psrs-backend/src/mir/lower/variant/tests.rs b/crates/psrs-backend/src/mir/lower/variant/tests.rs new file mode 100644 index 00000000..ffc4c8a9 --- /dev/null +++ b/crates/psrs-backend/src/mir/lower/variant/tests.rs @@ -0,0 +1,85 @@ +//! CC admits both abstract and concrete common variant result shapes. + +use crate::cc::{ + Assignment, AssignmentKind, Function, Module, RefShape, Reference, Representation, + RepresentationTable, ValueDecl, ValueId, ValueShape, VariantCase, +}; +use psrs_hir::{ModuleId, SymbolId}; +use psrs_span::TextRange; + +#[test] +fn variant_construction_lowers_at_abstract_and_common_representation_shapes() { + for case in 0..2 { + let mut table = RepresentationTable::default(); + let variant = table.reserve(); + table.set( + variant, + Representation::Variant { + cases: vec![ + VariantCase { + tag: 0, + fields: vec![ValueShape::Integer], + }, + VariantCase { + tag: 1, + fields: vec![], + }, + ], + }, + ); + for heap in [RefShape::Aggregate, RefShape::Repr(variant)] { + let shape = ValueShape::Reference(Reference { + nullable: false, + heap, + }); + let span = TextRange::new(0, 1); + let value = ValueId(0); + let result = ValueId(1); + let module = Module { + name: "VariantResult".into(), + externals: vec![], + representations: table.clone(), + entry: None, + span, + functions: vec![Function { + symbol: SymbolId::new(ModuleId(0), 0), + name: "construct".into(), + parameters: vec![], + span, + result, + result_type: shape, + values: vec![ + ValueDecl { + id: value, + ty: ValueShape::Integer, + }, + ValueDecl { + id: result, + ty: shape, + }, + ], + assignments: vec![ + Assignment { + destination: value, + kind: AssignmentKind::Constant(42), + span, + }, + Assignment { + destination: result, + kind: AssignmentKind::VariantNew { + destination: result, + representation: variant, + case, + fields: if case == 0 { vec![value] } else { vec![] }, + }, + span, + }, + ], + }], + }; + let (mir, _) = crate::mir::lower_module(module).expect("valid variant construction"); + crate::mir::verify_module(&mir) + .expect("the constructed case widens to its declared shape"); + } + } +} diff --git a/crates/psrs-backend/src/mir/lower/wit_call.rs b/crates/psrs-backend/src/mir/lower/wit_call.rs new file mode 100644 index 00000000..94a4ceec --- /dev/null +++ b/crates/psrs-backend/src/mir/lower/wit_call.rs @@ -0,0 +1,79 @@ +//! Canonical adapters are planned independently before instructions are emitted. +use super::*; + +impl FunctionLowerer<'_> { + pub(super) fn lower_wit_call( + &mut self, + import: &BoundWasiImport, + assignment: &cc::Assignment, + entry: BlockId, + ) -> Result> { + let AssignmentKind::DirectCall { arguments, .. } = &assignment.kind else { + return Err(vec![BackendError::invalid_ir( + "P9 canonical call planning", + assignment.span, + "canonical adapter requires a direct source call", + )]); + }; + let start = self + .find_block_mut(entry, assignment.span)? + .instructions + .len(); + let value_start = self.values.len(); + // The isolated builder shares checked layouts and helper ownership, + // but cannot inspect or mutate the actual instruction stream. + let mut builder = FunctionLowerer { + owner: self.owner, + wit_calls: Vec::new(), + next_block: self.next_block, + blocks: vec![BasicBlock { + id: entry, + parameters: Vec::new(), + instructions: Vec::new(), + terminator: None, + }], + values: self.values.clone(), + next_value: self.next_value, + wit_imports: self.wit_imports, + runtime_imports: self.runtime_imports, + layout: self.layout, + conversion_helpers: self.conversion_helpers.as_deref_mut(), + literals: self.literals.as_deref_mut(), + }; + let exit = wit::lower( + &mut builder, + &import.import, + &import.signature, + import.projection.as_ref(), + assignment.destination, + arguments, + assignment.span, + entry, + )?; + let plan = wit::CallPlan::new( + self.owner, + assignment.clone(), + start, + exit, + builder.blocks.clone(), + builder.values[value_start..].to_vec(), + ); + let next_block = builder.next_block; + let next_value = builder.next_value; + let blocks = builder.blocks; + let values = builder.values; + let entry_plan = &blocks[0]; + for instruction in &entry_plan.instructions { + self.append_instruction(entry, instruction.clone(), assignment.span)?; + } + if let Some(terminator) = &entry_plan.terminator { + self.set_terminator(entry, terminator.clone(), assignment.span)?; + } + self.blocks.extend(blocks.into_iter().skip(1)); + self.values.extend(values.into_iter().skip(value_start)); + self.next_block = next_block; + self.next_value = next_value; + self.wit_calls.push(plan); + Ok(exit) + } +} diff --git a/crates/psrs-backend/src/mir/lower/wit_tests.rs b/crates/psrs-backend/src/mir/lower/wit_tests.rs index d3ad36d6..f18b6885 100644 --- a/crates/psrs-backend/src/mir/lower/wit_tests.rs +++ b/crates/psrs-backend/src/mir/lower/wit_tests.rs @@ -22,6 +22,8 @@ fn gc_wit_record_projection_uses_the_planned_product_type() { }); let lowerer_span = TextRange::new(0, 1); let mut lowerer = FunctionLowerer { + owner: psrs_hir::SymbolId::new(psrs_hir::ModuleId(0), 0), + wit_calls: Vec::new(), conversion_helpers: None, next_block: 1, blocks: vec![BasicBlock { @@ -36,7 +38,7 @@ fn gc_wit_record_projection_uses_the_planned_product_type() { }], next_value: 1, wit_imports: &HashMap::new(), - scalar_helpers: &ScalarHelpers::default(), + runtime_imports: &HashMap::new(), layout: &layout, literals: None, }; diff --git a/crates/psrs-backend/src/mir/mod.rs b/crates/psrs-backend/src/mir/mod.rs index 2e39decf..6577b0ec 100644 --- a/crates/psrs-backend/src/mir/mod.rs +++ b/crates/psrs-backend/src/mir/mod.rs @@ -7,6 +7,8 @@ use psrs_span::TextRange; use std::collections::{HashMap, HashSet}; pub(crate) mod cfg; +mod control; +mod effects; mod instruction; pub(crate) mod layout; mod literals; @@ -15,17 +17,18 @@ mod numeric; pub mod opt; mod planner; mod reachable; -mod scalar_helpers; +mod runtime; +mod value_identity; mod verify; mod wit; use literals::StringLiterals; use lower::lower_function; use planner::{GcPlanner, RepresentationPlanner}; -use scalar_helpers::lower_scalar_helpers; pub use instruction::{Instruction, ListDirection}; pub use numeric::{NumericOp, UnaryOp}; +pub use runtime::RuntimeImport; pub use verify::{verify_module, verify_module_with_capabilities}; #[cfg(test)] @@ -35,6 +38,8 @@ mod gc_tests; #[cfg(test)] mod indirect_tests; #[cfg(test)] +mod number_format_tests; +#[cfg(test)] mod tests; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -49,8 +54,7 @@ pub struct Module { pub types: Vec, /// Static string literals referenced by `ArrayNewData` data indices. pub strings: Vec, - /// Runtime ABI imports the module may call. Their canonical signatures come - /// from the WIT runtime ABI; see `docs/decision/DEC-06`. + /// Checked physical ABI imports, selected from WIT or raw runtime contracts. pub imports: Vec, pub functions: Vec, /// The program entry declaration, if selected by the driver. @@ -63,11 +67,14 @@ pub struct Module { pub span: TextRange, } -/// A runtime ABI import, lowered to its canonical ABI signature. +/// An external call target with its checked physical ABI signature. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Import { - /// The canonical ABI symbol the call references. The interface, function, - /// and return-pointer details live in the ABI registry, not here. + /// Explicit runtime provider identity; ordinary source bindings retain this + /// through MIR instead of being assigned a reserved intrinsic symbol. + pub runtime: Option, + /// The call identity. WIT interface and return-pointer details live in the + /// ABI registry; raw runtime provider identity is retained above. pub symbol: SymbolId, pub parameters: Vec, pub result: Option, @@ -110,6 +117,10 @@ pub struct BasicBlock { #[derive(Clone, Debug, PartialEq, Eq)] pub enum Terminator { + /// Ends execution without a result operand or normal successor. + Trap { + span: TextRange, + }, Return { value: ValueId, span: TextRange, @@ -139,6 +150,7 @@ pub enum Terminator { /// A direct tail call. The arguments are evaluated and control transfers to /// `function`, whose result becomes this function's result. ReturnCall { + contract: Option, function: SymbolId, arguments: Vec, span: TextRange, @@ -185,6 +197,19 @@ pub fn lower_module_with_bindings( lower_module_after_binding_validation(module, bindings, target, wasi) } +/// Lowers CC to MIR over the linker's shared resolved definitions, so the +/// registry borrows the same parsed WIT rather than loading a second copy. +pub fn lower_module_with_bindings_and_resolve( + module: cc::Module, + bindings: crate::ExternalBindings, + target: TargetCapabilities, + resolve: std::sync::Arc, +) -> Result<(Module, WasiRegistry), Vec> { + bindings.validate_cc(&module)?; + let wasi = WasiRegistry::from_shared_resolve(resolve, target); + lower_module_after_binding_validation(module, bindings, target, wasi) +} + #[cfg(test)] pub(crate) fn lower_module_with_registry( module: cc::Module, @@ -202,6 +227,8 @@ fn lower_module_after_binding_validation( target: TargetCapabilities, mut wasi: WasiRegistry, ) -> Result<(Module, WasiRegistry), Vec> { + // P8 owns ordinary CC typing. Recheck actual dependency bodies at the + // physical boundary, including callers supplying CC directly. // Resolve every source-declared WIT binding and pair it with the CC // abstract signature. Conformance was already checked at the linking // boundary; a declaration the backend cannot lower is rejected here. @@ -269,9 +296,8 @@ fn lower_module_after_binding_validation( module.entry.map(|entry| entry.module), ) })?; - let (scalar_helpers, generated_helpers) = - lower_scalar_helpers(&module, module.functions.len() as u32); - let mut conversion_helpers = lower::ConversionHelpers::new(&module, &generated_helpers); + let runtime = runtime::RuntimeContext::checked(&bindings.runtime, &module, &layout)?; + let mut conversion_helpers = lower::ConversionHelpers::new(&module); let mut literals = StringLiterals::default(); let mut functions = Vec::with_capacity(module.functions.len()); for (id, function) in module.functions.iter().enumerate() { @@ -279,7 +305,7 @@ fn lower_module_after_binding_validation( function, FunctionId(id as u32), &wit_imports, - &scalar_helpers, + &runtime.imports, &layout, Some(&mut conversion_helpers), Some(&mut literals), @@ -293,14 +319,13 @@ fn lower_module_after_binding_validation( })?; functions.push(lowered); } - functions.extend(generated_helpers); let first_helper_id = functions.len() as u32; for (offset, helper) in conversion_helpers.into_functions().iter().enumerate() { let lowered = lower_function( helper, FunctionId(first_helper_id + offset as u32), &wit_imports, - &scalar_helpers, + &runtime.imports, &layout, None, Some(&mut literals), @@ -316,25 +341,36 @@ fn lower_module_after_binding_validation( .iter() .filter(|import| used.contains(&import.symbol)) .map(|import| Import { + runtime: None, symbol: import.symbol, parameters: import.parameters.clone(), result: import.result, }) .collect(); + imports.extend( + runtime + .imports + .into_values() + .filter(|import| used.contains(&import.symbol)), + ); if used.contains(&crate::abi::REALLOC_SYMBOL) || wasi .imports() .iter() .any(|import| used.contains(&import.symbol) && import.has_indirect_parameters()) { - imports.push(Import { - symbol: crate::abi::REALLOC_SYMBOL, - parameters: vec![ValueType::I32; 4], - result: Some(ValueType::I32), - }); + imports.push( + crate::target_intrinsics::generated::signature(crate::abi::REALLOC_SYMBOL, None) + .expect("allocator signature has no layout dependency"), + ); } - // The canonical ABI boundary transcodes between the GC string's UTF-16 and - // the component's UTF-8. The adapter calls these reserved helpers, which P10 + for implementation in crate::target_intrinsics::artifacts() { + if used.contains(&implementation.symbol) { + imports.push(implementation.import()); + } + } + // The canonical ABI boundary copies canonical UTF-8 between GC strings and + // component linear buffers. The adapter calls these reserved helpers, which P10 // synthesizes as ordinary Wasm functions; they are never core imports. if used.contains(&crate::abi::STRING_TO_BYTES_SYMBOL) || used.contains(&crate::abi::BYTES_TO_STRING_SYMBOL) @@ -351,19 +387,27 @@ fn lower_module_after_binding_validation( module.entry.map(|entry| entry.module), ) })?; - if used.contains(&crate::abi::STRING_TO_BYTES_SYMBOL) { - imports.push(Import { - symbol: crate::abi::STRING_TO_BYTES_SYMBOL, - parameters: vec![string_type], - result: Some(ValueType::I32), - }); - } - if used.contains(&crate::abi::BYTES_TO_STRING_SYMBOL) { - imports.push(Import { - symbol: crate::abi::BYTES_TO_STRING_SYMBOL, - parameters: vec![ValueType::I32, ValueType::I32], - result: Some(string_type), - }); + let ValueType::Ref(crate::types::RefType { + nullable: false, + heap: crate::types::HeapType::Index(string_id), + }) = string_type + else { + return Err(vec![BackendError::invalid_ir( + "P9 MIR lowering", + module.span, + "string layout must be a nonnullable concrete GC reference", + )]); + }; + for symbol in [ + crate::abi::STRING_TO_BYTES_SYMBOL, + crate::abi::BYTES_TO_STRING_SYMBOL, + ] { + if used.contains(&symbol) { + imports.push( + crate::target_intrinsics::generated::signature(symbol, Some(string_id)) + .expect("codec has its checked string layout"), + ); + } } } let strings = literals.into_strings(); diff --git a/crates/psrs-backend/src/mir/number_format_tests.rs b/crates/psrs-backend/src/mir/number_format_tests.rs new file mode 100644 index 00000000..5a4df6f1 --- /dev/null +++ b/crates/psrs-backend/src/mir/number_format_tests.rs @@ -0,0 +1,180 @@ +//! End-to-end formatter artifact execution through the checked plan. + +use super::{BasicBlock, BlockId, Function, Import, Instruction, Module, Terminator}; +use crate::types::{ValueDecl, ValueId, ValueType}; +use psrs_hir::{ModuleId, SymbolId}; +use psrs_span::TextRange; + +fn span() -> TextRange { + TextRange::new(0, 1) +} + +fn registry() -> crate::abi::WasiRegistry { + crate::abi::WasiRegistry::load().expect("the vendored WASI WIT should load") +} + +/// A MIR function that allocates a bounded buffer, calls the embedded +/// formatter artifact's raw export, and returns the initialized byte length. +fn number_format_module() -> Module { + Module { + name: "NumberFormat".into(), + entry: Some(SymbolId::new(ModuleId(0), 0)), + types: Vec::new(), + strings: Vec::new(), + layout: None, + imports: vec![ + Import { + runtime: None, + symbol: crate::abi::REALLOC_SYMBOL, + parameters: vec![ValueType::I32; 4], + result: Some(ValueType::I32), + }, + Import { + runtime: None, + symbol: crate::abi::NUMBER_TO_STRING_SYMBOL, + parameters: vec![ValueType::F64, ValueType::I32, ValueType::I32], + result: Some(ValueType::I32), + }, + ], + functions: vec![Function { + id: crate::types::FunctionId(0), + symbol: SymbolId::new(ModuleId(0), 0), + name: "main".into(), + parameters: Vec::new(), + values: vec![ + ValueDecl { + id: ValueId(0), + ty: ValueType::I32, + }, + ValueDecl { + id: ValueId(1), + ty: ValueType::F64, + }, + ValueDecl { + id: ValueId(2), + ty: ValueType::I32, + }, + ValueDecl { + id: ValueId(3), + ty: ValueType::I32, + }, + ValueDecl { + id: ValueId(4), + ty: ValueType::I32, + }, + ValueDecl { + id: ValueId(5), + ty: ValueType::I32, + }, + ], + entry: BlockId(0), + blocks: vec![BasicBlock { + id: BlockId(0), + parameters: Vec::new(), + instructions: vec![ + Instruction::NumberConstant { + destination: ValueId(1), + value: "1e21".into(), + span: span(), + }, + Instruction::Constant { + destination: ValueId(3), + value: 0, + span: span(), + }, + Instruction::Constant { + destination: ValueId(4), + value: 1, + span: span(), + }, + Instruction::Constant { + destination: ValueId(5), + value: psrs_runtime::NUMBER_CAPACITY as i32, + span: span(), + }, + Instruction::Call { + contract: None, + destination: ValueId(2), + function: crate::abi::REALLOC_SYMBOL, + arguments: vec![ValueId(3), ValueId(3), ValueId(4), ValueId(5)], + span: span(), + }, + Instruction::Call { + contract: None, + destination: ValueId(0), + function: crate::abi::NUMBER_TO_STRING_SYMBOL, + arguments: vec![ValueId(1), ValueId(2), ValueId(5)], + span: span(), + }, + ], + terminator: Some(Terminator::Return { + value: ValueId(0), + span: span(), + }), + }], + result: ValueId(0), + result_type: ValueType::I32, + span: span(), + }], + span: span(), + } +} + +/// Lowers and runs a MIR module through the default world's checked plan, +/// attaching the runtime artifact through component composition. +fn run_with_checked_plan(mir: &Module) -> Option { + let target = crate::TargetCapabilities::default(); + let context = crate::linking::default_context().expect("the default world resolves"); + let mut wasi = registry(); + let link = crate::linking::plan_for_module(&context, mir, &mut wasi, target) + .expect("the formatter module should plan"); + assert!( + !link.plan.artifacts().is_empty(), + "the formatter plan should select the runtime artifact" + ); + let wasm = crate::wasm::lower_module_with_plan(mir, &mut wasi, target, &link) + .expect("the formatter module should lower"); + let core = crate::wasm::encode_module(&wasm).expect("encoding the formatter module"); + crate::validator_for(target) + .validate_all(&core) + .expect("the core module should validate"); + let component = crate::linking::compose(&link, &core, mir.span, None) + .expect("the formatter component should compose"); + crate::validator_for(target) + .validate_all(&component) + .expect("the component should validate"); + + if std::process::Command::new("wasmtime") + .arg("--version") + .output() + .is_err() + { + eprintln!("skipping: wasmtime is not installed"); + return None; + } + let path = std::env::temp_dir().join(format!("psrs-format-{}.wasm", std::process::id())); + std::fs::write(&path, &component).unwrap(); + let output = std::process::Command::new("wasmtime") + .arg("run") + .arg(&path) + .output() + .unwrap(); + let _ = std::fs::remove_file(&path); + Some(output) +} + +/// The formatter artifact is selected by the checked plan, shares the +/// application's memory above the allocator boundary, and returns the +/// initialized token length through the command exit code. +#[test] +fn formats_a_number_through_the_runtime_artifact() { + let mir = number_format_module(); + let Some(output) = run_with_checked_plan(&mir) else { + return; + }; + assert_eq!( + output.status.code(), + Some(5), + "1e21 formats to the 5-byte token \"1e+21\": {output:?}" + ); +} diff --git a/crates/psrs-backend/src/mir/numeric.rs b/crates/psrs-backend/src/mir/numeric.rs index 933c9ec7..3569121e 100644 --- a/crates/psrs-backend/src/mir/numeric.rs +++ b/crates/psrs-backend/src/mir/numeric.rs @@ -7,6 +7,11 @@ pub enum UnaryOp { I32Neg, I32Complement, F64Neg, + F64Abs, + F64Sqrt, + F64Trunc, + F64Floor, + F64Ceil, BoolNot, I32ToF64, F64ToF32, @@ -23,6 +28,11 @@ impl From for UnaryOp { CcUnaryOp::IntNeg => Self::I32Neg, CcUnaryOp::IntComplement => Self::I32Complement, CcUnaryOp::NumberNeg => Self::F64Neg, + CcUnaryOp::NumberAbs => Self::F64Abs, + CcUnaryOp::NumberSqrt => Self::F64Sqrt, + CcUnaryOp::NumberTrunc => Self::F64Trunc, + CcUnaryOp::NumberFloor => Self::F64Floor, + CcUnaryOp::NumberCeil => Self::F64Ceil, CcUnaryOp::BooleanNot => Self::BoolNot, CcUnaryOp::IntToNumber => Self::I32ToF64, CcUnaryOp::NumberToInt => Self::F64ToI32Sat, @@ -78,7 +88,6 @@ impl TryFrom for NumericOp { BinaryOp::IntMul => Self::I32Mul, BinaryOp::IntQuot => Self::I32DivS, BinaryOp::IntRem => Self::I32RemS, - BinaryOp::IntDiv | BinaryOp::IntMod => return Err(value), BinaryOp::IntAnd => Self::I32And, BinaryOp::IntOr => Self::I32Or, BinaryOp::IntXor => Self::I32Xor, diff --git a/crates/psrs-backend/src/mir/opt/cfg.rs b/crates/psrs-backend/src/mir/opt/cfg.rs index 023f14a3..92759f6c 100644 --- a/crates/psrs-backend/src/mir/opt/cfg.rs +++ b/crates/psrs-backend/src/mir/opt/cfg.rs @@ -54,7 +54,8 @@ pub(super) fn simplify_terminators(module: &mut Module) -> bool { .unwrap_or(*default); (target, *span) } - Terminator::Return { .. } + Terminator::Trap { .. } + | Terminator::Return { .. } | Terminator::Jump { .. } | Terminator::ReturnCall { .. } | Terminator::ReturnCallRef { .. } => return None, @@ -100,19 +101,5 @@ pub(super) fn reachable_blocks(entry: BlockId, blocks: &[BasicBlock]) -> HashSet } pub(super) fn successors(terminator: &Terminator) -> Vec { - match terminator { - Terminator::Return { .. } => Vec::new(), - Terminator::Jump { target, .. } => vec![*target], - Terminator::Branch { - then_block, - else_block, - .. - } => vec![*then_block, *else_block], - Terminator::Switch { cases, default, .. } => cases - .iter() - .map(|(_, target)| *target) - .chain(std::iter::once(*default)) - .collect(), - Terminator::ReturnCall { .. } | Terminator::ReturnCallRef { .. } => Vec::new(), - } + crate::mir::cfg::successors(terminator) } diff --git a/crates/psrs-backend/src/mir/opt/constants.rs b/crates/psrs-backend/src/mir/opt/constants.rs index 2c9532a3..39cb89e2 100644 --- a/crates/psrs-backend/src/mir/opt/constants.rs +++ b/crates/psrs-backend/src/mir/opt/constants.rs @@ -45,7 +45,14 @@ pub(super) fn integer_constant(function: &Function, value: ValueId) -> Option bool { let facts = analyze(function); - let mut changed = materialize_block_parameters(function, &facts); + let changed = materialize_block_parameters(function, &facts); + changed | fold_scalar_instructions(function) +} + +/// Canonicalize proven scalar operations without changing calls, identities or CFG. +pub(in crate::mir) fn fold_scalar_instructions(function: &mut Function) -> bool { + let facts = analyze(function); + let mut changed = false; for block in &mut function.blocks { for instruction in &mut block.instructions { let Some(destination) = instruction.destination() else { @@ -164,7 +171,7 @@ fn incoming_values(function: &Function, reachable: &HashSet Terminator::Switch { .. } | Terminator::ReturnCall { .. } | Terminator::ReturnCallRef { .. } => {} - Terminator::Return { .. } => {} + Terminator::Return { .. } | Terminator::Trap { .. } => {} } } incoming diff --git a/crates/psrs-backend/src/mir/opt/imports.rs b/crates/psrs-backend/src/mir/opt/imports.rs index 267517c9..ff8c78f5 100644 --- a/crates/psrs-backend/src/mir/opt/imports.rs +++ b/crates/psrs-backend/src/mir/opt/imports.rs @@ -1,13 +1,14 @@ -//! Removes prevalidated imports no longer referenced by optimized code. +//! Projects executable functions and imports from the command entry closure. use super::cfg; use crate::mir::{Instruction, Module}; use psrs_hir::SymbolId; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; pub(super) fn project_reachable(module: &mut Module) { - let mut referenced = HashSet::::new(); + let mut edges = HashMap::new(); for function in &module.functions { + let mut referenced = HashSet::::new(); let reachable = cfg::reachable_blocks(function.entry, &function.blocks); for block in &function.blocks { if !reachable.contains(&block.id) { @@ -37,7 +38,35 @@ pub(super) fn project_reachable(module: &mut Module) { referenced.insert(*function); } } + edges.insert(function.symbol, referenced); } + // Modules without an entry expose all their definitions. Command modules + // expose only the entry and functions it can call or capture as values. + if let Some(entry) = module.entry { + let mut pending = vec![entry]; + let mut live = HashSet::new(); + while let Some(symbol) = pending.pop() { + if live.insert(symbol) + && let Some(references) = edges.get(&symbol) + { + pending.extend(references.iter().copied()); + } + } + module + .functions + .retain(|function| live.contains(&function.symbol)); + // FunctionId is a module position; call and capture identities remain + // SymbolIds and are unaffected by compacting the function vector. + for (position, function) in module.functions.iter_mut().enumerate() { + function.id = crate::types::FunctionId(position as u32); + } + } + let referenced = module + .functions + .iter() + .flat_map(|function| &edges[&function.symbol]) + .copied() + .collect::>(); module .imports .retain(|import| referenced.contains(&import.symbol)); @@ -53,17 +82,17 @@ mod tests { use psrs_hir::{ModuleId, SymbolId}; use psrs_span::TextRange; - #[test] - fn retains_imports_referenced_by_closure_construction() { + fn fixture() -> Module { let entry = SymbolId::new(ModuleId(0), 0); let imported = SymbolId::new(ModuleId(1), 0); let span = TextRange::new(0, 1); - let mut module = Module { + Module { name: "ImportProjectionTest".into(), types: Vec::new(), strings: Vec::new(), layout: None, imports: vec![Import { + runtime: None, symbol: imported, parameters: Vec::new(), result: None, @@ -109,11 +138,45 @@ mod tests { }], entry: Some(entry), span, - }; + } + } + #[test] + fn retains_imports_referenced_by_closure_construction() { + let mut module = fixture(); project_reachable(&mut module); + assert_eq!(module.imports.len(), 1); + assert_eq!(module.imports[0].symbol, SymbolId::new(ModuleId(1), 0)); + } + #[test] + fn only_command_entry_reachability_can_discard_definitions() { + let mut module = fixture(); + let dead_import = SymbolId::new(ModuleId(2), 0); + let mut dead = module.functions[0].clone(); + dead.symbol = SymbolId::new(ModuleId(0), 1); + dead.id = FunctionId(0); + module.functions[0].id = FunctionId(1); + let Instruction::ClosureNew { function, .. } = &mut dead.blocks[0].instructions[0] else { + panic!("fixture constructs a closure"); + }; + *function = dead_import; + module.functions.insert(0, dead); + module.imports.push(Import { + runtime: None, + symbol: dead_import, + parameters: Vec::new(), + result: None, + }); + let mut library = module.clone(); + library.entry = None; + project_reachable(&mut library); + assert_eq!(library.functions.len(), 2); + assert_eq!(library.imports.len(), 2); + project_reachable(&mut module); + assert_eq!(module.functions.len(), 1); + assert_eq!(module.functions[0].id, FunctionId(0)); assert_eq!(module.imports.len(), 1); - assert_eq!(module.imports[0].symbol, imported); + assert_eq!(module.imports[0].symbol, SymbolId::new(ModuleId(1), 0)); } } diff --git a/crates/psrs-backend/src/mir/opt/mod.rs b/crates/psrs-backend/src/mir/opt/mod.rs index 9035dc8d..add390d0 100644 --- a/crates/psrs-backend/src/mir/opt/mod.rs +++ b/crates/psrs-backend/src/mir/opt/mod.rs @@ -1,8 +1,8 @@ //! Representation-preserving P10 optimization over verified MIR. mod cfg; -mod constants; -mod effects; +pub(in crate::mir) mod constants; +use super::effects; mod imports; mod inline; mod values; diff --git a/crates/psrs-backend/src/mir/opt/tests/control_flow.rs b/crates/psrs-backend/src/mir/opt/tests/control_flow.rs index 2066ffe5..cb2bfc4a 100644 --- a/crates/psrs-backend/src/mir/opt/tests/control_flow.rs +++ b/crates/psrs-backend/src/mir/opt/tests/control_flow.rs @@ -82,6 +82,7 @@ fn folds_branch_merges_prunes_blocks_and_projects_imports() { span: span(), }; let imports = vec![Import { + runtime: None, symbol: import, parameters: Vec::new(), result: None, diff --git a/crates/psrs-backend/src/mir/opt/tests/inline.rs b/crates/psrs-backend/src/mir/opt/tests/inline.rs index 4497356c..702dcc9b 100644 --- a/crates/psrs-backend/src/mir/opt/tests/inline.rs +++ b/crates/psrs-backend/src/mir/opt/tests/inline.rs @@ -21,6 +21,7 @@ fn inlines_small_direct_functions_and_folds_the_result() { span: span(), }, Instruction::Call { + contract: None, destination: ValueId(1), function: callee_symbol, arguments: vec![ValueId(0)], @@ -116,6 +117,7 @@ fn inlining_uses_the_callee_function_result_value() { id: BlockId(0), parameters: Vec::new(), instructions: vec![Instruction::Call { + contract: None, destination: ValueId(0), function: callee_symbol, arguments: Vec::new(), diff --git a/crates/psrs-backend/src/mir/opt/values.rs b/crates/psrs-backend/src/mir/opt/values.rs index 5154bc90..a4e3c3cd 100644 --- a/crates/psrs-backend/src/mir/opt/values.rs +++ b/crates/psrs-backend/src/mir/opt/values.rs @@ -98,6 +98,7 @@ fn eliminate_function(function: &mut Function) { fn terminator_operands(terminator: &Terminator) -> Vec { match terminator { + Terminator::Trap { .. } => Vec::new(), Terminator::Return { value, .. } => vec![*value], Terminator::Jump { arguments, .. } => arguments.clone(), Terminator::Branch { condition, .. } => vec![*condition], @@ -122,6 +123,20 @@ pub(crate) fn remap_instruction( *value = mapping.get(value).copied().unwrap_or(*value); }; match instruction { + I::ControlEvidence { + destination, + value, + implementation, + arguments, + .. + } => { + replace(destination); + replace(value); + replace(implementation); + for argument in arguments { + replace(argument); + } + } I::Copy { destination, value, .. } => { @@ -224,6 +239,16 @@ pub(crate) fn remap_instruction( replace(destination); replace(length); } + I::ArrayNewFilled { + destination, + length, + value, + .. + } => { + replace(destination); + replace(length); + replace(value); + } I::CallRef { destination, function, @@ -348,6 +373,7 @@ pub(crate) fn remap_terminator(terminator: &mut Terminator, mapping: &HashMap {} Terminator::Return { value, .. } => replace(value), Terminator::Jump { arguments, .. } => arguments.iter_mut().for_each(replace), Terminator::Branch { condition, .. } => replace(condition), diff --git a/crates/psrs-backend/src/mir/reachable/assignments.rs b/crates/psrs-backend/src/mir/reachable/assignments.rs index ffa5dca8..37d65f63 100644 --- a/crates/psrs-backend/src/mir/reachable/assignments.rs +++ b/crates/psrs-backend/src/mir/reachable/assignments.rs @@ -62,6 +62,7 @@ pub(super) fn add_assignments( | AssignmentKind::VariantTag { representation, .. } | AssignmentKind::VariantGet { representation, .. } | AssignmentKind::ArrayNew { representation, .. } + | AssignmentKind::ArrayFill { representation, .. } | AssignmentKind::ArrayGet { representation, .. } | AssignmentKind::ArrayClone { representation, .. } | AssignmentKind::ArraySet { representation, .. } @@ -129,10 +130,12 @@ pub(super) fn add_assignments( } } AssignmentKind::Constant(_) + | AssignmentKind::ControlEvidence { .. } | AssignmentKind::NumberConstant(_) | AssignmentKind::StringConstant(_) | AssignmentKind::Primitive { .. } | AssignmentKind::Unary { .. } + | AssignmentKind::RuntimeCall { .. } | AssignmentKind::ArrayLen { .. } | AssignmentKind::Unreachable => {} AssignmentKind::ClosureGetCapture { .. } => { @@ -147,7 +150,7 @@ pub(super) fn add_assignments( } } -fn add_conversion( +pub(super) fn add_conversion( conversion: &ValueConversion, direct_calls: &mut HashSet, representations: &mut HashSet, diff --git a/crates/psrs-backend/src/mir/reachable/mod.rs b/crates/psrs-backend/src/mir/reachable/mod.rs index 1f957f32..8ab6b436 100644 --- a/crates/psrs-backend/src/mir/reachable/mod.rs +++ b/crates/psrs-backend/src/mir/reachable/mod.rs @@ -1,87 +1,33 @@ //! Reachability analysis for target-neutral CC layout requirements. mod assignments; +mod projection; use super::layout::LayoutError; use crate::cc::{ - GuestLayout, Module as CcModule, RefShape, Reference, ReprId, Representation, - RepresentationTable, Signature, SignatureId, ValueShape, + Module as CcModule, RefShape, Reference, ReprId, Representation, RepresentationTable, + Signature, SignatureId, ValueShape, }; use std::collections::{HashMap, HashSet}; use assignments::add_assignments; -/// Adds every representation the instance-aware projection names. The concrete -/// `value` node of an erased parameter field references a nested representation -/// the abstract signature does not reach, so the projection is what keeps a -/// nested `option` payload's struct and array types planned. -fn add_projection( - layout: &GuestLayout, - representations: &mut HashSet, - signatures: &mut HashSet, - representation_work: &mut Vec, - signature_work: &mut Vec, -) { - match layout { - GuestLayout::Scalar { shape } | GuestLayout::Boxed { shape } => add_value( - shape, - representations, - signatures, - representation_work, - signature_work, - ), - GuestLayout::Product { repr, fields, .. } => { - add_representation(*repr, representations, representation_work); - for field in fields { - add_projection( - &field.value, - representations, - signatures, - representation_work, - signature_work, - ); - } - } - GuestLayout::Variant { repr, cases } => { - add_representation(*repr, representations, representation_work); - for case in cases { - for field in &case.fields { - add_projection( - &field.value, - representations, - signatures, - representation_work, - signature_work, - ); - } - } - } - GuestLayout::Array { repr, element } => { - add_representation(*repr, representations, representation_work); - add_projection( - &element.value, - representations, - signatures, - representation_work, - signature_work, - ); - } - } -} +use projection::add_projection; -pub(super) struct ReachableHandles { - pub(super) representations: Vec, - pub(super) signatures: Vec, +pub(in crate::mir) struct ReachableHandles { + pub(in crate::mir) direct_calls: HashSet, + pub(in crate::mir) representations: Vec, + pub(in crate::mir) signatures: Vec, /// Whether any reachable value, field, element, capture, signature /// parameter/result, or constant is a `String`, so the planner reserves the /// GC `$string` type. - pub(super) needs_string: bool, + pub(in crate::mir) needs_string: bool, } impl ReachableHandles { /// Computes reachability for the GC planner, whose closure environments box /// integer and number captures for the `eqref` capture array. - pub(super) fn from_module(module: &CcModule) -> Result { + pub(in crate::mir) fn from_module(module: &CcModule) -> Result { let mut representations = HashSet::new(); let mut signatures = HashSet::new(); let mut representation_work = Vec::new(); @@ -142,20 +88,22 @@ impl ReachableHandles { for parameter in &projection.parameters { add_projection( parameter, + &module.representations, &mut representations, &mut signatures, &mut representation_work, &mut signature_work, - ); + )?; } if let Some(result) = &projection.result { add_projection( result, + &module.representations, &mut representations, &mut signatures, &mut representation_work, &mut signature_work, - ); + )?; } } } @@ -236,7 +184,16 @@ impl ReachableHandles { representations.sort_by_key(|id| id.0); let mut signatures = signatures.into_iter().collect::>(); signatures.sort_by_key(|id| id.0); - let needs_string = module.functions.iter().any(|function| { + let needs_string = module.externals.iter().any(|external| { + direct_calls.contains(&external.symbol) + && external.projection.as_ref().is_some_and(|layout| { + layout + .parameters + .iter() + .chain(layout.result.iter()) + .any(projection::has_string) + }) + }) || module.functions.iter().any(|function| { function .values .iter() @@ -252,6 +209,7 @@ impl ReachableHandles { .is_some_and(signature_has_string) }); Ok(Self { + direct_calls, representations, signatures, needs_string, @@ -339,7 +297,7 @@ fn add_value( } } -pub(super) fn add_reference( +pub(in crate::mir) fn add_reference( reference: &Reference, representations: &mut HashSet, signatures: &mut HashSet, @@ -353,7 +311,7 @@ pub(super) fn add_reference( } } -pub(super) fn add_representation( +pub(in crate::mir) fn add_representation( id: ReprId, representations: &mut HashSet, work: &mut Vec, @@ -363,7 +321,7 @@ pub(super) fn add_representation( } } -pub(super) fn add_signature( +pub(in crate::mir) fn add_signature( id: SignatureId, signatures: &mut HashSet, work: &mut Vec, diff --git a/crates/psrs-backend/src/mir/reachable/projection.rs b/crates/psrs-backend/src/mir/reachable/projection.rs new file mode 100644 index 00000000..760a65c8 --- /dev/null +++ b/crates/psrs-backend/src/mir/reachable/projection.rs @@ -0,0 +1,119 @@ +//! Reachability of concrete ABI projections and their storage conversion plans. +use super::*; +use crate::cc::{ + Field, GuestLayout, + payload::{PayloadPlanner, StoragePayloadPlanner}, +}; +use psrs_span::TextRange; + +pub(super) fn has_string(layout: &GuestLayout) -> bool { + match layout { + GuestLayout::Scalar { shape } | GuestLayout::Boxed { shape } => { + *shape == ValueShape::String + } + GuestLayout::Product { fields, .. } => fields.iter().any(|field| has_string(&field.value)), + GuestLayout::Variant { cases, .. } => cases + .iter() + .flat_map(|case| &case.fields) + .any(|field| has_string(&field.value)), + GuestLayout::Array { element, .. } => has_string(&element.value), + } +} + +/// Adds every representation the instance-aware projection names. The concrete +/// `value` node of an erased parameter field references a nested representation +/// the abstract signature does not reach, so the projection is what keeps a +/// nested `option` payload's struct and array types planned. +pub(super) fn add_projection( + layout: &GuestLayout, + table: &RepresentationTable, + representations: &mut HashSet, + signatures: &mut HashSet, + representation_work: &mut Vec, + signature_work: &mut Vec, +) -> Result<(), LayoutError> { + match layout { + GuestLayout::Scalar { shape } | GuestLayout::Boxed { shape } => add_value( + shape, + representations, + signatures, + representation_work, + signature_work, + ), + GuestLayout::Product { repr, fields, .. } => { + add_representation(*repr, representations, representation_work); + for field in fields { + add_field( + field, + table, + representations, + signatures, + representation_work, + signature_work, + )?; + } + } + GuestLayout::Variant { repr, cases } => { + add_representation(*repr, representations, representation_work); + for case in cases { + for field in &case.fields { + add_field( + field, + table, + representations, + signatures, + representation_work, + signature_work, + )?; + } + } + } + GuestLayout::Array { repr, element } => { + add_representation(*repr, representations, representation_work); + add_field( + element, + table, + representations, + signatures, + representation_work, + signature_work, + )?; + } + } + Ok(()) +} + +fn add_field( + field: &Field, + table: &RepresentationTable, + representations: &mut HashSet, + signatures: &mut HashSet, + representation_work: &mut Vec, + signature_work: &mut Vec, +) -> Result<(), LayoutError> { + add_projection( + &field.value, + table, + representations, + signatures, + representation_work, + signature_work, + )?; + if matches!(field.stored, ValueShape::Reference(reference) if reference.heap == RefShape::Erased) + && matches!( + field.value, + GuestLayout::Array { .. } | GuestLayout::Product { .. } + ) + { + let plan = StoragePayloadPlanner(table) + .erase_payload(field.value.shape(), TextRange::new(0, 0)) + .map_err(|errors| LayoutError::InvalidPayloadConversion(errors[0].message.clone()))?; + assignments::add_conversion( + &plan, + &mut HashSet::new(), + representations, + representation_work, + ); + } + Ok(()) +} diff --git a/crates/psrs-backend/src/mir/runtime/context.rs b/crates/psrs-backend/src/mir/runtime/context.rs new file mode 100644 index 00000000..74bc598a --- /dev/null +++ b/crates/psrs-backend/src/mir/runtime/context.rs @@ -0,0 +1,31 @@ +//! One checked runtime binding inventory shared by all function lowerers. +use super::*; +use psrs_hir::SymbolId; +use std::collections::HashMap; + +pub(in crate::mir) struct RuntimeContext { + pub(in crate::mir) imports: HashMap, +} + +impl RuntimeContext { + pub(in crate::mir) fn checked( + bindings: &[RuntimeBinding], + physical: &cc::Module, + layout: &PlannedLayout, + ) -> Result> { + let live = ReachableHandles::from_module(physical).map_err(|failure| { + vec![BackendError::invalid_ir( + "P9 runtime projection", + physical.span, + failure.to_string(), + )] + })?; + let mut imports = HashMap::new(); + for binding in bindings { + if live.direct_calls.contains(&binding.symbol) { + imports.insert(binding.symbol, plan_import(binding, physical, layout)?); + } + } + Ok(Self { imports }) + } +} diff --git a/crates/psrs-backend/src/mir/runtime/mod.rs b/crates/psrs-backend/src/mir/runtime/mod.rs new file mode 100644 index 00000000..76629430 --- /dev/null +++ b/crates/psrs-backend/src/mir/runtime/mod.rs @@ -0,0 +1,14 @@ +//! Source-anchored external calls using checked physical binding signatures. +use super::{Import, layout::PlannedLayout, reachable::ReachableHandles}; +use crate::bindings::plan_runtime_import as plan_import; +use crate::{BackendError, RuntimeBinding, cc}; + +mod context; +pub(in crate::mir) use context::RuntimeContext; + +#[derive(Clone, Debug, PartialEq, Eq)] +/// External provider/export identity retained after ABI lowering. +pub struct RuntimeImport { + pub module: String, + pub function: String, +} diff --git a/crates/psrs-backend/src/mir/scalar_helpers.rs b/crates/psrs-backend/src/mir/scalar_helpers.rs deleted file mode 100644 index 77a5fa0e..00000000 --- a/crates/psrs-backend/src/mir/scalar_helpers.rs +++ /dev/null @@ -1,327 +0,0 @@ -use crate::cc::{self, AssignmentKind, BinaryOp}; -use crate::mir::{BasicBlock, BlockId, Function, Instruction, NumericOp, Terminator}; -use crate::types::{FunctionId, ValueDecl, ValueId, ValueType}; -use crate::{BackendError, mir}; -use psrs_hir::{ModuleId, SymbolId}; -use psrs_span::TextRange; -use std::collections::HashSet; - -#[derive(Clone, Copy, Debug, Default)] -pub(super) struct ScalarHelpers { - pub(super) int_div: Option, - pub(super) int_mod: Option, -} - -impl ScalarHelpers { - pub(super) fn binary_instruction( - &self, - op: BinaryOp, - destination: ValueId, - left: ValueId, - right: ValueId, - span: TextRange, - ) -> Result> { - let helper = match op { - BinaryOp::IntDiv => self.int_div, - BinaryOp::IntMod => self.int_mod, - _ => None, - }; - if let Some(function) = helper { - return Ok(Instruction::Call { - destination, - function, - arguments: vec![left, right], - span, - }); - } - let op = mir::NumericOp::try_from(op).map_err(|unlowered| { - vec![BackendError::invalid_ir( - "P9 MIR lowering", - span, - format!("missing MIR helper for scalar operation {unlowered:?}"), - )] - })?; - Ok(Instruction::Primitive { - destination, - op, - left, - right, - span, - }) - } -} - -pub(super) fn lower_scalar_helpers( - module: &cc::Module, - first_function_id: u32, -) -> (ScalarHelpers, Vec) { - let needs_int_div = module - .functions - .iter() - .any(|function| contains_operation(&function.assignments, BinaryOp::IntDiv)); - let needs_int_mod = module - .functions - .iter() - .any(|function| contains_operation(&function.assignments, BinaryOp::IntMod)); - let mut used_symbols = module - .functions - .iter() - .map(|function| function.symbol) - .chain(module.externals.iter().map(|external| external.symbol)) - .collect::>(); - // Intrinsic symbols may be allocated downward from `u32::MAX`; never take - // one the canonical ABI or codec reserves. - used_symbols.extend(crate::abi::RESERVED_ABI_SYMBOLS); - let symbol_module = module - .functions - .first() - .map_or(ModuleId::INTRINSICS, |function| function.symbol.module); - let int_div = needs_int_div.then(|| allocate_symbol(symbol_module, &mut used_symbols)); - let int_mod = needs_int_mod.then(|| allocate_symbol(symbol_module, &mut used_symbols)); - let helpers = ScalarHelpers { int_div, int_mod }; - let mut functions = Vec::new(); - let mut next_id = first_function_id; - if let Some(symbol) = int_div { - functions.push(euclidean_helper( - FunctionId(next_id), - symbol, - EuclideanOperation::Divide, - module.span, - )); - next_id += 1; - } - if let Some(symbol) = int_mod { - functions.push(euclidean_helper( - FunctionId(next_id), - symbol, - EuclideanOperation::Modulo, - module.span, - )); - } - (helpers, functions) -} - -fn allocate_symbol(module: ModuleId, used: &mut HashSet) -> SymbolId { - let mut index = u32::MAX; - loop { - let symbol = SymbolId::new(module, index); - if used.insert(symbol) { - return symbol; - } - index = index - .checked_sub(1) - .expect("generated scalar helper symbol space exhausted"); - } -} - -fn contains_operation(assignments: &[cc::Assignment], needle: BinaryOp) -> bool { - assignments.iter().any(|assignment| match &assignment.kind { - AssignmentKind::Primitive { op, .. } => *op == needle, - AssignmentKind::If { - then_assignments, - else_assignments, - .. - } => { - contains_operation(then_assignments, needle) - || contains_operation(else_assignments, needle) - } - AssignmentKind::TagSwitch { - cases, - default_assignments, - .. - } => { - cases - .iter() - .any(|case| contains_operation(&case.assignments, needle)) - || contains_operation(default_assignments, needle) - } - _ => false, - }) -} - -#[derive(Clone, Copy)] -enum EuclideanOperation { - Divide, - Modulo, -} - -fn euclidean_helper( - id: FunctionId, - symbol: SymbolId, - operation: EuclideanOperation, - span: TextRange, -) -> Function { - let a = ValueId(0); - let b = ValueId(1); - let remainder = ValueId(2); - let zero = ValueId(3); - let nonzero_remainder = ValueId(4); - let remainder_negative = ValueId(5); - let divisor_negative = ValueId(6); - let signs_differ = ValueId(7); - let should_adjust = ValueId(8); - let result = ValueId(9); - let mut values = (0..=9) - .map(|id| ValueDecl { - id: ValueId(id), - ty: ValueType::I32, - }) - .collect::>(); - values[4].ty = ValueType::Boolean; - values[5].ty = ValueType::Boolean; - values[6].ty = ValueType::Boolean; - values[7].ty = ValueType::Boolean; - values[8].ty = ValueType::Boolean; - - let mut entry_instructions = vec![ - primitive(remainder, NumericOp::I32RemS, a, b, span), - Instruction::Constant { - destination: zero, - value: 0, - span, - }, - primitive(nonzero_remainder, NumericOp::I32Ne, remainder, zero, span), - primitive(remainder_negative, NumericOp::I32LtS, remainder, zero, span), - primitive(divisor_negative, NumericOp::I32LtS, b, zero, span), - primitive( - signs_differ, - NumericOp::BoolNe, - remainder_negative, - divisor_negative, - span, - ), - primitive( - should_adjust, - NumericOp::BoolAnd, - nonzero_remainder, - signs_differ, - span, - ), - ]; - let (adjust_value, unchanged_value, name) = match operation { - EuclideanOperation::Divide => { - let quotient = ValueId(10); - let one = ValueId(11); - let adjusted = ValueId(12); - let unchanged = ValueId(13); - values.extend([ - ValueDecl { - id: quotient, - ty: ValueType::I32, - }, - ValueDecl { - id: one, - ty: ValueType::I32, - }, - ValueDecl { - id: adjusted, - ty: ValueType::I32, - }, - ValueDecl { - id: unchanged, - ty: ValueType::I32, - }, - ]); - let adjusted_instructions = vec![ - primitive(quotient, NumericOp::I32DivS, a, b, span), - Instruction::Constant { - destination: one, - value: 1, - span, - }, - primitive(adjusted, NumericOp::I32Sub, quotient, one, span), - ]; - let unchanged_instructions = vec![primitive(unchanged, NumericOp::I32DivS, a, b, span)]; - ( - (adjusted, adjusted_instructions), - (unchanged, unchanged_instructions), - "__psrs_euclidean_int_div", - ) - } - EuclideanOperation::Modulo => { - let adjusted = ValueId(10); - values.push(ValueDecl { - id: adjusted, - ty: ValueType::I32, - }); - ( - ( - adjusted, - vec![primitive(adjusted, NumericOp::I32Add, remainder, b, span)], - ), - (remainder, Vec::new()), - "__psrs_euclidean_int_mod", - ) - } - }; - let merge = BlockId(3); - Function { - id, - symbol, - name: name.into(), - parameters: vec![a, b], - values, - entry: BlockId(0), - blocks: vec![ - BasicBlock { - id: BlockId(0), - parameters: Vec::new(), - instructions: std::mem::take(&mut entry_instructions), - terminator: Some(Terminator::Branch { - condition: should_adjust, - then_block: BlockId(1), - else_block: BlockId(2), - span, - }), - }, - BasicBlock { - id: BlockId(1), - parameters: Vec::new(), - instructions: adjust_value.1, - terminator: Some(Terminator::Jump { - target: merge, - arguments: vec![adjust_value.0], - span, - }), - }, - BasicBlock { - id: BlockId(2), - parameters: Vec::new(), - instructions: unchanged_value.1, - terminator: Some(Terminator::Jump { - target: merge, - arguments: vec![unchanged_value.0], - span, - }), - }, - BasicBlock { - id: merge, - parameters: vec![result], - instructions: Vec::new(), - terminator: Some(Terminator::Return { - value: result, - span, - }), - }, - ], - result, - result_type: ValueType::I32, - span, - } -} - -fn primitive( - destination: ValueId, - op: NumericOp, - left: ValueId, - right: ValueId, - span: TextRange, -) -> Instruction { - Instruction::Primitive { - destination, - op, - left, - right, - span, - } -} diff --git a/crates/psrs-backend/src/mir/tests.rs b/crates/psrs-backend/src/mir/tests/lowering.rs similarity index 51% rename from crates/psrs-backend/src/mir/tests.rs rename to crates/psrs-backend/src/mir/tests/lowering.rs index 4f4368d4..e2bc5172 100644 --- a/crates/psrs-backend/src/mir/tests.rs +++ b/crates/psrs-backend/src/mir/tests/lowering.rs @@ -1,19 +1,10 @@ -use super::NumericOp; -use super::{BasicBlock, BlockId, Function, Import, Instruction, Module, Terminator}; +//! Wasm lowering from a checked MIR module. +use super::{registry, span}; +use crate::mir::{BasicBlock, BlockId, Function, Import, Instruction, Module, Terminator, UnaryOp}; use crate::types::{ - CompositeType, DefinedType, FieldType, HeapType, RecGroup, RefType, StorageType, ValueDecl, - ValueId, ValueType, + CompositeType, DefinedType, FieldType, RecGroup, StorageType, ValueDecl, ValueId, ValueType, }; use psrs_hir::{ModuleId, SymbolId}; -use psrs_span::TextRange; - -fn span() -> TextRange { - TextRange::new(0, 1) -} - -fn registry() -> crate::abi::WasiRegistry { - crate::abi::WasiRegistry::load().expect("the vendored WASI WIT should load") -} /// MIR owns a defined-type table; the Wasm lowering must carry it into the /// encoded type section ahead of the function types. @@ -117,19 +108,19 @@ fn lowers_and_validates_f64_to_f32_abi_conversions() { }, Instruction::UnaryPrimitive { destination: ValueId(1), - op: super::UnaryOp::F64ToF32, + op: UnaryOp::F64ToF32, value: ValueId(0), span: span(), }, Instruction::UnaryPrimitive { destination: ValueId(2), - op: super::UnaryOp::F32ToF64, + op: UnaryOp::F32ToF64, value: ValueId(1), span: span(), }, Instruction::UnaryPrimitive { destination: ValueId(3), - op: super::UnaryOp::F64ToI32Sat, + op: UnaryOp::F64ToI32Sat, value: ValueId(2), span: span(), }, @@ -154,148 +145,6 @@ fn lowers_and_validates_f64_to_f32_abi_conversions() { .expect("the encoded f64/f32 conversion sequence should validate"); } -/// A MIR function that builds a GC struct and sums its fields, lowered through -/// the Wasm backend and executed under wasmtime. -#[test] -fn runs_a_mir_gc_struct_under_wasmtime() { - let struct_type = DefinedType { - final_type: true, - supertype: None, - composite: CompositeType::Struct(vec![ - FieldType { - storage: StorageType::I32, - mutable: false, - }, - FieldType { - storage: StorageType::I32, - mutable: false, - }, - ]), - }; - let reference = ValueType::Ref(RefType { - nullable: false, - heap: HeapType::Index(crate::types::DefinedTypeId(0)), - }); - let mir = Module { - name: "MirGc".into(), - entry: Some(SymbolId::new(ModuleId(0), 0)), - types: vec![RecGroup(vec![struct_type])], - strings: Vec::new(), - layout: None, - imports: Vec::new(), - functions: vec![Function { - id: crate::types::FunctionId(0), - symbol: SymbolId::new(ModuleId(0), 0), - name: "main".into(), - parameters: Vec::new(), - values: vec![ - ValueDecl { - id: ValueId(0), - ty: ValueType::I32, - }, - ValueDecl { - id: ValueId(1), - ty: reference, - }, - ValueDecl { - id: ValueId(2), - ty: ValueType::I32, - }, - ValueDecl { - id: ValueId(3), - ty: ValueType::I32, - }, - ValueDecl { - id: ValueId(4), - ty: ValueType::I32, - }, - ValueDecl { - id: ValueId(5), - ty: ValueType::I32, - }, - ], - entry: BlockId(0), - blocks: vec![BasicBlock { - id: BlockId(0), - parameters: Vec::new(), - instructions: vec![ - Instruction::Constant { - destination: ValueId(2), - value: 3, - span: span(), - }, - Instruction::Constant { - destination: ValueId(3), - value: 4, - span: span(), - }, - Instruction::StructNew { - destination: ValueId(1), - type_index: crate::types::DefinedTypeId(0), - arguments: vec![ValueId(2), ValueId(3)], - span: span(), - }, - Instruction::StructGet { - destination: ValueId(4), - type_index: crate::types::DefinedTypeId(0), - field: 0, - value: ValueId(1), - span: span(), - }, - Instruction::StructGet { - destination: ValueId(5), - type_index: crate::types::DefinedTypeId(0), - field: 1, - value: ValueId(1), - span: span(), - }, - Instruction::Primitive { - destination: ValueId(0), - op: NumericOp::I32Add, - left: ValueId(4), - right: ValueId(5), - span: span(), - }, - ], - terminator: Some(Terminator::Return { - value: ValueId(0), - span: span(), - }), - }], - result: ValueId(0), - result_type: ValueType::I32, - span: span(), - }], - span: span(), - }; - - let wasm = crate::wasm::lower_module(&mir, &mut registry()).expect("lowering to Wasm"); - let core = crate::wasm::encode_module(&wasm).expect("encoding"); - crate::validator() - .validate_all(&core) - .expect("the encoded module should validate"); - - if std::process::Command::new("wasmtime") - .arg("--version") - .output() - .is_err() - { - eprintln!("skipping: wasmtime is not installed"); - return; - } - let (resolve, world) = crate::component::command_world().expect("WASI WIT should load"); - let component = crate::component::componentize(&core, &resolve, world).expect("componentizing"); - let path = std::env::temp_dir().join(format!("psrs-mir-gc-{}.wasm", std::process::id())); - std::fs::write(&path, &component).unwrap(); - let output = std::process::Command::new("wasmtime") - .arg("run") - .arg(&path) - .output() - .unwrap(); - let _ = std::fs::remove_file(&path); - assert_eq!(output.status.code(), Some(7), "wasmtime output: {output:?}"); -} - /// A MIR module that calls a declared import: the Wasm lowering names the /// import from the ABI registry and the module validates. Canonical argument /// adaptation is covered through the driver's `log` test. @@ -313,6 +162,7 @@ fn lowers_an_imported_call() { strings: Vec::new(), layout: None, imports: vec![Import { + runtime: None, symbol: callee, parameters: import.parameters.clone(), result: import.result, @@ -331,6 +181,7 @@ fn lowers_an_imported_call() { id: BlockId(0), parameters: Vec::new(), instructions: vec![Instruction::Call { + contract: None, destination: ValueId(0), function: callee, arguments: Vec::new(), @@ -410,80 +261,3 @@ fn wasm_lowering_requires_an_explicit_entry_symbol() { "{errors:?}" ); } - -/// The MIR verifier checks struct field types, not just the type index range. -#[test] -fn rejects_a_struct_new_with_a_mistyped_field() { - let mir = Module { - name: "BadStruct".into(), - entry: None, - types: vec![RecGroup(vec![DefinedType { - final_type: true, - supertype: None, - composite: CompositeType::Struct(vec![FieldType { - storage: StorageType::I64, - mutable: false, - }]), - }])], - strings: Vec::new(), - layout: None, - imports: Vec::new(), - functions: vec![Function { - id: crate::types::FunctionId(0), - symbol: SymbolId::new(ModuleId(0), 0), - name: "main".into(), - parameters: Vec::new(), - values: vec![ - ValueDecl { - id: ValueId(0), - ty: ValueType::I64, - }, - ValueDecl { - id: ValueId(1), - ty: ValueType::I32, - }, - ValueDecl { - id: ValueId(2), - ty: ValueType::Ref(RefType { - nullable: false, - heap: HeapType::Index(crate::types::DefinedTypeId(0)), - }), - }, - ], - entry: BlockId(0), - blocks: vec![BasicBlock { - id: BlockId(0), - parameters: Vec::new(), - instructions: vec![ - Instruction::Constant { - destination: ValueId(1), - value: 1, - span: span(), - }, - Instruction::StructNew { - destination: ValueId(2), - type_index: crate::types::DefinedTypeId(0), - arguments: vec![ValueId(1)], - span: span(), - }, - ], - terminator: Some(Terminator::Return { - value: ValueId(0), - span: span(), - }), - }], - result: ValueId(0), - result_type: ValueType::I64, - span: span(), - }], - span: span(), - }; - - let errors = crate::mir::verify_module(&mir).unwrap_err(); - assert!( - errors.iter().any(|error| error - .message - .contains("struct.new argument has the wrong type")), - "{errors:?}" - ); -} diff --git a/crates/psrs-backend/src/mir/tests/mod.rs b/crates/psrs-backend/src/mir/tests/mod.rs new file mode 100644 index 00000000..1b19dd4c --- /dev/null +++ b/crates/psrs-backend/src/mir/tests/mod.rs @@ -0,0 +1,14 @@ +//! MIR-level tests grouped by the boundary each one exercises. +use psrs_span::TextRange; + +mod lowering; +mod runtime; +mod verify; + +pub(super) fn span() -> TextRange { + TextRange::new(0, 1) +} + +pub(super) fn registry() -> crate::abi::WasiRegistry { + crate::abi::WasiRegistry::load().expect("the vendored WASI WIT should load") +} diff --git a/crates/psrs-backend/src/mir/tests/runtime.rs b/crates/psrs-backend/src/mir/tests/runtime.rs new file mode 100644 index 00000000..39711cec --- /dev/null +++ b/crates/psrs-backend/src/mir/tests/runtime.rs @@ -0,0 +1,155 @@ +//! End-to-end execution of a lowered MIR module under a Wasm runtime. +use super::{registry, span}; +use crate::mir::{BasicBlock, BlockId, Function, Instruction, Module, NumericOp, Terminator}; +use crate::types::{ + CompositeType, DefinedType, FieldType, HeapType, RecGroup, RefType, StorageType, ValueDecl, + ValueId, ValueType, +}; +use psrs_hir::{ModuleId, SymbolId}; + +/// A MIR function that builds a GC struct and sums its fields, lowered through +/// the Wasm backend and executed under wasmtime. +#[test] +fn runs_a_mir_gc_struct_under_wasmtime() { + let struct_type = DefinedType { + final_type: true, + supertype: None, + composite: CompositeType::Struct(vec![ + FieldType { + storage: StorageType::I32, + mutable: false, + }, + FieldType { + storage: StorageType::I32, + mutable: false, + }, + ]), + }; + let reference = ValueType::Ref(RefType { + nullable: false, + heap: HeapType::Index(crate::types::DefinedTypeId(0)), + }); + let mir = Module { + name: "MirGc".into(), + entry: Some(SymbolId::new(ModuleId(0), 0)), + types: vec![RecGroup(vec![struct_type])], + strings: Vec::new(), + layout: None, + imports: Vec::new(), + functions: vec![Function { + id: crate::types::FunctionId(0), + symbol: SymbolId::new(ModuleId(0), 0), + name: "main".into(), + parameters: Vec::new(), + values: vec![ + ValueDecl { + id: ValueId(0), + ty: ValueType::I32, + }, + ValueDecl { + id: ValueId(1), + ty: reference, + }, + ValueDecl { + id: ValueId(2), + ty: ValueType::I32, + }, + ValueDecl { + id: ValueId(3), + ty: ValueType::I32, + }, + ValueDecl { + id: ValueId(4), + ty: ValueType::I32, + }, + ValueDecl { + id: ValueId(5), + ty: ValueType::I32, + }, + ], + entry: BlockId(0), + blocks: vec![BasicBlock { + id: BlockId(0), + parameters: Vec::new(), + instructions: vec![ + Instruction::Constant { + destination: ValueId(2), + value: 3, + span: span(), + }, + Instruction::Constant { + destination: ValueId(3), + value: 4, + span: span(), + }, + Instruction::StructNew { + destination: ValueId(1), + type_index: crate::types::DefinedTypeId(0), + arguments: vec![ValueId(2), ValueId(3)], + span: span(), + }, + Instruction::StructGet { + destination: ValueId(4), + type_index: crate::types::DefinedTypeId(0), + field: 0, + value: ValueId(1), + span: span(), + }, + Instruction::StructGet { + destination: ValueId(5), + type_index: crate::types::DefinedTypeId(0), + field: 1, + value: ValueId(1), + span: span(), + }, + Instruction::Primitive { + destination: ValueId(0), + op: NumericOp::I32Add, + left: ValueId(4), + right: ValueId(5), + span: span(), + }, + ], + terminator: Some(Terminator::Return { + value: ValueId(0), + span: span(), + }), + }], + result: ValueId(0), + result_type: ValueType::I32, + span: span(), + }], + span: span(), + }; + + let context = crate::linking::default_context().expect("resolved world"); + let mut wasi = registry(); + let target = crate::TargetCapabilities::default(); + let link = crate::linking::plan_for_module(&context, &mir, &mut wasi, target) + .expect("checked target plan"); + let wasm = crate::wasm::lower_module_with_plan(&mir, &mut wasi, target, &link) + .expect("lowering to Wasm"); + let core = crate::wasm::encode_module(&wasm).expect("encoding"); + crate::validator() + .validate_all(&core) + .expect("the encoded module should validate"); + + if std::process::Command::new("wasmtime") + .arg("--version") + .output() + .is_err() + { + eprintln!("skipping: wasmtime is not installed"); + return; + } + let component = crate::linking::compose(&link, &core, mir.span, None).expect("componentizing"); + let path = std::env::temp_dir().join(format!("psrs-mir-gc-{}.wasm", std::process::id())); + std::fs::write(&path, &component).unwrap(); + let output = std::process::Command::new("wasmtime") + .arg("run") + .arg(&path) + .output() + .unwrap(); + let _ = std::fs::remove_file(&path); + assert_eq!(output.status.code(), Some(7), "wasmtime output: {output:?}"); +} diff --git a/crates/psrs-backend/src/mir/tests/verify.rs b/crates/psrs-backend/src/mir/tests/verify.rs new file mode 100644 index 00000000..6ce8c572 --- /dev/null +++ b/crates/psrs-backend/src/mir/tests/verify.rs @@ -0,0 +1,85 @@ +//! MIR verification of a hand-built module. +use super::span; +use crate::mir::{BasicBlock, BlockId, Function, Instruction, Module, Terminator}; +use crate::types::{ + CompositeType, DefinedType, FieldType, HeapType, RecGroup, RefType, StorageType, ValueDecl, + ValueId, ValueType, +}; +use psrs_hir::{ModuleId, SymbolId}; + +/// The MIR verifier checks struct field types, not just the type index range. +#[test] +fn rejects_a_struct_new_with_a_mistyped_field() { + let mir = Module { + name: "BadStruct".into(), + entry: None, + types: vec![RecGroup(vec![DefinedType { + final_type: true, + supertype: None, + composite: CompositeType::Struct(vec![FieldType { + storage: StorageType::I64, + mutable: false, + }]), + }])], + strings: Vec::new(), + layout: None, + imports: Vec::new(), + functions: vec![Function { + id: crate::types::FunctionId(0), + symbol: SymbolId::new(ModuleId(0), 0), + name: "main".into(), + parameters: Vec::new(), + values: vec![ + ValueDecl { + id: ValueId(0), + ty: ValueType::I64, + }, + ValueDecl { + id: ValueId(1), + ty: ValueType::I32, + }, + ValueDecl { + id: ValueId(2), + ty: ValueType::Ref(RefType { + nullable: false, + heap: HeapType::Index(crate::types::DefinedTypeId(0)), + }), + }, + ], + entry: BlockId(0), + blocks: vec![BasicBlock { + id: BlockId(0), + parameters: Vec::new(), + instructions: vec![ + Instruction::Constant { + destination: ValueId(1), + value: 1, + span: span(), + }, + Instruction::StructNew { + destination: ValueId(2), + type_index: crate::types::DefinedTypeId(0), + arguments: vec![ValueId(1)], + span: span(), + }, + ], + terminator: Some(Terminator::Return { + value: ValueId(0), + span: span(), + }), + }], + result: ValueId(0), + result_type: ValueType::I64, + span: span(), + }], + span: span(), + }; + + let errors = crate::mir::verify_module(&mir).unwrap_err(); + assert!( + errors.iter().any(|error| error + .message + .contains("struct.new argument has the wrong type")), + "{errors:?}" + ); +} diff --git a/crates/psrs-backend/src/mir/value_identity.rs b/crates/psrs-backend/src/mir/value_identity.rs new file mode 100644 index 00000000..bbe7d0ed --- /dev/null +++ b/crates/psrs-backend/src/mir/value_identity.rs @@ -0,0 +1,245 @@ +#![allow(dead_code)] +//! Shared reference identity and loop parameter views for MIR consumers. +use super::{BlockId, Function, Instruction, Terminator}; +use crate::types::ValueId; +use std::collections::{HashMap, HashSet}; + +pub(crate) struct Definitions<'a> { + instructions: HashMap, + joins: HashMap>, +} + +impl<'a> std::ops::Deref for Definitions<'a> { + type Target = HashMap; + fn deref(&self) -> &Self::Target { + &self.instructions + } +} + +pub(crate) fn definitions(function: &Function) -> Definitions<'_> { + index(function, None) +} + +/// Restrict join inputs to paths from the semantic step being proved. The +/// executor loop header starts the next step and is not part of this result. +pub(crate) fn step_definitions(function: &Function, start: BlockId) -> Definitions<'_> { + let reachable = super::cfg::reachable_from(function, start, loop_header(function)); + index(function, Some(&reachable)) +} + +fn index<'a>(function: &'a Function, reachable: Option<&HashSet>) -> Definitions<'a> { + Definitions { + instructions: function + .blocks + .iter() + .flat_map(|block| &block.instructions) + .filter_map(|instruction| instruction.destination().map(|value| (value, instruction))) + .collect(), + joins: function + .blocks + .iter() + .flat_map(|block| &block.parameters) + .filter_map(|value| { + incoming_from(function, *value, reachable).map(|inputs| (*value, inputs)) + }) + .collect(), + } +} + +/// Copies, casts and unanimous non-loop joins preserve reference identity. +/// Loop parameters remain distinct: their incoming values represent different +/// machine iterations, rather than aliases of one execution result. +pub(crate) fn reference_origin(value: ValueId, definitions: &Definitions<'_>) -> ValueId { + fn resolve( + value: ValueId, + definitions: &Definitions<'_>, + active: &mut HashSet, + ) -> ValueId { + if !active.insert(value) { + return value; + } + let origin = match definitions.get(&value).copied() { + Some(Instruction::Copy { value, .. } | Instruction::RefCast { value, .. }) => { + resolve(*value, definitions, active) + } + _ => { + if let Some(inputs) = definitions.joins.get(&value) { + let mut origins = inputs + .iter() + .map(|input| resolve(*input, definitions, active)); + let first = origins.next().unwrap_or(value); + if origins.all(|origin| origin == first) { + first + } else { + value + } + } else { + value + } + } + }; + active.remove(&value); + origin + } + resolve(value, definitions, &mut HashSet::new()) +} + +/// Resolve a result along one concrete CFG path, retaining its incoming SSA +/// arguments instead of combining values from mutually exclusive exits. +pub(crate) fn path_origin( + mut value: ValueId, + definitions: &Definitions<'_>, + aliases: &HashMap, +) -> ValueId { + let mut seen = HashSet::new(); + while seen.insert(value) { + let next = + aliases + .get(&value) + .copied() + .or_else(|| match definitions.get(&value).copied() { + Some(Instruction::Copy { value, .. } | Instruction::RefCast { value, .. }) => { + Some(*value) + } + _ => None, + }); + match next { + Some(next) => value = next, + None => break, + } + } + value +} + +/// Integer evidence through copies and unanimous normal joins. Unknown values +/// and cyclic joins cannot establish a nominal constructor's physical tag. +pub(crate) fn integer_constant(value: ValueId, definitions: &Definitions<'_>) -> Option { + fn resolve( + value: ValueId, + definitions: &Definitions<'_>, + active: &mut HashSet, + ) -> Option { + if !active.insert(value) { + return None; + } + let constant = match definitions.get(&value).copied() { + Some(Instruction::Constant { value, .. }) => Some(*value), + Some(Instruction::Copy { value, .. }) => resolve(*value, definitions, active), + _ => definitions.joins.get(&value).and_then(|inputs| { + let first = resolve(*inputs.first()?, definitions, active)?; + inputs + .iter() + .skip(1) + .all(|input| resolve(*input, definitions, active) == Some(first)) + .then_some(first) + }), + }; + active.remove(&value); + constant + } + resolve(value, definitions, &mut HashSet::new()) +} + +pub(crate) fn loop_header(function: &Function) -> Option { + function + .blocks + .iter() + .find(|block| block.id == function.entry) + .and_then(|block| match &block.terminator { + Some(Terminator::Jump { + target, arguments, .. + }) if arguments == &function.parameters => Some(*target), + _ => None, + }) +} + +pub(crate) fn loop_parameters(function: &Function) -> &[ValueId] { + let header = loop_header(function); + function + .blocks + .iter() + .find(|block| Some(block.id) == header) + .map_or(&function.parameters, |block| &block.parameters) +} + +/// Incoming values of a non-loop SSA join parameter. Unknown edges are not +/// treated as a valid empty provenance set. +pub(crate) fn incoming_values(function: &Function, value: ValueId) -> Option> { + incoming_from(function, value, None) +} + +fn incoming_from( + function: &Function, + value: ValueId, + reachable: Option<&HashSet>, +) -> Option> { + let (block, index) = function.blocks.iter().find_map(|block| { + block + .parameters + .iter() + .position(|parameter| *parameter == value) + .map(|index| (block, index)) + })?; + if Some(block.id) == loop_header(function) { + return None; + } + let mut incoming = Vec::new(); + for predecessor in &function.blocks { + if reachable.is_some_and(|blocks| !blocks.contains(&predecessor.id)) { + continue; + } + let Some(terminator) = &predecessor.terminator else { + continue; + }; + if super::cfg::successors(terminator).contains(&block.id) { + let Terminator::Jump { + target, arguments, .. + } = terminator + else { + return None; + }; + if *target != block.id { + return None; + } + incoming.push(*arguments.get(index)?); + } + } + (!incoming.is_empty()).then_some(incoming) +} + +pub(crate) type Point = (BlockId, usize); + +pub(crate) fn precedes( + source: Point, + target: Point, + dominators: &HashMap>, +) -> bool { + if source.0 == target.0 { + source.1 < target.1 + } else { + dominators + .get(&target.0) + .is_some_and(|set| set.contains(&source.0)) + } +} + +/// Prove that a producer dominates its consumer and executes inside the current +/// loop iteration, excluding allocations reused from the entry preheader. +/// Callers separately prove that the producer is the required allocation. +pub(crate) fn fresh_iteration_allocation( + function: &Function, + allocation: ValueId, + consumer: Point, + points: &HashMap, + dominators: &HashMap>, +) -> bool { + let Some(point) = points.get(&allocation).copied() else { + return false; + }; + precedes(point, consumer, dominators) + && loop_header(function).is_none_or(|header| { + dominators + .get(&point.0) + .is_some_and(|set| set.contains(&header)) + }) +} diff --git a/crates/psrs-backend/src/mir/verify/array_map/mod.rs b/crates/psrs-backend/src/mir/verify/array_map/mod.rs index 9c7325ca..1dfc36ab 100644 --- a/crates/psrs-backend/src/mir/verify/array_map/mod.rs +++ b/crates/psrs-backend/src/mir/verify/array_map/mod.rs @@ -412,19 +412,5 @@ fn verify_loop_increment( } fn successors(terminator: &Terminator) -> Vec { - match terminator { - Terminator::Return { .. } => Vec::new(), - Terminator::Jump { target, .. } => vec![*target], - Terminator::Branch { - then_block, - else_block, - .. - } => vec![*then_block, *else_block], - Terminator::Switch { cases, default, .. } => cases - .iter() - .map(|(_, target)| *target) - .chain(std::iter::once(*default)) - .collect(), - Terminator::ReturnCall { .. } | Terminator::ReturnCallRef { .. } => Vec::new(), - } + crate::mir::cfg::successors(terminator) } diff --git a/crates/psrs-backend/src/mir/verify/capability.rs b/crates/psrs-backend/src/mir/verify/capability.rs index 7076b245..af2ef46a 100644 --- a/crates/psrs-backend/src/mir/verify/capability.rs +++ b/crates/psrs-backend/src/mir/verify/capability.rs @@ -163,6 +163,7 @@ fn mark_instruction(instruction: &mir::Instruction, required: &mut RequiredCapab | Instruction::ArrayNew { .. } | Instruction::ArrayNewDefault { .. } | Instruction::ArrayNewSized { .. } + | Instruction::ArrayNewFilled { .. } | Instruction::ArrayGet { .. } | Instruction::ArrayGetU { .. } | Instruction::ArrayClone { .. } @@ -321,6 +322,7 @@ mod tests { let mut module = module_with_function_type(Vec::new(), Vec::new()); module.types.clear(); module.imports.push(mir::Import { + runtime: None, symbol: SymbolId::new(ModuleId(4), 9), parameters: vec![ValueType::Ref(RefType { nullable: true, @@ -467,6 +469,7 @@ mod tests { parameters: Vec::new(), instructions: Vec::new(), terminator: Some(mir::Terminator::ReturnCall { + contract: None, function: SymbolId::new(ModuleId(4), 1), arguments: Vec::new(), span, diff --git a/crates/psrs-backend/src/mir/verify/captures.rs b/crates/psrs-backend/src/mir/verify/captures.rs new file mode 100644 index 00000000..a7f0fa44 --- /dev/null +++ b/crates/psrs-backend/src/mir/verify/captures.rs @@ -0,0 +1,118 @@ +//! Check closure slots against the local code target before eqref storage erases +//! their types. Surviving projections define a sparse contract after optimization. + +use super::util::{mir_error, value_type}; +use crate::BackendError; +use crate::mir::{Function, Instruction, Module, ValueId, ValueType}; +use crate::types::DefinedType; +use std::collections::{HashMap, HashSet}; + +pub(super) fn verify(module: &Module, defined: &[&DefinedType], errors: &mut Vec) { + let contracts = module + .functions + .iter() + .map(|function| { + let receivers = receiver_aliases(function); + let mut slots = HashMap::new(); + for instruction in function.blocks.iter().flat_map(|block| &block.instructions) { + if let Instruction::ClosureGetCapture { + destination, + closure, + index, + span, + .. + } = instruction + && receivers.contains(closure) + && let Some(ty) = value_type(function, *destination) + && slots + .insert(*index, ty) + .is_some_and(|previous| previous != ty) + { + errors.extend( + mir_error( + *span, + "MIR closure capture slot has incompatible projection types", + ) + .into_iter() + .map(|error| error.with_module(function.symbol.module)), + ); + } + } + (function.symbol, slots) + }) + .collect::>(); + + for function in &module.functions { + for instruction in function.blocks.iter().flat_map(|block| &block.instructions) { + let Instruction::ClosureNew { + function: target, + captures, + span, + .. + } = instruction + else { + continue; + }; + // Imported code has no local body from which to derive a contract. + // Its ABI remains a trusted external boundary. + let Some(slots) = contracts.get(target) else { + continue; + }; + for (index, expected) in slots { + let actual = captures + .get(*index as usize) + .and_then(|value| value_type(function, *value)); + if actual.is_none_or(|actual| !compatible(actual, *expected, defined)) { + errors.extend( + mir_error( + *span, + "MIR closure captures do not match target projection types", + ) + .into_iter() + .map(|error| error.with_module(function.symbol.module)), + ); + } + } + } + } +} + +fn receiver_aliases(function: &Function) -> HashSet { + let mut aliases = function + .parameters + .first() + .copied() + .into_iter() + .collect::>(); + loop { + let mut changed = false; + for instruction in function.blocks.iter().flat_map(|block| &block.instructions) { + if let Instruction::Copy { + destination, value, .. + } + | Instruction::RefCast { + destination, value, .. + } = instruction + && aliases.contains(value) + { + changed |= aliases.insert(*destination); + } + } + if !changed { + return aliases; + } + } +} + +fn compatible(actual: ValueType, expected: ValueType, defined: &[&DefinedType]) -> bool { + if actual == expected { + return true; + } + match (actual, expected) { + (ValueType::Ref(actual), ValueType::Ref(expected)) => { + (!actual.nullable || expected.nullable) + && super::subtype::heap_subtype(actual.heap, expected.heap, defined) + } + _ => false, + } +} diff --git a/crates/psrs-backend/src/mir/verify/function.rs b/crates/psrs-backend/src/mir/verify/function.rs index 130ace88..78fb98d9 100644 --- a/crates/psrs-backend/src/mir/verify/function.rs +++ b/crates/psrs-backend/src/mir/verify/function.rs @@ -109,7 +109,7 @@ pub(super) fn verify_function( } } - let dominators = compute_dominators(function.entry, &blocks); + let dominators = crate::mir::cfg::dominators(function); for block in &function.blocks { let mut available = block.parameters.iter().copied().collect::>(); for instruction in &block.instructions { @@ -195,87 +195,9 @@ fn value_available( .is_some_and(|set| set.contains(owner)) } -fn compute_dominators( - entry: BlockId, - blocks: &HashMap, -) -> HashMap> { - let all = blocks.keys().copied().collect::>(); - let mut predecessors = blocks - .keys() - .copied() - .map(|id| (id, HashSet::new())) - .collect::>>(); - for block in blocks.values() { - let Some(terminator) = &block.terminator else { - continue; - }; - for successor in terminator_successors(terminator) { - if let Some(preds) = predecessors.get_mut(&successor) { - preds.insert(block.id); - } - } - } - - let mut dominators = blocks - .keys() - .copied() - .map(|id| { - let initial = if id == entry { - HashSet::from([entry]) - } else { - all.clone() - }; - (id, initial) - }) - .collect::>(); - loop { - let mut changed = false; - for id in blocks.keys().copied().filter(|id| *id != entry) { - let mut next = all.clone(); - if let Some(preds) = predecessors.get(&id) { - if preds.is_empty() { - next.clear(); - } else { - for pred in preds { - if let Some(pred_dominators) = dominators.get(pred) { - next.retain(|candidate| pred_dominators.contains(candidate)); - } - } - } - } - next.insert(id); - if dominators.get(&id) != Some(&next) { - dominators.insert(id, next); - changed = true; - } - } - if !changed { - break; - } - } - dominators -} - -fn terminator_successors(terminator: &Terminator) -> Vec { - match terminator { - Terminator::Return { .. } => Vec::new(), - Terminator::Jump { target, .. } => vec![*target], - Terminator::Branch { - then_block, - else_block, - .. - } => vec![*then_block, *else_block], - Terminator::Switch { cases, default, .. } => cases - .iter() - .map(|(_, block)| *block) - .chain(std::iter::once(*default)) - .collect(), - Terminator::ReturnCall { .. } | Terminator::ReturnCallRef { .. } => Vec::new(), - } -} - fn terminator_operands(terminator: &Terminator) -> Vec { match terminator { + Terminator::Trap { .. } => Vec::new(), Terminator::Return { value, .. } => vec![*value], Terminator::Jump { arguments, .. } => arguments.clone(), Terminator::Branch { condition, .. } => vec![*condition], @@ -293,7 +215,8 @@ fn terminator_operands(terminator: &Terminator) -> Vec { fn terminator_span(terminator: &Terminator) -> psrs_span::TextRange { match terminator { - Terminator::Return { span, .. } + Terminator::Trap { span } + | Terminator::Return { span, .. } | Terminator::Jump { span, .. } | Terminator::Branch { span, .. } | Terminator::Switch { span, .. } @@ -311,6 +234,7 @@ fn verify_terminator( defined: &[&DefinedType], ) -> Result<(), Vec> { match terminator { + Terminator::Trap { .. } => {} Terminator::Return { value, span } => { if require_value(definitions, *value, *span)? != function.result_type { return Err(mir_error(*span, "MIR return value has the wrong type")); @@ -403,6 +327,7 @@ fn verify_terminator( function: callee, arguments, span, + .. } => { super::call::tail::verify_return_call( function, diff --git a/crates/psrs-backend/src/mir/verify/instruction/arrays.rs b/crates/psrs-backend/src/mir/verify/instruction/arrays.rs index a1ef2337..7b954451 100644 --- a/crates/psrs-backend/src/mir/verify/instruction/arrays.rs +++ b/crates/psrs-backend/src/mir/verify/instruction/arrays.rs @@ -380,3 +380,50 @@ pub(super) fn verify_len( } Ok(()) } + +/// Initialized allocation never creates a default/null logical source element. +pub(super) fn verify_array_new_filled( + function: &Function, + instruction: &Instruction, + definitions: &HashMap, + defined: &[&DefinedType], +) -> Result<(), Vec> { + let Instruction::ArrayNewFilled { + destination, + type_index, + length, + value, + span, + } = instruction + else { + unreachable!("filled array verifier received another instruction") + }; + let Some(CompositeType::Array(element)) = composite_at(defined, *type_index) else { + return Err(mir_error(*span, "MIR array.new type is not an array")); + }; + if require_value(definitions, *length, *span)? != ValueType::I32 { + return Err(mir_error(*span, "MIR array.new length must be i32")); + } + if !value_type_assignable( + require_value(definitions, *value, *span)?, + storage_value_type(&element.storage) + .ok_or_else(|| mir_error(*span, "MIR array element storage is not representable"))?, + ) { + return Err(mir_error( + *span, + "MIR array.new initializer does not match element storage", + )); + } + if !is_array_reference( + value_type(function, *destination) + .ok_or_else(|| mir_error(*span, "MIR array.new result has no value type"))?, + *type_index, + defined, + ) { + return Err(mir_error( + *span, + "MIR array.new result must match its array type", + )); + } + Ok(()) +} diff --git a/crates/psrs-backend/src/mir/verify/instruction/copy.rs b/crates/psrs-backend/src/mir/verify/instruction/copy.rs index a32e5357..82246d59 100644 --- a/crates/psrs-backend/src/mir/verify/instruction/copy.rs +++ b/crates/psrs-backend/src/mir/verify/instruction/copy.rs @@ -8,11 +8,17 @@ pub(super) fn verify_copy( instruction: &Instruction, definitions: &HashMap, ) -> Result<(), Vec> { - let Instruction::Copy { + let (Instruction::Copy { destination, value, span, - } = instruction + } + | Instruction::ControlEvidence { + destination, + value, + span, + .. + }) = instruction else { unreachable!("copy verifier received another instruction") }; diff --git a/crates/psrs-backend/src/mir/verify/instruction/lists.rs b/crates/psrs-backend/src/mir/verify/instruction/lists.rs index dc85c730..1c940412 100644 --- a/crates/psrs-backend/src/mir/verify/instruction/lists.rs +++ b/crates/psrs-backend/src/mir/verify/instruction/lists.rs @@ -3,7 +3,7 @@ use super::super::util::{composite_at, is_array_reference, mir_error, require_value}; use crate::BackendError; use crate::abi::canonical::CanonicalType; -use crate::cc::{GuestLayout, ValueShape}; +use crate::cc::{GuestLayout, RefShape, ValueShape}; use crate::mir::{Function, Instruction, ListDirection, ValueId, ValueType}; use crate::types::{CompositeType, DefinedType, StorageType}; use std::collections::HashMap; @@ -42,7 +42,20 @@ pub(super) fn verify_list_copy( let Some(CompositeType::Array(field)) = composite_at(defined, *array_type) else { return Err(mir_error(*span, "MIR list copy type is not an array")); }; - if !element_storage_matches(element, &field.storage) { + let erased_scalar = matches!(element_guest, + GuestLayout::Scalar { shape: ValueShape::Reference(reference) } + if reference.heap == RefShape::Erased) + && matches!(&field.storage, StorageType::Ref(reference) if reference.heap == crate::types::HeapType::Eq) + && matches!( + element, + CanonicalType::Bool + | CanonicalType::Int { .. } + | CanonicalType::Float { .. } + | CanonicalType::Char + | CanonicalType::Enum(_) + | CanonicalType::Handle { .. } + ); + if !erased_scalar && !element_storage_matches(element, &field.storage) { return Err(mir_error( *span, "MIR list copy element does not match the GC array", diff --git a/crates/psrs-backend/src/mir/verify/instruction/mod.rs b/crates/psrs-backend/src/mir/verify/instruction/mod.rs index 30b59bbc..e3becc58 100644 --- a/crates/psrs-backend/src/mir/verify/instruction/mod.rs +++ b/crates/psrs-backend/src/mir/verify/instruction/mod.rs @@ -29,7 +29,9 @@ pub(super) fn verify_instruction( defined: &[&DefinedType], ) -> Result<(), Vec> { match instruction { - Instruction::Copy { .. } => copy::verify_copy(function, instruction, definitions)?, + Instruction::Copy { .. } | Instruction::ControlEvidence { .. } => { + copy::verify_copy(function, instruction, definitions)? + } Instruction::Constant { .. } | Instruction::NumberConstant { .. } => { constant::verify_constant(function, instruction, definitions)? } @@ -53,6 +55,7 @@ pub(super) fn verify_instruction( function: callee, arguments, span, + .. } => { let Some(Some(signature)) = signatures.get(callee) else { return Err(mir_error(*span, "MIR call target has no valid signature")); @@ -378,6 +381,9 @@ pub(super) fn verify_instruction( Instruction::ArrayGet { .. } => { arrays::verify_array_get(function, instruction, false, definitions, defined)? } + Instruction::ArrayNewFilled { .. } => { + arrays::verify_array_new_filled(function, instruction, definitions, defined)?; + } Instruction::ArrayGetU { .. } => { arrays::verify_array_get(function, instruction, true, definitions, defined)? } diff --git a/crates/psrs-backend/src/mir/verify/instruction/unary.rs b/crates/psrs-backend/src/mir/verify/instruction/unary.rs index bc02e871..6b98b774 100644 --- a/crates/psrs-backend/src/mir/verify/instruction/unary.rs +++ b/crates/psrs-backend/src/mir/verify/instruction/unary.rs @@ -24,7 +24,12 @@ pub(super) fn verify_unary( UnaryOp::I32Neg | UnaryOp::I32Complement | UnaryOp::I32Identity => { (ValueType::I32, ValueType::I32) } - UnaryOp::F64Neg => (ValueType::F64, ValueType::F64), + UnaryOp::F64Abs + | UnaryOp::F64Sqrt + | UnaryOp::F64Neg + | UnaryOp::F64Trunc + | UnaryOp::F64Floor + | UnaryOp::F64Ceil => (ValueType::F64, ValueType::F64), UnaryOp::BoolNot => (ValueType::Boolean, ValueType::Boolean), UnaryOp::I32ToF64 => (ValueType::I32, ValueType::F64), UnaryOp::F64ToF32 => (ValueType::F64, ValueType::F32), diff --git a/crates/psrs-backend/src/mir/verify/mod.rs b/crates/psrs-backend/src/mir/verify/mod.rs index 325d14f6..1985c54e 100644 --- a/crates/psrs-backend/src/mir/verify/mod.rs +++ b/crates/psrs-backend/src/mir/verify/mod.rs @@ -11,10 +11,12 @@ use std::collections::{HashMap, HashSet}; mod array_map; mod call; mod capability; +mod captures; mod function; mod instruction; mod subtype; mod util; +mod variants; #[cfg(test)] mod tests; @@ -31,7 +33,9 @@ struct Signature { pub fn verify_module(module: &Module) -> Result<(), Vec> { let mut errors = Vec::new(); + super::control::verify(module, &mut errors); verify_defined_types(module, &mut errors); + variants::verify(module, &mut errors); let mut signatures = module .functions .iter() @@ -65,6 +69,7 @@ pub fn verify_module(module: &Module) -> Result<(), Vec> { } let defined_types = defined_type_count(module); let defined = defined_types_list(module); + captures::verify(module, &defined, &mut errors); let mut function_ids = HashSet::new(); for (position, function) in module.functions.iter().enumerate() { if !function_ids.insert(function.id) || function.id != FunctionId(position as u32) { @@ -133,8 +138,14 @@ fn defined_types_list(module: &Module) -> Vec<&DefinedType> { } fn verify_defined_types(module: &Module, errors: &mut Vec) { - let count = defined_type_count(module); + let mut count = DefinedTypeId(0); for group in &module.types { + if group.0.is_empty() { + errors.extend(mir_error(module.span, "MIR recursion group is empty")); + } + // A definition can refer to earlier groups or its own group, but + // never a later group. Runtime values use the full module type scope. + count.0 += group.0.len() as u32; for def in &group.0 { if let Some(supertype) = def.supertype && supertype.0 >= count.0 diff --git a/crates/psrs-backend/src/mir/verify/subtype.rs b/crates/psrs-backend/src/mir/verify/subtype.rs index ee7fd467..11eaeaac 100644 --- a/crates/psrs-backend/src/mir/verify/subtype.rs +++ b/crates/psrs-backend/src/mir/verify/subtype.rs @@ -126,7 +126,7 @@ pub(super) fn heap_related( heap_subtype(child, parent, definitions) || heap_subtype(parent, child, definitions) } -pub(super) fn heap_subtype( +pub(in crate::mir) fn heap_subtype( child: HeapType, parent: HeapType, definitions: &[&DefinedType], diff --git a/crates/psrs-backend/src/mir/verify/tests/arrays.rs b/crates/psrs-backend/src/mir/verify/tests/arrays.rs index 90090bf8..306ce3ff 100644 --- a/crates/psrs-backend/src/mir/verify/tests/arrays.rs +++ b/crates/psrs-backend/src/mir/verify/tests/arrays.rs @@ -300,3 +300,28 @@ fn rejects_array_clone_of_an_immutable_array() { "{errors:?}" ); } + +#[test] +fn filled_arrays_require_a_checked_initializer_and_integer_length() { + use crate::types::{DefinedTypeId, StorageType}; + for (storage, length, accepted) in [ + (StorageType::I32, ValueType::I32, true), + (StorageType::F64, ValueType::I32, false), + (StorageType::I32, ValueType::F64, false), + ] { + let (mut function, types) = array_conversion_function(storage, length, ValueType::I32); + function.blocks[0].instructions[2] = Instruction::ArrayNewFilled { + destination: ValueId(3), + type_index: DefinedTypeId(0), + length: ValueId(1), + value: ValueId(0), + span: span(), + }; + let result = verify_module(&module_with_function(function, types)); + assert_eq!( + result.is_ok(), + accepted, + "{storage:?}/{length:?}: {result:?}" + ); + } +} diff --git a/crates/psrs-backend/src/mir/verify/tests/dictionary.rs b/crates/psrs-backend/src/mir/verify/tests/dictionary.rs index a9560262..72f8bcc9 100644 --- a/crates/psrs-backend/src/mir/verify/tests/dictionary.rs +++ b/crates/psrs-backend/src/mir/verify/tests/dictionary.rs @@ -250,6 +250,7 @@ fn rejects_dictionary_method_call_with_wrong_arity() { value(1, ValueType::I32), ], vec![Instruction::Call { + contract: None, destination, function: SymbolId::new(ModuleId(0), 1), arguments: vec![method], diff --git a/crates/psrs-backend/src/mir/verify/tests/mod.rs b/crates/psrs-backend/src/mir/verify/tests/mod.rs index dc875863..ed88bf2c 100644 --- a/crates/psrs-backend/src/mir/verify/tests/mod.rs +++ b/crates/psrs-backend/src/mir/verify/tests/mod.rs @@ -98,13 +98,29 @@ fn rejects_a_unary_primitive_with_mistyped_operands() { result_type: ValueType::I32, span: span(), }; - let errors = verify_module(&module_with_function(function, Vec::new())).unwrap_err(); - assert!( - errors - .iter() - .any(|error| error.message.contains("unary operand or result type")), - "{errors:?}" - ); + for op in [ + crate::mir::UnaryOp::F64Abs, + crate::mir::UnaryOp::F64Sqrt, + crate::mir::UnaryOp::F64Neg, + crate::mir::UnaryOp::F64Trunc, + crate::mir::UnaryOp::F64Floor, + crate::mir::UnaryOp::F64Ceil, + ] { + let mut function = function.clone(); + function.blocks[0].instructions[0] = Instruction::UnaryPrimitive { + destination: output, + op, + value: input, + span: span(), + }; + let errors = verify_module(&module_with_function(function, Vec::new())).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("unary operand or result type")), + "{errors:?}" + ); + } } #[test] diff --git a/crates/psrs-backend/src/mir/verify/tests/scalar.rs b/crates/psrs-backend/src/mir/verify/tests/scalar.rs index 9ad3c180..8ef059a9 100644 --- a/crates/psrs-backend/src/mir/verify/tests/scalar.rs +++ b/crates/psrs-backend/src/mir/verify/tests/scalar.rs @@ -199,6 +199,7 @@ fn rejects_a_call_with_the_wrong_argument_type() { id: BlockId(0), parameters: Vec::new(), instructions: vec![Instruction::Call { + contract: None, destination: call_result, function: callee_symbol, arguments: vec![argument, argument], diff --git a/crates/psrs-backend/src/mir/verify/tests/structure.rs b/crates/psrs-backend/src/mir/verify/tests/structure.rs index 609a5919..0eb9715a 100644 --- a/crates/psrs-backend/src/mir/verify/tests/structure.rs +++ b/crates/psrs-backend/src/mir/verify/tests/structure.rs @@ -131,3 +131,46 @@ fn rejects_a_value_defined_by_two_instructions() { "{errors:?}" ); } + +#[test] +fn recursive_references_are_scoped_to_their_declared_group() { + use crate::types::{ + CompositeType, DefinedType, DefinedTypeId, FieldType, HeapType, RecGroup, RefType, + StorageType, + }; + let definition = |target| DefinedType { + final_type: true, + supertype: None, + composite: CompositeType::Struct(vec![FieldType { + storage: StorageType::Ref(RefType { + nullable: true, + heap: HeapType::Index(DefinedTypeId(target)), + }), + mutable: false, + }]), + }; + let mut source = Module { + name: "recursive_group_scope".into(), + types: vec![RecGroup(vec![definition(1), definition(0)])], + strings: Vec::new(), + layout: None, + imports: Vec::new(), + functions: Vec::new(), + entry: None, + span: span(), + }; + verify_module(&source).unwrap(); + source.types = vec![RecGroup(vec![definition(1)]), RecGroup(vec![definition(0)])]; + assert!(verify_module(&source).unwrap_err().iter().any(|error| { + error + .message + .contains("defined type reference is out of range") + })); + source.types = vec![RecGroup(Vec::new())]; + assert!( + verify_module(&source) + .unwrap_err() + .iter() + .any(|error| error.message.contains("recursion group is empty")) + ); +} diff --git a/crates/psrs-backend/src/mir/verify/tests/tail.rs b/crates/psrs-backend/src/mir/verify/tests/tail.rs index 92415d51..432df1d0 100644 --- a/crates/psrs-backend/src/mir/verify/tests/tail.rs +++ b/crates/psrs-backend/src/mir/verify/tests/tail.rs @@ -124,6 +124,7 @@ fn rejects_a_direct_return_call_whose_result_differs_from_its_caller() { Vec::new(), Vec::new(), Terminator::ReturnCall { + contract: None, function: symbol(1), arguments: Vec::new(), span: span(), @@ -159,6 +160,7 @@ fn rejects_a_direct_return_call_with_the_wrong_argument_type() { span: span(), }], Terminator::ReturnCall { + contract: None, function: symbol(1), arguments: vec![ValueId(0)], span: span(), diff --git a/crates/psrs-backend/src/mir/verify/variants/mod.rs b/crates/psrs-backend/src/mir/verify/variants/mod.rs new file mode 100644 index 00000000..d31e32a9 --- /dev/null +++ b/crates/psrs-backend/src/mir/verify/variants/mod.rs @@ -0,0 +1,68 @@ +//! Nominal variant constructors must initialize the tag of their planned case. +use super::util::mir_error; +use crate::BackendError; +use crate::mir::value_identity::{definitions, integer_constant}; +use crate::mir::{Instruction, Module}; +use std::collections::{HashMap, HashSet}; + +pub(super) fn verify(module: &Module, errors: &mut Vec) { + let Some(layout) = &module.layout else { + // Standalone structural MIR carries no source nominal layout contract. + return; + }; + let tags = layout.variant_tags().collect::>(); + let bases = layout.variant_bases().collect::>(); + let types = super::defined_types_list(module); + for index in tags.keys().chain(&bases) { + let valid = types.get(index.0 as usize).is_some_and(|ty| { + matches!(&ty.composite, crate::types::CompositeType::Struct(fields) + if fields.first().is_some_and(|field| field.storage == crate::types::StorageType::I32 && !field.mutable)) + }); + if !valid { + errors.extend(mir_error( + module.span, + "MIR nominal variant tag must remain immutable i32", + )); + } + } + for function in &module.functions { + let definitions = definitions(function); + for instruction in function.blocks.iter().flat_map(|block| &block.instructions) { + let Instruction::StructNew { + type_index, + arguments, + span, + .. + } = instruction + else { + continue; + }; + if bases.contains(type_index) { + errors.extend(mir_error( + *span, + "MIR nominal variant must be constructed at a planned case type", + )); + continue; + } + let Some(expected) = tags.get(type_index) else { + continue; + }; + let actual = arguments + .first() + .and_then(|value| integer_constant(*value, &definitions)); + if actual != Some(*expected) { + errors.extend( + mir_error( + *span, + "MIR variant constructor must initialize its planned case tag", + ) + .into_iter() + .map(|error| error.with_module(function.symbol.module)), + ); + } + } + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/psrs-backend/src/mir/verify/variants/tests.rs b/crates/psrs-backend/src/mir/verify/variants/tests.rs new file mode 100644 index 00000000..7e97ea42 --- /dev/null +++ b/crates/psrs-backend/src/mir/verify/variants/tests.rs @@ -0,0 +1,258 @@ +//! The constructor contract applies to ordinary variants without control metadata. +use crate::TargetCapabilities; +use crate::cc::{Representation, RepresentationTable, VariantCase}; +use crate::mir::layout::PlannedLayout; +use crate::mir::{BasicBlock, BlockId, Function, Instruction as I, Module, Terminator as T}; +use crate::types::{FunctionId, HeapType, RefType, ValueDecl, ValueId, ValueType}; +use psrs_hir::{ModuleId, SymbolId}; +use psrs_span::TextRange; + +fn constructor(tag: i32, copy: bool) -> Module { + let span = TextRange::new(0, 1); + let table = RepresentationTable::from_parts( + vec![Representation::Variant { + cases: vec![ + VariantCase { + tag: 7, + fields: vec![], + }, + VariantCase { + tag: 13, + fields: vec![], + }, + ], + }], + Default::default(), + Default::default(), + ); + let layout = PlannedLayout::plan(&table, TargetCapabilities::default()).unwrap(); + let type_index = layout.variant_index(crate::cc::ReprId(0), 7).unwrap(); + let result_type = ValueType::Ref(RefType { + nullable: false, + heap: HeapType::Index(type_index), + }); + let mut instructions = vec![I::Constant { + destination: ValueId(0), + value: tag, + span, + }]; + if copy { + instructions.push(I::Copy { + destination: ValueId(1), + value: ValueId(0), + span, + }); + } + instructions.push(I::StructNew { + destination: ValueId(2), + type_index, + arguments: vec![ValueId(u32::from(copy))], + span, + }); + let symbol = SymbolId::new(ModuleId(0), 0); + Module { + name: "NominalConstructor".into(), + types: layout.types().to_vec(), + layout: Some(layout), + strings: vec![], + imports: vec![], + entry: Some(symbol), + span, + functions: vec![Function { + id: FunctionId(0), + symbol, + name: "constructor".into(), + parameters: vec![], + values: vec![ + ValueDecl { + id: ValueId(0), + ty: ValueType::I32, + }, + ValueDecl { + id: ValueId(1), + ty: ValueType::I32, + }, + ValueDecl { + id: ValueId(2), + ty: result_type, + }, + ], + entry: BlockId(0), + blocks: vec![BasicBlock { + id: BlockId(0), + parameters: vec![], + instructions, + terminator: Some(T::Return { + value: ValueId(2), + span, + }), + }], + result: ValueId(2), + result_type, + span, + }], + } +} + +#[test] +fn ordinary_variants_require_their_own_tag() { + for copy in [false, true] { + crate::mir::verify_module(&constructor(7, copy)).expect("matching nominal tag"); + for wrong in [0, 13] { + let errors = crate::mir::verify_module(&constructor(wrong, copy)).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("initialize its planned case tag")), + "{errors:?}" + ); + } + } +} + +#[test] +fn ordinary_variant_base_cannot_bypass_case_construction() { + let mut module = constructor(7, false); + let base = module + .layout + .as_ref() + .unwrap() + .repr_index(crate::cc::ReprId(0)) + .unwrap(); + let I::StructNew { type_index, .. } = &mut module.functions[0].blocks[0].instructions[1] else { + unreachable!() + }; + *type_index = base; + let base_type = ValueType::Ref(RefType { + nullable: false, + heap: HeapType::Index(base), + }); + module.functions[0] + .values + .iter_mut() + .find(|value| value.id == ValueId(2)) + .unwrap() + .ty = base_type; + module.functions[0].result_type = base_type; + let errors = crate::mir::verify_module(&module).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("constructed at a planned case type")), + "{errors:?}" + ); +} + +#[test] +fn ordinary_variant_tag_cannot_become_mutable() { + let mut module = constructor(7, false); + let I::StructNew { type_index, .. } = module.functions[0].blocks[0].instructions[1] else { + unreachable!() + }; + let definition = module + .types + .iter_mut() + .flat_map(|group| &mut group.0) + .nth(type_index.0 as usize) + .unwrap(); + let crate::types::CompositeType::Struct(fields) = &mut definition.composite else { + unreachable!() + }; + fields[0].mutable = true; + let errors = crate::mir::verify_module(&module).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("tag must remain immutable i32")), + "{errors:?}" + ); +} + +#[test] +fn nominal_tag_proof_accepts_equal_constants_at_a_join() { + for alternative in [7, 13] { + let mut module = constructor(7, false); + let function = &mut module.functions[0]; + let span = function.span; + function.values.extend([ + ValueDecl { + id: ValueId(3), + ty: ValueType::Boolean, + }, + ValueDecl { + id: ValueId(4), + ty: ValueType::I32, + }, + ]); + let mut construct = function.blocks[0].instructions.pop().unwrap(); + let I::StructNew { arguments, .. } = &mut construct else { + unreachable!() + }; + arguments[0] = ValueId(4); + function.blocks = vec![ + BasicBlock { + id: BlockId(0), + parameters: vec![], + instructions: vec![I::Constant { + destination: ValueId(3), + value: 1, + span, + }], + terminator: Some(T::Branch { + condition: ValueId(3), + then_block: BlockId(1), + else_block: BlockId(2), + span, + }), + }, + BasicBlock { + id: BlockId(1), + parameters: vec![], + instructions: vec![I::Constant { + destination: ValueId(0), + value: 7, + span, + }], + terminator: Some(T::Jump { + target: BlockId(3), + arguments: vec![ValueId(0)], + span, + }), + }, + BasicBlock { + id: BlockId(2), + parameters: vec![], + instructions: vec![I::Constant { + destination: ValueId(1), + value: alternative, + span, + }], + terminator: Some(T::Jump { + target: BlockId(3), + arguments: vec![ValueId(1)], + span, + }), + }, + BasicBlock { + id: BlockId(3), + parameters: vec![ValueId(4)], + instructions: vec![construct], + terminator: Some(T::Return { + value: ValueId(2), + span, + }), + }, + ]; + let result = crate::mir::verify_module(&module); + if alternative == 7 { + result.expect("equal tag constants at a normal join"); + } else { + assert!( + result + .unwrap_err() + .iter() + .any(|error| error.message.contains("initialize its planned case tag")) + ); + } + } +} diff --git a/crates/psrs-backend/src/mir/wit/aggregate/decode.rs b/crates/psrs-backend/src/mir/wit/aggregate/decode.rs index 0b293582..8dbf52df 100644 --- a/crates/psrs-backend/src/mir/wit/aggregate/decode.rs +++ b/crates/psrs-backend/src/mir/wit/aggregate/decode.rs @@ -48,13 +48,16 @@ pub(super) fn build_payload( return Ok((Some(value), block)); } let (value, block) = build_concrete(lowerer, kind, value_node, address, offset, block, span)?; - // The storage slot is either the concrete representation (`stored` names - // the same repr, so no cast) or an erased/aggregate supertype. Cast the - // built reference to the declared storage slot. - let value = if erased { - cast_reference(lowerer, value, &stored, block, span)? + // Bare polymorphic slots store the aggregate owner's canonical protocol, + // rather than a reference to the specialized aggregate. + let (block, value) = if matches!(stored, ValueShape::Reference(reference) + if reference.heap == RefShape::Erased) + { + lowerer.wit_payload_conversion(block, value, value_node.shape(), true, span)? + } else if erased { + (block, cast_reference(lowerer, value, &stored, block, span)?) } else { - value + (block, value) }; Ok((Some(value), block)) } @@ -344,6 +347,7 @@ fn read_string( lowerer.append_wit_instruction( block, Instruction::Call { + contract: None, destination: value, function: abi::BYTES_TO_STRING_SYMBOL, arguments: vec![pointer, length], @@ -383,23 +387,17 @@ fn read_record( .iter() .position(|candidate| candidate == &label) .ok_or_else(|| unsupported(span))?; - let (value, next) = build_concrete( + let (value, next) = build_payload( lowerer, &wit_field.ty, &product[index].value, + product[index].stored, address, offset + field_offset, block, span, )?; - // The concrete node builds the nested representation; the record's - // storage slot may be an erased or aggregate supertype that needs a - // cast before the struct is built. - let value = if is_erased(product[index].stored) { - cast_reference(lowerer, value, &product[index].stored, next, span)? - } else { - value - }; + let value = value.ok_or_else(|| unsupported(span))?; values[index] = Some(value); block = next; } diff --git a/crates/psrs-backend/src/mir/wit/aggregate/mod.rs b/crates/psrs-backend/src/mir/wit/aggregate/mod.rs index f36936f0..492178a2 100644 --- a/crates/psrs-backend/src/mir/wit/aggregate/mod.rs +++ b/crates/psrs-backend/src/mir/wit/aggregate/mod.rs @@ -312,10 +312,10 @@ pub(super) fn recover_payload( kind: &CanonicalType, block: BlockId, span: TextRange, -) -> Result<(ValueId, crate::cc::GuestLayout), Vec> { +) -> Result<(ValueId, crate::cc::GuestLayout, BlockId), Vec> { // A concrete storage slot already stores the source value. if !is_erased(field.stored) { - return Ok((value, field.value.clone())); + return Ok((value, field.value.clone(), block)); } // An erased slot holds a reference (scalar box or erased aggregate). // Recover the concrete source value from the projected node; when the node @@ -327,6 +327,16 @@ pub(super) fn recover_payload( } else { field.value.clone() }; + if matches!(field.stored, ValueShape::Reference(reference) if reference.heap == RefShape::Erased) + && matches!( + concrete, + GuestLayout::Array { .. } | GuestLayout::Product { .. } + ) + { + let (block, recovered) = + lowerer.wit_payload_conversion(block, value, concrete.shape(), false, span)?; + return Ok((recovered, concrete, block)); + } let recovered = match &concrete { crate::cc::GuestLayout::Scalar { shape } => match shape { ValueShape::Integer => unbox_scalar(lowerer, value, false, block, span)?, @@ -337,7 +347,7 @@ pub(super) fn recover_payload( }, other => cast_reference(lowerer, value, &other.shape(), block, span)?, }; - Ok((recovered, concrete)) + Ok((recovered, concrete, block)) } /// Whether a projected node is the storage fallback rather than a concrete diff --git a/crates/psrs-backend/src/mir/wit/aggregate/parameter.rs b/crates/psrs-backend/src/mir/wit/aggregate/parameter.rs index d85c4e2d..7de8bc6e 100644 --- a/crates/psrs-backend/src/mir/wit/aggregate/parameter.rs +++ b/crates/psrs-backend/src/mir/wit/aggregate/parameter.rs @@ -38,11 +38,6 @@ pub(in crate::mir) fn lower_variant_parameter( .map(|_| lowerer.wit_new_block(Vec::new())) .collect::>(); let default = lowerer.wit_new_block(Vec::new()); - let merge_parameters = joined - .iter() - .map(|ty| lowerer.fresh_wit_value(*ty)) - .collect::>(); - let merge = lowerer.wit_new_block(merge_parameters.clone()); lowerer.wit_switch( current, tag, @@ -56,6 +51,7 @@ pub(in crate::mir) fn lower_variant_parameter( )?; lowerer.wit_jump(default, case_blocks[0], Vec::new(), span)?; + let mut lowered_cases = Vec::new(); for (source_index, _) in case_kinds.iter().enumerate() { let block = case_blocks[source_index]; // The switch is keyed by the guest tag; the selected payload is the @@ -66,7 +62,7 @@ pub(in crate::mir) fn lower_variant_parameter( (Some(payload), Some(case_field)) => (payload, case_field), _ => { let zeros = zero_arguments(lowerer, &joined, block, span)?; - lowerer.wit_jump(block, merge, zeros, span)?; + lowered_cases.push((block, zeros, Vec::new())); continue; } }; @@ -75,23 +71,72 @@ pub(in crate::mir) fn lower_variant_parameter( .ok_or_else(|| unsupported(span))?; let erased = lowerer.fresh_wit_value(field_type); lowerer.wit_variant_get(block, erased, repr, source_index as u32, 0, argument, span)?; - let (value, payload_guest) = + let (value, payload_guest, block) = recover_payload(lowerer, erased, case_field, payload, block, span)?; let mut case_flat = Vec::new(); + let mut case_frees = Vec::new(); let end = lower_parameter( lowerer, value, Some(&payload_guest), payload, &mut case_flat, - frees, + &mut case_frees, block, span, )?; let arguments = pad_to(lowerer, case_flat, &joined, end, span)?; + lowered_cases.push((end, arguments, case_frees)); + } + merge_cases(lowerer, lowered_cases, &joined, flat, frees, span) +} + +/// A branch's buffers remain live through the host call. Merge their pointer, +/// byte length and element count as well as the flattened canonical payload. +/// Inactive branches supply null/zero, so post-call frees are dominated and +/// free no storage for an unselected case, including nested variants. +fn merge_cases( + lowerer: &mut L, + cases: Vec<(BlockId, Vec, Vec)>, + joined: &[ValueType], + flat: &mut Vec, + frees: &mut Vec, + span: TextRange, +) -> Result> { + let payload = joined + .iter() + .map(|ty| lowerer.fresh_wit_value(*ty)) + .collect::>(); + let free_count = cases.iter().map(|(_, _, frees)| frees.len()).sum::(); + let free_parameters = (0..free_count * 3) + .map(|_| lowerer.fresh_wit_value(ValueType::I32)) + .collect::>(); + let merge = lowerer.wit_new_block(payload.iter().chain(&free_parameters).copied().collect()); + let mut offset = 0; + for (end, mut arguments, case_frees) in cases { + let zero = zero_argument(lowerer, ValueType::I32, end, span)?; + let mut free_arguments = vec![zero; free_parameters.len()]; + let count = case_frees.len(); + for (index, mut pending) in case_frees.into_iter().enumerate() { + let slot = (offset + index) * 3; + free_arguments[slot] = pending.pointer; + free_arguments[slot + 1] = pending.length; + free_arguments[slot + 2] = pending + .elements + .as_ref() + .map_or(zero, |elements| elements.count); + pending.pointer = free_parameters[slot]; + pending.length = free_parameters[slot + 1]; + if let Some(elements) = &mut pending.elements { + elements.count = free_parameters[slot + 2]; + } + frees.push(pending); + } + offset += count; + arguments.extend(free_arguments); lowerer.wit_jump(end, merge, arguments, span)?; } - flat.extend(merge_parameters); + flat.extend(payload); Ok(merge) } diff --git a/crates/psrs-backend/src/mir/wit/call_lowerer.rs b/crates/psrs-backend/src/mir/wit/call_lowerer.rs index 726c364c..edef6373 100644 --- a/crates/psrs-backend/src/mir/wit/call_lowerer.rs +++ b/crates/psrs-backend/src/mir/wit/call_lowerer.rs @@ -8,6 +8,10 @@ use crate::types::{DefinedTypeId, ValueId, ValueType}; use psrs_span::TextRange; pub(crate) trait WitCallLowerer { + /// The physical type of a checked concrete result before payload erasure. + fn wit_value_type(&self, _shape: ValueShape) -> Option { + None + } fn fresh_wit_value(&mut self, ty: ValueType) -> ValueId; fn append_wit_instruction( &mut self, @@ -125,6 +129,26 @@ pub(crate) trait WitCallLowerer { None } + fn wit_stored_guest_layout(&self, field: &crate::cc::Field) -> Option { + self.wit_guest_layout(field.stored) + } + + /// Converts an aggregate into or out of its bare-slot storage protocol. + fn wit_payload_conversion( + &mut self, + _block: BlockId, + _value: ValueId, + _shape: ValueShape, + _entering: bool, + span: TextRange, + ) -> Result<(BlockId, ValueId), Vec> { + Err(vec![BackendError::new( + "P9 MIR lowering", + span, + "canonical aggregate payload has no storage conversion", + )]) + } + /// The concrete GC type of a representation handle. fn wit_repr_index(&self, _repr: crate::cc::ReprId) -> Option { None diff --git a/crates/psrs-backend/src/mir/wit/function_lowerer.rs b/crates/psrs-backend/src/mir/wit/function_lowerer.rs index a0f86314..3e157c0d 100644 --- a/crates/psrs-backend/src/mir/wit/function_lowerer.rs +++ b/crates/psrs-backend/src/mir/wit/function_lowerer.rs @@ -7,6 +7,9 @@ use crate::types::{DefinedTypeId, ValueId, ValueType}; use psrs_span::TextRange; impl WitCallLowerer for FunctionLowerer<'_> { + fn wit_value_type(&self, shape: crate::cc::ValueShape) -> Option { + self.resolved_value_type(&shape) + } fn fresh_wit_value(&mut self, ty: ValueType) -> ValueId { self.fresh(ty) } @@ -169,6 +172,38 @@ impl WitCallLowerer for FunctionLowerer<'_> { self.resolved_guest_layout(shape) } + fn wit_stored_guest_layout(&self, field: &crate::cc::Field) -> Option { + crate::cc::payload::stored_guest_layout(field, self.layout.representation_table()) + } + + fn wit_payload_conversion( + &mut self, + block: BlockId, + value: ValueId, + shape: crate::cc::ValueShape, + entering: bool, + span: TextRange, + ) -> Result<(BlockId, ValueId), Vec> { + use crate::cc::payload::PayloadPlanner; + let mut planner = + crate::cc::payload::StoragePayloadPlanner(self.layout.representation_table()); + let plan = if entering { + planner.erase_payload(shape, span)? + } else { + planner.recover_payload(shape, span)? + }; + let source = if entering { + shape + } else { + crate::cc::ValueShape::Reference(crate::cc::Reference { + nullable: false, + heap: crate::cc::RefShape::Erased, + }) + }; + let (block, value, _) = self.lower_value_conversion(block, value, source, &plan, span)?; + Ok((block, value)) + } + fn wit_repr_index(&self, repr: crate::cc::ReprId) -> Option { self.resolved_repr_index(repr) } diff --git a/crates/psrs-backend/src/mir/wit/lists/mod.rs b/crates/psrs-backend/src/mir/wit/lists/mod.rs index d899e3dc..914e5852 100644 --- a/crates/psrs-backend/src/mir/wit/lists/mod.rs +++ b/crates/psrs-backend/src/mir/wit/lists/mod.rs @@ -179,11 +179,7 @@ pub(super) fn read_value_list_result( current: BlockId, span: TextRange, ) -> Result<(), Vec> { - if let CanonicalType::Handle { - ownership: crate::abi::canonical::Ownership::Borrow, - .. - } = element - { + if crate::abi::canonical::reject_borrowed_control_capture(std::iter::once(element)).is_err() { // A borrowed handle must not escape the call that produced it. An owned // handle list is an ordinary `i32` array: the standard library drops // each extracted handle (DEC-14), so the compiler copies the indices. @@ -269,7 +265,7 @@ fn element_guest( span: TextRange, ) -> Result> { if let Some(GuestLayout::Array { element: field, .. }) = list - && let Some(layout) = lowerer.wit_guest_layout(field.stored) + && let Some(layout) = lowerer.wit_stored_guest_layout(field) { return Ok(layout); } @@ -382,6 +378,7 @@ pub(super) fn allocate( lowerer.append_wit_instruction( current, Instruction::Call { + contract: None, destination: address, function: abi::REALLOC_SYMBOL, arguments, diff --git a/crates/psrs-backend/src/mir/wit/mod.rs b/crates/psrs-backend/src/mir/wit/mod.rs index efc251ef..f36facd7 100644 --- a/crates/psrs-backend/src/mir/wit/mod.rs +++ b/crates/psrs-backend/src/mir/wit/mod.rs @@ -14,10 +14,13 @@ mod function_lowerer; mod handles; mod lists; mod parameters; +mod plan; pub(super) use bind::BoundFn; pub(super) use call_lowerer::WitCallLowerer; pub(super) use handles::verify_function; +pub(super) use parameters::lower_string; +pub(in crate::mir) use plan::CallPlan; use super::{BlockId, instruction::Instruction}; use crate::BackendError; @@ -78,6 +81,88 @@ pub(super) fn lower( arguments: &[ValueId], span: TextRange, entry: BlockId, +) -> Result> { + // Canonical construction produces a concrete guest value. An erased + // source slot is filled only after the shared payload protocol discharges + // its concrete representation, including block parameter types. + let concrete = projection + .and_then(|projection| projection.result.as_ref()) + .map(GuestLayout::shape) + .filter(|shape| { + matches!( + signature.result, + crate::cc::ValueShape::Reference(crate::cc::Reference { + heap: crate::cc::RefShape::Erased | crate::cc::RefShape::Aggregate, + .. + }) + ) && *shape != signature.result + }); + let Some(shape) = concrete else { + return lower_concrete( + lowerer, + import, + signature, + projection, + destination, + arguments, + span, + entry, + ); + }; + let ty = lowerer.wit_value_type(shape).ok_or_else(|| { + vec![BackendError::invalid_ir( + "P9 canonical call planning", + span, + "canonical result has no checked concrete value type", + )] + })?; + let value = lowerer.fresh_wit_value(ty); + let current = lower_concrete( + lowerer, import, signature, projection, value, arguments, span, entry, + )?; + if signature.result == crate::cc::payload::erased_shape() { + let (current, value) = lowerer.wit_payload_conversion(current, value, shape, true, span)?; + lowerer.append_wit_instruction( + current, + Instruction::Copy { + destination, + value, + span, + }, + span, + )?; + return Ok(current); + } + let Some(ValueType::Ref(reference)) = lowerer.wit_value_type(signature.result) else { + return Err(vec![BackendError::invalid_ir( + "P9 canonical call planning", + span, + "canonical aggregate result has no checked reference slot", + )]); + }; + lowerer.append_wit_instruction( + current, + Instruction::RefCast { + destination, + value, + reference, + span, + }, + span, + )?; + Ok(current) +} + +#[allow(clippy::too_many_arguments)] +fn lower_concrete( + lowerer: &mut L, + import: &WasiImport, + signature: &crate::cc::Signature, + projection: Option<&crate::cc::ExternalProjection>, + destination: ValueId, + arguments: &[ValueId], + span: TextRange, + entry: BlockId, ) -> Result> { let bound = BoundFn::bind(import, signature, projection); let mut flat = Vec::new(); @@ -133,6 +218,7 @@ pub(super) fn lower( lowerer.append_wit_instruction( current, Instruction::Call { + contract: None, destination, function: crate::abi::BYTES_TO_STRING_SYMBOL, arguments: vec![pointer, length], @@ -185,6 +271,7 @@ pub(super) fn lower( lowerer.append_wit_instruction( current, Instruction::Call { + contract: None, destination: value, function: import.symbol, arguments: flat, @@ -207,6 +294,7 @@ pub(super) fn lower( lowerer.append_wit_instruction( current, Instruction::Call { + contract: None, destination: value, function: import.symbol, arguments: flat, @@ -228,6 +316,7 @@ pub(super) fn lower( Some(ValueType::I32 | ValueType::F64) => lowerer.append_wit_instruction( current, Instruction::Call { + contract: None, destination, function: import.symbol, arguments: flat, @@ -371,6 +460,7 @@ pub(super) fn free_buffer( lowerer.append_wit_instruction( current, Instruction::Call { + contract: None, destination: discarded, function: abi::REALLOC_SYMBOL, arguments: vec![pointer, length, align_value, zero], diff --git a/crates/psrs-backend/src/mir/wit/parameters/indirect.rs b/crates/psrs-backend/src/mir/wit/parameters/indirect.rs index 61320052..18af49e4 100644 --- a/crates/psrs-backend/src/mir/wit/parameters/indirect.rs +++ b/crates/psrs-backend/src/mir/wit/parameters/indirect.rs @@ -215,6 +215,7 @@ fn allocate_parameter_area( lowerer.append_wit_instruction( current, Instruction::Call { + contract: None, destination: address, function: abi::REALLOC_SYMBOL, arguments, diff --git a/crates/psrs-backend/src/mir/wit/parameters/mod.rs b/crates/psrs-backend/src/mir/wit/parameters/mod.rs index 378e2b42..bd7e66d2 100644 --- a/crates/psrs-backend/src/mir/wit/parameters/mod.rs +++ b/crates/psrs-backend/src/mir/wit/parameters/mod.rs @@ -198,7 +198,7 @@ pub(super) fn lower_parameter( /// Copies a GC string's canonical UTF-8 bytes into a fresh linear buffer. The /// helper returns the address of a length prefix; the canonical exchange passes /// the payload pointer and byte length, and the buffer is freed after the call. -fn lower_string( +pub(in crate::mir) fn lower_string( lowerer: &mut L, argument: ValueId, flat: &mut Vec, @@ -210,6 +210,7 @@ fn lower_string( lowerer.append_wit_instruction( current, Instruction::Call { + contract: None, destination: prefix, function: crate::abi::STRING_TO_BYTES_SYMBOL, arguments: vec![argument], diff --git a/crates/psrs-backend/src/mir/wit/plan.rs b/crates/psrs-backend/src/mir/wit/plan.rs new file mode 100644 index 00000000..5fc766bc --- /dev/null +++ b/crates/psrs-backend/src/mir/wit/plan.rs @@ -0,0 +1,80 @@ +//! Immutable canonical adapter plans, produced before physical emission. +use crate::BackendError; +use crate::cc; +use crate::mir::{BasicBlock, BlockId, Function}; +use crate::types::ValueDecl; +use psrs_hir::SymbolId; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(in crate::mir) struct CallPlan { + owner: SymbolId, + source: cc::Assignment, + start: usize, + exit: BlockId, + blocks: Vec, + temporaries: Vec, +} + +impl CallPlan { + #[allow(clippy::too_many_arguments)] + pub(in crate::mir) fn new( + owner: SymbolId, + source: cc::Assignment, + start: usize, + exit: BlockId, + blocks: Vec, + temporaries: Vec, + ) -> Self { + Self { + owner, + source, + start, + exit, + blocks, + temporaries, + } + } + + pub(in crate::mir) fn verify(&self, function: &Function) -> Result<(), Vec> { + let invalid = || { + vec![BackendError::invalid_ir( + "P9 canonical call verification", + self.source.span, + "canonical adapter differs from its immutable checked plan", + )] + }; + if function.symbol != self.owner { + return Err(invalid()); + } + for temporary in &self.temporaries { + if !function.values.iter().any(|actual| actual == temporary) { + return Err(invalid()); + } + } + for (index, expected) in self.blocks.iter().enumerate() { + let actual = function + .blocks + .iter() + .find(|block| block.id == expected.id) + .ok_or_else(invalid)?; + let start = if index == 0 { self.start } else { 0 }; + let end = start + .checked_add(expected.instructions.len()) + .ok_or_else(invalid)?; + if actual.instructions.get(start..end) != Some(expected.instructions.as_slice()) { + return Err(invalid()); + } + // The exit port belongs to the enclosing source continuation; + // every other adapter edge and block is fixed by the plan. + if expected.id != self.exit + && (actual.instructions.len() != end || actual.terminator != expected.terminator) + { + return Err(invalid()); + } + if index != 0 && actual.parameters != expected.parameters { + return Err(invalid()); + } + } + Ok(()) + } +} diff --git a/crates/psrs-backend/src/mir/wit/tests/handles.rs b/crates/psrs-backend/src/mir/wit/tests/handles.rs index 9d84ba19..bd8e4179 100644 --- a/crates/psrs-backend/src/mir/wit/tests/handles.rs +++ b/crates/psrs-backend/src/mir/wit/tests/handles.rs @@ -158,6 +158,7 @@ fn the_verifier_rejects_an_owned_handle_dropped_twice() { ); let instructions = vec![ Instruction::Call { + contract: None, destination: ValueId(1), function: symbol, arguments: Vec::new(), @@ -195,6 +196,7 @@ fn the_verifier_accepts_an_explicit_drop() { ); let instructions = vec![ Instruction::Call { + contract: None, destination: ValueId(1), function: symbol, arguments: Vec::new(), diff --git a/crates/psrs-backend/src/pipeline/cc_input.rs b/crates/psrs-backend/src/pipeline/cc_input.rs new file mode 100644 index 00000000..2df12f52 --- /dev/null +++ b/crates/psrs-backend/src/pipeline/cc_input.rs @@ -0,0 +1,23 @@ +//! The shared target pipeline is also usable from its defined CC boundary. + +use crate::{Artifact, BackendError, BackendInput, TargetCapabilities, annotate_errors, cc, mir}; + +pub(crate) fn compile(input: BackendInput) -> Result> { + let target = TargetCapabilities::default(); + let owner = input.cc.entry.map(|entry| entry.module); + cc::verify_module(&input.cc).map_err(|errors| annotate_errors(errors, owner))?; + let context = crate::linking::default_context()?; + let (mir, mut wasi) = mir::lower_module_with_bindings_and_resolve( + input.cc, + input.externals, + target, + context.shared_resolve(), + )?; + let mir = mir::opt::optimize(mir, target)?; + let emitted = super::emission::emit(&mir, &mut wasi, target, owner, None, None, None)?; + Ok(Artifact { + wasm: emitted.component, + wat: emitted.wat, + warnings: input.warnings, + }) +} diff --git a/crates/psrs-backend/src/pipeline/emission.rs b/crates/psrs-backend/src/pipeline/emission.rs new file mode 100644 index 00000000..cb660060 --- /dev/null +++ b/crates/psrs-backend/src/pipeline/emission.rs @@ -0,0 +1,279 @@ +use super::*; + +use crate::linking; + +pub(crate) struct EmittedWasm { + pub module: crate::wasm::Module, + pub component: Vec, + pub wat: String, +} + +pub(crate) fn emit( + mir: &mir::Module, + wasi: &mut crate::abi::WasiRegistry, + target: TargetCapabilities, + owner: Option, + mir_id: Option, + wasi_id: Option, + mut trace: Option<&mut TraceRecorder>, +) -> Result> { + let target_parameter = || { + vec![TraceParameter { + key: "target_capabilities", + value: format!("{target:?}"), + }] + }; + let context = linking::default_context().map_err(|errors| annotate_errors(errors, owner))?; + let mut plan_call = trace.as_deref_mut().map(|trace| { + trace.begin( + "backend.target.plan", + &[ + mir_id.expect("traced MIR before target planning"), + wasi_id.expect("traced WASI registry before target planning"), + ], + TraceValidationCoverage::Composite, + target_parameter(), + ) + }); + let link = match linking::plan_for_module(&context, mir, wasi, target) { + Ok(link) => link, + Err(errors) => { + if let (Some(trace), Some(call)) = (trace.as_deref_mut(), plan_call) { + trace.reject(call, errors.len()); + } + return Err(annotate_errors(errors, owner)); + } + }; + if let Some(call) = plan_call.as_mut() { + annotate_plan(call, &link); + } + if let (Some(trace), Some(call)) = (trace.as_deref_mut(), plan_call) { + trace.complete( + call, + &[TraceRepresentation::LinkPlan], + &[TraceValidationSpec::output( + "psrs_linker::plan", + 0, + TraceValidationCoverage::Composite, + )], + ); + } + + let wasm_call = trace.as_deref_mut().map(|trace| { + trace.begin( + "backend.wasm.lower", + &[ + mir_id.expect("traced MIR before Wasm lowering"), + wasi_id.expect("traced WASI registry before Wasm lowering"), + ], + TraceValidationCoverage::Composite, + target_parameter(), + ) + }); + let module = match crate::wasm::lower_module_with_plan(mir, wasi, target, &link) { + Ok(module) => module, + Err(errors) => { + if let (Some(trace), Some(call)) = (trace.as_deref_mut(), wasm_call) { + trace.reject(call, errors.len()); + } + return Err(annotate_errors(errors, owner)); + } + }; + let wasm_id = if let (Some(trace), Some(call)) = (trace.as_deref_mut(), wasm_call) { + trace + .complete( + call, + &[TraceRepresentation::WasmModule], + &[TraceValidationSpec::output( + "wasm::lower_module_with_plan", + 0, + TraceValidationCoverage::Composite, + )], + ) + .first() + .copied() + } else { + None + }; + + let encode_call = trace.as_deref_mut().map(|trace| { + trace.begin( + "backend.wasm.encode", + &[wasm_id.expect("traced Wasm module before encoding")], + TraceValidationCoverage::NotObserved, + Vec::new(), + ) + }); + let core = match crate::wasm::encode_module(&module) { + Ok(core) => core, + Err(errors) => { + if let (Some(trace), Some(call)) = (trace.as_deref_mut(), encode_call) { + trace.reject(call, errors.len()); + } + return Err(annotate_errors(errors, owner)); + } + }; + let core_id = if let (Some(trace), Some(call)) = (trace.as_deref_mut(), encode_call) { + trace + .complete(call, &[TraceRepresentation::WasmCoreBinary], &[]) + .first() + .copied() + } else { + None + }; + + let component_call = trace.as_deref_mut().map(|trace| { + trace.begin( + "backend.component.assemble", + &[core_id.expect("traced core Wasm before component assembly")], + TraceValidationCoverage::Composite, + Vec::new(), + ) + }); + let binary = match linking::compose(&link, &core, mir.span, owner) { + Ok(binary) => binary, + Err(errors) => { + if let (Some(trace), Some(call)) = (trace.as_deref_mut(), component_call) { + trace.reject(call, errors.len()); + } + return Err(annotate_errors(errors, owner)); + } + }; + let component_id = if let (Some(trace), Some(call)) = (trace.as_deref_mut(), component_call) { + trace + .complete( + call, + &[TraceRepresentation::ComponentBinary], + &[TraceValidationSpec::output( + "psrs_linker::compose", + 0, + TraceValidationCoverage::Composite, + )], + ) + .first() + .copied() + } else { + None + }; + + let validate_call = trace.as_deref_mut().map(|trace| { + trace.begin( + "backend.component.validate", + &[component_id.expect("traced component before validation")], + TraceValidationCoverage::Direct, + target_parameter(), + ) + }); + if let Err(error) = crate::validator_for(target).validate_all(&binary) { + let errors = annotate_errors( + vec![BackendError::new( + "P11 Wasm validation", + mir.span, + format!("generated WebAssembly failed validation: {error}"), + )], + owner, + ); + if let (Some(trace), Some(call)) = (trace.as_deref_mut(), validate_call) { + trace.reject_validation( + call, + errors.len(), + "wasmparser::Validator::validate_all", + &[component_id.expect("traced component before validation")], + ); + } + return Err(errors); + } + if let (Some(trace), Some(call)) = (trace.as_deref_mut(), validate_call) { + trace.complete( + call, + &[], + &[TraceValidationSpec::input( + "wasmparser::Validator::validate_all", + 0, + TraceValidationCoverage::Direct, + )], + ); + } + + let print_call = trace.as_deref_mut().map(|trace| { + trace.begin( + "backend.wat.print", + &[component_id.expect("traced component before WAT printing")], + TraceValidationCoverage::NotObserved, + Vec::new(), + ) + }); + let wat = match wasmprinter::print_bytes(&binary) { + Ok(wat) => wat, + Err(error) => { + if let (Some(trace), Some(call)) = (trace.as_deref_mut(), print_call) { + trace.reject(call, 1); + } + return Err(annotate_errors( + vec![BackendError::new( + "P11 WAT printing", + mir.span, + format!("generated WebAssembly could not be printed as WAT: {error}"), + )], + owner, + )); + } + }; + if let (Some(trace), Some(call)) = (trace, print_call) { + trace.complete(call, &[TraceRepresentation::WatText], &[]); + } + + Ok(EmittedWasm { + module, + component: binary, + wat, + }) +} + +/// Records the checked plan's lineage: selected providers, artifact digests, +/// memory boundary, and the planned external world. +fn annotate_plan(call: &mut crate::trace::TraceCall, link: &linking::LinkPlan) { + let plan = &link.plan; + call.add_parameter("requirements", plan.bindings().len().to_string()); + call.add_parameter("artifacts", plan.artifacts().len().to_string()); + call.add_parameter( + "selected_providers", + plan.bindings() + .iter() + .map(|binding| format!("{}={}.{}", binding.origin, binding.module, binding.field)) + .collect::>() + .join(";"), + ); + call.add_parameter( + "artifact_digests", + plan.artifacts() + .iter() + .map(|artifact| format!("{}={}", artifact.id, artifact.sha256)) + .collect::>() + .join(";"), + ); + call.add_parameter("external_world", plan.external_world().join(";")); + call.add_parameter( + "stack_bounds", + plan.artifacts() + .iter() + .filter_map(|artifact| { + artifact + .stack_bound_bytes + .map(|bound| format!("{}={bound}", artifact.id)) + }) + .collect::>() + .join(";"), + ); + call.add_parameter("heap_start", plan.memory().heap_start.to_string()); + call.add_parameter("minimum_pages", plan.memory().minimum_pages.to_string()); + call.add_parameter("growth_owner", plan.growth_owner().to_string()); + call.add_parameter( + "runtime_units", + plan.units() + .iter() + .map(|unit| unit.id.clone()) + .collect::>() + .join(";"), + ); +} diff --git a/crates/psrs-backend/src/pipeline/mod.rs b/crates/psrs-backend/src/pipeline/mod.rs new file mode 100644 index 00000000..85178826 --- /dev/null +++ b/crates/psrs-backend/src/pipeline/mod.rs @@ -0,0 +1,345 @@ +use crate::trace::{ + CompileTrace, TraceArtifactId, TraceParameter, TraceRecorder, TraceRepresentation, + TraceValidationCoverage, TraceValidationSpec, +}; +use crate::{ + Artifact, BackendError, CompileFailure, PartialStages, Stages, TargetCapabilities, + annotate_errors, +}; + +use crate::{cc, mir}; + +mod cc_input; +mod emission; + +pub(crate) use cc_input::compile as compile_cc; + +pub(crate) fn compile_with_context_inner( + module: psrs_core::Module, + target: TargetCapabilities, + mut capture: Option<&mut PartialStages>, + mut trace: Option<&mut TraceRecorder>, +) -> Result> { + let target_parameter = || { + vec![TraceParameter { + key: "target_capabilities", + value: format!("{target:?}"), + }] + }; + let mut core_id = trace.as_deref().map(|trace| trace.initial_core()); + let owner = module.entry.map(|entry| entry.module); + + let core_call = trace.as_deref_mut().map(|trace| { + trace.begin( + "backend.core.optimize", + &[core_id.expect("traced Core input")], + TraceValidationCoverage::Composite, + target_parameter(), + ) + }); + let module = match psrs_core::opt::optimize(module, psrs_core::opt::Budget::default()) { + Ok(module) => module, + Err(errors) => { + if let (Some(trace), Some(call)) = (trace.as_deref_mut(), core_call) { + trace.reject(call, errors.len()); + } + return Err(annotate_errors( + errors + .into_iter() + .map(|error| { + BackendError::new("P7 Core optimization", error.span, error.message) + .with_module(error.module) + }) + .collect(), + owner, + )); + } + }; + let optimized_core = module.clone(); + if let Some(capture) = capture.as_deref_mut() { + capture.core = Some(optimized_core.clone()); + } + if let (Some(trace), Some(call)) = (trace.as_deref_mut(), core_call) { + core_id = trace + .complete( + call, + &[TraceRepresentation::Core], + &[TraceValidationSpec::output( + "psrs_core::opt::optimize", + 0, + TraceValidationCoverage::Composite, + )], + ) + .first() + .copied(); + } + + let binding_call = trace.as_deref_mut().map(|trace| { + trace.begin( + "backend.external_bindings.extract", + &[core_id.expect("traced optimized Core")], + TraceValidationCoverage::NotObserved, + Vec::new(), + ) + }); + let external_bindings = crate::ExternalBindings::from_core(&module); + let binding_id = if let (Some(trace), Some(call)) = (trace.as_deref_mut(), binding_call) { + trace + .complete(call, &[TraceRepresentation::ExternalBindings], &[]) + .first() + .copied() + } else { + None + }; + + let conformance_call = trace.as_deref_mut().map(|trace| { + trace.begin( + "backend.external_bindings.validate_conformance", + &[ + core_id.expect("traced Core before binding validation"), + binding_id.expect("traced external bindings"), + ], + TraceValidationCoverage::Direct, + target_parameter(), + ) + }); + if let Err(errors) = external_bindings.validate_conformance(&module, target) { + if let (Some(trace), Some(call)) = (trace.as_deref_mut(), conformance_call) { + trace.reject_validation( + call, + errors.len(), + "ExternalBindings::validate_conformance", + &[ + core_id.expect("traced Core before binding validation"), + binding_id.expect("traced external bindings"), + ], + ); + } + return Err(errors); + } + if let (Some(trace), Some(call)) = (trace.as_deref_mut(), conformance_call) { + trace.complete( + call, + &[], + &[TraceValidationSpec::inputs( + "ExternalBindings::validate_conformance", + &[0, 1], + TraceValidationCoverage::Direct, + )], + ); + } + + let cc_call = trace.as_deref_mut().map(|trace| { + trace.begin( + "backend.cc.lower", + &[ + core_id.expect("traced Core before CC lowering"), + binding_id.expect("traced external bindings"), + ], + TraceValidationCoverage::Composite, + target_parameter(), + ) + }); + let lowered_cc = match cc::lower_module_with_relations( + module, + external_bindings, + None, + crate::boundary::RepresentationRegistry::new(), + ) { + Ok(lowered) => lowered, + Err(errors) => { + if let (Some(trace), Some(call)) = (trace.as_deref_mut(), cc_call) { + trace.reject(call, errors.len()); + } + return Err(errors); + } + }; + let cc = lowered_cc.cc; + let (cc_id, binding_id) = if let (Some(trace), Some(call)) = (trace.as_deref_mut(), cc_call) { + let outputs = trace.complete( + call, + &[ + TraceRepresentation::ClosureConverted, + TraceRepresentation::ExternalBindings, + ], + &[TraceValidationSpec::outputs( + "cc::lower_module_with_bindings", + &[0], + TraceValidationCoverage::Composite, + )], + ); + (outputs.first().copied(), outputs.get(1).copied()) + } else { + (None, None) + }; + if let Some(capture) = capture.as_deref_mut() { + capture.cc = Some(cc.clone()); + } + + let mir_call = trace.as_deref_mut().map(|trace| { + trace.begin( + "backend.mir.lower", + &[ + cc_id.expect("traced CC before MIR lowering"), + binding_id.expect("traced external bindings before MIR lowering"), + ], + TraceValidationCoverage::Composite, + target_parameter(), + ) + }); + let context = match crate::linking::default_context() { + Ok(context) => context, + Err(errors) => { + if let (Some(trace), Some(call)) = (trace.as_deref_mut(), mir_call) { + trace.reject(call, errors.len()); + } + return Err(annotate_errors(errors, cc.entry.map(|entry| entry.module))); + } + }; + let (mir, mut wasi) = match mir::lower_module_with_bindings_and_resolve( + cc.clone(), + lowered_cc.externals, + target, + context.shared_resolve(), + ) { + Ok(result) => result, + Err(errors) => { + if let (Some(trace), Some(call)) = (trace.as_deref_mut(), mir_call) { + trace.reject(call, errors.len()); + } + return Err(errors); + } + }; + let (mut mir, mir_ids) = if let (Some(trace), Some(call)) = (trace.as_deref_mut(), mir_call) { + let outputs = trace.complete( + call, + &[TraceRepresentation::Mir, TraceRepresentation::WasiRegistry], + &[TraceValidationSpec::output( + "mir::lower_module_with_bindings", + 0, + TraceValidationCoverage::Composite, + )], + ); + (mir, outputs) + } else { + (mir, Vec::new()) + }; + let mut mir_id = mir_ids.first().copied(); + let wasi_id = mir_ids.get(1).copied(); + if let Some(capture) = capture.as_deref_mut() { + capture.mir = Some(mir.clone()); + capture.mir_stage = Some("P9 MIR lowering"); + } + + let mir_opt_call = trace.as_deref_mut().map(|trace| { + trace.begin( + "backend.mir.optimize", + &[mir_id.expect("traced MIR before optimization")], + TraceValidationCoverage::Composite, + target_parameter(), + ) + }); + mir = match mir::opt::optimize(mir, target) { + Ok(mir) => mir, + Err(errors) => { + if let (Some(trace), Some(call)) = (trace.as_deref_mut(), mir_opt_call) { + trace.reject(call, errors.len()); + } + return Err(errors); + } + }; + if let (Some(trace), Some(call)) = (trace.as_deref_mut(), mir_opt_call) { + mir_id = trace + .complete( + call, + &[TraceRepresentation::Mir], + &[TraceValidationSpec::output( + "mir::opt::optimize", + 0, + TraceValidationCoverage::Composite, + )], + ) + .first() + .copied(); + } + if let Some(capture) = capture { + capture.mir = Some(mir.clone()); + capture.mir_stage = Some("P10 MIR optimization"); + } + + let owner = mir.entry.map(|entry| entry.module); + let target_call = trace.as_deref_mut().map(|trace| { + trace.begin( + "backend.target.requirements", + &[mir_id.expect("traced MIR before target check")], + TraceValidationCoverage::Direct, + target_parameter(), + ) + }); + if !target.component_model + || !target.wasi_p2 + || !target.wasi_cli + || !target.wasi_io + || !target.wasi_clocks + || !target.wasi_random + { + let errors = annotate_errors( + vec![BackendError::new( + "P11 target capabilities", + mir.span, + "the current artifact pipeline requires Component Model and WASI 0.2 capabilities", + )], + owner, + ); + if let (Some(trace), Some(call)) = (trace.as_deref_mut(), target_call) { + trace.reject(call, errors.len()); + } + return Err(errors); + } + if let (Some(trace), Some(call)) = (trace.as_deref_mut(), target_call) { + trace.complete( + call, + &[], + &[TraceValidationSpec::input( + "backend_component_wasi_target_requirements", + 0, + TraceValidationCoverage::Direct, + )], + ); + } + + let emitted = emission::emit(&mir, &mut wasi, target, owner, mir_id, wasi_id, trace)?; + let warnings = lowered_cc.warnings; + + Ok(Stages { + core: optimized_core, + cc, + mir, + wasm: emitted.module, + artifact: Artifact { + wasm: emitted.component, + wat: emitted.wat, + warnings, + }, + }) +} + +pub(crate) fn compile_with_context_traced( + module: psrs_core::Module, + target: TargetCapabilities, + capture_partial: bool, +) -> (Result, CompileTrace) { + let mut trace = TraceRecorder::new(); + let mut partial = PartialStages::default(); + let result = compile_with_context_inner( + module, + target, + capture_partial.then_some(&mut partial), + Some(&mut trace), + ) + .map_err(|errors| CompileFailure { + errors, + partial: Box::new(partial), + }); + (result, trace.finish()) +} diff --git a/crates/psrs-backend/src/target_intrinsics/generated.rs b/crates/psrs-backend/src/target_intrinsics/generated.rs new file mode 100644 index 00000000..b7e702f6 --- /dev/null +++ b/crates/psrs-backend/src/target_intrinsics/generated.rs @@ -0,0 +1,85 @@ +//! Reserved generated bindings and their concrete MIR contracts. +use crate::types::{CompositeType, DefinedTypeId, HeapType, RefType, StorageType, ValueType}; +use crate::{abi, mir}; +use psrs_hir::SymbolId; + +pub(crate) fn name(symbol: SymbolId) -> Option<&'static str> { + match symbol { + abi::REALLOC_SYMBOL => Some("realloc"), + abi::STRING_TO_BYTES_SYMBOL => Some("string_to_bytes"), + abi::BYTES_TO_STRING_SYMBOL => Some("bytes_to_string"), + abi::VALIDATE_STEP_SYMBOL => Some("validate_step"), + _ => None, + } +} + +/// Shared by the planner and the helper emitter, independent of a consumer call. +pub(crate) fn signature(symbol: SymbolId, string: Option) -> Option { + let string = || { + string.map(|index| { + ValueType::Ref(RefType { + nullable: false, + heap: HeapType::Index(index), + }) + }) + }; + let (parameters, result) = match symbol { + abi::REALLOC_SYMBOL => (vec![ValueType::I32; 4], ValueType::I32), + abi::STRING_TO_BYTES_SYMBOL => (vec![string()?], ValueType::I32), + abi::BYTES_TO_STRING_SYMBOL => (vec![ValueType::I32; 2], string()?), + abi::VALIDATE_STEP_SYMBOL => (vec![ValueType::I32; 2], ValueType::I32), + _ => return None, + }; + Some(mir::Import { + runtime: None, + symbol, + parameters, + result: Some(result), + }) +} + +pub(crate) fn verify(module: &mir::Module, import: &mir::Import) -> Result<(), String> { + let string = string_type_from_imports(module); + let expected = signature(import.symbol, string) + .ok_or("generated helper has no concrete string representation")?; + if *import != expected { + return Err("generated helper consumer signature differs from its provider".into()); + } + if matches!( + import.symbol, + abi::STRING_TO_BYTES_SYMBOL | abi::BYTES_TO_STRING_SYMBOL | abi::VALIDATE_STEP_SYMBOL + ) { + let index = string.ok_or("generated codec has no string type")?; + let ty = module + .types + .iter() + .flat_map(|group| &group.0) + .nth(index.0 as usize) + .ok_or("generated codec references an unknown string type")?; + if !matches!(ty.composite, CompositeType::Array(field) + if field.mutable && field.storage == StorageType::I8) + { + return Err("generated codec requires a mutable GC byte array".into()); + } + } + Ok(()) +} + +/// The GC string defined-type index, read from a reserved helper import. +pub(crate) fn string_type_from_imports(module: &crate::mir::Module) -> Option { + for import in &module.imports { + if import.symbol != crate::abi::STRING_TO_BYTES_SYMBOL + && import.symbol != crate::abi::BYTES_TO_STRING_SYMBOL + { + continue; + } + for ty in import.parameters.iter().chain(import.result.iter()) { + if let ValueType::Ref(reference) = ty + && let HeapType::Index(index) = reference.heap + { + return Some(index); + } + } + } + None +} diff --git a/crates/psrs-backend/src/target_intrinsics/mod.rs b/crates/psrs-backend/src/target_intrinsics/mod.rs new file mode 100644 index 00000000..06374258 --- /dev/null +++ b/crates/psrs-backend/src/target_intrinsics/mod.rs @@ -0,0 +1,147 @@ +//! Exhaustive target implementation selection for checked language identities. +pub(crate) mod generated; +use crate::cc::{BinaryOp, UnaryOp}; +use crate::target_runtime::{self, ArtifactImplementation}; +use psrs_hir::Intrinsic; + +#[derive(Clone, Copy, Debug)] +pub(crate) enum ScalarOperation { + Unary(UnaryOp), + Binary(BinaryOp), +} + +#[derive(Clone, Copy, Debug)] +pub(crate) enum GeneratedOperation { + ArrayLength, + ArrayIndex, + ArrayUpdate, + ArrayAppend, + ArrayFill, + ArrayWrite, + StringToBytes, + BytesToString, + UnsafeCoerce, +} + +pub(crate) enum Implementation { + Direct(ScalarOperation), + Generated(GeneratedOperation), + Artifact(&'static ArtifactImplementation), + Elaborated, + /// A compiler-owned delimited-control primitive, lowered to frame + /// construction or the executor by CC. + Control, + Unsupported, +} + +pub(crate) fn implementation(intrinsic: Intrinsic) -> Implementation { + use Implementation::*; + match intrinsic { + Intrinsic::IntAdd => Direct(ScalarOperation::Binary(BinaryOp::IntAdd)), + Intrinsic::IntSub => Direct(ScalarOperation::Binary(BinaryOp::IntSub)), + Intrinsic::IntMul => Direct(ScalarOperation::Binary(BinaryOp::IntMul)), + Intrinsic::IntQuot => Direct(ScalarOperation::Binary(BinaryOp::IntQuot)), + Intrinsic::IntRem => Direct(ScalarOperation::Binary(BinaryOp::IntRem)), + Intrinsic::IntAnd => Direct(ScalarOperation::Binary(BinaryOp::IntAnd)), + Intrinsic::IntOr => Direct(ScalarOperation::Binary(BinaryOp::IntOr)), + Intrinsic::IntXor => Direct(ScalarOperation::Binary(BinaryOp::IntXor)), + Intrinsic::IntShl => Direct(ScalarOperation::Binary(BinaryOp::IntShl)), + Intrinsic::IntShr => Direct(ScalarOperation::Binary(BinaryOp::IntShr)), + Intrinsic::IntZshr => Direct(ScalarOperation::Binary(BinaryOp::IntZshr)), + Intrinsic::IntEq => Direct(ScalarOperation::Binary(BinaryOp::IntEq)), + Intrinsic::IntNe => Direct(ScalarOperation::Binary(BinaryOp::IntNe)), + Intrinsic::IntLt => Direct(ScalarOperation::Binary(BinaryOp::IntLt)), + Intrinsic::IntLe => Direct(ScalarOperation::Binary(BinaryOp::IntLe)), + Intrinsic::IntGt => Direct(ScalarOperation::Binary(BinaryOp::IntGt)), + Intrinsic::IntGe => Direct(ScalarOperation::Binary(BinaryOp::IntGe)), + Intrinsic::NumberAdd => Direct(ScalarOperation::Binary(BinaryOp::NumberAdd)), + Intrinsic::NumberSub => Direct(ScalarOperation::Binary(BinaryOp::NumberSub)), + Intrinsic::NumberMul => Direct(ScalarOperation::Binary(BinaryOp::NumberMul)), + Intrinsic::NumberDiv => Direct(ScalarOperation::Binary(BinaryOp::NumberDiv)), + Intrinsic::NumberEq => Direct(ScalarOperation::Binary(BinaryOp::NumberEq)), + Intrinsic::NumberNe => Direct(ScalarOperation::Binary(BinaryOp::NumberNe)), + Intrinsic::NumberLt => Direct(ScalarOperation::Binary(BinaryOp::NumberLt)), + Intrinsic::NumberLe => Direct(ScalarOperation::Binary(BinaryOp::NumberLe)), + Intrinsic::NumberGt => Direct(ScalarOperation::Binary(BinaryOp::NumberGt)), + Intrinsic::NumberGe => Direct(ScalarOperation::Binary(BinaryOp::NumberGe)), + Intrinsic::BooleanAnd => Direct(ScalarOperation::Binary(BinaryOp::BooleanAnd)), + Intrinsic::BooleanOr => Direct(ScalarOperation::Binary(BinaryOp::BooleanOr)), + Intrinsic::BooleanEq => Direct(ScalarOperation::Binary(BinaryOp::BooleanEq)), + Intrinsic::BooleanNe => Direct(ScalarOperation::Binary(BinaryOp::BooleanNe)), + Intrinsic::CharEq => Direct(ScalarOperation::Binary(BinaryOp::CharEq)), + Intrinsic::CharNe => Direct(ScalarOperation::Binary(BinaryOp::CharNe)), + Intrinsic::CharLt => Direct(ScalarOperation::Binary(BinaryOp::CharLt)), + Intrinsic::CharLe => Direct(ScalarOperation::Binary(BinaryOp::CharLe)), + Intrinsic::CharGt => Direct(ScalarOperation::Binary(BinaryOp::CharGt)), + Intrinsic::CharGe => Direct(ScalarOperation::Binary(BinaryOp::CharGe)), + Intrinsic::IntNeg => Direct(ScalarOperation::Unary(UnaryOp::IntNeg)), + Intrinsic::IntComplement => Direct(ScalarOperation::Unary(UnaryOp::IntComplement)), + Intrinsic::NumberNeg => Direct(ScalarOperation::Unary(UnaryOp::NumberNeg)), + Intrinsic::NumberAbs => Direct(ScalarOperation::Unary(UnaryOp::NumberAbs)), + Intrinsic::NumberSqrt => Direct(ScalarOperation::Unary(UnaryOp::NumberSqrt)), + Intrinsic::NumberTrunc => Direct(ScalarOperation::Unary(UnaryOp::NumberTrunc)), + Intrinsic::NumberFloor => Direct(ScalarOperation::Unary(UnaryOp::NumberFloor)), + Intrinsic::NumberCeil => Direct(ScalarOperation::Unary(UnaryOp::NumberCeil)), + Intrinsic::BooleanNot => Direct(ScalarOperation::Unary(UnaryOp::BooleanNot)), + Intrinsic::IntToNumber => Direct(ScalarOperation::Unary(UnaryOp::IntToNumber)), + Intrinsic::NumberToInt => Direct(ScalarOperation::Unary(UnaryOp::NumberToInt)), + Intrinsic::BooleanToInt => Direct(ScalarOperation::Unary(UnaryOp::BooleanToInt)), + Intrinsic::IntToBoolean => Direct(ScalarOperation::Unary(UnaryOp::IntToBoolean)), + Intrinsic::CharToInt => Direct(ScalarOperation::Unary(UnaryOp::CharToInt)), + Intrinsic::IntToChar => Direct(ScalarOperation::Unary(UnaryOp::IntToChar)), + Intrinsic::ArrayLength => Generated(GeneratedOperation::ArrayLength), + Intrinsic::ArrayIndex => Generated(GeneratedOperation::ArrayIndex), + Intrinsic::ArrayUpdate => Generated(GeneratedOperation::ArrayUpdate), + Intrinsic::ArrayAppend => Generated(GeneratedOperation::ArrayAppend), + Intrinsic::ArrayFill => Generated(GeneratedOperation::ArrayFill), + Intrinsic::ArrayWrite => Generated(GeneratedOperation::ArrayWrite), + Intrinsic::StringToBytes => Generated(GeneratedOperation::StringToBytes), + Intrinsic::BytesToString => Generated(GeneratedOperation::BytesToString), + Intrinsic::UnsafeCoerce => Generated(GeneratedOperation::UnsafeCoerce), + Intrinsic::NumberToString => Artifact(&target_runtime::NUMBER_FORMAT), + Intrinsic::NumberFromDecimal => Artifact(&target_runtime::NUMBER_PARSE), + Intrinsic::NumberAcos => Artifact(&target_runtime::NUMBER_ACOS), + Intrinsic::NumberAsin => Artifact(&target_runtime::NUMBER_ASIN), + Intrinsic::NumberAtan => Artifact(&target_runtime::NUMBER_ATAN), + Intrinsic::NumberAtan2 => Artifact(&target_runtime::NUMBER_ATAN2), + Intrinsic::NumberSin => Artifact(&target_runtime::NUMBER_SIN), + Intrinsic::NumberCos => Artifact(&target_runtime::NUMBER_COS), + Intrinsic::NumberTan => Artifact(&target_runtime::NUMBER_TAN), + Intrinsic::NumberExp => Artifact(&target_runtime::NUMBER_EXP), + Intrinsic::NumberLog => Artifact(&target_runtime::NUMBER_LOG), + Intrinsic::NumberPow => Artifact(&target_runtime::NUMBER_POW), + Intrinsic::NumberMin => Artifact(&target_runtime::NUMBER_MIN), + Intrinsic::NumberMax => Artifact(&target_runtime::NUMBER_MAX), + Intrinsic::NumberSign => Artifact(&target_runtime::NUMBER_SIGN), + Intrinsic::NumberRemainder => Artifact(&target_runtime::NUMBER_REMAINDER), + Intrinsic::NumberIsNaN => Artifact(&target_runtime::NUMBER_IS_NAN), + Intrinsic::NumberNaN => Artifact(&target_runtime::NUMBER_NAN), + Intrinsic::NumberInfinity => Artifact(&target_runtime::NUMBER_INFINITY), + Intrinsic::BoolTrue | Intrinsic::BoolFalse | Intrinsic::Unit | Intrinsic::Coerce => { + Elaborated + } + Intrinsic::Undefined => Unsupported, + // Control primitives lower to guest frames and generated drivers. + Intrinsic::ControlReturn + | Intrinsic::ControlBind + | Intrinsic::ControlPerform + | Intrinsic::ControlRun + | Intrinsic::ControlHandle + | Intrinsic::ControlOnce + | Intrinsic::ControlResume + | Intrinsic::ControlCancel => Control, + } +} + +/// Artifact providers are derived from the exhaustive selection, never registered twice. +pub(crate) fn artifacts() -> impl Iterator { + Intrinsic::ALL + .into_iter() + .filter_map(|intrinsic| match implementation(intrinsic) { + Implementation::Artifact(provider) => Some(provider), + _ => None, + }) +} + +#[cfg(test)] +mod tests; diff --git a/crates/psrs-backend/src/target_intrinsics/tests.rs b/crates/psrs-backend/src/target_intrinsics/tests.rs new file mode 100644 index 00000000..9d5fe352 --- /dev/null +++ b/crates/psrs-backend/src/target_intrinsics/tests.rs @@ -0,0 +1,72 @@ +use super::*; + +#[test] +fn every_active_identity_has_an_explicit_implementation() { + for intrinsic in Intrinsic::ALL { + match implementation(intrinsic) { + Implementation::Direct(ScalarOperation::Unary(_)) => { + assert_eq!(intrinsic.descriptor().arity, 1) + } + Implementation::Direct(ScalarOperation::Binary(_)) => { + assert_eq!(intrinsic.descriptor().arity, 2) + } + Implementation::Artifact(provider) => { + assert_eq!(provider.intrinsic, intrinsic); + provider.validate_protocol().unwrap(); + assert_eq!( + provider.language_signature().unwrap().0.len(), + intrinsic.descriptor().arity as usize + ); + } + Implementation::Unsupported => assert_eq!(intrinsic, Intrinsic::Undefined), + Implementation::Control => { + assert!(matches!( + intrinsic, + Intrinsic::ControlReturn + | Intrinsic::ControlBind + | Intrinsic::ControlPerform + | Intrinsic::ControlRun + | Intrinsic::ControlHandle + | Intrinsic::ControlOnce + | Intrinsic::ControlResume + | Intrinsic::ControlCancel + )); + } + Implementation::Generated(_) | Implementation::Elaborated => {} + } + } + assert!(Intrinsic::from_binding("intDiv").is_none()); + assert!(Intrinsic::from_binding("intMod").is_none()); + for retired in ["stateArrayRead", "stateArrayFill", "stateArrayWrite"] { + assert!(Intrinsic::from_binding(retired).is_none()); + } +} + +#[test] +fn language_renaming_preserves_stable_symbols_and_retired_slots() { + for (intrinsic, id) in [ + (Intrinsic::IntAdd, 2), + (Intrinsic::IntQuot, 5), + (Intrinsic::IntRem, 6), + (Intrinsic::IntToChar, 25), + (Intrinsic::IntAnd, 28), + (Intrinsic::NumberAbs, 68), + (Intrinsic::NumberSqrt, 69), + (Intrinsic::NumberAcos, 70), + (Intrinsic::NumberAsin, 71), + (Intrinsic::NumberAtan, 72), + (Intrinsic::NumberAtan2, 73), + (Intrinsic::NumberSin, 74), + (Intrinsic::NumberInfinity, 86), + (Intrinsic::ControlReturn, 92), + (Intrinsic::ControlBind, 93), + (Intrinsic::ControlPerform, 94), + (Intrinsic::ControlRun, 95), + (Intrinsic::ControlHandle, 96), + ] { + assert_eq!(intrinsic.symbol().index, id); + } + for intrinsic in Intrinsic::ALL { + assert!(!Intrinsic::RESERVED_IDS.contains(&(intrinsic as u32))); + } +} diff --git a/crates/psrs-backend/src/target_runtime/language.rs b/crates/psrs-backend/src/target_runtime/language.rs new file mode 100644 index 00000000..676a96c4 --- /dev/null +++ b/crates/psrs-backend/src/target_runtime/language.rs @@ -0,0 +1,85 @@ +use super::*; +use crate::cc::ValueShape; +use psrs_hir::{BuiltinType, TypeKind}; +use psrs_runtime::RawCallProtocol; + +impl ArtifactImplementation { + pub(crate) fn language_signature(&self) -> Result<(Vec, ValueShape), String> { + let mut ty = (self.intrinsic.descriptor().scheme)(); + let mut parameters = Vec::new(); + while let TypeKind::Function { parameter, result } = ty.kind { + parameters.push(shape(¶meter.kind)?); + ty = *result; + } + Ok((parameters, shape(&ty.kind)?)) + } + + /// The value protocol must implement the checked language scheme and raw ABI. + pub(crate) fn validate_protocol(&self) -> Result<(), String> { + let (parameters, result) = self.language_signature()?; + let expected = match self.abi.protocol { + RawCallProtocol::Scalars => CoreSignature { + parameters: parameters.into_iter().map(raw).collect::>()?, + result: self.raw_result(result)?, + }, + RawCallProtocol::Utf8Input if parameters == [ValueShape::String] => CoreSignature { + parameters: vec![CoreType::I32, CoreType::I32], + result: self.raw_result(result)?, + }, + RawCallProtocol::Utf8Output { capacity } + if parameters.len() == 1 + && result == ValueShape::String + && capacity > 0 + && capacity <= i32::MAX as usize => + { + CoreSignature { + parameters: vec![raw(parameters[0])?, CoreType::I32, CoreType::I32], + result: Some(CoreType::I32), + } + } + _ => return Err("artifact value protocol cannot implement its language scheme".into()), + }; + if expected != self.signature() { + return Err("artifact value protocol does not match its raw ABI".into()); + } + if !matches!(self.abi.protocol, RawCallProtocol::Scalars) + && !self.intrinsic.descriptor().effects.may_trap + { + return Err("buffer allocation requires a possibly trapping language contract".into()); + } + Ok(()) + } +} + +fn shape(kind: &TypeKind) -> Result { + match kind { + TypeKind::Constructor(BuiltinType::Int) => Ok(ValueShape::Integer), + TypeKind::Constructor(BuiltinType::Number) => Ok(ValueShape::Number), + TypeKind::Constructor(BuiltinType::Boolean) => Ok(ValueShape::Boolean), + TypeKind::Constructor(BuiltinType::Char) => Ok(ValueShape::Integer), + TypeKind::Constructor(BuiltinType::Unit) => Ok(ValueShape::Integer), + TypeKind::Constructor(BuiltinType::String) => Ok(ValueShape::String), + _ => Err("artifact calls require a supported closed language scheme".into()), + } +} + +fn raw(shape: ValueShape) -> Result { + match shape { + ValueShape::Integer | ValueShape::Boolean => Ok(CoreType::I32), + ValueShape::Number => Ok(CoreType::F64), + _ => Err("a raw scalar protocol cannot transport a GC value".into()), + } +} +impl ArtifactImplementation { + fn raw_result(&self, result: ValueShape) -> Result, String> { + let mut ty = (self.intrinsic.descriptor().scheme)(); + while let TypeKind::Function { result, .. } = ty.kind { + ty = *result; + } + if matches!(ty.kind, TypeKind::Constructor(BuiltinType::Unit)) { + Ok(None) + } else { + raw(result).map(Some) + } + } +} diff --git a/crates/psrs-backend/src/target_runtime/mod.rs b/crates/psrs-backend/src/target_runtime/mod.rs new file mode 100644 index 00000000..adec4512 --- /dev/null +++ b/crates/psrs-backend/src/target_runtime/mod.rs @@ -0,0 +1,259 @@ +//! The embedded target library's implementation catalog entry. +//! +//! This module maps a checked language intrinsic to its embedded artifact +//! export and converts it into a linker-owned requirement. The runtime package +//! owns the raw contract; `psrs-linker` owns verification and planning. + +use psrs_hir::{Intrinsic, SymbolId}; +mod language; +use psrs_linker::{BindingRequirement, Boundary, CoreSignature, CoreType, Provider, RequirementId}; + +/// Connects a checked language intrinsic to its embedded implementation. +pub(crate) struct ArtifactImplementation { + pub intrinsic: Intrinsic, + pub symbol: SymbolId, + pub abi: &'static psrs_runtime::RawFunctionAbi, + pub artifact: &'static psrs_runtime::RuntimeArtifact, +} + +pub(crate) const NUMBER_FORMAT: ArtifactImplementation = ArtifactImplementation { + intrinsic: Intrinsic::NumberToString, + symbol: crate::abi::NUMBER_TO_STRING_SYMBOL, + abi: &psrs_runtime::NUMBER_FORMAT, + artifact: &psrs_runtime::NUMBER_RUNTIME, +}; + +pub(crate) const NUMBER_PARSE: ArtifactImplementation = ArtifactImplementation { + intrinsic: Intrinsic::NumberFromDecimal, + symbol: crate::abi::NUMBER_FROM_DECIMAL_SYMBOL, + abi: &psrs_runtime::NUMBER_PARSE, + artifact: &psrs_runtime::NUMBER_RUNTIME, +}; + +pub(crate) const NUMBER_ACOS: ArtifactImplementation = ArtifactImplementation { + intrinsic: Intrinsic::NumberAcos, + symbol: crate::abi::NUMBER_ACOS_SYMBOL, + abi: &psrs_runtime::NUMBER_ACOS, + artifact: &psrs_runtime::NUMBER_RUNTIME, +}; + +pub(crate) const NUMBER_ASIN: ArtifactImplementation = ArtifactImplementation { + intrinsic: Intrinsic::NumberAsin, + symbol: crate::abi::NUMBER_ASIN_SYMBOL, + abi: &psrs_runtime::NUMBER_ASIN, + artifact: &psrs_runtime::NUMBER_RUNTIME, +}; + +pub(crate) const NUMBER_ATAN: ArtifactImplementation = ArtifactImplementation { + intrinsic: Intrinsic::NumberAtan, + symbol: crate::abi::NUMBER_ATAN_SYMBOL, + abi: &psrs_runtime::NUMBER_ATAN, + artifact: &psrs_runtime::NUMBER_RUNTIME, +}; + +pub(crate) const NUMBER_ATAN2: ArtifactImplementation = ArtifactImplementation { + intrinsic: Intrinsic::NumberAtan2, + symbol: crate::abi::NUMBER_ATAN2_SYMBOL, + abi: &psrs_runtime::NUMBER_ATAN2, + artifact: &psrs_runtime::NUMBER_RUNTIME, +}; + +pub(crate) const NUMBER_SIN: ArtifactImplementation = ArtifactImplementation { + intrinsic: Intrinsic::NumberSin, + symbol: crate::abi::NUMBER_SIN_SYMBOL, + abi: &psrs_runtime::NUMBER_SIN, + artifact: &psrs_runtime::NUMBER_RUNTIME, +}; + +pub(crate) const NUMBER_COS: ArtifactImplementation = ArtifactImplementation { + intrinsic: Intrinsic::NumberCos, + symbol: crate::abi::NUMBER_COS_SYMBOL, + abi: &psrs_runtime::NUMBER_COS, + artifact: &psrs_runtime::NUMBER_RUNTIME, +}; + +pub(crate) const NUMBER_TAN: ArtifactImplementation = ArtifactImplementation { + intrinsic: Intrinsic::NumberTan, + symbol: crate::abi::NUMBER_TAN_SYMBOL, + abi: &psrs_runtime::NUMBER_TAN, + artifact: &psrs_runtime::NUMBER_RUNTIME, +}; + +pub(crate) const NUMBER_EXP: ArtifactImplementation = ArtifactImplementation { + intrinsic: Intrinsic::NumberExp, + symbol: crate::abi::NUMBER_EXP_SYMBOL, + abi: &psrs_runtime::NUMBER_EXP, + artifact: &psrs_runtime::NUMBER_RUNTIME, +}; + +pub(crate) const NUMBER_LOG: ArtifactImplementation = ArtifactImplementation { + intrinsic: Intrinsic::NumberLog, + symbol: crate::abi::NUMBER_LOG_SYMBOL, + abi: &psrs_runtime::NUMBER_LOG, + artifact: &psrs_runtime::NUMBER_RUNTIME, +}; + +pub(crate) const NUMBER_POW: ArtifactImplementation = ArtifactImplementation { + intrinsic: Intrinsic::NumberPow, + symbol: crate::abi::NUMBER_POW_SYMBOL, + abi: &psrs_runtime::NUMBER_POW, + artifact: &psrs_runtime::NUMBER_RUNTIME, +}; + +pub(crate) const NUMBER_MIN: ArtifactImplementation = ArtifactImplementation { + intrinsic: Intrinsic::NumberMin, + symbol: crate::abi::NUMBER_MIN_SYMBOL, + abi: &psrs_runtime::NUMBER_MIN, + artifact: &psrs_runtime::NUMBER_RUNTIME, +}; + +pub(crate) const NUMBER_MAX: ArtifactImplementation = ArtifactImplementation { + intrinsic: Intrinsic::NumberMax, + symbol: crate::abi::NUMBER_MAX_SYMBOL, + abi: &psrs_runtime::NUMBER_MAX, + artifact: &psrs_runtime::NUMBER_RUNTIME, +}; + +pub(crate) const NUMBER_SIGN: ArtifactImplementation = ArtifactImplementation { + intrinsic: Intrinsic::NumberSign, + symbol: crate::abi::NUMBER_SIGN_SYMBOL, + abi: &psrs_runtime::NUMBER_SIGN, + artifact: &psrs_runtime::NUMBER_RUNTIME, +}; + +pub(crate) const NUMBER_REMAINDER: ArtifactImplementation = ArtifactImplementation { + intrinsic: Intrinsic::NumberRemainder, + symbol: crate::abi::NUMBER_REMAINDER_SYMBOL, + abi: &psrs_runtime::NUMBER_REMAINDER, + artifact: &psrs_runtime::NUMBER_RUNTIME, +}; + +pub(crate) const NUMBER_IS_NAN: ArtifactImplementation = ArtifactImplementation { + intrinsic: Intrinsic::NumberIsNaN, + symbol: crate::abi::NUMBER_IS_NAN_SYMBOL, + abi: &psrs_runtime::NUMBER_IS_NAN, + artifact: &psrs_runtime::NUMBER_RUNTIME, +}; + +pub(crate) const NUMBER_NAN: ArtifactImplementation = ArtifactImplementation { + intrinsic: Intrinsic::NumberNaN, + symbol: crate::abi::NUMBER_NAN_SYMBOL, + abi: &psrs_runtime::NUMBER_NAN, + artifact: &psrs_runtime::NUMBER_RUNTIME, +}; + +pub(crate) const NUMBER_INFINITY: ArtifactImplementation = ArtifactImplementation { + intrinsic: Intrinsic::NumberInfinity, + symbol: crate::abi::NUMBER_INFINITY_SYMBOL, + abi: &psrs_runtime::NUMBER_INFINITY, + artifact: &psrs_runtime::NUMBER_RUNTIME, +}; + +/// The registered implementation for a MIR import symbol, if any. +pub(crate) fn for_symbol(symbol: SymbolId) -> Option<&'static ArtifactImplementation> { + crate::target_intrinsics::artifacts().find(|implementation| symbol == implementation.symbol) +} + +impl ArtifactImplementation { + pub fn import(&self) -> crate::mir::Import { + crate::mir::Import { + runtime: None, + symbol: self.symbol, + parameters: self + .abi + .parameters + .iter() + .copied() + .map(value_type) + .collect(), + result: self.abi.result.map(value_type), + } + } + + pub fn signature(&self) -> CoreSignature { + CoreSignature { + parameters: self.abi.parameters.iter().copied().map(core_type).collect(), + result: self.abi.result.map(core_type), + } + } + + /// The linker requirement for this runtime operation. + /// + /// `consumer` is the MIR call signature. Planning rejects it unless it + /// matches the catalog operation independently. + pub fn requirement(&self, id: RequirementId, consumer: CoreSignature) -> BindingRequirement { + BindingRequirement { + id, + origin: format!("{:?}", self.intrinsic), + boundary: Boundary::RawCore { + module: self.artifact.module_name.to_string(), + field: self.abi.export.to_string(), + }, + expected: Some(consumer), + provider: Provider::RuntimeOperation { + name: self.operation().name.to_string(), + version: self.operation().version.to_string(), + }, + } + } + + fn operation(&self) -> &'static psrs_runtime::ProvidedOperation { + psrs_runtime::PSRS_RUNTIME + .units + .iter() + .filter(|unit| unit.variant.id == self.artifact.id) + .find_map(|unit| { + unit.provided + .iter() + .find(|operation| operation.abi.export == self.abi.export) + }) + .expect("the runtime catalog publishes this intrinsic operation") + } +} + +fn core_type(ty: psrs_runtime::RawType) -> CoreType { + match ty { + psrs_runtime::RawType::I32 => CoreType::I32, + psrs_runtime::RawType::I64 => CoreType::I64, + psrs_runtime::RawType::F32 => CoreType::F32, + psrs_runtime::RawType::F64 => CoreType::F64, + } +} + +fn value_type(ty: psrs_runtime::RawType) -> crate::types::ValueType { + match ty { + psrs_runtime::RawType::I32 => crate::types::ValueType::I32, + psrs_runtime::RawType::I64 => crate::types::ValueType::I64, + psrs_runtime::RawType::F32 => crate::types::ValueType::F32, + psrs_runtime::RawType::F64 => crate::types::ValueType::F64, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_number_formatter_maps_to_a_raw_artifact_requirement() { + let formatter = &NUMBER_FORMAT; + let requirement = formatter.requirement(RequirementId(0), formatter.signature()); + assert_eq!( + requirement.boundary, + Boundary::RawCore { + module: psrs_runtime::MODULE_NAME.into(), + field: psrs_runtime::NUMBER_EXPORT.into(), + } + ); + assert!(matches!( + requirement.provider, + Provider::RuntimeOperation { .. } + )); + assert!(matches!( + crate::target_intrinsics::implementation(Intrinsic::IntAdd), + crate::target_intrinsics::Implementation::Direct(_) + )); + } +} + +#[cfg(test)] +mod protocol_tests; diff --git a/crates/psrs-backend/src/target_runtime/protocol_tests.rs b/crates/psrs-backend/src/target_runtime/protocol_tests.rs new file mode 100644 index 00000000..80369185 --- /dev/null +++ b/crates/psrs-backend/src/target_runtime/protocol_tests.rs @@ -0,0 +1,79 @@ +use super::*; +use psrs_runtime::{RawCallProtocol, RawFunctionAbi, RawType}; + +fn provider(intrinsic: Intrinsic, abi: &'static RawFunctionAbi) -> ArtifactImplementation { + ArtifactImplementation { + intrinsic, + symbol: NUMBER_FORMAT.symbol, + abi, + artifact: &psrs_runtime::NUMBER_RUNTIME, + } +} + +#[test] +fn scalar_and_void_protocols_require_matching_closed_language_schemes() { + const SCALAR: RawFunctionAbi = RawFunctionAbi { + export: "scalar", + parameters: &[RawType::F64], + result: Some(RawType::F64), + protocol: RawCallProtocol::Scalars, + }; + const VOID: RawFunctionAbi = RawFunctionAbi { + export: "void", + parameters: &[], + result: None, + protocol: RawCallProtocol::Scalars, + }; + provider(Intrinsic::NumberAbs, &SCALAR) + .validate_protocol() + .unwrap(); + provider(Intrinsic::Unit, &VOID) + .validate_protocol() + .unwrap(); + assert!( + provider(Intrinsic::NumberAbs, &VOID) + .validate_protocol() + .is_err() + ); + assert!( + provider(Intrinsic::NumberToString, &SCALAR) + .validate_protocol() + .is_err() + ); + assert!( + provider(Intrinsic::ArrayLength, &SCALAR) + .validate_protocol() + .is_err() + ); +} + +#[test] +fn buffer_protocols_require_matching_shapes_raw_types_and_bounded_capacity() { + const UNBOUNDED: RawFunctionAbi = RawFunctionAbi { + export: "format", + parameters: &[RawType::F64, RawType::I32, RawType::I32], + result: Some(RawType::I32), + protocol: RawCallProtocol::Utf8Output { capacity: 0 }, + }; + const WRONG_WIDTH: RawFunctionAbi = RawFunctionAbi { + export: "parse", + parameters: &[RawType::I32, RawType::I64], + result: Some(RawType::F64), + protocol: RawCallProtocol::Utf8Input, + }; + assert!( + provider(Intrinsic::NumberToString, &UNBOUNDED) + .validate_protocol() + .is_err() + ); + assert!( + provider(Intrinsic::NumberFromDecimal, &WRONG_WIDTH) + .validate_protocol() + .is_err() + ); + assert!( + provider(Intrinsic::NumberAbs, &psrs_runtime::NUMBER_PARSE) + .validate_protocol() + .is_err() + ); +} diff --git a/crates/psrs-backend/src/trace.rs b/crates/psrs-backend/src/trace.rs new file mode 100644 index 00000000..33d5d509 --- /dev/null +++ b/crates/psrs-backend/src/trace.rs @@ -0,0 +1,430 @@ +//! Plain pass and artifact records for one backend compile attempt. + +/// Version of the backend-owned trace vocabulary. +pub const COMPILE_TRACE_VERSION: u32 = 1; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct TraceArtifactId(pub u32); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct TraceExecutionId(pub u32); + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TraceRepresentation { + Core, + ExternalBindings, + ClosureConverted, + Mir, + WasmModule, + WasmCoreBinary, + ComponentBinary, + WatText, + WitWorld, + WasiRegistry, + /// The checked target link plan produced after MIR optimization. + LinkPlan, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TraceArtifactState { + Provided, + Produced, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TracePassStatus { + Completed, + Rejected, + NotApplicable, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TraceEdgeRole { + Input, + Output, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TraceValidationStatus { + Passed, + Rejected, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TraceValidationCoverage { + Direct, + Composite, + NotObserved, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TraceArtifact { + pub id: TraceArtifactId, + pub representation: TraceRepresentation, + pub state: TraceArtifactState, + pub producer: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TraceExecution { + pub id: TraceExecutionId, + pub pass_key: &'static str, + pub contract_version: u32, + pub inputs: Vec, + pub outputs: Vec, + pub status: TracePassStatus, + /// Indexes into the backend error list returned from this same compile. + pub diagnostic_indices: Vec, + /// Nested verifiers and transformations not exposed by this call remain + /// opaque; the CLI serializes this observed boundary explicitly. + pub validation_coverage: TraceValidationCoverage, + pub parameters: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TraceParameter { + pub key: &'static str, + pub value: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TraceEdge { + pub execution: TraceExecutionId, + pub artifact: TraceArtifactId, + pub role: TraceEdgeRole, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TraceValidation { + pub execution: TraceExecutionId, + pub validator_key: &'static str, + pub artifacts: Vec, + pub status: TraceValidationStatus, + pub coverage: TraceValidationCoverage, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TraceArtifactSelector { + Input(usize), + Output(usize), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TraceValidationSpec { + pub validator_key: &'static str, + pub artifacts: Vec, + pub coverage: TraceValidationCoverage, +} + +impl TraceValidationSpec { + pub(crate) fn input( + validator_key: &'static str, + index: usize, + coverage: TraceValidationCoverage, + ) -> Self { + Self { + validator_key, + artifacts: vec![TraceArtifactSelector::Input(index)], + coverage, + } + } + + pub(crate) fn output( + validator_key: &'static str, + index: usize, + coverage: TraceValidationCoverage, + ) -> Self { + Self { + validator_key, + artifacts: vec![TraceArtifactSelector::Output(index)], + coverage, + } + } + + pub(crate) fn inputs( + validator_key: &'static str, + indices: &[usize], + coverage: TraceValidationCoverage, + ) -> Self { + Self { + validator_key, + artifacts: indices + .iter() + .copied() + .map(TraceArtifactSelector::Input) + .collect(), + coverage, + } + } + + pub(crate) fn outputs( + validator_key: &'static str, + indices: &[usize], + coverage: TraceValidationCoverage, + ) -> Self { + Self { + validator_key, + artifacts: indices + .iter() + .copied() + .map(TraceArtifactSelector::Output) + .collect(), + coverage, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CompileTrace { + pub version: u32, + pub initial_core: TraceArtifactId, + pub artifacts: Vec, + pub executions: Vec, + pub edges: Vec, + pub validations: Vec, +} + +pub(crate) struct TraceRecorder { + trace: CompileTrace, + next_artifact: u32, + next_execution: u32, +} + +pub(crate) struct TraceCall { + id: TraceExecutionId, + inputs: Vec, + pass_key: &'static str, + validation_coverage: TraceValidationCoverage, + parameters: Vec, +} + +impl TraceCall { + /// Records an additional lineage parameter after the pass has produced a + /// result but before it is completed or rejected. + pub(crate) fn add_parameter(&mut self, key: &'static str, value: String) { + self.parameters.push(TraceParameter { key, value }); + } +} + +impl TraceRecorder { + pub(crate) fn new() -> Self { + let initial_core = TraceArtifactId(0); + Self { + trace: CompileTrace { + version: COMPILE_TRACE_VERSION, + initial_core, + artifacts: vec![TraceArtifact { + id: initial_core, + representation: TraceRepresentation::Core, + state: TraceArtifactState::Provided, + producer: None, + }], + executions: Vec::new(), + edges: Vec::new(), + validations: Vec::new(), + }, + next_artifact: 1, + next_execution: 0, + } + } + + pub(crate) fn begin( + &mut self, + pass_key: &'static str, + inputs: &[TraceArtifactId], + validation_coverage: TraceValidationCoverage, + parameters: Vec, + ) -> TraceCall { + let id = TraceExecutionId(self.next_execution); + self.next_execution += 1; + for artifact in inputs { + self.trace.edges.push(TraceEdge { + execution: id, + artifact: *artifact, + role: TraceEdgeRole::Input, + }); + } + TraceCall { + id, + inputs: inputs.to_vec(), + pass_key, + validation_coverage, + parameters, + } + } + + pub(crate) fn initial_core(&self) -> TraceArtifactId { + self.trace.initial_core + } + + pub(crate) fn complete( + &mut self, + call: TraceCall, + outputs: &[TraceRepresentation], + validations: &[TraceValidationSpec], + ) -> Vec { + let output_ids = outputs + .iter() + .map(|representation| { + let id = TraceArtifactId(self.next_artifact); + self.next_artifact += 1; + self.trace.artifacts.push(TraceArtifact { + id, + representation: *representation, + state: TraceArtifactState::Produced, + producer: Some(call.id), + }); + self.trace.edges.push(TraceEdge { + execution: call.id, + artifact: id, + role: TraceEdgeRole::Output, + }); + id + }) + .collect::>(); + for validation in validations { + let artifacts = validation + .artifacts + .iter() + .map(|selector| match selector { + TraceArtifactSelector::Input(index) => *call + .inputs + .get(*index) + .expect("validation input selector must name an existing artifact"), + TraceArtifactSelector::Output(index) => *output_ids + .get(*index) + .expect("validation output selector must name an existing artifact"), + }) + .collect(); + self.trace.validations.push(TraceValidation { + execution: call.id, + validator_key: validation.validator_key, + artifacts, + status: TraceValidationStatus::Passed, + coverage: validation.coverage, + }); + } + self.trace.executions.push(TraceExecution { + id: call.id, + pass_key: call.pass_key, + contract_version: 1, + inputs: call.inputs, + outputs: output_ids.clone(), + status: TracePassStatus::Completed, + diagnostic_indices: Vec::new(), + validation_coverage: call.validation_coverage, + parameters: call.parameters, + }); + output_ids + } + + pub(crate) fn reject(&mut self, call: TraceCall, diagnostic_count: usize) { + self.trace.executions.push(TraceExecution { + id: call.id, + pass_key: call.pass_key, + contract_version: 1, + inputs: call.inputs, + outputs: Vec::new(), + status: TracePassStatus::Rejected, + diagnostic_indices: (0..diagnostic_count).collect(), + validation_coverage: call.validation_coverage, + parameters: call.parameters, + }); + } + + pub(crate) fn reject_validation( + &mut self, + call: TraceCall, + diagnostic_count: usize, + validator_key: &'static str, + artifacts: &[TraceArtifactId], + ) { + self.trace.validations.push(TraceValidation { + execution: call.id, + validator_key, + artifacts: artifacts.to_vec(), + status: TraceValidationStatus::Rejected, + coverage: TraceValidationCoverage::Direct, + }); + self.reject(call, diagnostic_count); + } + + #[cfg(test)] + fn validations(&self) -> &[TraceValidation] { + &self.trace.validations + } + + pub(crate) fn finish(self) -> CompileTrace { + self.trace + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[should_panic(expected = "validation output selector must name an existing artifact")] + fn validation_selectors_cannot_silently_drop_missing_outputs() { + let mut trace = TraceRecorder::new(); + let core = trace.initial_core(); + let call = trace.begin( + "test.pass", + &[core], + TraceValidationCoverage::Direct, + Vec::new(), + ); + trace.complete( + call, + &[TraceRepresentation::Core], + &[TraceValidationSpec::output( + "test.validator", + 1, + TraceValidationCoverage::Direct, + )], + ); + } + + #[test] + fn validations_reference_only_the_declared_artifacts() { + let mut trace = TraceRecorder::new(); + let core = trace.initial_core(); + let call = trace.begin( + "test.pass", + &[core], + TraceValidationCoverage::Composite, + Vec::new(), + ); + let output = trace.complete( + call, + &[ + TraceRepresentation::Core, + TraceRepresentation::ExternalBindings, + ], + &[TraceValidationSpec::output( + "test.validator", + 0, + TraceValidationCoverage::Composite, + )], + ); + assert_eq!(trace.validations()[0].artifacts, vec![output[0]]); + } + + #[test] + fn rejected_validation_references_its_input_and_stays_rejected() { + let mut trace = TraceRecorder::new(); + let core = trace.initial_core(); + let call = trace.begin( + "test.validate", + &[core], + TraceValidationCoverage::Direct, + Vec::new(), + ); + trace.reject_validation(call, 1, "test.validator", &[core]); + let trace = trace.finish(); + assert_eq!(trace.validations[0].status, TraceValidationStatus::Rejected); + assert_eq!(trace.validations[0].artifacts, vec![core]); + assert_eq!(trace.executions[0].status, TracePassStatus::Rejected); + assert!(trace.executions[0].outputs.is_empty()); + } +} diff --git a/crates/psrs-backend/src/wasm/encode.rs b/crates/psrs-backend/src/wasm/encode.rs index 475599cb..7fd32315 100644 --- a/crates/psrs-backend/src/wasm/encode.rs +++ b/crates/psrs-backend/src/wasm/encode.rs @@ -73,6 +73,7 @@ pub fn encode_module(module: &Module) -> Result, Vec> { for global in &module.globals { let init = match global.init { GlobalInit::RefNull(heap) => ConstExpr::ref_null(heap), + GlobalInit::I32(value) => ConstExpr::i32_const(value as i32), }; globals.global( GlobalType { @@ -92,10 +93,12 @@ pub fn encode_module(module: &Module) -> Result, Vec> { let kind = match export.kind { ExportKind::Function => WasmExportKind::Func, ExportKind::Memory => WasmExportKind::Memory, + ExportKind::Global => WasmExportKind::Global, }; let index = match export.index { ExportIndex::Function(index) => index.0, ExportIndex::Memory(index) => index.0, + ExportIndex::Global(index) => index.0, }; exports.export(&export.name, kind, index); } diff --git a/crates/psrs-backend/src/wasm/lower/asm.rs b/crates/psrs-backend/src/wasm/lower/asm.rs index 4ad8e2da..31d72acb 100644 --- a/crates/psrs-backend/src/wasm/lower/asm.rs +++ b/crates/psrs-backend/src/wasm/lower/asm.rs @@ -75,10 +75,6 @@ impl Asm { self.stack.push(id); } - pub(crate) fn else_(&mut self) { - self.leaf(Instruction::Else); - } - pub(crate) fn end(&mut self) { self.leaf(Instruction::End); self.stack.pop(); diff --git a/crates/psrs-backend/src/wasm/lower/codec/mod.rs b/crates/psrs-backend/src/wasm/lower/codec/mod.rs index 8858c926..7872363a 100644 --- a/crates/psrs-backend/src/wasm/lower/codec/mod.rs +++ b/crates/psrs-backend/src/wasm/lower/codec/mod.rs @@ -24,48 +24,20 @@ mod encode; #[cfg(test)] mod tests; -use crate::types::{DefinedTypeId, HeapType, ValueType}; +use crate::types::DefinedTypeId; use crate::wasm::{FuncType, Function, FunctionIndex, TypeIndex}; use psrs_span::TextRange; -use wasm_encoder::ValType; /// The three function types used by the boundary helpers. -pub(super) fn signatures(string_ref: ValType) -> (FuncType, FuncType, FuncType) { +pub(super) fn signatures(string: DefinedTypeId) -> (FuncType, FuncType, FuncType) { + use crate::abi; ( - FuncType { - parameters: vec![string_ref], - results: vec![ValType::I32], - }, - FuncType { - parameters: vec![ValType::I32, ValType::I32], - results: vec![string_ref], - }, - FuncType { - parameters: vec![ValType::I32, ValType::I32], - results: vec![ValType::I32], - }, + generated_signature(abi::STRING_TO_BYTES_SYMBOL, Some(string)), + generated_signature(abi::BYTES_TO_STRING_SYMBOL, Some(string)), + generated_signature(abi::VALIDATE_STEP_SYMBOL, Some(string)), ) } -/// The GC string defined-type index, read from a reserved helper import. -pub(super) fn string_type_from_imports(module: &crate::mir::Module) -> Option { - for import in &module.imports { - if import.symbol != crate::abi::STRING_TO_BYTES_SYMBOL - && import.symbol != crate::abi::BYTES_TO_STRING_SYMBOL - { - continue; - } - for ty in import.parameters.iter().chain(import.result.iter()) { - if let ValueType::Ref(reference) = ty - && let HeapType::Index(index) = reference.heap - { - return Some(index); - } - } - } - None -} - /// Builds `string_to_bytes`, `bytes_to_string`, and `validate_step` in that order. pub(super) fn synthesize( string_type: DefinedTypeId, @@ -82,3 +54,19 @@ pub(super) fn synthesize( decode::validate_step(validate_step_type, span), ] } + +fn generated_signature( + symbol: psrs_hir::SymbolId, + string: Option, +) -> FuncType { + let signature = crate::target_intrinsics::generated::signature(symbol, string) + .expect("the selected generated helper has a concrete signature"); + FuncType { + parameters: signature + .parameters + .into_iter() + .map(super::val_type) + .collect(), + results: signature.result.into_iter().map(super::val_type).collect(), + } +} diff --git a/crates/psrs-backend/src/wasm/lower/extent/mod.rs b/crates/psrs-backend/src/wasm/lower/extent/mod.rs index ef2c2c2b..6a4e2049 100644 --- a/crates/psrs-backend/src/wasm/lower/extent/mod.rs +++ b/crates/psrs-backend/src/wasm/lower/extent/mod.rs @@ -4,7 +4,7 @@ mod access; mod address; use crate::BackendError; -use crate::abi::{HEAP_START, HEAP_STATE, SCRATCH_END}; +use crate::abi::SCRATCH_END; use crate::mir; const WASM32_ADDRESS_SPACE: u64 = 1_u64 << 32; @@ -14,8 +14,6 @@ const PASS: &str = "P10 Wasm structuring"; enum RegionKind { /// The reserved canonical return-area scratch. Scratch, - /// The allocator's free-list head and bump break. - HeapState, } #[derive(Clone, Copy, Debug)] @@ -27,13 +25,12 @@ struct Region { impl Region { fn writable(self) -> bool { - // Both regions are allocator-owned and read/write. - matches!(self.kind, RegionKind::Scratch | RegionKind::HeapState) + matches!(self.kind, RegionKind::Scratch) } } -/// Checks statically knowable MIR memory intervals against the ABI scratch and -/// heap-state regions. Dynamic reads retain WebAssembly's runtime bounds checks. +/// Checks statically knowable MIR memory intervals against the ABI scratch +/// region. Dynamic reads retain WebAssembly's runtime bounds checks. /// Dynamic writes must point into a buffer returned by the ABI allocator. GC /// string literals are not MIR-addressable and define no region. pub(super) fn verify_static_access_extents(module: &mir::Module) -> Result<(), Vec> { @@ -61,18 +58,11 @@ pub(super) fn verify_static_access_extents(module: &mir::Module) -> Result<(), V } fn build_regions() -> Vec { - vec![ - Region { - start: 0, - end: u64::from(SCRATCH_END), - kind: RegionKind::Scratch, - }, - Region { - start: u64::from(HEAP_STATE), - end: u64::from(HEAP_START), - kind: RegionKind::HeapState, - }, - ] + vec![Region { + start: 0, + end: u64::from(SCRATCH_END), + kind: RegionKind::Scratch, + }] } pub(super) fn function_error( diff --git a/crates/psrs-backend/src/wasm/lower/extent/tests/mod.rs b/crates/psrs-backend/src/wasm/lower/extent/tests/mod.rs index fb27a4bd..4fd089c3 100644 --- a/crates/psrs-backend/src/wasm/lower/extent/tests/mod.rs +++ b/crates/psrs-backend/src/wasm/lower/extent/tests/mod.rs @@ -124,7 +124,7 @@ fn allows_reads_and_writes_inside_the_scratch_region() { } #[test] -fn allows_reads_and_writes_inside_the_heap_state_region() { +fn rejects_reads_and_writes_at_the_old_heap_state_address() { let function = function( vec![ decl(0, ValueType::I32), @@ -170,7 +170,12 @@ fn allows_reads_and_writes_inside_the_heap_state_region() { ValueId(3), ); - verify(function).expect("heap state is a readable and writable ABI region"); + let errors = verify(function).expect_err("the old heap-state address is not reserved"); + assert!( + errors[0] + .message + .contains("outside the canonical ABI regions") + ); } #[test] diff --git a/crates/psrs-backend/src/wasm/lower/mod.rs b/crates/psrs-backend/src/wasm/lower/mod.rs index 31f71430..22e12d2a 100644 --- a/crates/psrs-backend/src/wasm/lower/mod.rs +++ b/crates/psrs-backend/src/wasm/lower/mod.rs @@ -1,13 +1,13 @@ use super::convert::val_type; use super::{ - DataIndex, DataMode, DataSegment, Entry, Export, ExportIndex, ExportKind, FuncType, - FunctionIndex, Import, Memory, MemoryIndex, Module, Op, TypeIndex, + Entry, Export, ExportIndex, ExportKind, FuncType, FunctionIndex, Import, Memory, MemoryIndex, + Module, Op, TypeIndex, }; use crate::BackendError; use crate::abi::{self, names}; use crate::capability::TargetCapabilities; use crate::mir::{self, Function as MirFunction}; -use crate::types::{DataId, HeapType, MemoryId, ValueId, ValueType}; +use crate::types::{MemoryId, ValueId, ValueType}; use psrs_hir::SymbolId; use psrs_span::TextRange; use std::collections::HashMap; @@ -24,7 +24,7 @@ mod structure; use function::types::collect_function_types; use function::{ListLocals, lower_function}; -use realloc::build_realloc; +use realloc::forward_realloc; use runtime::{collect_literal_globals, collect_strings}; /// Structures MIR control flow and builds the thin Wasm IR. The ABI registry @@ -36,11 +36,29 @@ pub fn lower_module( lower_module_with_capabilities(module, wasi, TargetCapabilities::default()) } -/// Lowers MIR using an explicit target capability profile. +/// Lowers MIR using an explicit target capability profile. Isolated callers +/// that already resolved custom WIT use a permissive context over their own +/// definitions; the production pipeline calls [`lower_module_with_plan`] with +/// the default world's checked plan. pub fn lower_module_with_capabilities( module: &mir::Module, wasi: &mut abi::WasiRegistry, target: TargetCapabilities, +) -> Result> { + let context = std::sync::Arc::new(psrs_linker::ResolvedWorldContext::permissive( + wasi.shared_resolve(), + )); + let link = crate::linking::plan_for_module(&context, module, wasi, target)?; + lower_module_with_plan(module, wasi, target, &link) +} + +/// Lowers MIR using the memory and import plan of one checked link plan. The +/// emitter cannot choose libraries or derive storage; it consumes the plan. +pub(crate) fn lower_module_with_plan( + module: &mir::Module, + wasi: &mut abi::WasiRegistry, + target: TargetCapabilities, + link: &crate::linking::LinkPlan, ) -> Result> { mir::verify_module_with_capabilities(module, target)?; let Some(entry_symbol) = module.entry else { @@ -67,7 +85,7 @@ pub fn lower_module_with_capabilities( )); } - let (mut data, string_lengths) = collect_strings(module); + let (data, string_lengths) = collect_strings(module); let literal_globals = collect_literal_globals(module); if !literal_globals.globals.is_empty() && !target.mutable_globals { return Err(wasm_error( @@ -101,7 +119,7 @@ pub fn lower_module_with_capabilities( for import in &module.imports { if matches!( import.symbol, - abi::REALLOC_SYMBOL | abi::STRING_TO_BYTES_SYMBOL | abi::BYTES_TO_STRING_SYMBOL + abi::STRING_TO_BYTES_SYMBOL | abi::BYTES_TO_STRING_SYMBOL ) { continue; } @@ -113,13 +131,13 @@ pub fn lower_module_with_capabilities( .map(|ty| vec![val_type(ty)]) .unwrap_or_default(), }); - let (module_name, field) = wasi.symbol_name(import.symbol).ok_or_else(|| { - wasm_error(module.span, "a MIR import symbol has no ABI registry entry") + let (module_name, field) = link.imports.get(&import.symbol).ok_or_else(|| { + wasm_error(module.span, "a MIR import symbol has no checked provider") })?; import_indices.insert(import.symbol, FunctionIndex(imports.len() as u32)); imports.push(Import { - module: module_name.to_string(), - name: field.to_string(), + module: module_name.clone(), + name: field.clone(), type_index, }); } @@ -136,18 +154,29 @@ pub fn lower_module_with_capabilities( message, )] })?; - let exit_type_index = TypeIndex(defined + types.len() as u32); - types.push(FuncType { - parameters: exit.parameters.iter().map(|ty| val_type(*ty)).collect(), - results: Vec::new(), - }); - let index = FunctionIndex(imports.len() as u32); - imports.push(Import { - module: exit.module, - name: exit.name, - type_index: exit_type_index, - }); - Some(index) + if let Some(index) = import_indices.get(&exit.symbol) { + Some(*index) + } else { + let (exit_module, exit_field) = + link.imports.get(&exit.symbol).cloned().ok_or_else(|| { + wasm_error( + module.span, + "the command entry exit has no checked provider", + ) + })?; + let exit_type_index = TypeIndex(defined + types.len() as u32); + types.push(FuncType { + parameters: exit.parameters.iter().map(|ty| val_type(*ty)).collect(), + results: Vec::new(), + }); + let index = FunctionIndex(imports.len() as u32); + imports.push(Import { + module: exit_module, + name: exit_field, + type_index: exit_type_index, + }); + Some(index) + } } else { None }; @@ -176,7 +205,7 @@ pub fn lower_module_with_capabilities( // The GC string type index is carried by the reserved helper imports' value // types, so the synthesized codec names the same concrete type MIR does. let string_type = if needs_helpers { - let string_type = codec::string_type_from_imports(module) + let string_type = crate::target_intrinsics::generated::string_type_from_imports(module) .ok_or_else(|| wasm_error(module.span, "the string codec has no GC string type"))?; function_indices.insert( abi::STRING_TO_BYTES_SYMBOL, @@ -216,11 +245,9 @@ pub fn lower_module_with_capabilities( let main_index = FunctionIndex(import_count + entry_function.id.0); - // `cabi_realloc` backs guest allocations for indirect parameter records and - // host allocations for returned lists/strings. Export it so the component - // host can allocate returned buffers. The bump allocator's free pointer - // lives after string data, and each allocation has a four-byte length - // prefix before its returned payload pointer. + // `cabi_realloc` backs guest allocations and host allocations for returned + // lists and strings. The export is a forwarder to the allocator runtime + // unit. The constant getter publishes the checked heap boundary. let mut exports = vec![ Export { name: abi::RUN_CORE_EXPORT.into(), @@ -233,42 +260,31 @@ pub fn lower_module_with_capabilities( index: ExportIndex::Memory(MemoryIndex(0)), }, ]; - let mut minimum = 1; + let minimum = link.plan.memory().minimum_pages; + let globals = literal_globals.globals; let mut realloc = None; if needs_realloc { - let realloc_type = TypeIndex(defined + types.len() as u32); - types.push(FuncType { - parameters: vec![ValType::I32; 4], - results: vec![ValType::I32], - }); + let import_index = *import_indices.get(&abi::REALLOC_SYMBOL).ok_or_else(|| { + wasm_error( + module.span, + "cabi_realloc has no checked allocator provider", + ) + })?; + let realloc_type = imports + .get(import_index.0 as usize) + .ok_or_else(|| wasm_error(module.span, "cabi_realloc import is missing"))? + .type_index; let index = indices.realloc.expect("a needed realloc has an index"); exports.push(Export { - name: "cabi_realloc".into(), + name: psrs_runtime::REALLOC_EXPORT.into(), kind: ExportKind::Function, index: ExportIndex::Function(index), }); - // The heap-state segment holds the free-list head (null) and the bump - // break (the first allocatable address). - let mut state = 0_u32.to_le_bytes().to_vec(); - state.extend_from_slice(&abi::HEAP_START.to_le_bytes()); - data.push(DataSegment { - id: DataId(data.len() as u32), - index: DataIndex(data.len() as u32), - mode: DataMode::Active { - offset: abi::HEAP_STATE, - }, - bytes: state, - }); - minimum = (u64::from(abi::HEAP_START)).div_ceil(0x10000) + 1; - realloc = Some(build_realloc(realloc_type, module.span)); + realloc = Some(forward_realloc(realloc_type, import_index.0, module.span)); } - let helpers = if needs_helpers { + let mut helpers = if needs_helpers { let string_type = string_type.expect("a needed codec has a GC string type"); - let string_ref = val_type(ValueType::Ref(crate::types::RefType { - nullable: false, - heap: HeapType::Index(string_type), - })); - let (stb, bts, step) = codec::signatures(string_ref); + let (stb, bts, step) = codec::signatures(string_type); let stb_type = TypeIndex(defined + types.len() as u32); types.push(stb); let bts_type = TypeIndex(defined + types.len() as u32); @@ -288,6 +304,24 @@ pub fn lower_module_with_capabilities( Vec::new() }; + if needs_realloc { + let boundary = + link.plan.memory().heap_getter.as_ref().ok_or_else(|| { + wasm_error(module.span, "the allocator provider has no heap getter") + })?; + let index = FunctionIndex(entry_index.0 + 2 + helpers.len() as u32); + exports.push(Export { + name: boundary.field.clone(), + kind: ExportKind::Function, + index: ExportIndex::Function(index), + }); + helpers.push(realloc::heap_getter( + entry_type, + link.plan.memory().heap_start, + module.span, + )); + } + // `wasi:cli/run` returns a scalar, so the command export contributes no // post-return. The synthesis entry points stay reachable so an export // mechanism can populate the descriptor lists without dead code. @@ -305,7 +339,7 @@ pub fn lower_module_with_capabilities( minimum, maximum: None, }], - globals: literal_globals.globals, + globals, data, exports, entry: Some(Entry { diff --git a/crates/psrs-backend/src/wasm/lower/post_return/tests.rs b/crates/psrs-backend/src/wasm/lower/post_return/tests.rs deleted file mode 100644 index d19b8a1c..00000000 --- a/crates/psrs-backend/src/wasm/lower/post_return/tests.rs +++ /dev/null @@ -1,449 +0,0 @@ -use super::super::super::{ - DataIndex, DataMode, DataSegment, Export, ExportIndex, ExportKind, FuncType, Function, - FunctionIndex, Import, Memory, MemoryIndex, Module, Op, TypeIndex, -}; -use super::super::asm::{Asm, constant, get, memarg, set}; -use super::{ - BufferExport, post_return_name, synthesize_buffer_post_return, - synthesize_owned_handle_post_return, -}; -use crate::component::componentize; -use psrs_hir::{ModuleId, SymbolId}; -use psrs_span::TextRange; -use wasm_encoder::{Instruction, ValType}; -use wit_parser::{Resolve, WorldId, WorldItem}; - -fn span() -> TextRange { - TextRange::new(0, 1) -} - -#[test] -fn post_return_drops_an_owned_export_handle() { - let wit = r#" - package fixture:handles@0.1.0; - interface types { - resource thing; - } - world guest { - import types; - use types.{thing}; - export take: func() -> thing; - } - "#; - let mut resolve = Resolve::default(); - let package = resolve - .push_str("handles.wit", wit) - .expect("the handle fixture should resolve"); - let world = resolve.packages[package] - .worlds - .get("guest") - .copied() - .expect("guest world"); - let export = match &resolve.worlds[world].exports.values().next() { - Some(WorldItem::Function(function)) => function.name.clone(), - other => panic!("expected one function export, found {other:?}"), - }; - assert_eq!(export, "take"); - assert_eq!(post_return_name(&export), "cabi_post_take"); - - let drop_type = TypeIndex(0); - let take_type = TypeIndex(1); - let post_type = TypeIndex(0); - let (post_signature, post_function, post_export) = synthesize_owned_handle_post_return( - &export, - post_type, - FunctionIndex(2), - FunctionIndex(0), - SymbolId::new(ModuleId(0), 1), - span(), - ); - assert_eq!(post_signature.parameters, vec![ValType::I32]); - assert!(post_signature.results.is_empty()); - assert_eq!(post_export.name, "cabi_post_take"); - - let module = Module { - name: "Handles".into(), - imports: vec![Import { - module: "fixture:handles/types@0.1.0".into(), - name: "[resource-drop]thing".into(), - type_index: drop_type, - }], - types: vec![ - FuncType { - parameters: vec![ValType::I32], - results: Vec::new(), - }, - FuncType { - parameters: Vec::new(), - results: vec![ValType::I32], - }, - ], - type_defs: Vec::new(), - functions: vec![ - Function { - symbol: SymbolId::new(ModuleId(0), 0), - name: export.clone(), - type_index: take_type, - parameters: Vec::new(), - locals: Vec::new(), - body: vec![Op::Leaf(Instruction::I32Const(1))], - span: span(), - }, - post_function, - ], - memories: vec![Memory { - id: crate::types::MemoryId(0), - index: MemoryIndex(0), - minimum: 1, - maximum: None, - }], - data: Vec::new(), - exports: vec![ - Export { - name: export, - kind: ExportKind::Function, - index: ExportIndex::Function(FunctionIndex(1)), - }, - post_export, - Export { - name: "memory".into(), - kind: ExportKind::Memory, - index: ExportIndex::Memory(MemoryIndex(0)), - }, - ], - entry: None, - realloc: None, - globals: Vec::new(), - helpers: Vec::new(), - span: span(), - }; - let core = crate::wasm::encode_module(&module).expect("encoding the core module"); - let component = componentize(&core, &resolve, world).expect("componentizing post-return"); - crate::validator() - .validate_all(&component) - .expect("the component should validate"); - let text = wasmprinter::print_bytes(&component).expect("printing the component"); - assert!( - text.contains("resource.drop"), - "post-return should lower to canon resource.drop: {text}" - ); - assert!( - text.contains("cabi_post_take") || text.contains("post-return"), - "the export should have a post-return: {text}" - ); -} - -const GET_STRING_INDEX: u32 = 0; -const POST_RETURN_INDEX: u32 = 1; -const REALLOC_INDEX: u32 = 2; -const DRIVER_INDEX: u32 = 3; - -const GET_STRING_TYPE: TypeIndex = TypeIndex(0); -const POST_RETURN_TYPE: TypeIndex = TypeIndex(1); -const REALLOC_TYPE: TypeIndex = TypeIndex(2); - -const STRING_LEN: i32 = 5; -const RETURN_AREA_SIZE: u32 = 8; -const RETURN_AREA_ALIGN: u32 = 4; -const CYCLES: i32 = 1_000; - -fn call_realloc(asm: &mut Asm, old: i32, old_len: i32, align: i32, new_len: i32) { - constant(asm, old); - constant(asm, old_len); - constant(asm, align); - constant(asm, new_len); - asm.leaf(Instruction::Call(REALLOC_INDEX)); -} - -/// Allocates the string buffer and the return area, writes `hello` and the -/// `(pointer, length)` pair, and returns the return-area pointer. This is -/// the shape of an export lifted through a canonical return area. -fn get_string_body() -> super::super::super::Body { - let mut asm = Asm::new(); - call_realloc(&mut asm, 0, 0, 1, STRING_LEN); - set(&mut asm, 0); - for (offset, byte) in b"hello".iter().enumerate() { - get(&mut asm, 0); - if offset != 0 { - constant(&mut asm, offset as i32); - asm.leaf(Instruction::I32Add); - } - constant(&mut asm, i32::from(*byte)); - asm.leaf(Instruction::I32Store8(memarg(0))); - } - call_realloc( - &mut asm, - 0, - 0, - RETURN_AREA_ALIGN as i32, - RETURN_AREA_SIZE as i32, - ); - set(&mut asm, 1); - get(&mut asm, 1); - get(&mut asm, 0); - asm.leaf(Instruction::I32Store(memarg(0))); - get(&mut asm, 1); - constant(&mut asm, STRING_LEN); - asm.leaf(Instruction::I32Store(memarg(4))); - get(&mut asm, 1); - asm.into_body() -} - -/// Calls the export and then its post-return `CYCLES` times, and reports -/// whether linear memory stayed at one page. Reclaimed buffers are reused. -fn driver_body() -> super::super::super::Body { - let mut asm = Asm::new(); - constant(&mut asm, CYCLES); - set(&mut asm, 0); - let done = asm.label(); - let again = asm.label(); - asm.block(done); - asm.loop_(again); - get(&mut asm, 0); - asm.leaf(Instruction::I32Eqz); - asm.br_if(done); - asm.leaf(Instruction::Call(GET_STRING_INDEX)); - set(&mut asm, 1); - get(&mut asm, 1); - asm.leaf(Instruction::Call(POST_RETURN_INDEX)); - get(&mut asm, 0); - constant(&mut asm, 1); - asm.leaf(Instruction::I32Sub); - set(&mut asm, 0); - asm.br(again); - asm.end(); - asm.end(); - asm.leaf(Instruction::MemorySize(0)); - constant(&mut asm, 1); - asm.leaf(Instruction::I32Eq); - asm.into_body() -} - -fn function( - name: &str, - symbol: u32, - type_index: TypeIndex, - locals: usize, - body: Vec, -) -> Function { - Function { - symbol: SymbolId::new(ModuleId(0), symbol), - name: name.into(), - type_index, - parameters: Vec::new(), - locals: vec![ValType::I32; locals], - body, - span: span(), - } -} - -/// A core module exporting `get-string` (string result), its synthesized -/// `cabi_post_get-string`, and a driver that runs the pair in a loop. -fn buffer_post_return_module() -> Module { - let (_, post_function, post_export) = synthesize_buffer_post_return( - &BufferExport { - core_name: "get-string".into(), - symbol: SymbolId::new(ModuleId(0), 1), - buffer_align: 1, - return_area_size: RETURN_AREA_SIZE, - return_area_align: RETURN_AREA_ALIGN, - }, - POST_RETURN_TYPE, - FunctionIndex(POST_RETURN_INDEX), - FunctionIndex(REALLOC_INDEX), - span(), - ); - let mut state = 0_u32.to_le_bytes().to_vec(); - state.extend_from_slice(&crate::abi::HEAP_START.to_le_bytes()); - Module { - name: "StringExport".into(), - imports: Vec::new(), - types: vec![ - FuncType { - parameters: Vec::new(), - results: vec![ValType::I32], - }, - FuncType { - parameters: vec![ValType::I32], - results: Vec::new(), - }, - FuncType { - parameters: vec![ValType::I32; 4], - results: vec![ValType::I32], - }, - ], - type_defs: Vec::new(), - functions: vec![ - function( - "get-string", - GET_STRING_INDEX, - GET_STRING_TYPE, - 2, - get_string_body(), - ), - post_function, - super::super::realloc::build_realloc(REALLOC_TYPE, span()), - function( - "check_post_return_reclaims", - DRIVER_INDEX, - GET_STRING_TYPE, - 2, - driver_body(), - ), - ], - memories: vec![Memory { - id: crate::types::MemoryId(0), - index: MemoryIndex(0), - minimum: 1, - maximum: Some(2), - }], - data: vec![DataSegment { - id: crate::types::DataId(0), - index: DataIndex(0), - mode: DataMode::Active { - offset: crate::abi::HEAP_STATE, - }, - bytes: state, - }], - exports: vec![ - Export { - name: "get-string".into(), - kind: ExportKind::Function, - index: ExportIndex::Function(FunctionIndex(GET_STRING_INDEX)), - }, - post_export, - Export { - name: "cabi_realloc".into(), - kind: ExportKind::Function, - index: ExportIndex::Function(FunctionIndex(REALLOC_INDEX)), - }, - Export { - name: "check_post_return_reclaims".into(), - kind: ExportKind::Function, - index: ExportIndex::Function(FunctionIndex(DRIVER_INDEX)), - }, - Export { - name: "memory".into(), - kind: ExportKind::Memory, - index: ExportIndex::Memory(MemoryIndex(0)), - }, - ], - entry: None, - realloc: None, - globals: Vec::new(), - helpers: Vec::new(), - span: span(), - } -} - -fn string_world() -> (Resolve, WorldId) { - let wit = r#" - package fixture:strings@0.1.0; - world guest { - export get-string: func() -> string; - } - "#; - let mut resolve = Resolve::default(); - let package = resolve - .push_str("strings.wit", wit) - .expect("the string fixture should resolve"); - let world = resolve.packages[package] - .worlds - .get("guest") - .copied() - .expect("guest world"); - (resolve, world) -} - -fn require_wasmtime() -> bool { - match std::process::Command::new("wasmtime") - .arg("--version") - .output() - { - Ok(_) => true, - Err(_) if std::env::var("PSRS_REQUIRE_WASMTIME").as_deref() == Ok("1") => { - panic!("PSRS_REQUIRE_WASMTIME=1 but wasmtime is not installed"); - } - Err(_) => { - eprintln!("skipping post-return execution: wasmtime is not installed"); - false - } - } -} - -#[test] -fn buffer_post_return_frees_the_returned_string() { - assert_eq!(post_return_name("get-string"), "cabi_post_get-string"); - let module = buffer_post_return_module(); - crate::wasm::verify::verify_module(&module).expect("thin Wasm module verifies"); - let binary = crate::wasm::encode_module(&module).expect("Wasm encodes"); - crate::validator() - .validate_all(&binary) - .expect("post-return module validates"); - - if !require_wasmtime() { - return; - } - - let path = std::env::temp_dir().join(format!( - "psrs-buffer-post-return-{}.wasm", - std::process::id() - )); - std::fs::write(&path, binary).expect("write post-return test module"); - let output = std::process::Command::new("wasmtime") - .arg("run") - .arg("--invoke") - .arg("check_post_return_reclaims") - .arg(&path) - .output() - .expect("run post-return test module"); - let _ = std::fs::remove_file(&path); - assert!(output.status.success(), "driver trapped: {output:?}"); - assert_eq!( - String::from_utf8_lossy(&output.stdout).trim(), - "1", - "post-return did not reclaim the returned buffer" - ); -} - -#[test] -fn component_attaches_the_buffer_post_return() { - let module = buffer_post_return_module(); - let binary = crate::wasm::encode_module(&module).expect("Wasm encodes"); - let (resolve, world) = string_world(); - let component = - componentize(&binary, &resolve, world).expect("componentizing the string export"); - crate::validator() - .validate_all(&component) - .expect("the component should validate"); - let text = wasmprinter::print_bytes(&component).expect("printing the component"); - assert!( - text.contains("post-return"), - "the component should attach a post-return: {text}" - ); - - if !require_wasmtime() { - return; - } - let path = std::env::temp_dir().join(format!( - "psrs-buffer-post-return-component-{}.wasm", - std::process::id() - )); - std::fs::write(&path, &component).expect("write post-return component"); - let output = std::process::Command::new("wasmtime") - .arg("run") - .arg("--invoke") - .arg("get-string()") - .arg(&path) - .output() - .expect("run post-return component"); - let _ = std::fs::remove_file(&path); - assert!( - output.status.success(), - "the component export failed: {output:?}" - ); - assert!( - String::from_utf8_lossy(&output.stdout).contains("hello"), - "the component should lift the string: {output:?}" - ); -} diff --git a/crates/psrs-backend/src/wasm/lower/post_return/tests/buffer.rs b/crates/psrs-backend/src/wasm/lower/post_return/tests/buffer.rs new file mode 100644 index 00000000..06aefb25 --- /dev/null +++ b/crates/psrs-backend/src/wasm/lower/post_return/tests/buffer.rs @@ -0,0 +1,281 @@ +//! String-export fixture whose `cabi_realloc` forwards to the allocator unit. + +use super::super::super::asm::{Asm, constant, get, memarg, set}; +use super::super::{BufferExport, synthesize_buffer_post_return}; +use crate::wasm::{ + Body, Export, ExportIndex, ExportKind, FuncType, Function, FunctionIndex, Import, Memory, + MemoryIndex, Module, Op, TypeIndex, +}; +use psrs_hir::{ModuleId, SymbolId}; +use psrs_span::TextRange; +use wasm_encoder::{Instruction, ValType}; +use wit_parser::{Resolve, WorldId}; + +const IMPORT_REALLOC: u32 = 0; +const GET_STRING_INDEX: u32 = 1; +const POST_RETURN_INDEX: u32 = 2; +const REALLOC_INDEX: u32 = 3; +const DRIVER_INDEX: u32 = 4; + +const GET_STRING_TYPE: TypeIndex = TypeIndex(0); +const POST_RETURN_TYPE: TypeIndex = TypeIndex(1); +const REALLOC_TYPE: TypeIndex = TypeIndex(2); + +const STRING_LEN: i32 = 5; +const RETURN_AREA_SIZE: u32 = 8; +const RETURN_AREA_ALIGN: u32 = 4; +const CYCLES: i32 = 1_000; + +fn span() -> TextRange { + TextRange::new(0, 1) +} + +fn call_realloc(asm: &mut Asm, old: i32, old_len: i32, align: i32, new_len: i32) { + constant(asm, old); + constant(asm, old_len); + constant(asm, align); + constant(asm, new_len); + asm.leaf(Instruction::Call(REALLOC_INDEX)); +} + +fn get_string_body() -> Body { + let mut asm = Asm::new(); + call_realloc(&mut asm, 0, 0, 1, STRING_LEN); + set(&mut asm, 0); + for (offset, byte) in b"hello".iter().enumerate() { + get(&mut asm, 0); + if offset != 0 { + constant(&mut asm, offset as i32); + asm.leaf(Instruction::I32Add); + } + constant(&mut asm, i32::from(*byte)); + asm.leaf(Instruction::I32Store8(memarg(0))); + } + call_realloc( + &mut asm, + 0, + 0, + RETURN_AREA_ALIGN as i32, + RETURN_AREA_SIZE as i32, + ); + set(&mut asm, 1); + get(&mut asm, 1); + get(&mut asm, 0); + asm.leaf(Instruction::I32Store(memarg(0))); + get(&mut asm, 1); + constant(&mut asm, STRING_LEN); + asm.leaf(Instruction::I32Store(memarg(4))); + get(&mut asm, 1); + asm.into_body() +} + +fn driver_body() -> Body { + let mut asm = Asm::new(); + asm.leaf(Instruction::Call(GET_STRING_INDEX)); + set(&mut asm, 2); + get(&mut asm, 2); + asm.leaf(Instruction::Call(POST_RETURN_INDEX)); + asm.leaf(Instruction::MemorySize(0)); + set(&mut asm, 1); + constant(&mut asm, CYCLES); + set(&mut asm, 0); + let done = asm.label(); + let again = asm.label(); + asm.block(done); + asm.loop_(again); + get(&mut asm, 0); + asm.leaf(Instruction::I32Eqz); + asm.br_if(done); + asm.leaf(Instruction::Call(GET_STRING_INDEX)); + set(&mut asm, 2); + get(&mut asm, 2); + asm.leaf(Instruction::Call(POST_RETURN_INDEX)); + get(&mut asm, 0); + constant(&mut asm, 1); + asm.leaf(Instruction::I32Sub); + set(&mut asm, 0); + asm.br(again); + asm.end(); + asm.end(); + asm.leaf(Instruction::MemorySize(0)); + get(&mut asm, 1); + asm.leaf(Instruction::I32Eq); + asm.into_body() +} + +fn forward_realloc() -> Function { + Function { + symbol: SymbolId::new(ModuleId(0), REALLOC_INDEX), + name: psrs_runtime::REALLOC_EXPORT.into(), + type_index: REALLOC_TYPE, + parameters: vec![ValType::I32; 4], + locals: Vec::new(), + body: vec![ + Op::Leaf(Instruction::LocalGet(0)), + Op::Leaf(Instruction::LocalGet(1)), + Op::Leaf(Instruction::LocalGet(2)), + Op::Leaf(Instruction::LocalGet(3)), + Op::Leaf(Instruction::Call(IMPORT_REALLOC)), + ], + span: span(), + } +} + +fn function( + name: &str, + symbol: u32, + type_index: TypeIndex, + locals: usize, + body: Vec, +) -> Function { + Function { + symbol: SymbolId::new(ModuleId(0), symbol), + name: name.into(), + type_index, + parameters: Vec::new(), + locals: vec![ValType::I32; locals], + body, + span: span(), + } +} + +/// A core module exporting `get-string`, its post-return, and a reclaim driver. +pub(super) fn buffer_post_return_module() -> Module { + let (_, post_function, post_export) = synthesize_buffer_post_return( + &BufferExport { + core_name: "get-string".into(), + symbol: SymbolId::new(ModuleId(0), 1), + buffer_align: 1, + return_area_size: RETURN_AREA_SIZE, + return_area_align: RETURN_AREA_ALIGN, + }, + POST_RETURN_TYPE, + FunctionIndex(POST_RETURN_INDEX), + FunctionIndex(REALLOC_INDEX), + span(), + ); + Module { + name: "StringExport".into(), + imports: vec![Import { + module: psrs_runtime::ALLOCATOR_MODULE.into(), + name: psrs_runtime::REALLOC_EXPORT.into(), + type_index: REALLOC_TYPE, + }], + types: vec![ + FuncType { + parameters: Vec::new(), + results: vec![ValType::I32], + }, + FuncType { + parameters: vec![ValType::I32], + results: Vec::new(), + }, + FuncType { + parameters: vec![ValType::I32; 4], + results: vec![ValType::I32], + }, + ], + type_defs: Vec::new(), + functions: vec![ + function( + "get-string", + GET_STRING_INDEX, + GET_STRING_TYPE, + 2, + get_string_body(), + ), + post_function, + forward_realloc(), + function( + "check-reclaim", + DRIVER_INDEX, + GET_STRING_TYPE, + 3, + driver_body(), + ), + ], + memories: vec![Memory { + id: crate::types::MemoryId(0), + index: MemoryIndex(0), + minimum: 1, + maximum: None, + }], + data: Vec::new(), + exports: vec![ + Export { + name: "get-string".into(), + kind: ExportKind::Function, + index: ExportIndex::Function(FunctionIndex(GET_STRING_INDEX)), + }, + post_export, + Export { + name: psrs_runtime::REALLOC_EXPORT.into(), + kind: ExportKind::Function, + index: ExportIndex::Function(FunctionIndex(REALLOC_INDEX)), + }, + Export { + name: "check-reclaim".into(), + kind: ExportKind::Function, + index: ExportIndex::Function(FunctionIndex(DRIVER_INDEX)), + }, + Export { + name: "memory".into(), + kind: ExportKind::Memory, + index: ExportIndex::Memory(MemoryIndex(0)), + }, + Export { + name: psrs_runtime::HEAP_BOUNDARY_IMPORT.into(), + kind: ExportKind::Function, + index: ExportIndex::Function(FunctionIndex(DRIVER_INDEX + 1)), + }, + ], + entry: None, + realloc: None, + globals: Vec::new(), + helpers: vec![crate::wasm::lower::realloc::heap_getter( + GET_STRING_TYPE, + 0, + span(), + )], + span: span(), + } +} + +pub(super) fn string_world() -> (Resolve, WorldId) { + let wit = r#" + package fixture:strings@0.1.0; + world guest { + export get-string: func() -> string; + export check-reclaim: func() -> s32; + } + "#; + let mut resolve = Resolve::default(); + let package = resolve + .push_str("strings.wit", wit) + .expect("the string fixture should resolve"); + let world = resolve.packages[package] + .worlds + .get("guest") + .copied() + .expect("guest world"); + (resolve, world) +} + +pub(super) fn allocator_requirement() -> psrs_linker::BindingRequirement { + psrs_linker::BindingRequirement { + id: psrs_linker::RequirementId(0), + origin: psrs_runtime::ALLOCATOR_REALLOC_OP.name.into(), + boundary: psrs_linker::Boundary::RawCore { + module: psrs_runtime::ALLOCATOR_MODULE.into(), + field: psrs_runtime::REALLOC_EXPORT.into(), + }, + expected: Some(psrs_linker::CoreSignature { + parameters: vec![psrs_linker::CoreType::I32; 4], + result: Some(psrs_linker::CoreType::I32), + }), + provider: psrs_linker::Provider::RuntimeOperation { + name: psrs_runtime::ALLOCATOR_REALLOC_OP.name.into(), + version: psrs_runtime::ALLOCATOR_REALLOC_OP.version.into(), + }, + } +} diff --git a/crates/psrs-backend/src/wasm/lower/post_return/tests/composition.rs b/crates/psrs-backend/src/wasm/lower/post_return/tests/composition.rs new file mode 100644 index 00000000..31cf873a --- /dev/null +++ b/crates/psrs-backend/src/wasm/lower/post_return/tests/composition.rs @@ -0,0 +1,62 @@ +use super::{Module, Resolve, WorldId}; + +/// Fixtures explicitly declare their imports, then emit the checked minimum. +pub(super) fn componentize( + mut module: Module, + resolve: Resolve, + world: WorldId, + requirements: Vec, +) -> Result, String> { + use psrs_linker::{MemoryDemand, TargetLinkInput, TargetPolicy}; + let context = psrs_linker::ResolvedWorldContext::from_resolve(resolve, world); + let plan = psrs_linker::plan( + &context, + TargetLinkInput { + policy: TargetPolicy { + permitted_host_interfaces: requirements + .iter() + .filter_map(|requirement| match &requirement.provider { + psrs_linker::Provider::HostInterface { interface } => { + Some(interface.clone()) + } + _ => None, + }) + .collect(), + }, + requirements, + units: psrs_linker::runtime::package_offers(&psrs_runtime::PSRS_RUNTIME).unwrap(), + memory: MemoryDemand { + canonical_scratch: (0, crate::abi::SCRATCH_SIZE), + allocator_state: (crate::abi::HEAP_START, crate::abi::HEAP_START), + base_heap_start: crate::abi::HEAP_START, + heap_alignment: crate::abi::MIN_BLOCK, + growth_owner: psrs_runtime::ALLOCATOR_UNIT.id.into(), + maximum_pages: None, + }, + }, + ) + .map_err(|error| error.to_string())?; + module.memories[0].minimum = plan.memory().minimum_pages; + if let Some(boundary) = plan.memory().heap_getter.as_ref() { + let export = module + .exports + .iter() + .find(|export| export.name == boundary.field) + .ok_or_else(|| format!("missing heap export `{}`", boundary.field))?; + let crate::wasm::ExportIndex::Function(_) = export.index else { + return Err("the heap boundary export is not a function".into()); + }; + let getter = module + .helpers + .iter_mut() + .find(|function| function.name == boundary.field) + .ok_or("the heap boundary getter is missing")?; + getter.body = vec![crate::wasm::Op::Leaf(wasm_encoder::Instruction::I32Const( + plan.memory().heap_start as i32, + ))]; + } + let core = crate::wasm::encode_module(&module).map_err(|error| format!("{error:?}"))?; + psrs_linker::compose(&context, &plan, &core) + .map(|artifact| artifact.bytes) + .map_err(|errors| errors.to_string()) +} diff --git a/crates/psrs-backend/src/wasm/lower/post_return/tests/mod.rs b/crates/psrs-backend/src/wasm/lower/post_return/tests/mod.rs new file mode 100644 index 00000000..b1eb69fe --- /dev/null +++ b/crates/psrs-backend/src/wasm/lower/post_return/tests/mod.rs @@ -0,0 +1,253 @@ +use super::super::super::{ + Export, ExportIndex, ExportKind, FuncType, Function, FunctionIndex, Import, Memory, + MemoryIndex, Module, Op, TypeIndex, +}; +use super::{post_return_name, synthesize_owned_handle_post_return}; +use psrs_hir::{ModuleId, SymbolId}; +use psrs_span::TextRange; +use wasm_encoder::{Instruction, ValType}; +use wit_parser::{Resolve, WorldId, WorldItem}; + +fn span() -> TextRange { + TextRange::new(0, 1) +} + +mod buffer; +mod composition; +use buffer::{allocator_requirement, buffer_post_return_module, string_world}; +use composition::componentize; + +#[test] +fn post_return_drops_an_owned_export_handle() { + let wit = r#" + package fixture:handles@0.1.0; + interface types { + resource thing; + } + interface api { + use types.{thing}; + take: func() -> thing; + } + world guest { + import types; + export api; + } + "#; + let mut resolve = Resolve::default(); + let package = resolve + .push_str("handles.wit", wit) + .expect("the handle fixture should resolve"); + let world = resolve.packages[package] + .worlds + .get("guest") + .copied() + .expect("guest world"); + let (key, item) = resolve.worlds[world].exports.iter().next().unwrap(); + let WorldItem::Interface { id, .. } = item else { + panic!("interface export"); + }; + let export = format!( + "{}#{}", + resolve.name_world_key(key), + resolve.interfaces[*id].functions["take"].name + ); + assert_eq!(export, "fixture:handles/api@0.1.0#take"); + assert_eq!( + post_return_name(&export), + "cabi_post_fixture:handles/api@0.1.0#take" + ); + + let drop_type = TypeIndex(0); + let take_type = TypeIndex(1); + let post_type = TypeIndex(0); + let (post_signature, post_function, post_export) = synthesize_owned_handle_post_return( + &export, + post_type, + FunctionIndex(2), + FunctionIndex(0), + SymbolId::new(ModuleId(0), 1), + span(), + ); + assert_eq!(post_signature.parameters, vec![ValType::I32]); + assert!(post_signature.results.is_empty()); + assert_eq!(post_export.name, post_return_name(&export)); + + let module = Module { + name: "Handles".into(), + imports: vec![Import { + module: "fixture:handles/types@0.1.0".into(), + name: "[resource-drop]thing".into(), + type_index: drop_type, + }], + types: vec![ + FuncType { + parameters: vec![ValType::I32], + results: Vec::new(), + }, + FuncType { + parameters: Vec::new(), + results: vec![ValType::I32], + }, + ], + type_defs: Vec::new(), + functions: vec![ + Function { + symbol: SymbolId::new(ModuleId(0), 0), + name: export.clone(), + type_index: take_type, + parameters: Vec::new(), + locals: Vec::new(), + body: vec![Op::Leaf(Instruction::I32Const(1))], + span: span(), + }, + post_function, + ], + memories: vec![Memory { + id: crate::types::MemoryId(0), + index: MemoryIndex(0), + minimum: 1, + maximum: None, + }], + data: Vec::new(), + exports: vec![ + Export { + name: export, + kind: ExportKind::Function, + index: ExportIndex::Function(FunctionIndex(1)), + }, + post_export, + Export { + name: "memory".into(), + kind: ExportKind::Memory, + index: ExportIndex::Memory(MemoryIndex(0)), + }, + ], + entry: None, + realloc: None, + globals: Vec::new(), + helpers: Vec::new(), + span: span(), + }; + let interface = "fixture:handles/types@0.1.0".to_string(); + let requirement = psrs_linker::BindingRequirement { + id: psrs_linker::RequirementId(0), + origin: "owned handle post-return".into(), + boundary: psrs_linker::Boundary::ResolvedWit { + interface: interface.clone(), + function: "[resource-drop]thing".into(), + }, + expected: Some(psrs_linker::CoreSignature { + parameters: vec![psrs_linker::CoreType::I32], + result: None, + }), + provider: psrs_linker::Provider::HostInterface { interface }, + }; + let component = componentize(module, resolve, world, vec![requirement]) + .expect("componentizing post-return"); + crate::validator() + .validate_all(&component) + .expect("the component should validate"); + let text = wasmprinter::print_bytes(&component).expect("printing the component"); + assert!( + text.contains("resource.drop"), + "post-return should lower to canon resource.drop: {text}" + ); + assert!( + text.contains("cabi_post_fixture:handles/api@0.1.0#take") || text.contains("post-return"), + "the export should have a post-return: {text}" + ); +} + +fn require_wasmtime() -> bool { + match std::process::Command::new("wasmtime") + .arg("--version") + .output() + { + Ok(_) => true, + Err(_) if std::env::var("PSRS_REQUIRE_WASMTIME").as_deref() == Ok("1") => { + panic!("PSRS_REQUIRE_WASMTIME=1 but wasmtime is not installed"); + } + Err(_) => { + eprintln!("skipping post-return execution: wasmtime is not installed"); + false + } + } +} + +#[test] +fn buffer_post_return_frees_the_returned_string() { + assert_eq!(post_return_name("get-string"), "cabi_post_get-string"); + let module = buffer_post_return_module(); + crate::wasm::verify::verify_module(&module).expect("thin Wasm module verifies"); + let binary = crate::wasm::encode_module(&module).expect("Wasm encodes"); + crate::validator() + .validate_all(&binary) + .expect("post-return module validates"); + + if !require_wasmtime() { + return; + } + let (resolve, world) = string_world(); + let component = componentize(module, resolve, world, vec![allocator_requirement()]) + .expect("componentizing the reclaim driver"); + let path = std::env::temp_dir().join(format!( + "psrs-buffer-post-return-{}.wasm", + std::process::id() + )); + std::fs::write(&path, component).expect("write post-return test module"); + let output = std::process::Command::new("wasmtime") + .arg("run") + .arg("--invoke") + .arg("check-reclaim()") + .arg(&path) + .output() + .expect("run post-return test module"); + let _ = std::fs::remove_file(&path); + assert!(output.status.success(), "driver trapped: {output:?}"); + assert_eq!( + String::from_utf8_lossy(&output.stdout).trim(), + "1", + "post-return did not reclaim the returned buffer" + ); +} + +#[test] +fn component_attaches_the_buffer_post_return() { + let module = buffer_post_return_module(); + let (resolve, world) = string_world(); + let component = componentize(module, resolve, world, vec![allocator_requirement()]) + .expect("componentizing the string export"); + crate::validator() + .validate_all(&component) + .expect("the component should validate"); + let text = wasmprinter::print_bytes(&component).expect("printing the component"); + assert!( + text.contains("post-return"), + "the component should attach a post-return: {text}" + ); + + if !require_wasmtime() { + return; + } + let path = std::env::temp_dir().join(format!( + "psrs-buffer-post-return-component-{}.wasm", + std::process::id() + )); + std::fs::write(&path, &component).expect("write post-return component"); + let output = std::process::Command::new("wasmtime") + .arg("run") + .arg("--invoke") + .arg("get-string()") + .arg(&path) + .output() + .expect("run post-return component"); + let _ = std::fs::remove_file(&path); + assert!( + output.status.success(), + "the component export failed: {output:?}" + ); + assert!( + String::from_utf8_lossy(&output.stdout).contains("hello"), + "the component should lift the string: {output:?}" + ); +} diff --git a/crates/psrs-backend/src/wasm/lower/realloc/block.rs b/crates/psrs-backend/src/wasm/lower/realloc/block.rs deleted file mode 100644 index 9d074ed6..00000000 --- a/crates/psrs-backend/src/wasm/lower/realloc/block.rs +++ /dev/null @@ -1,306 +0,0 @@ -//! Block allocation, free-list reuse, and coalescing for `cabi_realloc`. - -use super::*; -use crate::abi; -use wasm_encoder::Instruction; - -/// Allocates `NEW_LEN` bytes aligned to `ALIGN` (rounded up to the block -/// granularity) and leaves the payload pointer in `L_P`. -pub(super) fn emit_allocate(asm: &mut Asm) { - // a = max(align, MIN_BLOCK) - let small = asm.label(); - get(asm, ALIGN); - constant(asm, abi::MIN_BLOCK as i32); - asm.leaf(Instruction::I32LtU); - asm.if_(small); - constant(asm, abi::MIN_BLOCK as i32); - set(asm, L_A); - asm.else_(); - get(asm, ALIGN); - set(asm, L_A); - asm.end(); - - constant(asm, 0); - set(asm, L_PREV); - state_load(asm, 0); - set(asm, L_NODE); - - let found = asm.label(); - let scan = asm.label(); - asm.block(found); - asm.loop_(scan); - get(asm, L_NODE); - asm.leaf(Instruction::I32Eqz); - asm.br_if(found); - word_load(asm, L_NODE, 0); - set(asm, L_SIZE); - // P = align_up(NODE + HEADER, a) - get(asm, L_NODE); - constant(asm, abi::HEADER_SIZE as i32); - asm.leaf(Instruction::I32Add); - set(asm, L_PNODE); - local_trap_if_wrapped(asm, L_PNODE, L_NODE); - checked_align_up(asm, L_PNODE, L_A); - // front = align_up((P - NODE) + NEW_LEN, MIN_BLOCK) - get(asm, L_PNODE); - get(asm, L_NODE); - asm.leaf(Instruction::I32Sub); - get(asm, NEW_LEN); - asm.leaf(Instruction::I32Add); - set(asm, L_FRONT); - checked_align_up_const(asm, L_FRONT, abi::MIN_BLOCK); - // front <= size: this block fits - get(asm, L_FRONT); - get(asm, L_SIZE); - asm.leaf(Instruction::I32LeU); - asm.br_if(found); - get(asm, L_NODE); - set(asm, L_PREV); - word_load(asm, L_NODE, 4); - set(asm, L_NODE); - asm.br(scan); - asm.end(); - asm.end(); - - let bump = asm.label(); - get(asm, L_NODE); - asm.leaf(Instruction::I32Eqz); - asm.if_(bump); - emit_bump(asm); - asm.else_(); - emit_reuse(asm); - asm.end(); -} - -/// Carves a fresh block off the bump end, growing memory when needed, and -/// leaves the payload pointer in `L_P`. -fn emit_bump(asm: &mut Asm) { - state_load(asm, 4); - set(asm, L_BREAK); - // P = align_up(BREAK + HEADER, a) - get(asm, L_BREAK); - constant(asm, abi::HEADER_SIZE as i32); - asm.leaf(Instruction::I32Add); - set(asm, L_P); - local_trap_if_wrapped(asm, L_P, L_BREAK); - checked_align_up(asm, L_P, L_A); - // END = align_up(P + NEW_LEN, MIN_BLOCK) - get(asm, L_P); - get(asm, NEW_LEN); - asm.leaf(Instruction::I32Add); - set(asm, L_END); - local_trap_if_wrapped(asm, L_END, L_P); - checked_align_up_const(asm, L_END, abi::MIN_BLOCK); - // block_size = END - BREAK - get(asm, L_END); - get(asm, L_BREAK); - asm.leaf(Instruction::I32Sub); - set(asm, L_TMP); - word_store(asm, L_BREAK, 0, L_TMP); - word_store(asm, L_BREAK, 4, NEW_LEN); - // pages = ceil(END / 65536) - get(asm, L_END); - constant(asm, 16); - asm.leaf(Instruction::I32ShrU); - get(asm, L_END); - constant(asm, 0xffff); - asm.leaf(Instruction::I32And); - asm.leaf(Instruction::I32Eqz); - asm.leaf(Instruction::I32Eqz); - asm.leaf(Instruction::I32Add); - set(asm, L_PAGES); - let grow = asm.label(); - get(asm, L_PAGES); - asm.leaf(Instruction::MemorySize(0)); - asm.leaf(Instruction::I32GtU); - asm.if_(grow); - get(asm, L_PAGES); - asm.leaf(Instruction::MemorySize(0)); - asm.leaf(Instruction::I32Sub); - asm.leaf(Instruction::MemoryGrow(0)); - constant(asm, -1); - asm.leaf(Instruction::I32Eq); - let failed = asm.label(); - asm.if_(failed); - asm.leaf(Instruction::Unreachable); - asm.end(); - asm.end(); - state_store(asm, 4, L_END); -} - -/// Takes the first-fitting block `L_NODE`, splitting off the tail when the -/// remainder can hold another block, and leaves the payload pointer in `L_P`. -fn emit_reuse(asm: &mut Asm) { - let split = asm.label(); - get(asm, L_SIZE); - get(asm, L_FRONT); - asm.leaf(Instruction::I32Sub); - constant(asm, abi::MIN_BLOCK as i32); - asm.leaf(Instruction::I32GeU); - asm.if_(split); - get(asm, L_NODE); - get(asm, L_FRONT); - asm.leaf(Instruction::I32Add); - set(asm, L_TAIL); - get(asm, L_SIZE); - get(asm, L_FRONT); - asm.leaf(Instruction::I32Sub); - set(asm, L_TMP); - word_store(asm, L_TAIL, 0, L_TMP); - word_load(asm, L_NODE, 4); - set(asm, L_TMP2); - word_store(asm, L_TAIL, 4, L_TMP2); - link_replace(asm, L_TAIL); - word_store(asm, L_NODE, 0, L_FRONT); - asm.else_(); - word_load(asm, L_NODE, 4); - set(asm, L_TMP2); - link_replace(asm, L_TMP2); - asm.end(); - word_store(asm, L_NODE, 4, NEW_LEN); - get(asm, L_PNODE); - set(asm, L_P); -} - -/// Makes `PREV.next` (or the free-list head when `PREV == 0`) point at `L_NODE`. -fn link_replace(asm: &mut Asm, new_node: u32) { - let head = asm.label(); - get(asm, L_PREV); - asm.leaf(Instruction::I32Eqz); - asm.if_(head); - state_store(asm, 0, new_node); - asm.else_(); - word_store(asm, L_PREV, 4, new_node); - asm.end(); -} - -/// Frees the payload pointer in `fp`, coalescing with adjacent free blocks. -pub(super) fn emit_free(asm: &mut Asm, fp: u32) { - get(asm, fp); - constant(asm, abi::HEADER_SIZE as i32); - asm.leaf(Instruction::I32Sub); - set(asm, L_H); - word_load(asm, L_H, 0); - set(asm, L_SIZE); - - constant(asm, 0); - set(asm, L_PREV); - state_load(asm, 0); - set(asm, L_NODE); - let done = asm.label(); - let scan = asm.label(); - asm.block(done); - asm.loop_(scan); - get(asm, L_NODE); - asm.leaf(Instruction::I32Eqz); - asm.br_if(done); - get(asm, L_NODE); - get(asm, L_H); - asm.leaf(Instruction::I32GeU); - asm.br_if(done); - get(asm, L_NODE); - set(asm, L_PREV); - word_load(asm, L_NODE, 4); - set(asm, L_NODE); - asm.br(scan); - asm.end(); - asm.end(); - - // Merge with the following free block when adjacent. - let has_next = asm.label(); - get(asm, L_NODE); - asm.leaf(Instruction::I32Eqz); - asm.leaf(Instruction::I32Eqz); - asm.if_(has_next); - let adjacent_next = asm.label(); - get(asm, L_H); - get(asm, L_SIZE); - asm.leaf(Instruction::I32Add); - get(asm, L_NODE); - asm.leaf(Instruction::I32Eq); - asm.if_(adjacent_next); - word_load(asm, L_NODE, 0); - set(asm, L_TMP); - get(asm, L_SIZE); - get(asm, L_TMP); - asm.leaf(Instruction::I32Add); - set(asm, L_SIZE); - word_load(asm, L_NODE, 4); - set(asm, L_NODE); - asm.end(); - asm.end(); - - // Merge with the preceding free block when adjacent, otherwise insert. - let has_prev = asm.label(); - get(asm, L_PREV); - asm.leaf(Instruction::I32Eqz); - asm.leaf(Instruction::I32Eqz); - asm.if_(has_prev); - let adjacent_prev = asm.label(); - get(asm, L_PREV); - word_load(asm, L_PREV, 0); - asm.leaf(Instruction::I32Add); - get(asm, L_H); - asm.leaf(Instruction::I32Eq); - asm.if_(adjacent_prev); - word_load(asm, L_PREV, 0); - get(asm, L_SIZE); - asm.leaf(Instruction::I32Add); - set(asm, L_TMP); - word_store(asm, L_PREV, 0, L_TMP); - word_store(asm, L_PREV, 4, L_NODE); - asm.else_(); - emit_insert(asm); - asm.end(); - asm.else_(); - emit_insert(asm); - asm.end(); -} - -/// Inserts the free block at `L_H` before `L_NODE` in the address-ordered list. -fn emit_insert(asm: &mut Asm) { - word_store(asm, L_H, 0, L_SIZE); - word_store(asm, L_H, 4, L_NODE); - link_replace(asm, L_H); -} - -/// Rounds the local `value` up to the power-of-two `align` local, trapping on -/// pointer-width overflow. -fn checked_align_up(asm: &mut Asm, value: u32, align: u32) { - get(asm, value); - get(asm, align); - constant(asm, 1); - asm.leaf(Instruction::I32Sub); - asm.leaf(Instruction::I32Add); - set(asm, L_TMP); - local_trap_if_wrapped(asm, L_TMP, value); - get(asm, L_TMP); - constant(asm, 0); - get(asm, align); - asm.leaf(Instruction::I32Sub); - asm.leaf(Instruction::I32And); - set(asm, value); -} - -/// Rounds the local `value` up to the power-of-two constant `align`, trapping on -/// pointer-width overflow. -fn checked_align_up_const(asm: &mut Asm, value: u32, align: u32) { - get(asm, value); - constant(asm, (align - 1) as i32); - asm.leaf(Instruction::I32Add); - set(asm, L_TMP); - local_trap_if_wrapped(asm, L_TMP, value); - get(asm, L_TMP); - constant(asm, (0i32).wrapping_sub(align as i32)); - asm.leaf(Instruction::I32And); - set(asm, value); -} - -/// Traps when `result` is numerically below `source`, i.e. an add wrapped. -fn local_trap_if_wrapped(asm: &mut Asm, result: u32, source: u32) { - trap_if(asm, |a| { - get(a, result); - get(a, source); - a.leaf(Instruction::I32LtU); - }); -} diff --git a/crates/psrs-backend/src/wasm/lower/realloc/mod.rs b/crates/psrs-backend/src/wasm/lower/realloc/mod.rs index 2ad13028..a8e384d4 100644 --- a/crates/psrs-backend/src/wasm/lower/realloc/mod.rs +++ b/crates/psrs-backend/src/wasm/lower/realloc/mod.rs @@ -1,274 +1,44 @@ -//! The synthesized linear-memory allocator used by canonical ABI lowering. +//! Local forwarder for the allocator runtime unit's `cabi_realloc`. //! -//! `cabi_realloc` implements the full canonical `(old_ptr, old_len, align, -//! new_len) -> pointer` contract: it allocates an aligned block, frees when the -//! new length is zero, and resizes with copy otherwise. Freed blocks go on an -//! address-ordered free list and are coalesced with their neighbors, so a -//! request reuses reclaimed storage instead of exhausting memory. The block -//! layout and ownership rules are fixed by -//! `docs/design/backend/wasm/canonical-buffer-allocation-and-lifetime.md`. +//! Codecs, cleanup, and the component host call this function. It forwards the +//! canonical arguments to the imported provider and returns that provider's +//! pointer. The provider owns allocation, freeing, and resizing. -use super::super::{Function, TypeIndex}; -use super::asm::*; +use super::super::{Function, Op, TypeIndex}; use crate::abi; use psrs_span::TextRange; -use wasm_encoder::{Instruction, MemArg, ValType}; - -mod block; - -use block::{emit_allocate, emit_free}; - -pub(super) const OLD_PTR: u32 = 0; -pub(super) const OLD_LEN: u32 = 1; -pub(super) const ALIGN: u32 = 2; -pub(super) const NEW_LEN: u32 = 3; - -// Scratch locals. Parameters occupy 0..4, so the first scratch local is 4. -pub(super) const L_A: u32 = 4; -pub(super) const L_PREV: u32 = 5; -pub(super) const L_NODE: u32 = 6; -pub(super) const L_SIZE: u32 = 7; -pub(super) const L_PNODE: u32 = 8; -pub(super) const L_FRONT: u32 = 9; -pub(super) const L_TAIL: u32 = 10; -pub(super) const L_P: u32 = 11; -pub(super) const L_END: u32 = 12; -pub(super) const L_PAGES: u32 = 13; -pub(super) const L_BREAK: u32 = 14; -pub(super) const L_SRC: u32 = 15; -pub(super) const L_DST: u32 = 16; -pub(super) const L_BYTE: u32 = 17; -pub(super) const L_COUNT: u32 = 18; -pub(super) const L_H: u32 = 19; -pub(super) const L_CAP: u32 = 20; -pub(super) const L_TMP: u32 = 21; -pub(super) const L_TMP2: u32 = 22; -pub(super) const LOCAL_COUNT: usize = 19; - -pub(super) const WORD_ALIGN: u32 = 2; -pub(super) const BYTE_ALIGN: u32 = 0; - -/// Builds the reclaiming `cabi_realloc` used by canonical ABI boundary buffers, -/// including indirect parameter records and returned lists/strings. -pub(super) fn build_realloc(type_index: TypeIndex, span: TextRange) -> Function { - let mut asm = Asm::new(); - - // Alignment is an unsigned, nonzero power of two. Validate it even for a - // zero-sized allocation so malformed calls trap consistently. - trap_if(&mut asm, |a| { - get(a, ALIGN); - a.leaf(Instruction::I32Eqz); - get(a, ALIGN); - constant(a, 1); - a.leaf(Instruction::I32Sub); - get(a, ALIGN); - a.leaf(Instruction::I32And); - a.leaf(Instruction::I32Eqz); - a.leaf(Instruction::I32Eqz); - a.leaf(Instruction::I32Or); - }); - - // new_len == 0 frees the old block (when any) and returns null. - let zero_length = asm.label(); - get(&mut asm, NEW_LEN); - asm.leaf(Instruction::I32Eqz); - asm.if_(zero_length); - { - let has_old = asm.label(); - get(&mut asm, OLD_PTR); - asm.leaf(Instruction::I32Eqz); - asm.leaf(Instruction::I32Eqz); - asm.if_(has_old); - emit_free(&mut asm, OLD_PTR); - asm.end(); - constant(&mut asm, 0); - asm.leaf(Instruction::Return); - } - asm.end(); - - // A non-null old pointer must describe a complete block whose stored length - // agrees with the caller's. If the block already has capacity and alignment - // for the new length, resize in place. - let old_nonnull = asm.label(); - get(&mut asm, OLD_PTR); - asm.leaf(Instruction::I32Eqz); - asm.leaf(Instruction::I32Eqz); - asm.if_(old_nonnull); - { - trap_if(&mut asm, |a| { - get(a, OLD_PTR); - constant(a, abi::HEADER_SIZE as i32); - a.leaf(Instruction::I32LtU); - }); - trap_if(&mut asm, |a| { - get(a, OLD_PTR); - constant(a, 4); - a.leaf(Instruction::I32Sub); - a.leaf(Instruction::I32Load(word_memarg())); - get(a, OLD_LEN); - a.leaf(Instruction::I32Ne); - }); - get(&mut asm, OLD_PTR); - constant(&mut asm, abi::HEADER_SIZE as i32); - asm.leaf(Instruction::I32Sub); - set(&mut asm, L_H); - word_load(&mut asm, L_H, 0); - set(&mut asm, L_SIZE); - get(&mut asm, L_SIZE); - constant(&mut asm, abi::HEADER_SIZE as i32); - asm.leaf(Instruction::I32Sub); - set(&mut asm, L_CAP); - - let fits_in_place = asm.label(); - get(&mut asm, NEW_LEN); - get(&mut asm, L_CAP); - asm.leaf(Instruction::I32LeU); - get(&mut asm, OLD_PTR); - get(&mut asm, ALIGN); - constant(&mut asm, 1); - asm.leaf(Instruction::I32Sub); - asm.leaf(Instruction::I32And); - asm.leaf(Instruction::I32Eqz); - asm.leaf(Instruction::I32And); - asm.if_(fits_in_place); - { - get(&mut asm, OLD_PTR); - constant(&mut asm, 4); - asm.leaf(Instruction::I32Sub); - get(&mut asm, NEW_LEN); - asm.leaf(Instruction::I32Store(word_memarg())); - get(&mut asm, OLD_PTR); - asm.leaf(Instruction::Return); - } - asm.end(); - } - asm.end(); - - // A null old pointer requires an empty old length. - trap_if(&mut asm, |a| { - get(a, OLD_PTR); - a.leaf(Instruction::I32Eqz); - get(a, OLD_LEN); - a.leaf(Instruction::I32Eqz); - a.leaf(Instruction::I32Eqz); - a.leaf(Instruction::I32And); - }); - - emit_allocate(&mut asm); - - let has_old_bytes = asm.label(); - get(&mut asm, OLD_PTR); - asm.leaf(Instruction::I32Eqz); - asm.leaf(Instruction::I32Eqz); - asm.if_(has_old_bytes); - { - let old_is_smaller = asm.label(); - get(&mut asm, OLD_LEN); - get(&mut asm, NEW_LEN); - asm.leaf(Instruction::I32LtU); - asm.if_(old_is_smaller); - get(&mut asm, OLD_LEN); - set(&mut asm, L_COUNT); - asm.else_(); - get(&mut asm, NEW_LEN); - set(&mut asm, L_COUNT); - asm.end(); - get(&mut asm, OLD_PTR); - set(&mut asm, L_SRC); - get(&mut asm, L_P); - set(&mut asm, L_DST); - let done = asm.label(); - let next = asm.label(); - asm.block(done); - asm.loop_(next); - get(&mut asm, L_COUNT); - asm.leaf(Instruction::I32Eqz); - asm.br_if(done); - get(&mut asm, L_SRC); - asm.leaf(Instruction::I32Load8U(byte_memarg())); - set(&mut asm, L_BYTE); - get(&mut asm, L_DST); - get(&mut asm, L_BYTE); - asm.leaf(Instruction::I32Store8(byte_memarg())); - advance(&mut asm, L_SRC, 1); - advance(&mut asm, L_DST, 1); - get(&mut asm, L_COUNT); - constant(&mut asm, 1); - asm.leaf(Instruction::I32Sub); - set(&mut asm, L_COUNT); - asm.br(next); - asm.end(); - asm.end(); - emit_free(&mut asm, OLD_PTR); - } - asm.end(); - - get(&mut asm, L_P); +use wasm_encoder::{Instruction, ValType}; +/// Builds the forwarding `cabi_realloc`. +/// +/// `import` is the core function index of the allocator unit's export. +pub(super) fn forward_realloc(type_index: TypeIndex, import: u32, span: TextRange) -> Function { Function { - symbol: crate::abi::REALLOC_SYMBOL, - name: "cabi_realloc".into(), + symbol: abi::REALLOC_SYMBOL, + name: psrs_runtime::REALLOC_EXPORT.into(), type_index, parameters: vec![ValType::I32; 4], - locals: vec![ValType::I32; LOCAL_COUNT], - body: asm.into_body(), + locals: Vec::new(), + body: vec![ + Op::Leaf(Instruction::LocalGet(0)), + Op::Leaf(Instruction::LocalGet(1)), + Op::Leaf(Instruction::LocalGet(2)), + Op::Leaf(Instruction::LocalGet(3)), + Op::Leaf(Instruction::Call(import)), + ], span, } } -pub(super) fn word_memarg() -> MemArg { - MemArg { - offset: 0, - align: WORD_ALIGN, - memory_index: 0, - } -} - -pub(super) fn byte_memarg() -> MemArg { - MemArg { - offset: 0, - align: BYTE_ALIGN, - memory_index: 0, - } -} - -pub(super) fn word_load(asm: &mut Asm, base: u32, offset: u32) { - get(asm, base); - if offset != 0 { - constant(asm, offset as i32); - asm.leaf(Instruction::I32Add); - } - asm.leaf(Instruction::I32Load(word_memarg())); -} - -pub(super) fn word_store(asm: &mut Asm, base: u32, offset: u32, value: u32) { - get(asm, base); - if offset != 0 { - constant(asm, offset as i32); - asm.leaf(Instruction::I32Add); +/// Returns the checked application's constant boundary without using linear memory. +pub(super) fn heap_getter(type_index: TypeIndex, heap_start: u32, span: TextRange) -> Function { + Function { + symbol: abi::HEAP_BASE_SYMBOL, + name: psrs_runtime::HEAP_BOUNDARY_IMPORT.into(), + type_index, + parameters: Vec::new(), + locals: Vec::new(), + body: vec![Op::Leaf(Instruction::I32Const(heap_start as i32))], + span, } - get(asm, value); - asm.leaf(Instruction::I32Store(word_memarg())); } - -pub(super) fn state_load(asm: &mut Asm, offset: u32) { - constant(asm, (abi::HEAP_STATE + offset) as i32); - asm.leaf(Instruction::I32Load(word_memarg())); -} - -pub(super) fn state_store(asm: &mut Asm, offset: u32, value: u32) { - constant(asm, (abi::HEAP_STATE + offset) as i32); - get(asm, value); - asm.leaf(Instruction::I32Store(word_memarg())); -} - -pub(super) fn trap_if(asm: &mut Asm, condition: impl FnOnce(&mut Asm)) { - condition(asm); - let failed = asm.label(); - asm.if_(failed); - asm.leaf(Instruction::Unreachable); - asm.end(); -} - -#[cfg(test)] -mod tests; diff --git a/crates/psrs-backend/src/wasm/lower/realloc/tests.rs b/crates/psrs-backend/src/wasm/lower/realloc/tests.rs deleted file mode 100644 index 99bad80f..00000000 --- a/crates/psrs-backend/src/wasm/lower/realloc/tests.rs +++ /dev/null @@ -1,381 +0,0 @@ -use super::*; -use crate::types::{DataId, MemoryId}; -use crate::wasm::lower::asm::{Asm, constant, get, set}; -use crate::wasm::{ - Body, DataIndex, DataMode, DataSegment, Export, ExportIndex, ExportKind, FuncType, Function, - FunctionIndex, Memory, MemoryIndex, Module, -}; -use psrs_hir::{ModuleId, SymbolId}; -use psrs_span::TextRange; -use wasm_encoder::{Instruction as I, MemArg, ValType}; - -const CHECK_COUNT: u32 = 8; -const REALLOC_INDEX: u32 = CHECK_COUNT; - -fn mm(align: u32) -> MemArg { - MemArg { - offset: 0, - align, - memory_index: 0, - } -} - -fn span() -> TextRange { - TextRange::new(0, 1) -} - -fn realloc_call(asm: &mut Asm, old: i32, old_len: i32, align: i32, new_len: i32) { - constant(asm, old); - constant(asm, old_len); - constant(asm, align); - constant(asm, new_len); - asm.leaf(I::Call(REALLOC_INDEX)); -} - -fn allocate_into(asm: &mut Asm, local: u32, n: i32, align: i32) { - realloc_call(asm, 0, 0, align, n); - set(asm, local); -} - -fn free_call(asm: &mut Asm, pointer: u32, len: i32) { - get(asm, pointer); - constant(asm, len); - constant(asm, 1); - constant(asm, 0); - asm.leaf(I::Call(REALLOC_INDEX)); - asm.leaf(I::Drop); -} - -fn equal(asm: &mut Asm, left: u32, right: u32) { - get(asm, left); - get(asm, right); - asm.leaf(I::I32Eq); -} - -/// Allocates, frees, and confirms the freed block is reused and that two -/// adjacent freed blocks coalesce into one larger block. -fn check_reuse_and_coalesce() -> Body { - let mut asm = Asm::new(); - allocate_into(&mut asm, 0, 16, 8); - allocate_into(&mut asm, 1, 16, 8); - // The two bump allocations are distinct. - get(&mut asm, 0); - get(&mut asm, 1); - asm.leaf(I::I32Ne); - - free_call(&mut asm, 0, 16); - allocate_into(&mut asm, 2, 16, 8); - // The freed block is reused. - equal(&mut asm, 2, 0); - asm.leaf(I::I32And); - - free_call(&mut asm, 1, 16); - free_call(&mut asm, 2, 16); - allocate_into(&mut asm, 3, 32, 8); - // The two adjacent freed blocks coalesced into the first one. - equal(&mut asm, 3, 0); - asm.leaf(I::I32And); - asm.into_body() -} - -/// Confirms alignment and in-place resize, then a grow-with-copy resize. -fn check_alignment_and_resize() -> Body { - let mut asm = Asm::new(); - allocate_into(&mut asm, 0, 3, 1); - for (offset, value) in [(0, 0x12), (1, 0x34), (2, 0x56)] { - get(&mut asm, 0); - if offset != 0 { - constant(&mut asm, offset); - asm.leaf(I::I32Add); - } - constant(&mut asm, value); - asm.leaf(I::I32Store8(mm(0))); - } - - // Seed the accumulator. - constant(&mut asm, 1); - - // In-place resize within capacity keeps the pointer and the bytes. - get(&mut asm, 0); - constant(&mut asm, 3); - constant(&mut asm, 16); - constant(&mut asm, 5); - asm.leaf(I::Call(REALLOC_INDEX)); - set(&mut asm, 1); - get(&mut asm, 1); - get(&mut asm, 0); - asm.leaf(I::I32Eq); - asm.leaf(I::I32And); - get(&mut asm, 1); - constant(&mut asm, 15); - asm.leaf(I::I32And); - asm.leaf(I::I32Eqz); - asm.leaf(I::I32And); - - // Resize past capacity reallocates, copies the old bytes, and aligns. - get(&mut asm, 1); - constant(&mut asm, 5); - constant(&mut asm, 8); - constant(&mut asm, 32); - asm.leaf(I::Call(REALLOC_INDEX)); - set(&mut asm, 2); - get(&mut asm, 2); - constant(&mut asm, 7); - asm.leaf(I::I32And); - asm.leaf(I::I32Eqz); - asm.leaf(I::I32And); - for (offset, expected) in [(0, 0x12), (1, 0x34), (2, 0x56)] { - get(&mut asm, 2); - if offset != 0 { - constant(&mut asm, offset); - asm.leaf(I::I32Add); - } - asm.leaf(I::I32Load8U(mm(0))); - constant(&mut asm, expected); - asm.leaf(I::I32Eq); - asm.leaf(I::I32And); - } - // The length prefix records the resized length. - get(&mut asm, 2); - constant(&mut asm, 4); - asm.leaf(I::I32Sub); - asm.leaf(I::I32Load(mm(2))); - constant(&mut asm, 32); - asm.leaf(I::I32Eq); - asm.leaf(I::I32And); - asm.into_body() -} - -fn bad_alignment() -> Body { - let mut asm = Asm::new(); - realloc_call(&mut asm, 0, 0, 3, 1); - asm.leaf(I::Drop); - constant(&mut asm, 0); - asm.into_body() -} - -fn null_with_length() -> Body { - let mut asm = Asm::new(); - realloc_call(&mut asm, 0, 7, 4, 8); - asm.leaf(I::Drop); - constant(&mut asm, 0); - asm.into_body() -} - -fn mismatched_length() -> Body { - let mut asm = Asm::new(); - allocate_into(&mut asm, 0, 8, 4); - get(&mut asm, 0); - constant(&mut asm, 9); - constant(&mut asm, 4); - constant(&mut asm, 16); - asm.leaf(I::Call(REALLOC_INDEX)); - asm.leaf(I::Drop); - constant(&mut asm, 0); - asm.into_body() -} - -fn grow_failure() -> Body { - let mut asm = Asm::new(); - // Exceeds the memory maximum, so memory.grow returns -1. - realloc_call(&mut asm, 0, 0, 4, 200_000); - asm.leaf(I::Drop); - constant(&mut asm, 0); - asm.into_body() -} - -fn address_overflow() -> Body { - let mut asm = Asm::new(); - // Force the bump break to the top of the wasm32 address space. - constant(&mut asm, (abi::HEAP_STATE + 4) as i32); - constant(&mut asm, -8); - asm.leaf(I::I32Store(mm(2))); - realloc_call(&mut asm, 0, 0, 4, 16); - asm.leaf(I::Drop); - constant(&mut asm, 0); - asm.into_body() -} - -/// Allocates and frees in a loop far past one page's worth of blocks, then -/// reports whether linear memory grew. Reclamation keeps it at one page. -fn check_bounded_growth() -> Body { - let mut asm = Asm::new(); - constant(&mut asm, 10_000); - set(&mut asm, 1); - let done = asm.label(); - let again = asm.label(); - asm.block(done); - asm.loop_(again); - get(&mut asm, 1); - asm.leaf(I::I32Eqz); - asm.br_if(done); - allocate_into(&mut asm, 0, 16, 8); - free_call(&mut asm, 0, 16); - get(&mut asm, 1); - constant(&mut asm, 1); - asm.leaf(I::I32Sub); - set(&mut asm, 1); - asm.br(again); - asm.end(); - asm.end(); - asm.leaf(I::MemorySize(0)); - constant(&mut asm, 1); - asm.leaf(I::I32Eq); - asm.into_body() -} - -fn function(name: &str, body: Body, locals: usize) -> Function { - Function { - symbol: SymbolId::new(ModuleId(0), 0), - name: name.into(), - type_index: TypeIndex(0), - parameters: Vec::new(), - locals: vec![ValType::I32; locals], - body, - span: span(), - } -} - -fn fixture() -> Module { - let mut functions = vec![ - function("check_reuse_and_coalesce", check_reuse_and_coalesce(), 4), - function( - "check_alignment_and_resize", - check_alignment_and_resize(), - 3, - ), - function("bad_alignment", bad_alignment(), 0), - function("null_with_length", null_with_length(), 0), - function("mismatched_length", mismatched_length(), 1), - function("grow_failure", grow_failure(), 0), - function("address_overflow", address_overflow(), 0), - function("check_bounded_growth", check_bounded_growth(), 2), - ]; - let realloc_type = TypeIndex(1); - functions.push(build_realloc(realloc_type, span())); - - let mut state = 0_u32.to_le_bytes().to_vec(); - state.extend_from_slice(&abi::HEAP_START.to_le_bytes()); - - let mut exports: Vec = functions - .iter() - .enumerate() - .map(|(index, function)| Export { - name: function.name.clone(), - kind: ExportKind::Function, - index: ExportIndex::Function(FunctionIndex(index as u32)), - }) - .collect(); - exports.push(Export { - name: "memory".into(), - kind: ExportKind::Memory, - index: ExportIndex::Memory(MemoryIndex(0)), - }); - - Module { - name: "ReallocExecutionTest".into(), - imports: Vec::new(), - types: vec![ - FuncType { - parameters: Vec::new(), - results: vec![ValType::I32], - }, - FuncType { - parameters: vec![ValType::I32; 4], - results: vec![ValType::I32], - }, - ], - type_defs: Vec::new(), - functions, - memories: vec![Memory { - id: MemoryId(0), - index: MemoryIndex(0), - minimum: 1, - maximum: Some(2), - }], - data: vec![DataSegment { - id: DataId(0), - index: DataIndex(0), - mode: DataMode::Active { - offset: abi::HEAP_STATE, - }, - bytes: state, - }], - exports, - entry: None, - realloc: None, - globals: Vec::new(), - helpers: Vec::new(), - span: span(), - } -} - -fn require_wasmtime() -> bool { - match std::process::Command::new("wasmtime") - .arg("--version") - .output() - { - Ok(_) => true, - Err(_) if std::env::var("PSRS_REQUIRE_WASMTIME").as_deref() == Ok("1") => { - panic!("PSRS_REQUIRE_WASMTIME=1 but wasmtime is not installed"); - } - Err(_) => { - eprintln!("skipping allocator execution: wasmtime is not installed"); - false - } - } -} - -#[test] -fn realloc_reclaims_reuses_coalesces_aligns_and_traps() { - let module = fixture(); - super::super::super::verify::verify_module(&module).expect("thin Wasm module verifies"); - let binary = super::super::super::encode_module(&module).expect("Wasm encodes"); - crate::validator() - .validate_all(&binary) - .expect("allocator module validates"); - - if !require_wasmtime() { - return; - } - - use std::sync::atomic::{AtomicU32, Ordering}; - static COUNTER: AtomicU32 = AtomicU32::new(0); - let id = COUNTER.fetch_add(1, Ordering::Relaxed); - let path = std::env::temp_dir().join(format!("psrs-realloc-{}-{id}.wasm", std::process::id())); - std::fs::write(&path, binary).expect("write allocator test module"); - let run = |export: &str| { - std::process::Command::new("wasmtime") - .arg("run") - .arg("--invoke") - .arg(export) - .arg(&path) - .output() - .expect("run allocator test module") - }; - - for export in [ - "check_reuse_and_coalesce", - "check_alignment_and_resize", - "check_bounded_growth", - ] { - let output = run(export); - assert!(output.status.success(), "{export} trapped: {output:?}"); - assert_eq!( - String::from_utf8_lossy(&output.stdout).trim(), - "1", - "{export} reported a failed check" - ); - } - for export in [ - "bad_alignment", - "null_with_length", - "mismatched_length", - "grow_failure", - "address_overflow", - ] { - let output = run(export); - assert!(!output.status.success(), "{export} unexpectedly succeeded"); - } - let _ = std::fs::remove_file(path); -} diff --git a/crates/psrs-backend/src/wasm/lower/structure/cfg/mod.rs b/crates/psrs-backend/src/wasm/lower/structure/cfg/mod.rs index 107a103d..8845b137 100644 --- a/crates/psrs-backend/src/wasm/lower/structure/cfg/mod.rs +++ b/crates/psrs-backend/src/wasm/lower/structure/cfg/mod.rs @@ -395,21 +395,7 @@ fn direct_children<'a>( } fn successors(terminator: &Terminator) -> Vec { - match terminator { - Terminator::Return { .. } => Vec::new(), - Terminator::Jump { target, .. } => vec![*target], - Terminator::Branch { - then_block, - else_block, - .. - } => vec![*then_block, *else_block], - Terminator::Switch { cases, default, .. } => cases - .iter() - .map(|(_, target)| *target) - .chain(std::iter::once(*default)) - .collect(), - Terminator::ReturnCall { .. } | Terminator::ReturnCallRef { .. } => Vec::new(), - } + crate::mir::cfg::successors(terminator) } fn block_span(block: &BasicBlock, fallback: psrs_span::TextRange) -> psrs_span::TextRange { @@ -417,7 +403,8 @@ fn block_span(block: &BasicBlock, fallback: psrs_span::TextRange) -> psrs_span:: .terminator .as_ref() .map_or(fallback, |terminator| match terminator { - Terminator::Return { span, .. } + Terminator::Trap { span } + | Terminator::Return { span, .. } | Terminator::Jump { span, .. } | Terminator::Branch { span, .. } | Terminator::Switch { span, .. } diff --git a/crates/psrs-backend/src/wasm/lower/structure/dispatcher.rs b/crates/psrs-backend/src/wasm/lower/structure/dispatcher.rs index bdc7fe80..a7b5944f 100644 --- a/crates/psrs-backend/src/wasm/lower/structure/dispatcher.rs +++ b/crates/psrs-backend/src/wasm/lower/structure/dispatcher.rs @@ -110,6 +110,10 @@ impl Structurer<'_> { .ok_or_else(|| wasm_error(self.function.span, "MIR block has no terminator"))?; let span = terminator_span(terminator); match terminator { + Terminator::Trap { .. } => { + body.push(Op::Leaf(Instruction::Unreachable)); + return Ok(()); + } Terminator::Return { value, .. } => { self.emit_load(*value, span, body)?; body.push(Op::Leaf(Instruction::Return)); @@ -240,7 +244,8 @@ fn block_span(block: Option<&BasicBlock>, fallback: TextRange) -> TextRange { fn terminator_span(terminator: &Terminator) -> TextRange { match terminator { - Terminator::Return { span, .. } + Terminator::Trap { span } + | Terminator::Return { span, .. } | Terminator::Jump { span, .. } | Terminator::Branch { span, .. } | Terminator::Switch { span, .. } diff --git a/crates/psrs-backend/src/wasm/lower/structure/instructions.rs b/crates/psrs-backend/src/wasm/lower/structure/instructions.rs index fd7ea514..c5116faa 100644 --- a/crates/psrs-backend/src/wasm/lower/structure/instructions.rs +++ b/crates/psrs-backend/src/wasm/lower/structure/instructions.rs @@ -12,6 +12,12 @@ impl Structurer<'_> { destination, value, span, + } + | MirInstruction::ControlEvidence { + destination, + value, + span, + .. } => { self.load(body, *value, *span)?; self.store(body, *destination, *span)?; @@ -122,6 +128,7 @@ impl Structurer<'_> { function, arguments, span, + .. } => { for argument in arguments { self.load(body, *argument, *span)?; @@ -297,6 +304,18 @@ impl Structurer<'_> { } => { self.emit_array_new_default(body, *destination, *type_index, *length, *span)? } + MirInstruction::ArrayNewFilled { + destination, + type_index, + length, + value, + span, + } => { + self.load(body, *value, *span)?; + self.load(body, *length, *span)?; + body.push(Op::Leaf(Instruction::ArrayNew(type_index.0))); + self.store(body, *destination, *span)?; + } MirInstruction::ArrayGet { destination, type_index, diff --git a/crates/psrs-backend/src/wasm/lower/structure/irreducible_dispatch_tests.rs b/crates/psrs-backend/src/wasm/lower/structure/irreducible_dispatch_tests.rs index 2395b511..fc585ef6 100644 --- a/crates/psrs-backend/src/wasm/lower/structure/irreducible_dispatch_tests.rs +++ b/crates/psrs-backend/src/wasm/lower/structure/irreducible_dispatch_tests.rs @@ -417,3 +417,17 @@ fn dispatcher_uses_a_br_table_and_explicit_trap_for_invalid_state() { ) )); } + +#[test] +fn dispatcher_accepts_an_explicit_trap_exit_without_return_operands() { + let mut function = irreducible_function(); + function + .blocks + .iter_mut() + .find(|block| block.id == BlockId(9)) + .unwrap() + .terminator = Some(Terminator::Trap { span: span() }); + // The cyclic multiple-entry region still requires a dispatcher. Its trap + // exit has no normal successor or result operand. + lower_and_validate(&function); +} diff --git a/crates/psrs-backend/src/wasm/lower/structure/lists/aggregate.rs b/crates/psrs-backend/src/wasm/lower/structure/lists/aggregate.rs index 43d3ec57..b49758c4 100644 --- a/crates/psrs-backend/src/wasm/lower/structure/lists/aggregate.rs +++ b/crates/psrs-backend/src/wasm/lower/structure/lists/aggregate.rs @@ -39,7 +39,7 @@ impl Structurer<'_> { signed: false } ) { - return self.emit_byte_store(body, context, offset, path, span); + return self.emit_byte_store(body, context, offset, guest, path, span); } match (canonical, guest) { ( diff --git a/crates/psrs-backend/src/wasm/lower/structure/lists/element.rs b/crates/psrs-backend/src/wasm/lower/structure/lists/element.rs index 71b67c1a..d654ae6c 100644 --- a/crates/psrs-backend/src/wasm/lower/structure/lists/element.rs +++ b/crates/psrs-backend/src/wasm/lower/structure/lists/element.rs @@ -18,16 +18,30 @@ impl Structurer<'_> { /// Stores one `list` element: a source `Array Int` value is /// range-checked to `0..255` before it is narrowed to one canonical byte /// ([DEC-16](../../../decision/DEC-16-scalar-strings-and-utf8-storage.md)). + #[allow(clippy::too_many_arguments)] pub(super) fn emit_byte_store( &self, body: &mut Body, context: &ListLoop, offset: u32, + guest: &GuestLayout, path: &[Projection], span: TextRange, ) -> Result<(), Vec> { self.element_address(body, context, offset, span)?; self.emit_project(body, context, path, span)?; + if let GuestLayout::Scalar { shape } = guest + && is_erased(*shape) + { + self.emit_unbox( + body, + &CanonicalType::Int { + width: 8, + signed: false, + }, + span, + )?; + } let scratch = self.node_locals(context.depth).scratch_local; body.push(Op::Leaf(Instruction::LocalSet(scratch))); // Trap unless the value is a canonical byte. diff --git a/crates/psrs-backend/src/wasm/lower/structure/lists/mod.rs b/crates/psrs-backend/src/wasm/lower/structure/lists/mod.rs index 67f455db..a17614a7 100644 --- a/crates/psrs-backend/src/wasm/lower/structure/lists/mod.rs +++ b/crates/psrs-backend/src/wasm/lower/structure/lists/mod.rs @@ -273,6 +273,24 @@ impl Structurer<'_> { .ok_or_else(|| wasm_error(span, "canonical list element has no guest layout")) } + pub(super) fn resolve_field( + &self, + field: &crate::cc::Field, + span: TextRange, + ) -> Result> { + let layout = self + .layout + .ok_or_else(|| wasm_error(span, "canonical list copy has no layout table"))?; + crate::cc::payload::stored_guest_layout(field, layout.representation_table()).ok_or_else( + || { + wasm_error( + span, + "canonical list field has no checked storage projection", + ) + }, + ) + } + /// The concrete defined type of a representation handle. pub(super) fn repr_index( &self, diff --git a/crates/psrs-backend/src/wasm/lower/structure/lists/record.rs b/crates/psrs-backend/src/wasm/lower/structure/lists/record.rs index 35cfe291..f500ddfa 100644 --- a/crates/psrs-backend/src/wasm/lower/structure/lists/record.rs +++ b/crates/psrs-backend/src/wasm/lower/structure/lists/record.rs @@ -37,7 +37,7 @@ impl Structurer<'_> { for (field, (field_offset, _)) in canonical_fields.iter().zip(&layouts) { let label = abi::source_field_name(&field.name); let index = label_index(labels, &label, span)?; - let guest = self.resolve_guest(shapes[index as usize].stored, span)?; + let guest = self.resolve_field(&shapes[index as usize], span)?; let mut nested = path.to_vec(); nested.push(Projection::Field { ty: repr_index, @@ -83,7 +83,7 @@ impl Structurer<'_> { .position(|field| &abi::source_field_name(&field.name) == label) .ok_or_else(|| wasm_error(span, "canonical record element has no field"))?; let field_offset = layouts[canonical_index].0; - let guest = self.resolve_guest(shape.stored, span)?; + let guest = self.resolve_field(shape, span)?; self.emit_load_node( body, context, diff --git a/crates/psrs-backend/src/wasm/lower/structure/region.rs b/crates/psrs-backend/src/wasm/lower/structure/region.rs index 1d775d9c..cfeb70a6 100644 --- a/crates/psrs-backend/src/wasm/lower/structure/region.rs +++ b/crates/psrs-backend/src/wasm/lower/structure/region.rs @@ -132,6 +132,9 @@ impl Structurer<'_> { )); }; match terminator { + Terminator::Trap { .. } => { + body.push(Op::Leaf(Instruction::Unreachable)); + } Terminator::Return { value, span } => { self.emit_load(*value, *span, body)?; body.push(Op::Leaf(Instruction::Return)); @@ -185,6 +188,7 @@ impl Structurer<'_> { function, arguments, span, + .. } => { self.emit_return_call(*function, arguments, *span, body)?; } diff --git a/crates/psrs-backend/src/wasm/lower/structure/unary.rs b/crates/psrs-backend/src/wasm/lower/structure/unary.rs index b7eb76d2..347d69a6 100644 --- a/crates/psrs-backend/src/wasm/lower/structure/unary.rs +++ b/crates/psrs-backend/src/wasm/lower/structure/unary.rs @@ -23,10 +23,30 @@ impl Structurer<'_> { body.push(Op::Leaf(Instruction::I32Const(-1))); body.push(Op::Leaf(Instruction::I32Xor)); } + UnaryOp::F64Abs => { + body.push(Op::Leaf(Instruction::LocalGet(value_local))); + body.push(Op::Leaf(Instruction::F64Abs)); + } + UnaryOp::F64Sqrt => { + body.push(Op::Leaf(Instruction::LocalGet(value_local))); + body.push(Op::Leaf(Instruction::F64Sqrt)); + } UnaryOp::F64Neg => { body.push(Op::Leaf(Instruction::LocalGet(value_local))); body.push(Op::Leaf(Instruction::F64Neg)); } + UnaryOp::F64Trunc => { + body.push(Op::Leaf(Instruction::LocalGet(value_local))); + body.push(Op::Leaf(Instruction::F64Trunc)); + } + UnaryOp::F64Floor => { + body.push(Op::Leaf(Instruction::LocalGet(value_local))); + body.push(Op::Leaf(Instruction::F64Floor)); + } + UnaryOp::F64Ceil => { + body.push(Op::Leaf(Instruction::LocalGet(value_local))); + body.push(Op::Leaf(Instruction::F64Ceil)); + } UnaryOp::BoolNot => { body.push(Op::Leaf(Instruction::LocalGet(value_local))); body.push(Op::Leaf(Instruction::I32Eqz)); diff --git a/crates/psrs-backend/src/wasm/mod.rs b/crates/psrs-backend/src/wasm/mod.rs index a5d3f0fd..bab9de26 100644 --- a/crates/psrs-backend/src/wasm/mod.rs +++ b/crates/psrs-backend/src/wasm/mod.rs @@ -7,13 +7,16 @@ use crate::types::{DataId, MemoryId}; mod convert; mod encode; mod lower; +mod type_contract; mod verify; #[cfg(test)] mod tests; pub use encode::encode_module; +pub(crate) use lower::lower_module_with_plan; pub use lower::{lower_module, lower_module_with_capabilities}; +pub(crate) use type_contract::raw_signature; /// Final index domains assigned by P10. These are deliberately distinct from /// MIR's module-local IDs and from one another; conversion to raw `u32` is @@ -48,6 +51,8 @@ pub struct Global { pub enum GlobalInit { /// `ref.null` of the given heap type. RefNull(HeapType), + /// `i32.const`. + I32(u32), } /// A WebAssembly function signature in the thin Wasm IR. @@ -79,6 +84,7 @@ pub struct Memory { pub enum ExportKind { Function, Memory, + Global, } /// An exported item. @@ -95,6 +101,7 @@ pub struct Export { pub enum ExportIndex { Function(FunctionIndex), Memory(MemoryIndex), + Global(GlobalIndex), } /// A structured function body. diff --git a/crates/psrs-backend/src/wasm/type_contract.rs b/crates/psrs-backend/src/wasm/type_contract.rs new file mode 100644 index 00000000..b956b4c7 --- /dev/null +++ b/crates/psrs-backend/src/wasm/type_contract.rs @@ -0,0 +1,25 @@ +//! Explicit conversion of MIR type arenas to linker-owned raw contracts. +use crate::{mir, types::RecGroup}; +use psrs_linker::{CoreSignature, CoreTypes}; + +pub(crate) fn raw_signature( + import: &mir::Import, + groups: &[RecGroup], +) -> Result { + let mut types = wasm_encoder::TypeSection::new(); + for group in groups { + types.ty().rec(group.0.iter().map(super::convert::sub_type)); + } + let signature = groups.iter().map(|group| group.0.len()).sum::() as u32; + types.ty().function( + import + .parameters + .iter() + .copied() + .map(super::convert::val_type), + import.result.into_iter().map(super::convert::val_type), + ); + let mut schema = wasm_encoder::Module::new(); + schema.section(&types); + CoreTypes::from_module(&schema.finish())?.signature(signature) +} diff --git a/crates/psrs-backend/src/wasm/verify.rs b/crates/psrs-backend/src/wasm/verify.rs index 25fd96c7..de0128af 100644 --- a/crates/psrs-backend/src/wasm/verify.rs +++ b/crates/psrs-backend/src/wasm/verify.rs @@ -46,13 +46,17 @@ pub fn verify_module(module: &Module) -> Result<(), Vec> { "Wasm global indices are duplicated or not deterministic", )); } - if let GlobalInit::RefNull(heap) = global.init - && global.ty - != ValType::Ref(RefType { - nullable: true, - heap_type: heap, - }) - { + let init_matches = match global.init { + GlobalInit::RefNull(heap) => { + global.ty + == ValType::Ref(RefType { + nullable: true, + heap_type: heap, + }) + } + GlobalInit::I32(_) => global.ty == ValType::I32, + }; + if !init_matches { errors.push(wasm_error( module.span, "Wasm global initializer type does not match its value type", @@ -78,6 +82,9 @@ pub fn verify_module(module: &Module) -> Result<(), Vec> { (ExportKind::Memory, ExportIndex::Memory(index)) => { (index.0 as usize) < module.memories.len() } + (ExportKind::Global, ExportIndex::Global(index)) => { + (index.0 as usize) < module.globals.len() + } _ => false, }; if !in_range { diff --git a/crates/psrs-cli/Cargo.toml b/crates/psrs-cli/Cargo.toml index 016b3e03..b1657efc 100644 --- a/crates/psrs-cli/Cargo.toml +++ b/crates/psrs-cli/Cargo.toml @@ -11,7 +11,13 @@ path = "src/main.rs" [dependencies] psrs-ast.workspace = true psrs-driver.workspace = true +psrs-linker.workspace = true psrs-hir.workspace = true psrs-resolve.workspace = true psrs-span.workspace = true psrs-syntax.workspace = true +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[dev-dependencies] +wat = "=1.245.1" diff --git a/crates/psrs-cli/src/build.rs b/crates/psrs-cli/src/build.rs new file mode 100644 index 00000000..e8d38252 --- /dev/null +++ b/crates/psrs-cli/src/build.rs @@ -0,0 +1,145 @@ +//! Source compilation with optional explicit guest composition and joined lineage. +use super::*; + +pub(super) fn run(command: &str, raw_args: Vec) -> Result<(), String> { + let mut paths = Vec::new(); + let mut output_path = None; + let mut manifest_path = None; + let mut report_path = None; + let mut index = 0; + while index < raw_args.len() { + match raw_args[index].as_str() { + "--manifest" | "--report" if command == "build" => { + let slot = if raw_args[index] == "--manifest" { + &mut manifest_path + } else { + &mut report_path + }; + if slot.is_some() { + return Err(usage()); + } + *slot = Some(raw_args.get(index + 1).ok_or_else(usage)?.clone()); + index += 2; + } + "-o" => { + if output_path.is_some() { + return Err(usage()); + } + let Some(output) = raw_args.get(index + 1) else { + return Err(usage()); + }; + output_path = Some(output.clone()); + index += 2; + } + path if path.starts_with('-') => return Err(usage()), + path => { + paths.push(path.to_owned()); + index += 1; + } + } + } + if paths.is_empty() { + return Err(usage()); + } + + let sources = psrs_driver::load_program_files(&paths)?; + let inputs = sources + .iter() + .map(|(path, text)| (path.as_str(), text.as_str())) + .collect::>(); + let mut source_lineage = None; + let compilation = if report_path.is_some() { + let report = psrs_driver::compile_program_sources_with_prelude_diagnosis(&inputs, false); + source_lineage = Some(diagnose::build_lineage(&sources, &report)?); + report.artifact.ok_or(report.diagnostics) + } else { + psrs_driver::compile_program_sources_with_prelude(&inputs) + }; + let artifact = match compilation { + Ok(artifact) => artifact, + Err(errors) => { + for error in errors { + // A diagnostic in a standard-library module names a file the + // caller did not pass, so there is no snippet to print, and + // naming the first source instead would blame a file the caller + // wrote for the library's error. + let Some((path, text)) = error + .source + .source_index() + .and_then(|source| sources.get(source)) + else { + eprintln!( + "{}: {} [{}]: {}", + match error.source { + psrs_driver::DiagnosticOrigin::Library => "standard library", + _ => "program", + }, + error.diagnostic.message, + error.diagnostic.stage, + error.diagnostic.code.unwrap_or("no error code") + ); + continue; + }; + let source = SourceFile::new(path.as_str(), text.as_str()); + print_coded_diagnostic( + &source, + error.diagnostic.span, + error.diagnostic.stage, + error.diagnostic.code, + &error.diagnostic.message, + ); + } + return Err(String::new()); + } + }; + print_warnings(&artifact.warnings, &sources); + if command == "wat" { + if let Some(output) = output_path { + fs::write(&output, artifact.wat).map_err(|error| format!("{output}: {error}"))?; + } else { + print!("{}", artifact.wat); + } + } else { + let output = output_path.unwrap_or_else(|| { + std::path::Path::new(&paths[0]) + .with_extension("wasm") + .to_string_lossy() + .into_owned() + }); + let application_sha256 = psrs_linker::sha256_hex(&artifact.wasm); + let (bytes, composition) = if let Some(manifest) = manifest_path { + match link::compose(&artifact.wasm, &manifest, false) { + Ok((bytes, evidence)) => (bytes, Some(evidence)), + Err(error) => { + if let Some(path) = &report_path { + link::write_report( + path, + &serde_json::json!({ + "schema_version": 1, "status": "rejected", + "source_compilation": source_lineage, + "application_sha256": application_sha256, + "composition": { "status": "rejected", "diagnostic": error }, + }), + )?; + } + return Err(error); + } + } + } else { + (artifact.wasm, None) + }; + if let Some(path) = report_path { + link::write_report( + &path, + &serde_json::json!({ + "schema_version": 1, "status": "completed", "source_compilation": source_lineage, + "application_sha256": application_sha256, "composition": composition, + "output_sha256": psrs_linker::sha256_hex(&bytes), + }), + )?; + } + fs::write(&output, bytes).map_err(|error| format!("{output}: {error}"))?; + println!("wrote {output}"); + } + Ok(()) +} diff --git a/crates/psrs-cli/src/diagnose/artifacts.rs b/crates/psrs-cli/src/diagnose/artifacts.rs new file mode 100644 index 00000000..8b906c02 --- /dev/null +++ b/crates/psrs-cli/src/diagnose/artifacts.rs @@ -0,0 +1,346 @@ +use super::{ + BundleContext, CompilerRevision, Snapshot, SourceInput, WorkerResponse, empty_case_trace, +}; +use serde::de::DeserializeOwned; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::{env, fs}; + +pub(super) fn write_bundle( + bundle: &Path, + sources: &[SourceInput], + response: &WorkerResponse, + case: &str, + input_set_complete: bool, + context: &BundleContext, + capture_trace: bool, +) -> Result<(), String> { + let _ = fs::remove_dir_all(bundle); + fs::create_dir_all(bundle.join("inputs")) + .map_err(|error| format!("{}: {error}", bundle.display()))?; + let mut paths = Vec::new(); + for (index, source) in sources.iter().enumerate() { + let file = format!("inputs/{index:04}-{}.purs", safe_name(&source.name)); + fs::write(bundle.join(&file), &source.text) + .map_err(|error| format!("{}: {error}", bundle.join(&file).display()))?; + paths.push(file); + } + write_dumps(bundle, response)?; + let command = replay_script(&paths, &context.executable, capture_trace); + fs::write( + bundle.join("replay.sh"), + format!("#!/bin/sh\nset -eu\ncd \"$(dirname \"$0\")\"\n{command}\n"), + ) + .map_err(|error| error.to_string())?; + let metadata = serde_json::json!({ + "case": case, + "input_names": sources.iter().map(|source| &source.name).collect::>(), + "replay_argv": std::iter::once(context.executable.clone()) + .chain(std::iter::once("build".to_owned())) + .chain(paths.iter().cloned()) + .chain(["-o".into(), "output.wasm".into()]) + .collect::>(), + "trace_replay_argv": if capture_trace { + trace_replay_argv(&paths) + } else { + None + }, + "trace_mode": if capture_trace { "dumps" } else { "manifest" }, + "trace_capture_status": if response.trace.is_some() { "recorded" } else { "unavailable" }, + "trace": &response.trace, + "diagnostics": &response.diagnostics, + "input_fingerprint": &response.input_fingerprint, + "compiler": &context.compiler, + "trusted_stdlib_fingerprint": &context.trusted_stdlib_fingerprint, + "input_set_complete": input_set_complete, + }); + fs::write( + bundle.join("case.json"), + serde_json::to_vec_pretty(&metadata).map_err(|error| error.to_string())?, + ) + .map_err(|error| format!("{}: {error}", bundle.display())) +} + +pub(super) fn write_empty_bundle( + bundle: &Path, + sources: &[SourceInput], + failure: EmptyBundleFailure<'_>, + context: &BundleContext, + capture_trace: bool, +) -> Result<(), String> { + let empty = WorkerResponse { + passed: false, + input_fingerprint: fingerprint_sources(sources), + sources: sources.to_vec(), + input_set_complete: failure.input_set_complete, + elapsed_ms: 0, + diagnostics: Vec::new(), + core_stage: None, + core: None, + cc_stage: None, + cc: None, + mir_stage: None, + mir: None, + trace: Some(empty_case_trace( + sources, + failure.input_set_complete, + &fingerprint_sources(sources), + capture_trace, + )), + }; + write_bundle( + bundle, + sources, + &empty, + failure.case, + failure.input_set_complete, + context, + capture_trace, + )?; + fs::write(bundle.join("worker-failure.txt"), failure.reason) + .map_err(|error| error.to_string())?; + if !failure.stderr.is_empty() { + fs::write(bundle.join("worker-stderr.log"), failure.stderr) + .map_err(|error| error.to_string())?; + } + Ok(()) +} + +pub(super) struct EmptyBundleFailure<'a> { + pub(super) case: &'a str, + pub(super) reason: &'a str, + pub(super) stderr: &'a str, + pub(super) input_set_complete: bool, +} + +fn write_dumps(bundle: &Path, response: &WorkerResponse) -> Result<(), String> { + for (name, stage, text) in [ + ("core", &response.core_stage, &response.core), + ("cc", &response.cc_stage, &response.cc), + ("mir", &response.mir_stage, &response.mir), + ] { + if let Some(text) = text { + fs::write(bundle.join(format!("{name}.debug")), text) + .map_err(|error| error.to_string())?; + fs::write( + bundle.join(format!("{name}.stage")), + stage.as_deref().unwrap_or("unknown"), + ) + .map_err(|error| error.to_string())?; + } + } + Ok(()) +} + +fn replay_script(paths: &[String], executable: &str, capture_trace: bool) -> String { + let args = paths + .iter() + .map(|path| shell_quote(path)) + .collect::>() + .join(" "); + let build = format!("\"$PSRS_BIN\" build {args} -o output.wasm"); + let trace = if capture_trace { + trace_replay_argv(paths).map_or_else( + || "echo 'trace recapture unavailable: bundle contains no source inputs' >&2\n".into(), + |argv| { + let diagnose = argv + .iter() + .map(|arg| shell_quote(arg)) + .collect::>() + .join(" "); + format!( + "if ! \"$PSRS_BIN\" {diagnose}; then\n echo 'trace recapture failed; continuing with build replay' >&2\nfi\n" + ) + }, + ) + } else { + String::new() + }; + format!( + "if [ -z \"${{PSRS_BIN:-}}\" ]; then PSRS_BIN={}; fi\n{trace}{build}", + shell_quote(executable) + ) +} + +fn trace_replay_argv(paths: &[String]) -> Option> { + let first = paths.first()?; + let mut args = vec!["diagnose".to_owned(), first.clone()]; + for path in paths.iter().skip(1) { + args.push("--input".into()); + args.push(path.clone()); + } + args.extend(["--trace".into(), "--out".into(), "replay-trace.json".into()]); + Some(args) +} + +pub(super) fn trusted_stdlib_fingerprint() -> Result { + Ok(psrs_driver::standard_library_info()?.source_fingerprint) +} + +pub(super) fn compiler_revision() -> CompilerRevision { + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."); + let head = Command::new("git") + .arg("-C") + .arg(&root) + .args(["rev-parse", "HEAD"]) + .output() + .ok() + .filter(|output| output.status.success()) + .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_owned()); + let diff = Command::new("git") + .arg("-C") + .arg(&root) + .args(["diff", "HEAD", "--binary"]) + .output() + .ok() + .map(|output| output.stdout) + .unwrap_or_default(); + let status = Command::new("git") + .arg("-C") + .arg(&root) + .args(["status", "--porcelain"]) + .output() + .ok() + .map(|output| output.stdout) + .unwrap_or_default(); + let dirty = !status.is_empty(); + let mut bytes = diff; + bytes.extend_from_slice(&status); + if let Ok(output) = Command::new("git") + .arg("-C") + .arg(&root) + .args(["ls-files", "--others", "--exclude-standard", "-z"]) + .output() + { + for path in output + .stdout + .split(|byte| *byte == 0) + .filter(|path| !path.is_empty()) + { + bytes.extend_from_slice(path); + if let Ok(content) = fs::read(root.join(String::from_utf8_lossy(path).as_ref())) { + bytes.extend_from_slice(&content); + } + } + } + let binary_fingerprint = env::current_exe() + .ok() + .and_then(|path| fs::read(path).ok()) + .map(|binary| hash_bytes(&binary)) + .unwrap_or_else(|| "unavailable".into()); + CompilerRevision { + head, + dirty, + working_tree_fingerprint: hash_bytes(&bytes), + binary_fingerprint, + } +} + +pub(super) fn corpus_root() -> Result { + if let Ok(path) = env::var("PURESCRIPT_REPO") { + let root = PathBuf::from(path).join("tests/purs"); + return if root.is_dir() { + Ok(root) + } else { + Err(format!( + "{}: corpus directory does not exist", + root.display() + )) + }; + } + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/upstream"); + if root.is_dir() { + Ok(root) + } else { + Err(format!("{}: vendored corpus not found", root.display())) + } +} + +pub(super) fn is_ffi_excluded(path: &Path, text: &str) -> bool { + text.contains("foreign import") || path.with_extension("js").is_file() +} + +pub(super) fn fingerprint_sources(sources: &[SourceInput]) -> String { + let mut bytes = Vec::new(); + for source in sources { + bytes.extend_from_slice(source.name.as_bytes()); + bytes.push(0); + bytes.extend_from_slice(source.text.as_bytes()); + bytes.push(0xff); + } + hash_bytes(&bytes) +} + +fn hash_bytes(bytes: &[u8]) -> String { + let mut hash = 0xcbf29ce484222325u64; + for byte in bytes { + hash ^= u64::from(*byte); + hash = hash.wrapping_mul(0x100000001b3); + } + format!("fnv1a64:{hash:016x}") +} + +pub(super) fn safe_name(value: &str) -> String { + let name = Path::new(value) + .file_stem() + .unwrap_or_default() + .to_string_lossy(); + let cleaned = name + .chars() + .map(|ch| { + if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' { + ch + } else { + '_' + } + }) + .collect::(); + if cleaned.is_empty() { + "case".into() + } else { + cleaned + } +} + +fn shell_quote(value: &str) -> String { + format!("'{}'", value.replace('\'', "'\\''")) +} + +pub(super) fn absolute_path(path: &Path) -> Result { + if path.is_absolute() { + Ok(path.to_path_buf()) + } else { + env::current_dir() + .map(|cwd| cwd.join(path)) + .map_err(|error| error.to_string()) + } +} + +pub(super) fn atomic_write(path: &Path, bytes: &[u8]) -> Result<(), String> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).map_err(|error| format!("{}: {error}", parent.display()))?; + } + let temporary = path.with_extension(format!("tmp-{}", std::process::id())); + fs::write(&temporary, bytes).map_err(|error| format!("{}: {error}", temporary.display()))?; + fs::rename(&temporary, path).map_err(|error| format!("{}: {error}", path.display())) +} + +pub(super) fn write_snapshot(path: &Path, snapshot: &Snapshot) -> Result<(), String> { + let bytes = serde_json::to_vec_pretty(snapshot).map_err(|error| error.to_string())?; + atomic_write(path, &bytes) +} + +pub(super) fn read_json(path: &str) -> Result { + let bytes = fs::read(path).map_err(|error| format!("{path}: {error}"))?; + serde_json::from_slice(&bytes).map_err(|error| format!("{path}: invalid JSON: {error}")) +} + +pub(super) fn first_line(text: &str) -> &str { + text.lines() + .next() + .unwrap_or("worker crashed without a message") +} + +pub(super) fn usage() -> String { + "usage: psrs diagnose [--input FILE]... [--trace] [--out report.json] [--timeout SECONDS]\n psrs diagnose --corpus passing [--filter TEXT] [--limit N] [--trace] [--out report.json] [--timeout SECONDS]\n psrs diagnose --compare OLD.json NEW.json".into() +} diff --git a/crates/psrs-cli/src/diagnose/mod.rs b/crates/psrs-cli/src/diagnose/mod.rs new file mode 100644 index 00000000..158aac71 --- /dev/null +++ b/crates/psrs-cli/src/diagnose/mod.rs @@ -0,0 +1,444 @@ +use std::collections::BTreeMap; +use std::path::PathBuf; +use std::{env, fs}; + +mod artifacts; +use artifacts::*; +mod report; +use report::*; +mod schema; +use schema::*; +mod trace; +use trace::*; +mod worker; +pub(super) use worker::worker; +use worker::{WorkerOutcome, WorkerRequest, WorkerResponse, run_worker}; + +const SCHEMA_VERSION: u32 = 2; +const DEFAULT_TIMEOUT: u64 = 20; + +pub(super) fn run(args: Vec) -> Result<(), String> { + if args.first().is_some_and(|arg| arg == "--compare") { + if args.len() != 3 { + return Err(usage()); + } + return compare(&args[1], &args[2]); + } + let options = Options::parse(args)?; + let output = options.output.clone(); + let snapshot = diagnose(options)?; + print_summary(&snapshot); + write_snapshot(&output, &snapshot)?; + println!("snapshot: {}", output.display()); + Ok(()) +} + +impl Options { + fn parse(args: Vec) -> Result { + let mut file = None; + let mut additional_inputs = Vec::new(); + let mut corpus = None; + let mut filter = None; + let mut limit = None; + let mut timeout_seconds = DEFAULT_TIMEOUT; + let mut output = PathBuf::from("diagnose.json"); + let mut trace = false; + let mut index = 0; + while index < args.len() { + match args[index].as_str() { + "--corpus" => { + index += 1; + let Some(value) = args.get(index) else { + return Err(usage()); + }; + corpus = Some(value.clone()); + } + "--filter" => { + index += 1; + let Some(value) = args.get(index) else { + return Err(usage()); + }; + filter = Some(value.clone()); + } + "--limit" => { + index += 1; + let Some(value) = args.get(index) else { + return Err(usage()); + }; + limit = Some(value.parse().map_err(|_| usage())?); + } + "--timeout" => { + index += 1; + let Some(value) = args.get(index) else { + return Err(usage()); + }; + timeout_seconds = value.parse().map_err(|_| usage())?; + if timeout_seconds == 0 { + return Err(usage()); + } + } + "--out" => { + index += 1; + let Some(value) = args.get(index) else { + return Err(usage()); + }; + output = PathBuf::from(value); + } + "--input" => { + index += 1; + let Some(value) = args.get(index) else { + return Err(usage()); + }; + additional_inputs.push(PathBuf::from(value)); + } + "--trace" => trace = true, + value if value.starts_with('-') => return Err(usage()), + value => { + if file.replace(PathBuf::from(value)).is_some() { + return Err(usage()); + } + } + } + index += 1; + } + if file.is_some() == corpus.is_some() + || corpus.as_deref().is_some_and(|name| name != "passing") + || (corpus.is_some() && !additional_inputs.is_empty()) + { + return Err(usage()); + } + if let Some(main) = file.as_ref() { + let mut seen = BTreeMap::new(); + for input in std::iter::once(main).chain(additional_inputs.iter()) { + let identity = fs::canonicalize(input).unwrap_or_else(|_| input.clone()); + if seen.insert(identity, ()).is_some() { + return Err(format!("{}: duplicate diagnosis input", input.display())); + } + } + } + Ok(Self { + file, + additional_inputs, + corpus, + filter, + limit, + timeout_seconds, + output, + trace, + }) + } +} + +fn diagnose(options: Options) -> Result { + let corpus_root = options.corpus.as_ref().map(|_| corpus_root()).transpose()?; + let mode = if options.file.is_some() { + "file" + } else { + "corpus" + }; + let mut selected = if let Some(file) = &options.file { + vec![(file.clone(), None)] + } else { + let root = corpus_root.as_ref().expect("corpus mode has a root"); + let passing = root.join("passing"); + let mut paths = Vec::new(); + psrs_driver::collect_purs_files(&passing, &mut paths); + paths.sort(); + paths + .into_iter() + .filter(|path| { + options.filter.as_ref().is_none_or(|filter| { + path.strip_prefix(root) + .unwrap_or(path) + .to_string_lossy() + .contains(filter) + }) + }) + .take(options.limit.unwrap_or(usize::MAX)) + .map(|path| (path, Some(passing.clone()))) + .collect() + }; + if let Some(file) = &options.file { + for input in std::iter::once(file).chain(options.additional_inputs.iter()) { + if !input.is_file() { + return Err(format!("{}: file does not exist", input.display())); + } + } + } + + let output = absolute_path(&options.output)?; + let bundles = output.with_extension("bundles"); + let work_root = env::temp_dir().join(format!("psrs-diagnose-{}", std::process::id())); + let _ = fs::remove_dir_all(&work_root); + fs::create_dir_all(&work_root).map_err(|error| format!("{}: {error}", work_root.display()))?; + let bundle_context = BundleContext { + compiler: compiler_revision(), + trusted_stdlib_fingerprint: trusted_stdlib_fingerprint()?, + executable: env::var("PSRS_BIN") + .ok() + .or_else(|| { + env::current_exe() + .ok() + .map(|path| path.to_string_lossy().into_owned()) + }) + .unwrap_or_else(|| "psrs".into()), + }; + let mut records = Vec::with_capacity(selected.len()); + let total = selected.len(); + let mut worker_index = 0usize; + for (path, category) in selected.drain(..) { + let text = + fs::read_to_string(&path).map_err(|error| format!("{}: {error}", path.display()))?; + let display_path = corpus_root + .as_ref() + .and_then(|root| path.strip_prefix(root).ok()) + .unwrap_or(&path) + .to_string_lossy() + .into_owned(); + if options.corpus.is_some() && is_ffi_excluded(&path, &text) { + records.push(CaseRecord { + path: display_path, + input_fingerprint: fingerprint_sources(&[SourceInput { + name: path.to_string_lossy().into_owned(), + text, + }]), + input_set_complete: false, + elapsed_ms: 0, + status: CaseStatus::Excluded, + excluded_reason: Some("foreign import or adjacent JavaScript FFI file".into()), + diagnostics: Vec::new(), + first_blocker: None, + bundle: None, + trace: None, + }); + eprintln!( + "[{}/{}] {}: excluded (FFI)", + worker_index + 1, + total, + path.display() + ); + worker_index += 1; + continue; + } + let bundle = bundles.join(format!("{:04}-{}", worker_index, safe_name(&display_path))); + let request = WorkerRequest { + path: path.to_string_lossy().into_owned(), + category_dir: category + .as_ref() + .map(|path| path.to_string_lossy().into_owned()), + explicit_inputs: if category.is_none() && !options.additional_inputs.is_empty() { + std::iter::once(&path) + .chain(options.additional_inputs.iter()) + .map(|input| input.to_string_lossy().into_owned()) + .collect() + } else { + Vec::new() + }, + capture_dumps: options.trace, + trusted_stdlib_fingerprint: bundle_context.trusted_stdlib_fingerprint.clone(), + }; + let result = run_worker(&work_root, worker_index, options.timeout_seconds, request)?; + let entry_source = vec![SourceInput { + name: path.to_string_lossy().into_owned(), + text, + }]; + let ( + status, + diagnostics, + input_fingerprint, + input_set_complete, + elapsed_ms, + blocker, + bundle_path, + trace_record, + ) = match result { + WorkerOutcome::Completed(response) => { + let response = *response; + let status = if response.passed { + CaseStatus::Passed + } else { + CaseStatus::Failed + }; + let blocker = response.diagnostics.first().map(first_blocker); + let should_bundle = !response.passed || options.trace; + if should_bundle { + write_bundle( + &bundle, + &response.sources, + &response, + &display_path, + response.input_set_complete, + &bundle_context, + options.trace, + )?; + } + ( + status, + response.diagnostics, + response.input_fingerprint, + response.input_set_complete, + response.elapsed_ms, + blocker, + should_bundle.then(|| bundle.to_string_lossy().into_owned()), + response.trace, + ) + } + WorkerOutcome::TimedOut { elapsed_ms } => { + write_empty_bundle( + &bundle, + &entry_source, + EmptyBundleFailure { + case: &display_path, + reason: "compiler worker timed out", + stderr: "", + input_set_complete: false, + }, + &bundle_context, + options.trace, + )?; + ( + CaseStatus::TimedOut, + Vec::new(), + fingerprint_sources(&entry_source), + false, + elapsed_ms, + Some(FirstBlocker { + stage: "timeout".into(), + category: "worker_timeout".into(), + message: format!("exceeded {}s", options.timeout_seconds), + }), + Some(bundle.to_string_lossy().into_owned()), + Some(empty_case_trace( + &entry_source, + false, + &fingerprint_sources(&entry_source), + options.trace, + )), + ) + } + WorkerOutcome::Crashed { + message, + stderr, + elapsed_ms, + } => { + write_empty_bundle( + &bundle, + &entry_source, + EmptyBundleFailure { + case: &display_path, + reason: &message, + stderr: &stderr, + input_set_complete: false, + }, + &bundle_context, + options.trace, + )?; + ( + CaseStatus::Crashed, + Vec::new(), + fingerprint_sources(&entry_source), + false, + elapsed_ms, + Some(FirstBlocker { + stage: "worker".into(), + category: "worker_crash".into(), + message, + }), + Some(bundle.to_string_lossy().into_owned()), + Some(empty_case_trace( + &entry_source, + false, + &fingerprint_sources(&entry_source), + options.trace, + )), + ) + } + }; + eprintln!( + "[{}/{}] {}: {:?} in {} ms", + worker_index + 1, + total, + display_path, + status, + elapsed_ms + ); + records.push(CaseRecord { + path: display_path, + input_fingerprint, + input_set_complete, + elapsed_ms, + status, + excluded_reason: None, + diagnostics, + first_blocker: blocker, + bundle: bundle_path, + trace: trace_record, + }); + worker_index += 1; + } + let _ = fs::remove_dir_all(&work_root); + let cohort = Cohort { + mode: mode.into(), + corpus: corpus_root.map(|root| root.to_string_lossy().into_owned()), + filter: options.filter.clone(), + limit: options.limit, + timeout_seconds: options.timeout_seconds, + trusted_stdlib_fingerprint: bundle_context.trusted_stdlib_fingerprint.clone(), + }; + let mut snapshot = Snapshot { + schema_version: SCHEMA_VERSION, + cohort, + compiler: bundle_context.compiler, + environment: observed_environment(), + trace_mode: if options.trace { + TraceMode::Dumps + } else { + TraceMode::Manifest + }, + cases: records, + groups: Vec::new(), + counts: BTreeMap::new(), + }; + group_cases(&mut snapshot); + if snapshot.cases.is_empty() { + return Err("diagnosis selected no cases".into()); + } + Ok(snapshot) +} + +/// Reuse the diagnosis schema for the source side of a build/link report. +pub(super) fn build_lineage( + sources: &[(String, String)], + report: &psrs_driver::CompilationReport, +) -> Result { + let inputs = sources + .iter() + .map(|(name, text)| SourceInput { + name: name.clone(), + text: text.clone(), + }) + .collect::>(); + let trace = from_report( + &inputs, + true, + &fingerprint_sources(&inputs), + &trusted_stdlib_fingerprint()?, + false, + report, + )?; + let mut value = serde_json::to_value(&trace).map_err(|error| error.to_string())?; + if let Some(artifact) = &report.artifact { + let outputs = trace + .artifacts + .iter() + .filter(|artifact| artifact.representation == "component_binary") + .collect::>(); + let [output] = outputs.as_slice() else { + return Err("source build must produce exactly one component trace artifact".into()); + }; + value["component_output"] = serde_json::json!({ + "artifact": output.id, "sha256": psrs_linker::sha256_hex(&artifact.wasm), + }); + } + Ok(value) +} diff --git a/crates/psrs-cli/src/diagnose/report.rs b/crates/psrs-cli/src/diagnose/report.rs new file mode 100644 index 00000000..94ccec5a --- /dev/null +++ b/crates/psrs-cli/src/diagnose/report.rs @@ -0,0 +1,353 @@ +use super::*; +use std::collections::{BTreeMap, BTreeSet}; + +pub(super) fn first_blocker(diagnostic: &DiagnosticRecord) -> FirstBlocker { + let category = diagnostic.code.as_deref().unwrap_or_else(|| { + if diagnostic.stage == "P8 CC verification" + && (diagnostic.message.starts_with("call shape mismatch:") + || diagnostic + .message + .starts_with("call result shape mismatch:")) + { + "call_shape_mismatch" + } else { + diagnostic.kind.as_deref().unwrap_or("uncoded") + } + }); + FirstBlocker { + stage: diagnostic.stage.clone(), + category: category.to_owned(), + message: diagnostic.message.clone(), + } +} + +pub(super) fn group_cases(snapshot: &mut Snapshot) { + let mut groups: BTreeMap<(String, String), (BTreeSet, BTreeSet)> = + BTreeMap::new(); + let mut counts = BTreeMap::new(); + for case in &snapshot.cases { + let key = match case.status { + CaseStatus::Passed => "passed", + CaseStatus::Failed => "failed", + CaseStatus::Excluded => "excluded", + CaseStatus::TimedOut => "timed_out", + CaseStatus::Crashed => "crashed", + }; + *counts.entry(key.to_owned()).or_default() += 1; + if let Some(first) = &case.first_blocker { + let (cases, messages) = groups + .entry((first.stage.clone(), first.category.clone())) + .or_default(); + cases.insert(case.path.clone()); + messages.insert(first.message.clone()); + } + } + snapshot.groups = groups + .into_iter() + .map(|((stage, category), (cases, messages))| GroupRecord { + stage, + category, + sample_messages: messages.into_iter().take(3).collect(), + cases: cases.into_iter().collect(), + }) + .collect(); + snapshot.groups.sort_by(|left, right| { + right + .cases + .len() + .cmp(&left.cases.len()) + .then_with(|| left.stage.cmp(&right.stage)) + }); + snapshot.counts = counts; +} + +pub(super) fn print_summary(snapshot: &Snapshot) { + println!( + "{} cases: {} passed, {} failed, {} excluded, {} timed out, {} crashed", + snapshot.cases.len(), + count(snapshot, "passed"), + count(snapshot, "failed"), + count(snapshot, "excluded"), + count(snapshot, "timed_out"), + count(snapshot, "crashed") + ); + println!( + "Largest first-blocker signatures (identical signature does not prove a shared root cause):" + ); + for group in snapshot.groups.iter().take(12) { + println!( + " {} case(s) at {} [{}]", + group.cases.len(), + group.stage, + group.category + ); + for message in &group.sample_messages { + println!(" {message}"); + } + } +} + +pub(super) fn count(snapshot: &Snapshot, key: &str) -> usize { + snapshot.counts.get(key).copied().unwrap_or(0) +} + +pub(super) fn compare(before_path: &str, after_path: &str) -> Result<(), String> { + let before: Snapshot = read_json(before_path)?; + let after: Snapshot = read_json(after_path)?; + if !matches!(before.schema_version, 1 | SCHEMA_VERSION) + || !matches!(after.schema_version, 1 | SCHEMA_VERSION) + { + return Err("cannot compare unsupported diagnosis snapshot schema".into()); + } + if before.cohort.mode != after.cohort.mode + || before.cohort.corpus != after.cohort.corpus + || before.cohort.filter != after.cohort.filter + || before.cohort.limit != after.cohort.limit + || before.cohort.timeout_seconds != after.cohort.timeout_seconds + || before.cohort.trusted_stdlib_fingerprint != after.cohort.trusted_stdlib_fingerprint + { + return Err("snapshot cohorts differ (mode, selected cases, timeout, or trusted stdlib); comparison is not meaningful".into()); + } + let old = before + .cases + .iter() + .map(|case| (case.path.as_str(), case)) + .collect::>(); + let new = after + .cases + .iter() + .map(|case| (case.path.as_str(), case)) + .collect::>(); + let paths = old + .keys() + .chain(new.keys()) + .copied() + .collect::>(); + let changes = paths + .into_iter() + .map(|path| { + let left = old.get(path).copied(); + let right = new.get(path).copied(); + let (change, input_comparison) = match (left, right) { + (None, Some(_)) => ("unmatched_new_case", "unavailable"), + (Some(_), None) => ("unmatched_removed_case", "unavailable"), + (Some(a), Some(b)) + if a.input_set_complete + && b.input_set_complete + && a.input_fingerprint != b.input_fingerprint => + { + ("input_changed", "changed") + } + (Some(a), Some(b)) if !a.input_set_complete || !b.input_set_complete => { + (compare_status(a, b), "incomplete") + } + (Some(a), Some(b)) => (compare_status(a, b), "same"), + (None, None) => unreachable!(), + }; + CompareRow { + path: path.to_owned(), + change: change.into(), + input_comparison: input_comparison.into(), + before: left.and_then(case_label), + after: right.and_then(case_label), + trace_comparison: match (left, right) { + (Some(a), Some(b)) + if before.schema_version == SCHEMA_VERSION + && after.schema_version == SCHEMA_VERSION => + { + Some(compare_traces( + a.trace.as_ref(), + b.trace.as_ref(), + a.input_set_complete + && b.input_set_complete + && a.input_fingerprint == b.input_fingerprint, + )) + } + _ => None, + }, + } + }) + .collect::>(); + let report = CompareReport { + compatible_cohort: true, + observed_environment_compatibility: compare_observed_environment( + &before.environment, + &after.environment, + before.schema_version, + after.schema_version, + ), + build_toolchain_compatibility: "unavailable_not_embedded_in_compiler_binary".into(), + before_compiler: before.compiler, + after_compiler: after.compiler, + changes, + }; + println!( + "{}", + serde_json::to_string_pretty(&report).map_err(|error| error.to_string())? + ); + Ok(()) +} + +fn compare_observed_environment( + before: &EnvironmentMetadata, + after: &EnvironmentMetadata, + before_schema: u32, + after_schema: u32, +) -> String { + if before_schema < SCHEMA_VERSION || after_schema < SCHEMA_VERSION { + return "unavailable_legacy_snapshot".into(); + } + let values = [ + (&before.host_os, &after.host_os), + (&before.host_arch, &after.host_arch), + ( + &before.rustc_observed_version, + &after.rustc_observed_version, + ), + ( + &before.cargo_observed_version, + &after.cargo_observed_version, + ), + ]; + if values + .iter() + .any(|(left, right)| left.is_some() && right.is_some() && left != right) + { + "observed_values_differ".into() + } else if values + .iter() + .all(|(left, right)| left.is_some() && right.is_some()) + { + "same_observed_values".into() + } else { + "incomplete_observation".into() + } +} + +pub(super) fn compare_status(before: &CaseRecord, after: &CaseRecord) -> &'static str { + match (before.status, after.status) { + (CaseStatus::Passed, CaseStatus::Passed) => "unchanged_pass", + (CaseStatus::Passed, CaseStatus::Excluded) => "scope_changed", + (CaseStatus::Passed, _) => "regressed", + (CaseStatus::Excluded, CaseStatus::Excluded) => "unchanged_excluded", + (CaseStatus::Excluded, _) => "scope_changed", + (_, CaseStatus::Excluded) => "scope_changed", + (_, CaseStatus::Passed) => "recovered", + (CaseStatus::Failed, CaseStatus::Failed) => { + match (&before.first_blocker, &after.first_blocker) { + (Some(left), Some(right)) if left.stage != right.stage => "stage_changed", + (Some(left), Some(right)) if left.category != right.category => "category_changed", + (Some(left), Some(right)) if left.message != right.message => { + "diagnostic_details_changed" + } + _ => "unchanged_failure", + } + } + (a, b) if status_key(a) == status_key(b) => "unchanged_failure", + _ => "execution_failure_changed", + } +} + +pub(super) fn status_key(status: CaseStatus) -> &'static str { + match status { + CaseStatus::Passed => "passed", + CaseStatus::Failed => "failed", + CaseStatus::Excluded => "excluded", + CaseStatus::TimedOut => "timed_out", + CaseStatus::Crashed => "crashed", + } +} + +pub(super) fn case_label(case: &CaseRecord) -> Option { + Some(match &case.first_blocker { + Some(first) => format!("{} [{}]: {}", first.stage, first.category, first.message), + None => status_key(case.status).to_owned(), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn failed_case(path: &str, message: &str) -> CaseRecord { + CaseRecord { + path: path.into(), + input_fingerprint: "input".into(), + input_set_complete: true, + elapsed_ms: 1, + status: CaseStatus::Failed, + excluded_reason: None, + diagnostics: Vec::new(), + first_blocker: Some(FirstBlocker { + stage: "P8 CC verification".into(), + category: "call_shape_mismatch".into(), + message: message.into(), + }), + bundle: None, + trace: None, + } + } + + #[test] + fn first_blocker_groups_call_shapes_by_stable_category() { + let diagnostic = DiagnosticRecord { + origin: "source".into(), + source: Some("Main.purs".into()), + stage: "P8 CC verification".into(), + start: 4, + end: 9, + code: None, + kind: Some("InvalidCompilerIr".into()), + message: "call shape mismatch: expected Integer, got Number".into(), + }; + assert_eq!(first_blocker(&diagnostic).category, "call_shape_mismatch"); + } + + #[test] + fn grouping_keeps_distinct_messages_inside_one_stage_category() { + let mut snapshot = Snapshot { + schema_version: SCHEMA_VERSION, + cohort: Cohort { + mode: "file".into(), + corpus: None, + filter: None, + limit: None, + timeout_seconds: 20, + trusted_stdlib_fingerprint: "stdlib".into(), + }, + compiler: CompilerRevision { + head: None, + dirty: false, + working_tree_fingerprint: "tree".into(), + binary_fingerprint: "binary".into(), + }, + environment: EnvironmentMetadata::default(), + trace_mode: TraceMode::Manifest, + cases: vec![ + failed_case("A.purs", "expected Integer, got Number"), + failed_case("B.purs", "expected Integer, got Boolean"), + ], + groups: Vec::new(), + counts: BTreeMap::new(), + }; + group_cases(&mut snapshot); + assert_eq!(snapshot.groups.len(), 1); + assert_eq!(snapshot.groups[0].cases, ["A.purs", "B.purs"]); + assert_eq!(snapshot.groups[0].sample_messages.len(), 2); + } + + #[test] + fn incomplete_inputs_do_not_hide_a_pass_recovery() { + let before = CaseRecord { + status: CaseStatus::TimedOut, + input_set_complete: false, + ..failed_case("Main.purs", "worker timeout") + }; + let after = CaseRecord { + status: CaseStatus::Passed, + first_blocker: None, + ..failed_case("Main.purs", "worker timeout") + }; + assert_eq!(compare_status(&before, &after), "recovered"); + } +} diff --git a/crates/psrs-cli/src/diagnose/schema.rs b/crates/psrs-cli/src/diagnose/schema.rs new file mode 100644 index 00000000..8d3243ca --- /dev/null +++ b/crates/psrs-cli/src/diagnose/schema.rs @@ -0,0 +1,151 @@ +use super::trace::{CaseTrace, EnvironmentMetadata, TraceMode}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::path::PathBuf; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(super) struct SourceInput { + pub(super) name: String, + pub(super) text: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(super) struct DiagnosticRecord { + pub(super) origin: String, + pub(super) source: Option, + pub(super) stage: String, + pub(super) start: u32, + pub(super) end: u32, + pub(super) code: Option, + pub(super) kind: Option, + pub(super) message: String, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(super) enum CaseStatus { + Passed, + Failed, + Excluded, + TimedOut, + Crashed, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub(super) struct FirstBlocker { + pub(super) stage: String, + pub(super) category: String, + pub(super) message: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(super) struct CaseRecord { + pub(super) path: String, + pub(super) input_fingerprint: String, + pub(super) input_set_complete: bool, + pub(super) elapsed_ms: u64, + pub(super) status: CaseStatus, + pub(super) excluded_reason: Option, + pub(super) diagnostics: Vec, + pub(super) first_blocker: Option, + pub(super) bundle: Option, + #[serde(default)] + pub(super) trace: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(super) struct GroupRecord { + pub(super) stage: String, + pub(super) category: String, + pub(super) sample_messages: Vec, + pub(super) cases: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(super) struct Cohort { + pub(super) mode: String, + pub(super) corpus: Option, + pub(super) filter: Option, + pub(super) limit: Option, + pub(super) timeout_seconds: u64, + pub(super) trusted_stdlib_fingerprint: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(super) struct CompilerRevision { + pub(super) head: Option, + pub(super) dirty: bool, + pub(super) working_tree_fingerprint: String, + pub(super) binary_fingerprint: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(super) struct BundleContext { + pub(super) compiler: CompilerRevision, + pub(super) trusted_stdlib_fingerprint: String, + pub(super) executable: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(super) struct Snapshot { + pub(super) schema_version: u32, + pub(super) cohort: Cohort, + pub(super) compiler: CompilerRevision, + #[serde(default)] + pub(super) environment: EnvironmentMetadata, + #[serde(default)] + pub(super) trace_mode: TraceMode, + pub(super) cases: Vec, + pub(super) groups: Vec, + pub(super) counts: BTreeMap, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(super) struct CompareRow { + pub(super) path: String, + pub(super) change: String, + pub(super) input_comparison: String, + pub(super) before: Option, + pub(super) after: Option, + #[serde(default)] + pub(super) trace_comparison: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(super) struct TraceComparison { + pub(super) status: String, + pub(super) first_pass_difference: Option, + pub(super) artifact_content: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(super) struct PassDifference { + pub(super) index: usize, + pub(super) kind: String, + pub(super) before_pass: Option, + pub(super) after_pass: Option, + pub(super) before_status: Option, + pub(super) after_status: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(super) struct CompareReport { + pub(super) compatible_cohort: bool, + pub(super) observed_environment_compatibility: String, + pub(super) build_toolchain_compatibility: String, + pub(super) before_compiler: CompilerRevision, + pub(super) after_compiler: CompilerRevision, + pub(super) changes: Vec, +} + +#[derive(Clone, Debug, Default)] +pub(super) struct Options { + pub(super) file: Option, + pub(super) additional_inputs: Vec, + pub(super) corpus: Option, + pub(super) filter: Option, + pub(super) limit: Option, + pub(super) timeout_seconds: u64, + pub(super) output: PathBuf, + pub(super) trace: bool, +} diff --git a/crates/psrs-cli/src/diagnose/trace/build.rs b/crates/psrs-cli/src/diagnose/trace/build.rs new file mode 100644 index 00000000..b48e2249 --- /dev/null +++ b/crates/psrs-cli/src/diagnose/trace/build.rs @@ -0,0 +1,405 @@ +use super::*; +use psrs_driver::{ + TraceArtifactState, TraceEdgeRole, TracePassStatus, TraceRepresentation, + TraceValidationCoverage, TraceValidationStatus, +}; +use std::collections::BTreeMap; + +fn representation_key(value: TraceRepresentation) -> &'static str { + match value { + TraceRepresentation::Core => "core", + TraceRepresentation::ExternalBindings => "external_bindings", + TraceRepresentation::ClosureConverted => "closure_converted", + TraceRepresentation::Mir => "mir", + TraceRepresentation::WasmModule => "wasm_module", + TraceRepresentation::WasmCoreBinary => "wasm_core_binary", + TraceRepresentation::ComponentBinary => "component_binary", + TraceRepresentation::WatText => "wat_text", + TraceRepresentation::WitWorld => "wit_world", + TraceRepresentation::WasiRegistry => "wasi_registry", + TraceRepresentation::LinkPlan => "link_plan", + } +} + +fn artifact_state_key(value: TraceArtifactState) -> &'static str { + match value { + TraceArtifactState::Provided => "provided", + TraceArtifactState::Produced => "produced", + } +} + +fn pass_status_key(value: TracePassStatus) -> &'static str { + match value { + TracePassStatus::Completed => "completed", + TracePassStatus::Rejected => "rejected", + TracePassStatus::NotApplicable => "not_applicable", + } +} + +fn edge_role_key(value: TraceEdgeRole) -> &'static str { + match value { + TraceEdgeRole::Input => "input", + TraceEdgeRole::Output => "output", + } +} + +fn validation_status_key(value: TraceValidationStatus) -> &'static str { + match value { + TraceValidationStatus::Passed => "passed", + TraceValidationStatus::Rejected => "rejected", + } +} + +fn coverage_key(value: TraceValidationCoverage) -> &'static str { + match value { + TraceValidationCoverage::Direct => "direct", + TraceValidationCoverage::Composite => "composite", + TraceValidationCoverage::NotObserved => "not_observed", + } +} + +pub fn from_report( + sources: &[SourceInput], + complete: bool, + fingerprint: &str, + trusted_stdlib_fingerprint: &str, + dumps_requested: bool, + report: &psrs_driver::CompilationReport, +) -> Result { + let mut trace = empty_case_trace(sources, complete, fingerprint, dumps_requested); + let mut artifact_representations = BTreeMap::new(); + let trusted_source_names = report + .frontend_trace + .as_ref() + .map(|frontend| { + frontend + .source_names + .iter() + .take(frontend.trusted_prefix) + .cloned() + .collect::>() + }) + .unwrap_or_default(); + let user_source_names = report + .frontend_trace + .as_ref() + .map(|frontend| { + frontend + .source_names + .iter() + .skip(frontend.trusted_prefix) + .cloned() + .collect::>() + }) + .unwrap_or_default(); + + if let Some(frontend) = report.frontend_trace.as_ref() { + if frontend.trusted_prefix > frontend.source_names.len() { + return Err("frontend trace trusted prefix exceeds its source list".into()); + } + trace.artifacts.push(TraceArtifactRecord { + id: "inputs:trusted_stdlib:i0".into(), + representation: "trusted_library_set".into(), + state: "provided".into(), + producer: None, + format_version: Some(1), + input_fingerprint: Some(InputFingerprint { + algorithm: "fnv1a64_trusted_stdlib".into(), + version: 1, + value: trusted_stdlib_fingerprint.to_owned(), + }), + sources: trusted_source_names + .iter() + .map(|logical_name| TraceSourceRecord { + logical_name: logical_name.clone(), + fingerprint: None, + }) + .collect(), + canonical_summary: TraceAvailability { + status: "not_applicable".into(), + reason: None, + }, + retained_dump: None, + }); + let source_input = "inputs:i0".to_owned(); + let trusted_input = "inputs:trusted_stdlib:i0".to_owned(); + let output = frontend.output_core.map(|id| format!("backend:a{}", id.0)); + trace.executions.push(TraceExecutionRecord { + id: "frontend:e0".into(), + pass_key: frontend.pass_key.into(), + contract_version: frontend.contract_version, + inputs: vec![source_input.clone(), trusted_input.clone()], + outputs: output.iter().cloned().collect(), + input_representations: vec!["source_set".into(), "trusted_library_set".into()], + output_representations: output + .as_ref() + .map(|_| vec!["core".into()]) + .unwrap_or_default(), + status: pass_status_key(frontend.status).into(), + diagnostic_indices: frontend.diagnostic_indices.clone(), + validation_coverage: "not_observed".into(), + parameters: BTreeMap::from([( + "trusted_prefix".into(), + frontend.trusted_prefix.to_string(), + )]), + user_source_names: user_source_names.clone(), + trusted_source_names: trusted_source_names.clone(), + }); + trace.edges.extend([ + TraceEdgeRecord { + execution: "frontend:e0".into(), + artifact: source_input, + role: "input".into(), + }, + TraceEdgeRecord { + execution: "frontend:e0".into(), + artifact: trusted_input, + role: "input".into(), + }, + ]); + if let Some(artifact) = output { + trace.edges.push(TraceEdgeRecord { + execution: "frontend:e0".into(), + artifact, + role: "output".into(), + }); + } + } + + if let Some(backend) = report.backend_trace.as_ref() { + let frontend_core = report + .frontend_trace + .as_ref() + .and_then(|frontend| frontend.output_core); + if frontend_core.is_some_and(|id| id != backend.initial_core) { + return Err("frontend trace output does not match backend initial Core".into()); + } + for artifact in &backend.artifacts { + let id = format!("backend:a{}", artifact.id.0); + let representation = representation_key(artifact.representation).to_owned(); + artifact_representations.insert(artifact.id.0, representation.clone()); + let frontend_produced = artifact.id == backend.initial_core && frontend_core.is_some(); + trace.artifacts.push(TraceArtifactRecord { + id, + representation, + state: if frontend_produced { + "produced" + } else { + artifact_state_key(artifact.state) + } + .into(), + producer: if frontend_produced { + Some("frontend:e0".into()) + } else { + artifact + .producer + .map(|producer| format!("backend:e{}", producer.0)) + }, + format_version: None, + input_fingerprint: None, + sources: Vec::new(), + canonical_summary: TraceAvailability::unavailable( + "canonical artifact summaries are not implemented", + ), + retained_dump: retained_dump_for(artifact.id.0, report, dumps_requested), + }); + } + validate_dump_refs(report, backend)?; + for execution in &backend.executions { + let inputs = execution + .inputs + .iter() + .map(|artifact| format!("backend:a{}", artifact.0)) + .collect::>(); + let outputs = execution + .outputs + .iter() + .map(|artifact| format!("backend:a{}", artifact.0)) + .collect::>(); + let input_representations = representation_list( + execution.id.0, + "input", + &execution.inputs, + &artifact_representations, + )?; + let output_representations = representation_list( + execution.id.0, + "output", + &execution.outputs, + &artifact_representations, + )?; + trace.executions.push(TraceExecutionRecord { + id: format!("backend:e{}", execution.id.0), + pass_key: execution.pass_key.into(), + contract_version: execution.contract_version, + input_representations, + output_representations, + inputs, + outputs, + status: pass_status_key(execution.status).into(), + diagnostic_indices: execution.diagnostic_indices.clone(), + validation_coverage: coverage_key(execution.validation_coverage).into(), + parameters: execution + .parameters + .iter() + .map(|parameter| (parameter.key.to_owned(), parameter.value.clone())) + .collect(), + user_source_names: Vec::new(), + trusted_source_names: Vec::new(), + }); + } + trace + .edges + .extend(backend.edges.iter().map(|edge| TraceEdgeRecord { + execution: format!("backend:e{}", edge.execution.0), + artifact: format!("backend:a{}", edge.artifact.0), + role: edge_role_key(edge.role).into(), + })); + for validation in &backend.validations { + trace.validations.push(TraceValidationRecord { + execution: format!("backend:e{}", validation.execution.0), + validator_key: validation.validator_key.into(), + artifacts: validation + .artifacts + .iter() + .map(|artifact| format!("backend:a{}", artifact.0)) + .collect(), + status: validation_status_key(validation.status).into(), + coverage: coverage_key(validation.coverage).into(), + }); + } + } + trace.pass_capture = match (&report.frontend_trace, &report.artifact) { + (None, _) => TraceAvailability::unavailable( + "trusted prelude loading stopped before the frontend trace boundary", + ), + (Some(_), Some(_)) => TraceAvailability { + status: "complete".into(), + reason: None, + }, + (Some(_), None) => TraceAvailability { + status: "observed_prefix".into(), + reason: Some("compilation stopped after the recorded execution prefix".into()), + }, + }; + validate_references(&trace)?; + Ok(trace) +} + +fn representation_list( + execution_id: u32, + role: &str, + ids: &[psrs_driver::TraceArtifactId], + representations: &BTreeMap, +) -> Result, String> { + ids.iter() + .map(|artifact| { + representations.get(&artifact.0).cloned().ok_or_else(|| { + format!( + "backend trace execution {execution_id} references missing {role} artifact {}", + artifact.0 + ) + }) + }) + .collect() +} + +fn retained_dump_for( + artifact_id: u32, + report: &psrs_driver::CompilationReport, + requested: bool, +) -> Option { + if !requested { + return None; + } + if report + .dump_artifacts + .core + .is_some_and(|id| id.0 == artifact_id) + { + Some("core.debug".into()) + } else if report + .dump_artifacts + .cc + .is_some_and(|id| id.0 == artifact_id) + { + Some("cc.debug".into()) + } else if report + .dump_artifacts + .mir + .is_some_and(|id| id.0 == artifact_id) + { + Some("mir.debug".into()) + } else { + None + } +} + +fn validate_dump_refs( + report: &psrs_driver::CompilationReport, + backend: &psrs_driver::CompileTrace, +) -> Result<(), String> { + for id in [ + report.dump_artifacts.core, + report.dump_artifacts.cc, + report.dump_artifacts.mir, + ] + .into_iter() + .flatten() + { + if !backend.artifacts.iter().any(|artifact| artifact.id == id) { + return Err(format!( + "IR dump references missing backend artifact {}", + id.0 + )); + } + } + Ok(()) +} + +fn validate_references(trace: &CaseTrace) -> Result<(), String> { + let artifact_ids = trace + .artifacts + .iter() + .map(|artifact| artifact.id.as_str()) + .collect::>(); + let execution_ids = trace + .executions + .iter() + .map(|execution| execution.id.as_str()) + .collect::>(); + for execution in &trace.executions { + for artifact in execution.inputs.iter().chain(&execution.outputs) { + if !artifact_ids.contains(artifact.as_str()) { + return Err(format!( + "trace execution {} references unknown artifact {artifact}", + execution.id + )); + } + } + } + for edge in &trace.edges { + if !execution_ids.contains(edge.execution.as_str()) + || !artifact_ids.contains(edge.artifact.as_str()) + { + return Err(format!( + "trace edge references unknown execution/artifact {} / {}", + edge.execution, edge.artifact + )); + } + } + for validation in &trace.validations { + if !execution_ids.contains(validation.execution.as_str()) + || validation + .artifacts + .iter() + .any(|artifact| !artifact_ids.contains(artifact.as_str())) + { + return Err(format!( + "trace validation references unknown execution/artifact {}", + validation.execution + )); + } + } + Ok(()) +} diff --git a/crates/psrs-cli/src/diagnose/trace/compare.rs b/crates/psrs-cli/src/diagnose/trace/compare.rs new file mode 100644 index 00000000..98dc26ca --- /dev/null +++ b/crates/psrs-cli/src/diagnose/trace/compare.rs @@ -0,0 +1,112 @@ +use super::super::{PassDifference, TraceComparison}; +use super::{CaseTrace, TraceExecutionRecord}; + +pub fn compare_traces( + before: Option<&CaseTrace>, + after: Option<&CaseTrace>, + same_inputs: bool, +) -> TraceComparison { + let unavailable = |status: &str| TraceComparison { + status: status.into(), + first_pass_difference: None, + artifact_content: "unavailable_no_canonical_summary".into(), + }; + if !same_inputs { + return unavailable("not_compared_input_mismatch"); + } + let (Some(before), Some(after)) = (before, after) else { + return unavailable("unavailable_trace_missing"); + }; + if before.version != after.version { + return unavailable("unavailable_trace_version_mismatch"); + } + if before.version != 1 { + return unavailable("unavailable_unsupported_trace_version"); + } + if before.pass_capture.status == "unavailable" || after.pass_capture.status == "unavailable" { + return unavailable("unavailable_pass_capture_gap"); + } + + let max_len = before.executions.len().max(after.executions.len()); + for index in 0..max_len { + let left = before.executions.get(index); + let right = after.executions.get(index); + let Some(kind) = execution_difference(left, right) else { + continue; + }; + return TraceComparison { + status: "pass_difference_observed".into(), + first_pass_difference: Some(PassDifference { + index, + kind: kind.into(), + before_pass: left.map(|pass| pass.pass_key.clone()), + after_pass: right.map(|pass| pass.pass_key.clone()), + before_status: left.map(|pass| pass.status.clone()), + after_status: right.map(|pass| pass.status.clone()), + }), + artifact_content: "unavailable_no_canonical_summary".into(), + }; + } + if before.pass_capture.status != after.pass_capture.status { + let index = max_len; + return TraceComparison { + status: "pass_capture_coverage_changed".into(), + first_pass_difference: Some(PassDifference { + index, + kind: "pass_capture_coverage_changed".into(), + before_pass: None, + after_pass: None, + before_status: Some(before.pass_capture.status.clone()), + after_status: Some(after.pass_capture.status.clone()), + }), + artifact_content: "unavailable_no_canonical_summary".into(), + }; + } + if before.pass_capture.status == "observed_prefix" { + return TraceComparison { + status: "no_difference_in_observed_prefix".into(), + first_pass_difference: None, + artifact_content: "unavailable_no_canonical_summary".into(), + }; + } + TraceComparison { + status: "no_observed_pass_difference".into(), + first_pass_difference: None, + artifact_content: "unavailable_no_canonical_summary".into(), + } +} + +fn execution_difference( + before: Option<&TraceExecutionRecord>, + after: Option<&TraceExecutionRecord>, +) -> Option<&'static str> { + match (before, after) { + (None, Some(_)) => Some("pass_added"), + (Some(_), None) => Some("pass_removed"), + (Some(before), Some(after)) if before.pass_key != after.pass_key => { + Some("pass_order_or_identity_changed") + } + (Some(before), Some(after)) if before.contract_version != after.contract_version => { + Some("pass_contract_changed") + } + (Some(before), Some(after)) if before.status != after.status => Some("pass_status_changed"), + (Some(before), Some(after)) + if before.input_representations != after.input_representations + || before.output_representations != after.output_representations => + { + Some("pass_boundary_changed") + } + (Some(before), Some(after)) if before.validation_coverage != after.validation_coverage => { + Some("validation_coverage_changed") + } + (Some(before), Some(after)) + if before.parameters != after.parameters + || before.user_source_names != after.user_source_names + || before.trusted_source_names != after.trusted_source_names => + { + Some("pass_inputs_or_parameters_changed") + } + (Some(_), Some(_)) => None, + (None, None) => None, + } +} diff --git a/crates/psrs-cli/src/diagnose/trace/environment.rs b/crates/psrs-cli/src/diagnose/trace/environment.rs new file mode 100644 index 00000000..a9172432 --- /dev/null +++ b/crates/psrs-cli/src/diagnose/trace/environment.rs @@ -0,0 +1,19 @@ +use super::EnvironmentMetadata; + +pub fn observed_environment() -> EnvironmentMetadata { + EnvironmentMetadata { + host_os: Some(std::env::consts::OS.into()), + host_arch: Some(std::env::consts::ARCH.into()), + rustc_observed_version: command_version("rustc", "--version"), + cargo_observed_version: command_version("cargo", "--version"), + } +} + +fn command_version(command: &str, argument: &str) -> Option { + std::process::Command::new(command) + .arg(argument) + .output() + .ok() + .filter(|output| output.status.success()) + .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_owned()) +} diff --git a/crates/psrs-cli/src/diagnose/trace/mod.rs b/crates/psrs-cli/src/diagnose/trace/mod.rs new file mode 100644 index 00000000..ef62a08e --- /dev/null +++ b/crates/psrs-cli/src/diagnose/trace/mod.rs @@ -0,0 +1,170 @@ +use super::{SourceInput, fingerprint_sources}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(super) enum TraceMode { + #[default] + Manifest, + Dumps, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub(super) struct EnvironmentMetadata { + pub(super) host_os: Option, + pub(super) host_arch: Option, + pub(super) rustc_observed_version: Option, + pub(super) cargo_observed_version: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(super) struct CaseTrace { + pub(super) version: u32, + pub(super) dumps_requested: bool, + pub(super) source_set_complete: bool, + pub(super) pass_capture: TraceAvailability, + pub(super) artifacts: Vec, + pub(super) executions: Vec, + pub(super) edges: Vec, + pub(super) validations: Vec, + pub(super) lineage: TraceAvailability, + pub(super) canonical_summaries: TraceAvailability, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(super) struct TraceArtifactRecord { + pub(super) id: String, + pub(super) representation: String, + pub(super) state: String, + pub(super) producer: Option, + pub(super) format_version: Option, + pub(super) input_fingerprint: Option, + pub(super) sources: Vec, + pub(super) canonical_summary: TraceAvailability, + pub(super) retained_dump: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(super) struct InputFingerprint { + pub(super) algorithm: String, + pub(super) version: u32, + pub(super) value: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(super) struct TraceSourceRecord { + pub(super) logical_name: String, + pub(super) fingerprint: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(super) struct TraceExecutionRecord { + pub(super) id: String, + pub(super) pass_key: String, + pub(super) contract_version: u32, + pub(super) inputs: Vec, + pub(super) outputs: Vec, + pub(super) input_representations: Vec, + pub(super) output_representations: Vec, + pub(super) status: String, + pub(super) diagnostic_indices: Vec, + pub(super) validation_coverage: String, + pub(super) parameters: BTreeMap, + pub(super) user_source_names: Vec, + pub(super) trusted_source_names: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(super) struct TraceEdgeRecord { + pub(super) execution: String, + pub(super) artifact: String, + pub(super) role: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(super) struct TraceValidationRecord { + pub(super) execution: String, + pub(super) validator_key: String, + pub(super) artifacts: Vec, + pub(super) status: String, + pub(super) coverage: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub(super) struct TraceAvailability { + pub(super) status: String, + pub(super) reason: Option, +} + +impl TraceAvailability { + fn unavailable(reason: &str) -> Self { + Self { + status: "unavailable".into(), + reason: Some(reason.into()), + } + } +} + +pub(super) fn empty_case_trace( + sources: &[SourceInput], + complete: bool, + fingerprint: &str, + dumps_requested: bool, +) -> CaseTrace { + CaseTrace { + version: 1, + dumps_requested, + source_set_complete: complete, + pass_capture: TraceAvailability::unavailable( + "compiler did not reach an instrumented frontend pass boundary", + ), + artifacts: vec![source_set_artifact(sources, complete, fingerprint)], + executions: Vec::new(), + edges: Vec::new(), + validations: Vec::new(), + lineage: TraceAvailability::unavailable("node lineage is not captured by this trace slice"), + canonical_summaries: TraceAvailability::unavailable( + "no canonical artifact summary is implemented for this representation", + ), + } +} + +fn source_set_artifact( + sources: &[SourceInput], + complete: bool, + fingerprint: &str, +) -> TraceArtifactRecord { + TraceArtifactRecord { + id: "inputs:i0".into(), + representation: "source_set".into(), + state: if complete { "provided" } else { "partial" }.into(), + producer: None, + format_version: Some(1), + input_fingerprint: Some(InputFingerprint { + algorithm: "fnv1a64_ordered_sources".into(), + version: 1, + value: fingerprint.to_owned(), + }), + sources: sources + .iter() + .map(|source| TraceSourceRecord { + logical_name: source.name.clone(), + fingerprint: Some(fingerprint_sources(std::slice::from_ref(source))), + }) + .collect(), + canonical_summary: TraceAvailability { + status: "not_applicable".into(), + reason: None, + }, + retained_dump: None, + } +} + +mod build; +mod compare; +mod environment; + +pub(super) use build::from_report; +pub(super) use compare::compare_traces; +pub(super) use environment::observed_environment; diff --git a/crates/psrs-cli/src/diagnose/worker.rs b/crates/psrs-cli/src/diagnose/worker.rs new file mode 100644 index 00000000..b6c443a2 --- /dev/null +++ b/crates/psrs-cli/src/diagnose/worker.rs @@ -0,0 +1,330 @@ +use super::{ + CaseTrace, DiagnosticRecord, SourceInput, atomic_write, empty_case_trace, fingerprint_sources, + first_line, from_report, +}; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::time::{Duration, Instant}; +use std::{env, fs, thread}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(super) struct WorkerRequest { + pub(super) path: String, + pub(super) category_dir: Option, + #[serde(default)] + pub(super) explicit_inputs: Vec, + #[serde(default)] + pub(super) capture_dumps: bool, + #[serde(default)] + pub(super) trusted_stdlib_fingerprint: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(super) struct WorkerResponse { + pub(super) passed: bool, + pub(super) input_fingerprint: String, + pub(super) sources: Vec, + pub(super) input_set_complete: bool, + pub(super) elapsed_ms: u64, + pub(super) diagnostics: Vec, + pub(super) core_stage: Option, + pub(super) core: Option, + pub(super) cc_stage: Option, + pub(super) cc: Option, + pub(super) mir_stage: Option, + pub(super) mir: Option, + pub(super) trace: Option, +} + +#[derive(Debug)] +pub(super) enum WorkerOutcome { + Completed(Box), + TimedOut { + elapsed_ms: u64, + }, + Crashed { + message: String, + stderr: String, + elapsed_ms: u64, + }, +} + +pub fn worker(request_path: &str, response_path: &str) -> Result<(), String> { + let request_bytes = + fs::read(request_path).map_err(|error| format!("{request_path}: {error}"))?; + let request: WorkerRequest = serde_json::from_slice(&request_bytes) + .map_err(|error| format!("{request_path}: invalid worker request: {error}"))?; + let started = Instant::now(); + let entry_path = PathBuf::from(&request.path); + let entry_text = match fs::read_to_string(&entry_path) { + Ok(text) => text, + Err(error) => { + let response = WorkerResponse { + passed: false, + input_fingerprint: fingerprint_sources(&[]), + sources: Vec::new(), + input_set_complete: false, + elapsed_ms: started.elapsed().as_millis() as u64, + diagnostics: vec![DiagnosticRecord { + origin: "program".into(), + source: Some(request.path), + stage: "input loading".into(), + start: 0, + end: 0, + code: None, + kind: None, + message: format!("{}: {error}", entry_path.display()), + }], + core_stage: None, + core: None, + cc_stage: None, + cc: None, + mir_stage: None, + mir: None, + trace: None, + }; + return atomic_write( + Path::new(response_path), + &serde_json::to_vec(&response).map_err(|error| error.to_string())?, + ); + } + }; + let loaded = if let Some(category) = request.category_dir.as_deref() { + psrs_driver::load_program_case_sources(&entry_path, Path::new(category), &entry_text) + .map(|case| case.own.into_iter().chain(case.loaded).collect::>()) + } else if request.explicit_inputs.is_empty() { + psrs_driver::load_program_files(std::slice::from_ref(&request.path)) + } else { + request + .explicit_inputs + .iter() + .map(|path| { + fs::read_to_string(path) + .map(|text| (path.clone(), text)) + .map_err(|error| format!("{path}: {error}")) + }) + .collect::, _>>() + }; + let (sources, input_set_complete, load_error) = match loaded { + Ok(sources) => (sources, true, None), + Err(error) => (vec![(request.path.clone(), entry_text)], false, Some(error)), + }; + let inputs = sources + .iter() + .map(|(name, text)| SourceInput { + name: name.clone(), + text: text.clone(), + }) + .collect::>(); + if let Some(error) = load_error { + let input_fingerprint = fingerprint_sources(&inputs); + let trace = empty_case_trace( + &inputs, + input_set_complete, + &input_fingerprint, + request.capture_dumps, + ); + let response = WorkerResponse { + passed: false, + input_fingerprint, + sources: inputs, + input_set_complete, + elapsed_ms: started.elapsed().as_millis() as u64, + diagnostics: vec![DiagnosticRecord { + origin: "program".into(), + source: Some(request.path), + stage: "input loading".into(), + start: 0, + end: 0, + code: None, + kind: None, + message: error, + }], + core_stage: None, + core: None, + cc_stage: None, + cc: None, + mir_stage: None, + mir: None, + trace: Some(trace), + }; + return atomic_write( + Path::new(response_path), + &serde_json::to_vec(&response).map_err(|error| error.to_string())?, + ); + } + let sources = inputs; + let source_refs = sources + .iter() + .map(|source| (source.name.as_str(), source.text.as_str())) + .collect::>(); + let report = psrs_driver::compile_program_sources_with_prelude_diagnosis( + &source_refs, + request.capture_dumps, + ); + let input_fingerprint = fingerprint_sources(&sources); + let trace = from_report( + &sources, + input_set_complete, + &input_fingerprint, + &request.trusted_stdlib_fingerprint, + request.capture_dumps, + &report, + )?; + let diagnostics = report + .diagnostics + .iter() + .map(|item| { + let (origin, source) = match item.source { + psrs_driver::DiagnosticOrigin::Source(index) => ( + "source".to_owned(), + sources.get(index).map(|source| source.name.clone()), + ), + psrs_driver::DiagnosticOrigin::Library => ("library".to_owned(), None), + psrs_driver::DiagnosticOrigin::Program => ("program".to_owned(), None), + }; + DiagnosticRecord { + origin, + source, + stage: item.diagnostic.stage.to_owned(), + start: item.diagnostic.span.start, + end: item.diagnostic.span.end, + code: item.diagnostic.code.map(str::to_owned), + kind: item.diagnostic.kind.map(|kind| format!("{kind:?}")), + message: item.diagnostic.message.clone(), + } + }) + .collect::>(); + let response = WorkerResponse { + passed: report.artifact.is_some(), + input_fingerprint, + sources, + input_set_complete, + elapsed_ms: started.elapsed().as_millis() as u64, + diagnostics, + core_stage: report.dumps.core_stage.map(str::to_owned), + core: report.dumps.core, + cc_stage: report.dumps.cc_stage.map(str::to_owned), + cc: report.dumps.cc, + mir_stage: report.dumps.mir_stage.map(str::to_owned), + mir: report.dumps.mir, + trace: Some(trace), + }; + atomic_write( + Path::new(response_path), + &serde_json::to_vec(&response).map_err(|error| error.to_string())?, + ) +} + +pub(super) fn run_worker( + work_root: &Path, + index: usize, + timeout_seconds: u64, + request: WorkerRequest, +) -> Result { + let worker_dir = work_root.join(format!("case-{index}")); + let _ = fs::remove_dir_all(&worker_dir); + fs::create_dir_all(&worker_dir) + .map_err(|error| format!("{}: {error}", worker_dir.display()))?; + let request_path = worker_dir.join("request.json"); + let response_path = worker_dir.join("response.json"); + let stderr_path = worker_dir.join("stderr.log"); + fs::write( + &request_path, + serde_json::to_vec(&request).map_err(|error| error.to_string())?, + ) + .map_err(|error| format!("{}: {error}", request_path.display()))?; + let stderr = fs::File::create(&stderr_path) + .map_err(|error| format!("{}: {error}", stderr_path.display()))?; + let executable = env::current_exe().map_err(|error| error.to_string())?; + let mut child = Command::new(executable) + .arg("__diagnose-worker") + .arg(&request_path) + .arg(&response_path) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::from(stderr)) + .spawn() + .map_err(|error| format!("could not start compiler worker: {error}"))?; + let started = Instant::now(); + loop { + if let Some(status) = child.try_wait().map_err(|error| error.to_string())? { + let result = if response_path.is_file() { + let bytes = fs::read(&response_path).map_err(|error| error.to_string())?; + serde_json::from_slice(&bytes) + .map_err(|error| format!("worker returned invalid report: {error}"))? + } else { + let stderr = fs::read_to_string(&stderr_path).unwrap_or_default(); + let message = if stderr.trim().is_empty() { + format!("worker exited with {status} before writing a report") + } else { + first_line(&stderr).to_owned() + }; + return Ok(WorkerOutcome::Crashed { + message, + stderr, + elapsed_ms: started.elapsed().as_millis() as u64, + }); + }; + return Ok(WorkerOutcome::Completed(Box::new(result))); + } + if started.elapsed() >= Duration::from_secs(timeout_seconds) { + child + .kill() + .map_err(|error| format!("could not stop timed-out worker: {error}"))?; + let _ = child.wait(); + return Ok(WorkerOutcome::TimedOut { + elapsed_ms: started.elapsed().as_millis() as u64, + }); + } + thread::sleep(Duration::from_millis(15)); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{SystemTime, UNIX_EPOCH}; + + #[test] + fn missing_entry_is_a_loading_error_without_fabricated_source() { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock after UNIX epoch") + .as_nanos(); + let root = env::temp_dir().join(format!("psrs-diagnose-worker-{unique}")); + fs::create_dir_all(&root).expect("create worker test directory"); + let missing = root.join("missing.purs"); + let request_path = root.join("request.json"); + let response_path = root.join("response.json"); + let request = WorkerRequest { + path: missing.to_string_lossy().into_owned(), + category_dir: None, + explicit_inputs: Vec::new(), + capture_dumps: false, + trusted_stdlib_fingerprint: String::new(), + }; + fs::write( + &request_path, + serde_json::to_vec(&request).expect("serialize worker request"), + ) + .expect("write worker request"); + + worker( + request_path.to_str().expect("request path is UTF-8"), + response_path.to_str().expect("response path is UTF-8"), + ) + .expect("worker writes loading-error response"); + let response: WorkerResponse = + serde_json::from_slice(&fs::read(&response_path).expect("read worker response")) + .expect("parse worker response"); + + assert!(!response.passed); + assert!(!response.input_set_complete); + assert!(response.sources.is_empty()); + assert_eq!(response.diagnostics[0].stage, "input loading"); + assert!(response.diagnostics[0].message.contains("missing.purs")); + let _ = fs::remove_dir_all(root); + } +} diff --git a/crates/psrs-cli/src/link.rs b/crates/psrs-cli/src/link.rs new file mode 100644 index 00000000..9da49091 --- /dev/null +++ b/crates/psrs-cli/src/link.rs @@ -0,0 +1,160 @@ +//! Explicit pinned guest-provider composition for an application component. +use psrs_linker::guest::{ + ComponentLinkInput, ComponentReference, GuestBinding, compose_component, plan_components, +}; +use serde::Deserialize; +use std::{fs, path::Path}; + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct Manifest { + schema_version: u32, + application_sha256: Option, + providers: Vec, + bindings: Vec, + permitted_host_interfaces: Vec, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct Artifact { + id: String, + path: String, + sha256: String, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct Binding { + interface: String, + provider: String, + export: Option, +} + +pub fn run(args: Vec) -> Result<(), String> { + let mut application = None; + let mut manifest = None; + let mut output = None; + let mut report = None; + let mut args = args.into_iter(); + while let Some(arg) = args.next() { + let slot = match arg.as_str() { + "--manifest" => &mut manifest, + "-o" => &mut output, + "--report" => &mut report, + value if !value.starts_with('-') && application.is_none() => { + application = Some(arg); + continue; + } + _ => return Err(usage()), + }; + if slot.is_some() { + return Err(usage()); + } + *slot = Some(args.next().ok_or_else(usage)?); + } + let application = application.ok_or_else(usage)?; + let manifest_path = manifest.ok_or_else(usage)?; + let output = output.ok_or_else(usage)?; + let application_bytes = read(&application)?; + let (bytes, evidence) = compose(&application_bytes, &manifest_path, true)?; + if let Some(report) = report { + write_report(&report, &evidence)?; + } + fs::write(&output, bytes).map_err(|error| format!("{output}: {error}")) +} + +/// Compiler-produced roots are pinned by the bytes emitted in this invocation. +/// Standalone inputs require an explicit manifest root pin. +pub(super) fn compose( + application_bytes: &[u8], + manifest_path: &str, + require_root_pin: bool, +) -> Result<(Vec, serde_json::Value), String> { + let manifest_bytes = read(manifest_path)?; + let manifest: Manifest = serde_json::from_slice(&manifest_bytes) + .map_err(|error| format!("{manifest_path}: {error}"))?; + if manifest.schema_version != 1 { + return Err("unsupported link manifest schema".into()); + } + let target = psrs_driver::TargetCapabilities::default(); + let world = psrs_linker::resolve_default_definitions().map_err(|error| error.to_string())?; + for interface in &manifest.permitted_host_interfaces { + if interface.starts_with("wasi:") + && (!world.imports_interface(interface) || !target.wasi_interface_enabled(interface)) + { + return Err(format!( + "host interface `{interface}` is outside the selected target profile" + )); + } + } + let base = Path::new(manifest_path) + .parent() + .unwrap_or_else(|| Path::new(".")); + let guests = manifest + .providers + .into_iter() + .map(|artifact| { + let path = base.join(&artifact.path); + Ok(ComponentReference { + id: artifact.id, + sha256: artifact.sha256, + bytes: fs::read(&path).map_err(|error| format!("{}: {error}", path.display()))?, + }) + }) + .collect::, String>>()?; + let bindings = manifest + .bindings + .into_iter() + .map(|binding| GuestBinding { + export: binding.export.unwrap_or_else(|| binding.interface.clone()), + interface: binding.interface, + artifact: binding.provider, + }) + .collect(); + let plan = plan_components(ComponentLinkInput { + application: ComponentReference { + id: "application".into(), + sha256: match manifest.application_sha256 { + Some(pin) => pin, + None if !require_root_pin => psrs_linker::sha256_hex(application_bytes), + None => return Err("link requires application_sha256".into()), + }, + bytes: application_bytes.to_vec(), + }, + guests, + bindings, + policy: psrs_linker::TargetPolicy { + permitted_host_interfaces: manifest.permitted_host_interfaces, + }, + features: target.wasm_features(), + }) + .map_err(|error| error.to_string())?; + let linked = compose_component(&plan, application_bytes).map_err(|error| error.to_string())?; + let evidence = serde_json::json!({ + "schema_version":1,"manifest_sha256":psrs_linker::sha256_hex(&manifest_bytes),"component_artifacts":plan.component_artifacts(), + "bindings":plan.bindings().iter().map(|binding| serde_json::json!({ + "consumer":binding.consumer,"interface":binding.interface, + "provider":binding.provider,"export":binding.export, + })).collect::>(), + "target_features":plan.features().bits(), + "external_world":linked.external_world,"output_sha256":psrs_linker::sha256_hex(&linked.bytes), + }); + Ok((linked.bytes, evidence)) +} + +pub(super) fn write_report(path: &str, evidence: &serde_json::Value) -> Result<(), String> { + fs::write( + path, + serde_json::to_vec_pretty(evidence).map_err(|error| error.to_string())?, + ) + .map_err(|error| format!("{path}: {error}")) +} + +fn read(path: &str) -> Result, String> { + fs::read(path).map_err(|error| format!("{path}: {error}")) +} + +fn usage() -> String { + "usage: psrs link application.wasm --manifest providers.json -o linked.wasm [--report report.json]".into() +} diff --git a/crates/psrs-cli/src/main.rs b/crates/psrs-cli/src/main.rs index f727b2d9..71f187e1 100644 --- a/crates/psrs-cli/src/main.rs +++ b/crates/psrs-cli/src/main.rs @@ -2,6 +2,10 @@ use psrs_span::{SourceFile, TextRange}; use psrs_syntax::{LayoutTokenKind, RawToken, RawTokenKind, add_layout, lex, parse_module}; use std::{env, fs, process::ExitCode}; +mod build; +mod diagnose; +mod link; + fn main() -> ExitCode { match run() { Ok(()) => ExitCode::SUCCESS, @@ -19,6 +23,20 @@ fn run() -> Result<(), String> { let Some(command) = args.next() else { return Err(usage()); }; + if command == "__diagnose-worker" { + let request = args.next().ok_or_else(usage)?; + let response = args.next().ok_or_else(usage)?; + if args.next().is_some() { + return Err(usage()); + } + return diagnose::worker(&request, &response); + } + if command == "diagnose" { + return diagnose::run(args.collect()); + } + if command == "link" { + return link::run(args.collect()); + } if command == "check-program" { let paths: Vec = args.collect(); if paths.is_empty() { @@ -46,7 +64,7 @@ fn run() -> Result<(), String> { return dump_ir(&stage, &path); } if matches!(command.as_str(), "build" | "wat") { - return compile_program(&command, args.collect()); + return build::run(&command, args.collect()); } let Some(path) = args.next() else { return Err(usage()); @@ -166,97 +184,8 @@ fn run() -> Result<(), String> { Ok(()) } -fn compile_program(command: &str, raw_args: Vec) -> Result<(), String> { - let mut paths = Vec::new(); - let mut output_path = None; - let mut index = 0; - while index < raw_args.len() { - match raw_args[index].as_str() { - "-o" => { - if output_path.is_some() { - return Err(usage()); - } - let Some(output) = raw_args.get(index + 1) else { - return Err(usage()); - }; - output_path = Some(output.clone()); - index += 2; - } - path if path.starts_with('-') => return Err(usage()), - path => { - paths.push(path.to_owned()); - index += 1; - } - } - } - if paths.is_empty() { - return Err(usage()); - } - - let sources = psrs_driver::load_program_files(&paths)?; - let inputs = sources - .iter() - .map(|(path, text)| (path.as_str(), text.as_str())) - .collect::>(); - let artifact = match psrs_driver::compile_program_sources_with_prelude(&inputs) { - Ok(artifact) => artifact, - Err(errors) => { - for error in errors { - // A diagnostic in a standard-library module names a file the - // caller did not pass, so there is no snippet to print, and - // naming the first source instead would blame a file the caller - // wrote for the library's error. - let Some((path, text)) = error - .source - .source_index() - .and_then(|source| sources.get(source)) - else { - eprintln!( - "{}: {} [{}]: {}", - match error.source { - psrs_driver::DiagnosticOrigin::Library => "standard library", - _ => "program", - }, - error.diagnostic.message, - error.diagnostic.stage, - error.diagnostic.code.unwrap_or("no error code") - ); - continue; - }; - let source = SourceFile::new(path.as_str(), text.as_str()); - print_coded_diagnostic( - &source, - error.diagnostic.span, - error.diagnostic.stage, - error.diagnostic.code, - &error.diagnostic.message, - ); - } - return Err(String::new()); - } - }; - print_warnings(&artifact.warnings, &sources); - if command == "wat" { - if let Some(output) = output_path { - fs::write(&output, artifact.wat).map_err(|error| format!("{output}: {error}"))?; - } else { - print!("{}", artifact.wat); - } - } else { - let output = output_path.unwrap_or_else(|| { - std::path::Path::new(&paths[0]) - .with_extension("wasm") - .to_string_lossy() - .into_owned() - }); - fs::write(&output, artifact.wasm).map_err(|error| format!("{output}: {error}"))?; - println!("wrote {output}"); - } - Ok(()) -} - fn usage() -> String { - "usage: psrs \n psrs check-program ...\n psrs check-program-kinds ...\n psrs build ... [-o output.wasm]\n psrs wat ... [-o output.wat]\n psrs dump ".into() + "usage: psrs \n psrs check-program ...\n psrs check-program-kinds ...\n psrs build ... [-o output.wasm] [--manifest providers.json] [--report report.json]\n psrs link application.wasm --manifest providers.json -o linked.wasm [--report report.json]\n psrs wat ... [-o output.wat]\n psrs dump \n psrs diagnose [--input FILE]... [--trace] [--out report.json]\n psrs diagnose --corpus passing [--filter TEXT] [--limit N] [--trace] [--out report.json]\n psrs diagnose --compare OLD.json NEW.json".into() } fn check_program(paths: &[String], kinds: bool) -> Result<(), String> { diff --git a/crates/psrs-cli/tests/cli.rs b/crates/psrs-cli/tests/cli.rs index 20cd789a..09405f15 100644 --- a/crates/psrs-cli/tests/cli.rs +++ b/crates/psrs-cli/tests/cli.rs @@ -133,3 +133,77 @@ fn check_commands_print_typecheck_warnings() { let _ = std::fs::remove_dir_all(root); } + +#[test] +fn diagnose_writes_a_diagnostic_bundle_that_replays_outside_the_bundle() { + use std::time::{SystemTime, UNIX_EPOCH}; + + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock after UNIX epoch") + .as_nanos(); + let root = + std::env::temp_dir().join(format!("psrs-diagnose-cli-{}-{unique}", std::process::id())); + std::fs::create_dir_all(&root).expect("create diagnosis test directory"); + let source = root.join("Broken.purs"); + let snapshot = root.join("snapshot.json"); + std::fs::write(&source, "module Broken where\nmain = @\n").expect("write malformed source"); + + let result = Command::new(env!("CARGO_BIN_EXE_psrs")) + .args([ + "diagnose", + source.to_str().expect("source path is UTF-8"), + "--out", + snapshot.to_str().expect("snapshot path is UTF-8"), + ]) + .output() + .expect("run psrs diagnose"); + assert!( + result.status.success(), + "diagnosis command failed: {}", + String::from_utf8_lossy(&result.stderr) + ); + + let report: serde_json::Value = + serde_json::from_slice(&std::fs::read(&snapshot).expect("read diagnosis snapshot")) + .expect("parse diagnosis snapshot"); + let case = &report["cases"][0]; + assert_eq!(case["status"], "failed"); + assert_eq!(case["input_set_complete"], true); + let diagnostic = &case["diagnostics"][0]; + assert!(!diagnostic["stage"].as_str().unwrap_or_default().is_empty()); + assert!( + !diagnostic["message"] + .as_str() + .unwrap_or_default() + .is_empty() + ); + + let bundle = std::path::PathBuf::from( + case["bundle"] + .as_str() + .expect("failed case has bundle path"), + ); + assert!(bundle.join("case.json").is_file()); + assert!(bundle.join("inputs/0000-Broken.purs").is_file()); + let replay = Command::new("sh") + .arg(bundle.join("replay.sh")) + .current_dir(&root) + .output() + .expect("replay diagnosis bundle from outside its directory"); + assert!( + !replay.status.success(), + "broken source unexpectedly compiled" + ); + let replay_output = format!( + "{}{}", + String::from_utf8_lossy(&replay.stdout), + String::from_utf8_lossy(&replay.stderr) + ); + assert!( + replay_output.contains(diagnostic["message"].as_str().unwrap()), + "replay did not reproduce the recorded diagnostic: {replay_output}" + ); + + let _ = std::fs::remove_dir_all(root); +} diff --git a/crates/psrs-cli/tests/command_runner.rs b/crates/psrs-cli/tests/command_runner.rs new file mode 100644 index 00000000..8936a795 --- /dev/null +++ b/crates/psrs-cli/tests/command_runner.rs @@ -0,0 +1,245 @@ +use std::fs; +use std::path::PathBuf; +use std::process::{Command, Output}; +use std::sync::atomic::{AtomicUsize, Ordering}; + +const TYPES: &str = "module Types where\nnewtype Job a = Job (Unit -> a)\n"; +const RUNNER: &str = "module Runner where\nimport Types (Job(..))\nrun :: Job Int -> Int\nrun (Job action) = action unit\n"; +const MAIN: &str = + "module Main where\nimport Types (Job(..))\nmain :: Job Int\nmain = Job (\\_ -> 42)\n"; + +struct Package(PathBuf); + +impl Package { + fn new() -> Self { + static NEXT: AtomicUsize = AtomicUsize::new(0); + let root = std::env::temp_dir().join(format!( + "psrs-command-runner-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(root.join("lib")).unwrap(); + fs::create_dir_all(root.join("conformance")).unwrap(); + fs::write(root.join("upstream-lock.json"), "{}").unwrap(); + fs::write(root.join("lib/trusted"), "Prelude\nTypes\nRunner\n").unwrap(); + fs::write(root.join("lib/Prelude.purs"), "module Prelude where\n").unwrap(); + fs::write(root.join("lib/Types.purs"), TYPES).unwrap(); + fs::write(root.join("lib/Runner.purs"), RUNNER).unwrap(); + fs::write(root.join("Main.purs"), MAIN).unwrap(); + let package = Self(root); + package.configure("Runner", "run"); + package + } + + fn configure(&self, module: &str, function: &str) { + let manifest = serde_json::json!({ + "schema_version": 1, + "name": "psrs-stdlib", + "source_root": "lib", + "trusted_modules": "lib/trusted", + "upstream_lock": "upstream-lock.json", + "compiler_contract": { + "primitive_binding_protocol": 1, + "string_semantics": "unicode-scalar-utf8", + "int_semantics": "signed-i32", + "command_runner": { "module": module, "function": function } + } + }); + fs::write( + self.0.join("manifest.json"), + serde_json::to_vec(&manifest).unwrap(), + ) + .unwrap(); + } + + fn build(&self) -> Output { + Command::new(env!("CARGO_BIN_EXE_psrs")) + .env("PSRS_STDLIB_ROOT", &self.0) + .arg("build") + .arg(self.0.join("Main.purs")) + .arg("-o") + .arg(self.0.join("main.wasm")) + .output() + .unwrap() + } + + fn reject(&self, message: &str) { + let output = self.build(); + assert!(!output.status.success(), "invalid runner accepted"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains(message), "{stderr}"); + } + + fn execute(&self, expected: i32) { + let output = self.build(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let output = Command::new("wasmtime") + .arg("run") + .arg(self.0.join("main.wasm")) + .output(); + match output { + Ok(output) => assert_eq!( + output.status.code(), + Some(expected), + "{}", + String::from_utf8_lossy(&output.stderr) + ), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + assert_ne!( + std::env::var("PSRS_REQUIRE_WASMTIME").as_deref(), + Ok("1"), + "mandatory Wasmtime is missing" + ); + eprintln!("Wasmtime unavailable; execution skipped"); + } + Err(error) => panic!("Wasmtime failed: {error}"), + } + } +} + +impl Drop for Package { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +#[test] +fn reports_and_diagnosis_retain_the_configured_runner() { + const CHILD_ROOT: &str = "PSRS_COMMAND_REPORT_ROOT"; + if let Some(root) = std::env::var_os(CHILD_ROOT) { + let source = fs::read_to_string(PathBuf::from(root).join("Main.purs")).unwrap(); + let sources = [("Main.purs", source.as_str())]; + let report = psrs_driver::compile_program_sources_with_prelude_report(&sources); + assert!(report.artifact.is_some(), "{:?}", report.diagnostics); + assert!(report.dumps.core.unwrap().contains("$command_entry")); + let report = psrs_driver::compile_program_sources_with_prelude_diagnosis(&sources, true); + assert!(report.artifact.is_some(), "{:?}", report.diagnostics); + assert!(report.backend_trace.is_some()); + let frontend = report.frontend_trace.unwrap(); + assert!(frontend.output_core.is_some()); + assert!( + frontend + .source_names + .iter() + .any(|name| name.ends_with("Runner.purs")) + ); + return; + } + let package = Package::new(); + let output = Command::new(std::env::current_exe().unwrap()) + .args([ + "--exact", + "reports_and_diagnosis_retain_the_configured_runner", + "--nocapture", + ]) + .env(CHILD_ROOT, &package.0) + .env("PSRS_STDLIB_ROOT", &package.0) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn configured_source_runner_executes_an_unrelated_newtype() { + let package = Package::new(); + fs::write( + package.0.join("lib/Prelude.purs"), + "module Prelude where\nforeign import data Unrelated :: Type -> Type\n", + ) + .unwrap(); + fs::write( + package.0.join("Main.purs"), + MAIN.replace("import Types", "import Prelude (Unrelated)\nimport Types"), + ) + .unwrap(); + // Main neither imports Runner nor calls it: configuration must retain it. + // An unrelated opaque declaration must not activate any special handling. + package.execute(42); + fs::write( + package.0.join("Main.purs"), + "module Main where\nmain :: Int\nmain = 7\n", + ) + .unwrap(); + package.execute(7); +} + +#[test] +fn configured_runner_rejects_missing_identity_and_invalid_checked_types() { + let package = Package::new(); + package.configure("Missing", "run"); + package.reject("absent from the trusted package inventory"); + package.configure("Runner", "missing"); + package.reject("exactly one source declaration"); + package.configure("Runner", "run"); + fs::write( + package.0.join("lib/Runner.purs"), + "module Runner where\nimport Types (Job)\nrun :: Job Int -> Boolean\nrun _ = true\n", + ) + .unwrap(); + package.reject("must return Int"); + fs::write( + package.0.join("lib/Runner.purs"), + "module Runner where\nrun :: forall a. a -> Int\nrun _ = 0\n", + ) + .unwrap(); + package.reject("must be monomorphic"); + fs::write( + package.0.join("lib/Runner.purs"), + "module Runner where\nnewtype Other = Other Int\nrun :: Other -> Int\nrun (Other value) = value\n", + ) + .unwrap(); + package.reject("entry type disagrees"); +} + +#[test] +fn check_only_does_not_require_an_executable_runner_root() { + const CHILD_ROOT: &str = "PSRS_COMMAND_CHECK_ONLY_ROOT"; + if let Some(root) = std::env::var_os(CHILD_ROOT) { + let source = fs::read_to_string(PathBuf::from(root).join("Main.purs")).unwrap(); + psrs_driver::check_program_types_lenient_with_prelude(&[("Main.purs", &source)]).unwrap(); + let sources = [("Main.purs", source.as_str())]; + for report in [ + psrs_driver::compile_program_sources_with_prelude_report(&sources), + psrs_driver::compile_program_sources_with_prelude_diagnosis(&sources, true), + ] { + assert!(report.artifact.is_none()); + assert!(report.diagnostics.iter().any(|error| { + error.source == psrs_driver::DiagnosticOrigin::Library + && error + .diagnostic + .message + .contains("absent from the trusted package inventory") + })); + } + return; + } + let package = Package::new(); + package.configure("Missing", "run"); + // A child process isolates the package cache from other integration cases. + let output = Command::new(std::env::current_exe().unwrap()) + .args([ + "--exact", + "check_only_does_not_require_an_executable_runner_root", + "--nocapture", + ]) + .env(CHILD_ROOT, &package.0) + .env("PSRS_STDLIB_ROOT", &package.0) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + package.reject("absent from the trusted package inventory"); +} diff --git a/crates/psrs-cli/tests/diagnose_trace.rs b/crates/psrs-cli/tests/diagnose_trace.rs new file mode 100644 index 00000000..d43eac52 --- /dev/null +++ b/crates/psrs-cli/tests/diagnose_trace.rs @@ -0,0 +1,310 @@ +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; +use std::time::{SystemTime, UNIX_EPOCH}; + +fn test_dir(label: &str) -> PathBuf { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock after UNIX epoch") + .as_nanos(); + let path = std::env::temp_dir().join(format!("psrs-{label}-{}-{unique}", std::process::id())); + std::fs::create_dir_all(&path).expect("create CLI test directory"); + path +} + +fn diagnose(args: &[&str]) -> Output { + Command::new(env!("CARGO_BIN_EXE_psrs")) + .args(["diagnose"]) + .args(args) + .output() + .expect("run psrs diagnose") +} + +fn value(path: &Path) -> serde_json::Value { + serde_json::from_slice(&std::fs::read(path).expect("read JSON fixture")) + .expect("parse JSON fixture") +} + +#[test] +fn diagnose_trace_captures_pass_dumps_and_keeps_v1_status_comparison() { + let root = test_dir("diagnose-trace"); + let source = root.join("Main.purs"); + let manifest = root.join("manifest.json"); + let traced_manifest = root.join("trace.json"); + let changed_manifest = root.join("changed-trace.json"); + let legacy_manifest = root.join("legacy-v1.json"); + std::fs::write( + &source, + "module Main where\nimport Prelude\nmain = 40 + 2\n", + ) + .expect("write accepted source"); + + for (args, label) in [ + ( + vec![ + source.to_str().unwrap(), + "--out", + manifest.to_str().unwrap(), + ], + "manifest", + ), + ( + vec![ + source.to_str().unwrap(), + "--trace", + "--out", + traced_manifest.to_str().unwrap(), + ], + "trace", + ), + ] { + let result = diagnose(&args); + assert!( + result.status.success(), + "{label} diagnosis failed: {}", + String::from_utf8_lossy(&result.stderr) + ); + } + + let before = value(&manifest); + assert_eq!(before["schema_version"], 2); + assert_eq!(before["trace_mode"], "manifest"); + let before_case = &before["cases"][0]; + assert_eq!(before_case["status"], "passed"); + assert_eq!(before_case["trace"]["dumps_requested"], false); + assert_eq!( + before_case["trace"]["canonical_summaries"]["status"], + "unavailable" + ); + assert!(before_case["trace"]["executions"].as_array().unwrap().len() > 1); + let frontend = &before_case["trace"]["executions"][0]; + assert_eq!(frontend["id"], "frontend:e0"); + assert!( + frontend["inputs"] + .as_array() + .unwrap() + .contains(&"inputs:i0".into()) + ); + assert!( + frontend["inputs"] + .as_array() + .unwrap() + .contains(&"inputs:trusted_stdlib:i0".into()) + ); + let core = before_case["trace"]["artifacts"] + .as_array() + .unwrap() + .iter() + .find(|artifact| artifact["id"] == "backend:a0") + .unwrap(); + assert_eq!(core["state"], "produced"); + assert_eq!(core["producer"], "frontend:e0"); + assert!( + before_case["trace"]["artifacts"] + .as_array() + .unwrap() + .iter() + .all(|artifact| artifact["retained_dump"].is_null()) + ); + + let traced = value(&traced_manifest); + assert_eq!(traced["trace_mode"], "dumps"); + let traced_case = &traced["cases"][0]; + assert_eq!(traced_case["status"], "passed"); + assert_eq!(traced_case["trace"]["dumps_requested"], true); + let bundle = PathBuf::from(traced_case["bundle"].as_str().unwrap()); + for name in ["core.debug", "cc.debug", "mir.debug"] { + assert!(bundle.join(name).is_file(), "missing retained {name}"); + assert!( + traced_case["trace"]["artifacts"] + .as_array() + .unwrap() + .iter() + .any(|artifact| artifact["retained_dump"] == name) + ); + } + assert!(value(&bundle.join("case.json"))["trace"].is_object()); + let trace_replay = + std::fs::read_to_string(bundle.join("replay.sh")).expect("read trace replay script"); + assert!(trace_replay.contains("'diagnose'")); + assert!(trace_replay.contains(" build ")); + + let same_trace_compare = Command::new(env!("CARGO_BIN_EXE_psrs")) + .args([ + "diagnose", + "--compare", + manifest.to_str().unwrap(), + traced_manifest.to_str().unwrap(), + ]) + .output() + .expect("compare manifest and trace runs"); + assert!(same_trace_compare.status.success()); + let same_trace_report: serde_json::Value = + serde_json::from_slice(&same_trace_compare.stdout).expect("parse trace comparison"); + assert_eq!( + same_trace_report["changes"][0]["trace_comparison"]["status"], + "no_observed_pass_difference" + ); + assert_eq!( + same_trace_report["changes"][0]["trace_comparison"]["artifact_content"], + "unavailable_no_canonical_summary" + ); + + let mut changed = traced.clone(); + let passes = changed["cases"][0]["trace"]["executions"] + .as_array_mut() + .unwrap(); + passes[0]["status"] = "rejected".into(); + std::fs::write( + &changed_manifest, + serde_json::to_vec_pretty(&changed).unwrap(), + ) + .expect("write changed trace fixture"); + let changed_compare = Command::new(env!("CARGO_BIN_EXE_psrs")) + .args([ + "diagnose", + "--compare", + traced_manifest.to_str().unwrap(), + changed_manifest.to_str().unwrap(), + ]) + .output() + .expect("compare pass status change"); + assert!(changed_compare.status.success()); + let changed_report: serde_json::Value = + serde_json::from_slice(&changed_compare.stdout).expect("parse changed comparison"); + assert_eq!( + changed_report["changes"][0]["trace_comparison"]["first_pass_difference"]["kind"], + "pass_status_changed" + ); + + let mut legacy = before; + legacy["schema_version"] = 1.into(); + legacy.as_object_mut().unwrap().remove("environment"); + legacy.as_object_mut().unwrap().remove("trace_mode"); + legacy["cases"][0].as_object_mut().unwrap().remove("trace"); + std::fs::write( + &legacy_manifest, + serde_json::to_vec_pretty(&legacy).unwrap(), + ) + .expect("write v1 snapshot fixture"); + let comparison = Command::new(env!("CARGO_BIN_EXE_psrs")) + .args([ + "diagnose", + "--compare", + legacy_manifest.to_str().unwrap(), + traced_manifest.to_str().unwrap(), + ]) + .output() + .expect("compare v1 and v2 snapshots"); + assert!( + comparison.status.success(), + "v1 comparison failed: {}", + String::from_utf8_lossy(&comparison.stderr) + ); + let comparison_json: serde_json::Value = + serde_json::from_slice(&comparison.stdout).expect("parse compare report"); + assert_eq!( + comparison_json["observed_environment_compatibility"], + "unavailable_legacy_snapshot" + ); + assert_eq!( + comparison_json["build_toolchain_compatibility"], + "unavailable_not_embedded_in_compiler_binary" + ); + assert!(comparison_json["changes"][0]["trace_comparison"].is_null()); + + let _ = std::fs::remove_dir_all(root); +} + +#[test] +fn diagnose_input_list_is_ordered_exact_and_replayed_with_trace() { + let root = test_dir("diagnose-inputs"); + let main = root.join("Main.purs"); + let spare = root.join("Spare.purs"); + let helper = root.join("Helper.purs"); + let hidden = root.join("Hidden.purs"); + let snapshot = root.join("snapshot.json"); + std::fs::write( + &main, + "module Main where\nimport Prelude\nimport Spare\nimport Helper\nmain = spare + helper\n", + ) + .expect("write entry source"); + std::fs::write(&spare, "module Spare where\nspare = 1\n").expect("write first explicit input"); + std::fs::write( + &helper, + "module Helper where\nimport Hidden\nhelper = hidden\n", + ) + .expect("write second explicit input"); + std::fs::write(&hidden, "module Hidden where\nhidden = 2\n") + .expect("write unlisted adjacent module"); + + let result = diagnose(&[ + main.to_str().unwrap(), + "--input", + spare.to_str().unwrap(), + "--input", + helper.to_str().unwrap(), + "--trace", + "--out", + snapshot.to_str().unwrap(), + ]); + assert!( + result.status.success(), + "diagnosis command failed: {}", + String::from_utf8_lossy(&result.stderr) + ); + let report = value(&snapshot); + let case = &report["cases"][0]; + assert_eq!(case["status"], "failed"); + let sources = case["trace"]["artifacts"][0]["sources"] + .as_array() + .unwrap() + .iter() + .map(|source| source["logical_name"].as_str().unwrap()) + .collect::>(); + assert_eq!( + sources, + [ + main.to_str().unwrap(), + spare.to_str().unwrap(), + helper.to_str().unwrap() + ] + ); + assert!( + case["diagnostics"] + .as_array() + .unwrap() + .iter() + .any(|diagnostic| diagnostic["message"].as_str().unwrap().contains("Hidden")) + ); + + let bundle = PathBuf::from(case["bundle"].as_str().unwrap()); + let replay = Command::new("sh") + .arg(bundle.join("replay.sh")) + .current_dir(&root) + .output() + .expect("run trace replay"); + assert!( + !replay.status.success(), + "the unresolved import should fail" + ); + assert!( + bundle.join("replay-trace.json").is_file(), + "replay did not recapture a trace" + ); + + let duplicate = diagnose(&[ + main.to_str().unwrap(), + "--input", + helper.to_str().unwrap(), + "--input", + root.join(".").join("Helper.purs").to_str().unwrap(), + ]); + assert!(!duplicate.status.success()); + assert!(String::from_utf8_lossy(&duplicate.stderr).contains("duplicate diagnosis input")); + + let corpus_input = diagnose(&["--corpus", "passing", "--input", helper.to_str().unwrap()]); + assert!(!corpus_input.status.success()); + + let _ = std::fs::remove_dir_all(root); +} diff --git a/crates/psrs-cli/tests/link.rs b/crates/psrs-cli/tests/link.rs new file mode 100644 index 00000000..7265ba33 --- /dev/null +++ b/crates/psrs-cli/tests/link.rs @@ -0,0 +1,188 @@ +use std::{fs, process::Command}; + +const API: &str = "test:cli/api@1.0.0"; + +#[test] +fn link_uses_manifest_relative_pins_and_reports_the_closed_graph() { + let root = std::env::temp_dir().join(format!("psrs-cli-link-{}", std::process::id())); + let package = root.join("package"); + fs::create_dir_all(&package).unwrap(); + let application = wat::parse_str(format!( + r#"(component + (type $api (instance (export "value" (func (result u32))))) + (import "{API}" (instance $api (type $api))) + (export "{API}" (instance $api)))"# + )) + .unwrap(); + let provider = wat::parse_str(format!( + r#"(component + (core module $m (func (export "value") (result i32) i32.const 42)) + (core instance $m (instantiate $m)) + (func $value (result u32) (canon lift (core func $m "value"))) + (instance $api (export "value" (func $value))) + (export "{API}" (instance $api)))"# + )) + .unwrap(); + let app_path = root.join("app.wasm"); + let manifest_path = package.join("providers.json"); + let output_path = root.join("linked.wasm"); + let report_path = root.join("report.json"); + fs::write(&app_path, &application).unwrap(); + fs::write(package.join("provider.wasm"), &provider).unwrap(); + let mut manifest = serde_json::json!({ + "schema_version":1,"application_sha256":psrs_linker::sha256_hex(&application), + "providers":[{"id":"provider","path":"provider.wasm","sha256":psrs_linker::sha256_hex(&provider)}], + "bindings":[{"interface":API,"provider":"provider"}],"permitted_host_interfaces":[], + }); + fs::write(&manifest_path, serde_json::to_vec(&manifest).unwrap()).unwrap(); + let run = || { + Command::new(env!("CARGO_BIN_EXE_psrs")) + .arg("link") + .arg(&app_path) + .arg("--manifest") + .arg(&manifest_path) + .arg("-o") + .arg(&output_path) + .arg("--report") + .arg(&report_path) + .output() + .unwrap() + }; + let result = run(); + assert!(result.status.success(), "{result:?}"); + let report: serde_json::Value = + serde_json::from_slice(&fs::read(&report_path).unwrap()).unwrap(); + assert_eq!(report["external_world"], serde_json::json!([])); + assert_eq!(report["component_artifacts"].as_array().unwrap().len(), 2); + assert_eq!( + report["output_sha256"], + psrs_linker::sha256_hex(&fs::read(&output_path).unwrap()) + ); + fs::remove_file(&output_path).unwrap(); + fs::remove_file(&report_path).unwrap(); + manifest["providers"][0]["sha256"] = "0".repeat(64).into(); + fs::write(&manifest_path, serde_json::to_vec(&manifest).unwrap()).unwrap(); + let result = run(); + assert!(!result.status.success()); + assert!(String::from_utf8_lossy(&result.stderr).contains("digest")); + assert!(!output_path.exists()); + assert!(!report_path.exists()); + manifest["providers"][0]["sha256"] = psrs_linker::sha256_hex(&provider).into(); + manifest["permitted_host_interfaces"] = + serde_json::json!(["wasi:http/outgoing-handler@0.2.12"]); + fs::write(&manifest_path, serde_json::to_vec(&manifest).unwrap()).unwrap(); + let result = run(); + assert!(!result.status.success()); + assert!(String::from_utf8_lossy(&result.stderr).contains("target profile")); + assert!(!output_path.exists()); + fs::remove_dir_all(root).unwrap(); +} + +#[test] +fn source_build_composes_a_guest_and_joins_the_exact_artifact_lineage() { + let root = std::env::temp_dir().join(format!("psrs-cli-build-link-{}", std::process::id())); + fs::create_dir_all(&root).unwrap(); + let interface = "wasi:random/insecure-seed@0.2.12"; + let provider = wat::parse_str(format!( + r#"(component + (core module $m + (memory (export "memory") 1) + (func (export "insecure-seed") (result i32) + i32.const 0 i64.const 5 i64.store + i32.const 8 i64.const 37 i64.store i32.const 0)) + (core instance $m (instantiate $m)) + (func $seed (result (tuple u64 u64)) + (canon lift (core func $m "insecure-seed") (memory $m "memory"))) + (instance $api (export "insecure-seed" (func $seed))) + (export "{interface}" (instance $api)))"# + )) + .unwrap(); + fs::write(root.join("seed.wasm"), &provider).unwrap(); + fs::write(root.join("Main.purs"), "module Main where\nimport Prelude\nimport Effect.Unsafe (unsafePerformEffect)\nimport WASI.Random (insecureSeed)\nmain = let seed = unsafePerformEffect insecureSeed in seed._1 + seed._2\n").unwrap(); + let mut manifest = serde_json::json!({ + "schema_version":1, + "providers":[{"id":"seed", "path":"seed.wasm", "sha256":psrs_linker::sha256_hex(&provider)}], + "bindings":[{"interface":interface, "provider":"seed"}], + "permitted_host_interfaces":["wasi:cli/exit@0.2.12"], + }); + let manifest_path = root.join("providers.json"); + let output = root.join("linked.wasm"); + let report = root.join("report.json"); + fs::write(&manifest_path, serde_json::to_vec(&manifest).unwrap()).unwrap(); + let run = || { + Command::new(env!("CARGO_BIN_EXE_psrs")) + .arg("build") + .arg(root.join("Main.purs")) + .arg("--manifest") + .arg(&manifest_path) + .arg("-o") + .arg(&output) + .arg("--report") + .arg(&report) + .output() + .unwrap() + }; + let result = run(); + assert!(result.status.success(), "{result:?}"); + let evidence: serde_json::Value = serde_json::from_slice(&fs::read(&report).unwrap()).unwrap(); + assert_eq!( + evidence["composition"]["component_artifacts"][0][1], + evidence["application_sha256"] + ); + assert_eq!( + evidence["composition"]["output_sha256"], + evidence["output_sha256"] + ); + assert_eq!( + evidence["output_sha256"], + psrs_linker::sha256_hex(&fs::read(&output).unwrap()) + ); + assert_eq!( + evidence["source_compilation"]["component_output"]["sha256"], + evidence["application_sha256"] + ); + let output_id = &evidence["source_compilation"]["component_output"]["artifact"]; + assert!( + evidence["source_compilation"]["artifacts"] + .as_array() + .unwrap() + .iter() + .any(|artifact| &artifact["id"] == output_id + && artifact["representation"] == "component_binary") + ); + let executions = evidence["source_compilation"]["executions"] + .as_array() + .unwrap(); + assert!( + executions + .iter() + .any(|pass| pass["pass_key"] == "backend.target.plan"), + "{executions:?}" + ); + let executed = Command::new("wasmtime") + .arg("run") + .arg(&output) + .output() + .expect("Wasmtime required"); + assert_eq!(executed.status.code(), Some(42), "{executed:?}"); + assert!(executed.stdout.is_empty()); + assert!(executed.stderr.is_empty()); + fs::remove_file(&output).unwrap(); + fs::remove_file(&report).unwrap(); + manifest["application_sha256"] = "0".repeat(64).into(); + fs::write(&manifest_path, serde_json::to_vec(&manifest).unwrap()).unwrap(); + let result = run(); + assert!(!result.status.success()); + assert!(String::from_utf8_lossy(&result.stderr).contains("digest")); + assert!(!output.exists()); + let rejection: serde_json::Value = serde_json::from_slice(&fs::read(&report).unwrap()).unwrap(); + assert_eq!(rejection["status"], "rejected"); + assert!( + rejection["composition"]["diagnostic"] + .as_str() + .unwrap() + .contains("digest") + ); + assert!(rejection.get("output_sha256").is_none()); + fs::remove_dir_all(root).unwrap(); +} diff --git a/crates/psrs-core/src/command.rs b/crates/psrs-core/src/command.rs new file mode 100644 index 00000000..d656cc9e --- /dev/null +++ b/crates/psrs-core/src/command.rs @@ -0,0 +1,129 @@ +//! Checked command entry adaptation through ordinary library functions. +use crate::{Declaration, Expr, ExprKind, Module, Type, TypeConstructor, VerifyError, arrow_parts}; +use psrs_hir::SymbolId; + +/// Normalize a selected entry with a source runner of type `T -> Int`. +/// Both identities must have been resolved by the caller. This neither names +/// an effect type nor synthesizes its operations. Failure leaves Core unchanged. +pub fn normalize_entry( + module: &mut Module, + entry: SymbolId, + runner: SymbolId, +) -> Result> { + module.verify()?; + let source = module + .declarations + .iter() + .find(|decl| decl.symbol == entry) + .ok_or_else(|| { + error( + module, + entry, + "selected command entry has no source declaration", + ) + })?; + let runner_decl = module + .declarations + .iter() + .find(|decl| decl.symbol == runner) + .ok_or_else(|| { + error( + module, + entry, + "configured command runner has no source declaration", + ) + })?; + if !crate::types::free_variables(module, runner_decl.ty).is_empty() { + return Err(error( + module, + runner, + "configured command runner must be monomorphic", + )); + } + let (input, result) = arrow_parts(&module.types, runner_decl.ty).ok_or_else(|| { + error( + module, + runner, + "configured command runner must have type T -> Int", + ) + })?; + if module.types.get(result.0 as usize) != Some(&Type::Constructor(TypeConstructor::Int)) { + return Err(error( + module, + runner, + "configured command runner must return Int", + )); + } + if !crate::types::free_variables(module, source.ty).is_empty() { + return Err(error(module, entry, "command entry must be monomorphic")); + } + if module.types.get(source.ty.0 as usize) == Some(&Type::Constructor(TypeConstructor::Int)) { + module.entry = Some(entry); + return Ok(entry); + } + if !module.types_equivalent(source.ty, input) { + return Err(error( + module, + entry, + "command entry type disagrees with the configured runner input", + )); + } + let index = module + .declarations + .iter() + .map(|decl| decl.symbol) + .chain(module.externals.iter().map(|external| external.symbol)) + .chain( + module + .constructors + .iter() + .map(|constructor| constructor.symbol), + ) + .filter(|symbol| symbol.module == entry.module) + .map(|symbol| symbol.index) + .max() + .unwrap_or(0) + .checked_add(1) + .ok_or_else(|| error(module, entry, "command entry symbol space is exhausted"))?; + let symbol = SymbolId::new(entry.module, index); + let span = source.name_span; + let global = |symbol, ty| Expr { + kind: ExprKind::Global(symbol), + ty, + span, + }; + let wrapper = Declaration { + symbol, + name: "$command_entry".into(), + name_span: span, + quantified: vec![], + ty: result, + value: Expr { + kind: ExprKind::Application( + Box::new(global(runner, runner_decl.ty)), + Box::new(global(entry, source.ty)), + ), + ty: result, + span, + }, + span, + }; + let mut candidate = module.clone(); + candidate.declarations.push(wrapper); + candidate.entry = Some(symbol); + candidate.verify()?; + *module = candidate; + Ok(symbol) +} + +fn error(module: &Module, symbol: SymbolId, message: &'static str) -> Vec { + vec![VerifyError { + module: symbol.module, + span: module + .declarations + .iter() + .find(|decl| decl.symbol == symbol) + .map_or(module.span, |decl| decl.name_span), + message, + }] +} diff --git a/crates/psrs-core/src/control.rs b/crates/psrs-core/src/control.rs new file mode 100644 index 00000000..07139378 --- /dev/null +++ b/crates/psrs-core/src/control.rs @@ -0,0 +1,272 @@ +//! The authoritative checked contract for the eight generic control operations. +use crate::{Module, Type, TypeConstructor, TypeId}; +use psrs_hir::Intrinsic; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ControlDomain { + pub request: TypeId, + pub answer: TypeId, +} + +/// Constructed only after carrier, callable and rigid quantified-answer checks. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CheckedControlContract { + operation: Intrinsic, + result: TypeId, + arguments: Vec, + source: Option, + target: Option, +} + +impl CheckedControlContract { + pub fn operation(&self) -> Intrinsic { + self.operation + } + pub fn result(&self) -> TypeId { + self.result + } + pub fn arguments(&self) -> &[TypeId] { + &self.arguments + } + pub fn source(&self) -> Option { + self.source + } + pub fn target(&self) -> Option { + self.target + } +} + +pub fn checked_control_contract( + module: &Module, + intrinsic: Intrinsic, + result: TypeId, + arguments: &[TypeId], +) -> Result { + if intrinsic.descriptor().category != psrs_hir::IntrinsicCategory::Control { + return Err("operation is not a control primitive"); + } + if arguments.len() != intrinsic.descriptor().arity as usize { + return Err("control primitive has an incompatible argument count"); + } + if intrinsic == Intrinsic::ControlHandle { + check_handle(module, result, arguments)?; + } else { + check(module, intrinsic, result, arguments)?; + } + let source_type = match intrinsic { + Intrinsic::ControlBind => Some(arguments[0]), + Intrinsic::ControlRun | Intrinsic::ControlHandle => Some(arguments[2]), + Intrinsic::ControlOnce => call(module, arguments[0], 1).map(|(_, result)| result), + _ => None, + }; + let source = source_type.and_then(|id| domain(module, id)).or_else(|| { + if matches!( + intrinsic, + Intrinsic::ControlResume | Intrinsic::ControlCancel + ) { + let parts = carrier_parts(module, arguments[0], psrs_hir::TypeId::RESUMPTION, 3)?; + Some(ControlDomain { + request: parts[0], + answer: parts[2], + }) + } else { + None + } + }); + Ok(CheckedControlContract { + operation: intrinsic, + result, + arguments: arguments.to_vec(), + source, + target: domain(module, result), + }) +} + +fn domain(module: &Module, id: TypeId) -> Option { + let (request, answer) = control_parts(module, id)?; + Some(ControlDomain { request, answer }) +} + +fn carrier_parts( + module: &Module, + id: TypeId, + carrier: psrs_hir::TypeId, + count: usize, +) -> Option> { + let (_, mut id) = crate::scheme_parts(&module.types, id)?; + let mut arguments = Vec::new(); + while let Some(Type::Application(head, argument)) = module.types.get(id.0 as usize) { + if arguments.len() >= count { + return None; + } + arguments.push(*argument); + id = *head; + } + if arguments.len() != count + || !matches!(module.types.get(id.0 as usize), + Some(Type::Constructor(TypeConstructor::User(actual))) if *actual == carrier) + { + return None; + } + arguments.reverse(); + Some(arguments) +} + +fn control_parts(module: &Module, id: TypeId) -> Option<(TypeId, TypeId)> { + let parts = carrier_parts(module, id, psrs_hir::TypeId::CONTROL, 2)?; + Some((parts[0], parts[1])) +} + +fn pair_parts(module: &Module, id: TypeId) -> Option<(TypeId, TypeId)> { + let fields = module.record_fields(id)?; + if fields.len() != 2 { + return None; + } + Some(( + fields.iter().find(|(label, _)| label == "_1")?.1, + fields.iter().find(|(label, _)| label == "_2")?.1, + )) +} + +fn call(module: &Module, id: TypeId, count: usize) -> Option<(Vec, TypeId)> { + let (parameters, result) = crate::call_parts(&module.types, id).ok()?; + (parameters.len() == count).then_some((parameters, result)) +} + +fn request_parts(module: &Module, clause: TypeId, request: TypeId) -> Option<(TypeId, TypeId)> { + let (variables, _) = crate::scheme_parts(&module.types, clause)?; + let Type::Application(functor, answer) = module.types.get(request.0 as usize)? else { + return None; + }; + let Type::Variable(variable) = module.types.get(answer.0 as usize)? else { + return None; + }; + (variables.len() == 1 && variables.contains(variable)).then_some((*functor, *answer)) +} + +fn check( + module: &Module, + intrinsic: Intrinsic, + use_type: TypeId, + arguments: &[TypeId], +) -> Result<(), &'static str> { + let equal = |a, b| crate::verify::equivalent_types(a, b, module); + let valid = (|| { + use psrs_hir::Intrinsic::*; + let args = arguments; + match intrinsic { + ControlReturn => { + let (_, answer) = control_parts(module, use_type)?; + equal(args[0], answer).then_some(()) + } + ControlPerform => { + let (functor, answer) = control_parts(module, use_type)?; + let Type::Application(f, a) = module.types.get(args[0].0 as usize)? else { + return None; + }; + (equal(*f, functor) && equal(*a, answer)).then_some(()) + } + ControlBind => { + let (source, answer) = control_parts(module, args[0])?; + let (target, _) = control_parts(module, use_type)?; + let (params, result) = call(module, args[1], 1)?; + (equal(source, target) && equal(params[0], answer) && equal(result, use_type)) + .then_some(()) + } + ControlRun => { + let (functor, answer) = control_parts(module, args[2])?; + let (params, result) = call(module, args[1], 3)?; + let (request, x) = request_parts(module, args[1], params[1])?; + let (raw, source) = call(module, params[2], 1)?; + let (state, next) = pair_parts(module, result)?; + let (out_state, out_answer) = pair_parts(module, use_type)?; + (equal(params[0], args[0]) + && equal(request, functor) + && equal(raw[0], x) + && equal(source, args[2]) + && equal(state, args[0]) + && equal(next, args[2]) + && equal(out_state, args[0]) + && equal(out_answer, answer)) + .then_some(()) + } + ControlOnce => { + let owner = carrier_parts(module, use_type, psrs_hir::TypeId::RESUMPTION, 3)?; + let (params, result) = call(module, args[0], 1)?; + let (functor, answer) = control_parts(module, result)?; + (equal(params[0], owner[1]) && equal(functor, owner[0]) && equal(answer, owner[2])) + .then_some(()) + } + ControlResume => { + let owner = carrier_parts(module, args[0], psrs_hir::TypeId::RESUMPTION, 3)?; + let (functor, answer) = control_parts(module, use_type)?; + (equal(args[1], owner[1]) && equal(functor, owner[0]) && equal(answer, owner[2])) + .then_some(()) + } + ControlCancel => { + carrier_parts(module, args[0], psrs_hir::TypeId::RESUMPTION, 3)?; + matches!( + module.types.get(use_type.0 as usize), + Some(Type::Constructor(TypeConstructor::Unit)) + ) + .then_some(()) + } + _ => None, + } + })() + .is_some(); + if valid { + Ok(()) + } else { + Err("control primitive does not preserve its checked carrier and callable contracts") + } +} + +fn check_handle( + module: &Module, + use_type: TypeId, + arguments: &[TypeId], +) -> Result<(), &'static str> { + let valid = (|| { + let (source_request, source_answer) = control_parts(module, arguments[2])?; + control_parts(module, use_type)?; + let (ret_parameters, ret_result) = crate::call_parts(&module.types, arguments[0]).ok()?; + if ret_parameters.len() != 1 { + return None; + } + let (variables, body) = crate::scheme_parts(&module.types, arguments[1])?; + let (parameters, result) = crate::call_parts(&module.types, body).ok()?; + if variables.len() != 1 || parameters.len() != 2 { + return None; + } + let Type::Application(request, answer) = module.types.get(parameters[0].0 as usize)? else { + return None; + }; + let Type::Variable(variable) = module.types.get(answer.0 as usize)? else { + return None; + }; + if !variables.contains(variable) { + return None; + } + let (raw_parameters, raw_result) = crate::call_parts(&module.types, parameters[1]).ok()?; + if raw_parameters.len() != 1 { + return None; + } + // Use the checking owner's single rigid relation for all repeated + // positions. Quantified answer variables must remain the same. + let equal = |a, b| crate::verify::equivalent_types(a, b, module); + (equal(ret_parameters[0], source_answer) + && equal(ret_result, use_type) + && equal(*request, source_request) + && equal(raw_parameters[0], *answer) + && equal(raw_result, arguments[2]) + && equal(result, use_type)) + .then_some(()) + })() + .is_some(); + if valid { + Ok(()) + } else { + Err("control handle clauses do not preserve their checked source and target contracts") + } +} diff --git a/crates/psrs-core/src/effect/mod.rs b/crates/psrs-core/src/effect/mod.rs deleted file mode 100644 index db0657a4..00000000 --- a/crates/psrs-core/src/effect/mod.rs +++ /dev/null @@ -1,306 +0,0 @@ -//! Representation lowering for the library effect type. -//! -//! `Effect τ` stays an abstract application through type checking and Typed -//! Core. This pass is the only one that matches that type's identity. It -//! replaces each application with a closure whose parameter list is the -//! runtime token, it supplies bodies for `pure`, `bind`, and `runEffect`, and -//! it records every closure it wrote so the shape can be verified before any -//! later pass sees it. - -mod operations; -mod supplies; - -use crate::{Module, Type, TypeConstructor, TypeId, VerifyError, closure_parts}; -use psrs_hir::{LocalId, ModuleId, SymbolId, TypeId as HirTypeId}; -use std::collections::HashSet; - -use operations::synthesize_operations; -use supplies::LocalSupply; - -const EFFECT_INTERFACE: &str = "psrs:effect"; - -/// The resolved identity of the trusted effect library interface. The driver -/// creates this only from its trusted library prefix and carries it to P8. -/// Consumers must not reconstruct trust from qualified names or opacity. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TrustedEffect { - pub effect_type: HirTypeId, - pub operations: Vec, -} - -/// A source command entry that the frontend has checked to have type -/// `Effect Unit`. P8 wraps it after lowering the abstract effect type. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct EffectCommandEntry { - pub symbol: SymbolId, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct EffectCompilation { - pub trusted: TrustedEffect, - pub command_entry: Option, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct EffectOperationBinding { - pub operation: EffectOperation, - pub symbol: SymbolId, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum EffectOperation { - Pure, - Bind, - Run, - Trap, -} - -impl EffectOperation { - pub fn wit_function(self) -> &'static str { - match self { - Self::Pure => "pure", - Self::Bind => "bind", - Self::Run => "run", - Self::Trap => "trap", - } - } -} - -/// An import signature classified while its result still names the abstract -/// trusted Effect constructor. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct EffectImportType { - pub quantified: Vec, - pub parameters: Vec, - pub application: TypeId, - pub payload: TypeId, -} - -/// One representation closure written for an `Effect` application. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct EffectClosure { - /// The closure type node the lowering wrote. - pub ty: TypeId, - /// The runtime token the closure must take as its only parameter. - pub token: TypeId, - /// The lowered result of the `Effect` application the closure replaced. - pub result: TypeId, -} - -/// Symbols whose abstract `psrs:effect` imports became ordinary declarations. -#[derive(Clone, Debug, Default)] -pub struct EffectLowering { - pub synthesized: Vec, - /// The representation closures written for `Effect` applications. - pub closures: Vec, -} - -impl EffectLowering { - /// Rejects a representation closure whose parameter list is not - /// `[token]`, or whose result is not the lowered effect result. - /// - /// No later pass recognizes `Effect`, so this Core check is the only place - /// that can reject such a closure. It runs over the closures this lowering - /// wrote, before closure conversion and before any encoding. - pub fn verify(&self, module: &Module) -> Result<(), Vec> { - let mut errors = Vec::new(); - for closure in &self.closures { - let Some((parameters, result)) = closure_parts(&module.types, closure.ty) else { - errors.push(verification_error( - module, - "a lowered effect application is not a closure", - )); - continue; - }; - if parameters.len() != 1 || parameters[0] != closure.token { - errors.push(verification_error( - module, - "a lowered effect closure must take only the runtime token", - )); - } - if result != closure.result { - errors.push(verification_error( - module, - "a lowered effect closure must return the lowered effect result", - )); - } - } - if errors.is_empty() { - Ok(()) - } else { - Err(errors) - } - } -} - -/// Lowers library effects in a linked Core module. -/// -/// Programs that do not contain the opaque `Prelude.Effect` type are unchanged. -pub fn lower_effects( - module: &mut Module, - trusted: &TrustedEffect, -) -> Result> { - let effect = trusted.effect_type; - if !module.opaque_ids.contains(&effect) { - return Err(vec![verification_error( - module, - "trusted Effect identity is missing or is not an opaque type", - )]); - } - let token = intern(module, Type::Constructor(TypeConstructor::Int)); - let closures = rewrite_effect_applications(module, effect, token); - let synthesized = synthesize_operations(module, token, trusted)?; - let lowering = EffectLowering { - synthesized, - closures, - }; - lowering.verify(module)?; - Ok(lowering) -} - -/// Classifies a WIT source signature by its abstract result type. The returned -/// structure is evidence for a later wrapper; no closure shape is inspected. -pub fn classify_effect_import( - module: &Module, - ty: TypeId, - effect: HirTypeId, -) -> Result, &'static str> { - let mut current = ty; - let mut quantified = Vec::new(); - let mut parameters = Vec::new(); - let mut visited = HashSet::new(); - loop { - if !visited.insert(current) { - return Err("cyclic type spine in effect import signature"); - } - let Some(node) = module.types.get(current.0 as usize) else { - return Err("effect import signature references an invalid type id"); - }; - if let Type::ForAll { variables, body } = node { - quantified.extend_from_slice(variables); - current = *body; - continue; - } - if let Type::Application(function, argument) = node - && (module.types.get(function.0 as usize).is_none() - || module.types.get(argument.0 as usize).is_none()) - { - return Err("effect import signature contains an invalid application spine"); - } - if let Some((function, payload)) = effect_application(module, current, effect) { - return Ok(Some(EffectImportType { - quantified, - parameters, - application: function, - payload, - })); - } - let Some((parameter, result)) = crate::arrow_parts(&module.types, current) else { - return Ok(None); - }; - if module.types.get(parameter.0 as usize).is_none() - || module.types.get(result.0 as usize).is_none() - { - return Err("effect import signature contains an invalid function spine"); - } - parameters.push(parameter); - current = result; - } -} - -pub fn effect_application( - module: &Module, - id: TypeId, - effect: HirTypeId, -) -> Option<(TypeId, TypeId)> { - let Type::Application(function, payload) = module.types.get(id.0 as usize)? else { - return None; - }; - is_effect_constructor(module, *function, effect).then_some((id, *payload)) -} - -/// Writes `Type::Closure` over each `Effect` application and records the shape -/// each written closure must keep. -fn rewrite_effect_applications( - module: &mut Module, - effect: HirTypeId, - token: TypeId, -) -> Vec { - let targets = module - .types - .iter() - .enumerate() - .filter_map(|(index, ty)| { - let Type::Application(function, argument) = ty else { - return None; - }; - is_effect_constructor(module, *function, effect).then_some((index, *argument)) - }) - .collect::>(); - let mut closures = Vec::with_capacity(targets.len()); - for (index, result) in targets { - module.types[index] = Type::Closure { - parameters: vec![token], - result, - }; - closures.push(EffectClosure { - ty: TypeId(index as u32), - token, - result, - }); - } - closures -} - -fn is_effect_constructor(module: &Module, id: TypeId, effect: HirTypeId) -> bool { - matches!( - module.types.get(id.0 as usize), - Some(Type::Constructor(TypeConstructor::User(id))) if *id == effect - ) -} - -fn verification_error(module: &Module, message: &'static str) -> VerifyError { - VerifyError { - module: module.id, - span: module.span, - message, - } -} - -fn intern(module: &mut Module, ty: Type) -> TypeId { - if let Some(index) = module.types.iter().position(|existing| existing == &ty) { - return TypeId(index as u32); - } - module.types.push(ty); - TypeId((module.types.len() - 1) as u32) -} - -/// Builds `parameters -> result` in the module type table. -pub fn function_type(module: &mut Module, parameters: &[TypeId], result: TypeId) -> TypeId { - let mut ty = result; - for parameter in parameters.iter().rev() { - ty = operations::arrow(module, *parameter, ty); - } - ty -} - -/// The next foreign-symbol index above the symbols already in the module. -pub fn fresh_foreign_symbol(module: &Module) -> SymbolId { - let mut next = psrs_hir::FOREIGN_SYMBOL_BASE; - for external in &module.externals { - if external.symbol.module == ModuleId::INTRINSICS { - next = next.max(external.symbol.index.saturating_add(1)); - } - } - for declaration in &module.declarations { - if declaration.symbol.module == ModuleId::INTRINSICS { - next = next.max(declaration.symbol.index.saturating_add(1)); - } - } - SymbolId::new(ModuleId::INTRINSICS, next) -} - -/// A fresh local id above every binder already in the module. -pub fn fresh_local(module: &Module) -> LocalId { - LocalSupply::new(module).fresh() -} diff --git a/crates/psrs-core/src/effect/operations.rs b/crates/psrs-core/src/effect/operations.rs deleted file mode 100644 index 7af14f12..00000000 --- a/crates/psrs-core/src/effect/operations.rs +++ /dev/null @@ -1,340 +0,0 @@ -//! Ordinary declarations for the abstract `psrs:effect` operations. -//! -//! `lower_effects` replaces the library's opaque imports with the values the -//! design specifies: `pure` returns a closure over the token, `bind` runs the -//! first effect before the continuation, `run` applies the closure to the -//! integer `0`, and `trap` is the effect that escapes instead of returning. - -use super::supplies::{LocalSupply, VariableSupply}; -use super::{EFFECT_INTERFACE, TrustedEffect, intern}; -use crate::{Binder, Declaration, Expr, ExprKind, Module, Type, TypeConstructor, TypeId}; -use psrs_hir::{ExternalKind, LocalId, SymbolId, TypeVariableId}; -use psrs_span::TextRange; - -/// Synthesizes `pure`, `bind`, `run`, and `trap`, dropping the abstract imports -/// they replace. Returns the symbols that became ordinary declarations. -pub(super) fn synthesize_operations( - module: &mut Module, - token: TypeId, - trusted: &TrustedEffect, -) -> Result, Vec> { - let mut operations = Vec::new(); - let mut seen_symbols = std::collections::HashSet::new(); - let mut seen_operations = std::collections::HashSet::new(); - for binding in &trusted.operations { - if !seen_symbols.insert(binding.symbol) || !seen_operations.insert(binding.operation) { - return Err(vec![verification_error( - module, - "trusted Effect operation bindings are duplicated", - )]); - } - let Some((index, external)) = module - .externals - .iter() - .enumerate() - .find(|(_, external)| external.symbol == binding.symbol) - else { - return Err(vec![verification_error( - module, - "trusted Effect operation binding has no external declaration", - )]); - }; - let ExternalKind::Wit { - interface, - function, - } = &external.kind - else { - return Err(vec![verification_error( - module, - "trusted Effect operation is not a WIT import", - )]); - }; - if interface != EFFECT_INTERFACE || function != binding.operation.wit_function() { - return Err(vec![verification_error( - module, - "trusted Effect operation identity does not match its WIT binding", - )]); - } - operations.push(( - index, - binding.symbol, - external.name.clone(), - function.clone(), - external_span(external), - )); - } - if module.externals.iter().any(|external| { - matches!( - &external.kind, - ExternalKind::Wit { interface, .. } if interface == EFFECT_INTERFACE - ) && !seen_symbols.contains(&external.symbol) - }) { - return Err(vec![verification_error( - module, - "Effect WIT import is missing from the trusted operation bindings", - )]); - } - let mut locals = LocalSupply::new(module); - let mut variables = VariableSupply::new(module); - let mut synthesized = Vec::new(); - let mut remove = Vec::new(); - for (index, symbol, name, function, span) in operations { - let declaration = match function.as_str() { - "pure" => Some(pure_declaration( - module, - symbol, - &name, - span, - token, - &mut locals, - &mut variables, - )), - "bind" => Some(bind_declaration( - module, - symbol, - &name, - span, - token, - &mut locals, - &mut variables, - )), - "run" => Some(run_declaration( - module, - symbol, - &name, - span, - token, - &mut locals, - &mut variables, - )), - "trap" => Some(trap_declaration( - module, - symbol, - &name, - span, - token, - &mut locals, - )), - _ => None, - }; - let Some(declaration) = declaration else { - return Err(vec![verification_error( - module, - "trusted Effect operation has no lowering rule", - )]); - }; - module.declarations.push(declaration); - synthesized.push(symbol); - remove.push(index); - } - remove.sort_unstable(); - for index in remove.into_iter().rev() { - module.externals.remove(index); - } - module - .external_types - .retain(|external| !seen_symbols.contains(&external.symbol)); - Ok(synthesized) -} - -fn verification_error(module: &Module, message: &'static str) -> crate::VerifyError { - crate::VerifyError { - module: module.id, - span: module.span, - message, - } -} - -fn pure_declaration( - module: &mut Module, - symbol: SymbolId, - name: &str, - span: TextRange, - token: TypeId, - locals: &mut LocalSupply, - variables: &mut VariableSupply, -) -> Declaration { - let parameter = variables.fresh(); - let parameter_ty = intern(module, Type::Variable(parameter)); - let result = closure_type(module, token, parameter_ty); - let ty = arrow(module, parameter_ty, result); - let value_local = locals.fresh(); - let token_local = locals.fresh(); - let body = expr(ExprKind::Local(value_local), parameter_ty, span); - let closure = lambda(token_local, "token", token, body, result, span); - let value = lambda(value_local, "value", parameter_ty, closure, ty, span); - declaration(symbol, name, vec![parameter], ty, value, span) -} - -fn bind_declaration( - module: &mut Module, - symbol: SymbolId, - name: &str, - span: TextRange, - token: TypeId, - locals: &mut LocalSupply, - variables: &mut VariableSupply, -) -> Declaration { - let first_var = variables.fresh(); - let next_var = variables.fresh(); - let first_ty = intern(module, Type::Variable(first_var)); - let next_ty = intern(module, Type::Variable(next_var)); - let first_effect = closure_type(module, token, first_ty); - let next_effect = closure_type(module, token, next_ty); - let continuation = arrow(module, first_ty, next_effect); - let suspended = arrow(module, continuation, next_effect); - let ty = arrow(module, first_effect, suspended); - let first_local = locals.fresh(); - let next_local = locals.fresh(); - let token_local = locals.fresh(); - let ran_first = application( - expr(ExprKind::Local(first_local), first_effect, span), - expr(ExprKind::Local(token_local), token, span), - first_ty, - span, - ); - let ran_next = application( - expr(ExprKind::Local(next_local), continuation, span), - ran_first, - next_effect, - span, - ); - let body = application( - ran_next, - expr(ExprKind::Local(token_local), token, span), - next_ty, - span, - ); - let closure = lambda(token_local, "token", token, body, next_effect, span); - let with_next = lambda(next_local, "next", continuation, closure, suspended, span); - let value = lambda(first_local, "first", first_effect, with_next, ty, span); - declaration(symbol, name, vec![first_var, next_var], ty, value, span) -} - -fn run_declaration( - module: &mut Module, - symbol: SymbolId, - name: &str, - span: TextRange, - token: TypeId, - locals: &mut LocalSupply, - variables: &mut VariableSupply, -) -> Declaration { - let result_var = variables.fresh(); - let result_ty = intern(module, Type::Variable(result_var)); - let action_ty = closure_type(module, token, result_ty); - let ty = arrow(module, action_ty, result_ty); - let action = locals.fresh(); - let token_value = expr(ExprKind::Integer(0), token, span); - let body = application( - expr(ExprKind::Local(action), action_ty, span), - token_value, - result_ty, - span, - ); - let value = lambda(action, "action", action_ty, body, ty, span); - declaration(symbol, name, vec![result_var], ty, value, span) -} - -/// The `trap :: Effect Unit` operation: an effect over the runtime token whose -/// body never returns, so running it escapes instead of producing `Unit`. -/// -/// It is a closure rather than a bare trap so that referencing the value stays -/// inert, exactly as `pure` is: an `Effect` only does something when the entry -/// applies it to a token. `Effect` is abstract, so this is the only place that -/// can state that an uncaught failure is a target trap. -fn trap_declaration( - module: &mut Module, - symbol: SymbolId, - name: &str, - span: TextRange, - token: TypeId, - locals: &mut LocalSupply, -) -> Declaration { - let unit = intern(module, Type::Constructor(TypeConstructor::Unit)); - let action = closure_type(module, token, unit); - let token_local = locals.fresh(); - let body = expr(ExprKind::Trap, unit, span); - let value = lambda(token_local, "token", token, body, action, span); - declaration(symbol, name, Vec::new(), action, value, span) -} - -fn declaration( - symbol: SymbolId, - name: &str, - quantified: Vec, - ty: TypeId, - value: Expr, - span: TextRange, -) -> Declaration { - Declaration { - symbol, - name: name.to_string(), - name_span: span, - quantified, - ty, - value, - span, - } -} - -fn lambda( - id: LocalId, - name: &str, - ty: TypeId, - body: Expr, - function_ty: TypeId, - span: TextRange, -) -> Expr { - expr( - ExprKind::Lambda { - binder: Binder { - id, - name: name.to_string(), - ty, - span, - }, - body: Box::new(body), - }, - function_ty, - span, - ) -} - -fn application(function: Expr, argument: Expr, ty: TypeId, span: TextRange) -> Expr { - expr( - ExprKind::Application(Box::new(function), Box::new(argument)), - ty, - span, - ) -} - -fn expr(kind: ExprKind, ty: TypeId, span: TextRange) -> Expr { - Expr { kind, ty, span } -} - -fn external_span(external: &psrs_hir::ExternalSymbol) -> TextRange { - external - .signature - .as_ref() - .map(|signature| signature.span) - .unwrap_or_default() -} - -/// Builds the token closure type `[token] -> result` in the module type table. -pub(super) fn closure_type(module: &mut Module, token: TypeId, result: TypeId) -> TypeId { - intern( - module, - Type::Closure { - parameters: vec![token], - result, - }, - ) -} - -/// Builds `parameter -> result` in the module type table. -pub(super) fn arrow(module: &mut Module, parameter: TypeId, result: TypeId) -> TypeId { - let function = intern(module, Type::Constructor(TypeConstructor::Function)); - let partial = intern(module, Type::Application(function, parameter)); - intern(module, Type::Application(partial, result)) -} diff --git a/crates/psrs-core/src/effect/supplies.rs b/crates/psrs-core/src/effect/supplies.rs deleted file mode 100644 index 4a75c508..00000000 --- a/crates/psrs-core/src/effect/supplies.rs +++ /dev/null @@ -1,115 +0,0 @@ -//! Identifier supplies used while representation lowering adds declarations. - -use super::Module; -use crate::{Expr, ExprKind, Type}; -use psrs_hir::{LocalId, TypeVariableId}; - -/// A local id above every binder the module already holds. -pub(super) struct LocalSupply { - next: u32, -} - -impl LocalSupply { - pub(super) fn new(module: &Module) -> Self { - let mut next = 0; - for declaration in &module.declarations { - next = next.max(max_local(&declaration.value) + 1); - } - Self { next } - } - - pub(super) fn fresh(&mut self) -> LocalId { - let id = LocalId(self.next); - self.next += 1; - id - } -} - -fn max_local(expression: &Expr) -> u32 { - match &expression.kind { - ExprKind::Local(id) => id.0, - ExprKind::Lambda { binder, body } => binder.id.0.max(max_local(body)), - ExprKind::Application(function, argument) => max_local(function).max(max_local(argument)), - ExprKind::Let { bindings, body } => bindings - .iter() - .map(|binding| binding.binder.id.0.max(max_local(&binding.value))) - .chain(std::iter::once(max_local(body))) - .max() - .unwrap_or(0), - ExprKind::Case { - scrutinee, - branches, - } => branches - .iter() - .map(|branch| max_local(&branch.value)) - .chain(std::iter::once(max_local(scrutinee))) - .max() - .unwrap_or(0), - ExprKind::If { - condition, - then_branch, - else_branch, - } => max_local(condition) - .max(max_local(then_branch)) - .max(max_local(else_branch)), - ExprKind::Constructor { arguments, .. } - | ExprKind::IntrinsicCall { arguments, .. } - | ExprKind::Array { - elements: arguments, - } => arguments.iter().map(max_local).max().unwrap_or(0), - ExprKind::Record { fields } => fields - .iter() - .map(|(_, value)| max_local(value)) - .max() - .unwrap_or(0), - ExprKind::RecordUpdate { record, fields } => fields - .iter() - .map(|(_, value)| max_local(value)) - .chain(std::iter::once(max_local(record))) - .max() - .unwrap_or(0), - ExprKind::FieldAccess { record, .. } - | ExprKind::RepresentationCast { value: record, .. } => max_local(record), - ExprKind::Global(_) - | ExprKind::Integer(_) - | ExprKind::Number(_) - | ExprKind::Boolean(_) - | ExprKind::String(_) - | ExprKind::Char(_) => 0, - ExprKind::Unit | ExprKind::Trap => 0, - } -} - -/// A type variable above every binder the module already holds. -pub(super) struct VariableSupply { - next: u32, -} - -impl VariableSupply { - pub(super) fn new(module: &Module) -> Self { - let mut next = 0; - for ty in &module.types { - match ty { - Type::Variable(variable) => next = next.max(variable.0 + 1), - Type::ForAll { variables, .. } => { - for variable in variables { - next = next.max(variable.0 + 1); - } - } - _ => {} - } - } - for declaration in &module.declarations { - for variable in &declaration.quantified { - next = next.max(variable.0 + 1); - } - } - Self { next } - } - - pub(super) fn fresh(&mut self) -> TypeVariableId { - let id = TypeVariableId(self.next); - self.next += 1; - id - } -} diff --git a/crates/psrs-core/src/instantiation/local_rows.rs b/crates/psrs-core/src/instantiation/local_rows.rs new file mode 100644 index 00000000..dbb398fb --- /dev/null +++ b/crates/psrs-core/src/instantiation/local_rows.rs @@ -0,0 +1,357 @@ +//! Lower finite closed uses of row-polymorphic local lambdas through checked +//! instantiation. Residual open uses retain their source scheme and remain an +//! explicit unsupported layout at the representation boundary. +use crate::locals::{FreshLocals, clone_with_fresh_locals}; +use crate::{ + Binding, Expr, ExprKind, LocalId, Module, Pattern, PatternKind, Type, TypeId, VerifyError, +}; +use std::collections::{HashMap, HashSet}; + +/// Materializes closed row instantiations of nonrecursive local lambdas. +/// This preparation has no optimization budget: every eligible closed use is +/// lowered. It changes only lambda creation, preserving evaluation and capture +/// of surrounding computations. Core checks the input and transformed output. +pub fn instantiate_local_rows( + mut module: Module, + source: Option<&Module>, +) -> Result> { + module.verify_with_source(source.unwrap_or(&module))?; + for index in 0..module.declarations.len() { + let mut expression = module.declarations[index].value.clone(); + let mut fresh = FreshLocals::for_declaration(&expression); + rewrite( + &mut expression, + &mut module, + &mut fresh, + &mut HashMap::new(), + ); + module.declarations[index].value = expression; + } + module.verify_with_source(source.unwrap_or(&module))?; + Ok(module) +} + +fn rewrite( + expr: &mut Expr, + module: &mut Module, + fresh: &mut FreshLocals, + scope: &mut HashMap, +) { + psrs_span::with_sufficient_stack(|| rewrite_inner(expr, module, fresh, scope)) +} + +fn rewrite_inner( + expr: &mut Expr, + module: &mut Module, + fresh: &mut FreshLocals, + scope: &mut HashMap, +) { + if let ExprKind::Local(id) = expr.kind { + if let Some(binding) = scope.get(&id).cloned() + && let Some(value) = instantiate(module, &binding, expr.ty, fresh) + { + *expr = value; + rewrite(expr, module, fresh, scope); + } + return; + } + match &mut expr.kind { + ExprKind::Lambda { binder, body } => { + let previous = scope.remove(&binder.id); + rewrite(body, module, fresh, scope); + if let Some(binding) = previous { + scope.insert(binder.id, binding); + } + } + ExprKind::Let { bindings, body } => { + let dependencies = bindings + .iter() + .map(|binding| { + ( + binding.binder.id, + bindings + .iter() + .filter(|other| uses(&binding.value, other.binder.id)) + .map(|other| other.binder.id) + .collect::>(), + ) + }) + .collect::>(); + let mut previous = Vec::new(); + for binding in bindings.iter() { + previous.push((binding.binder.id, scope.remove(&binding.binder.id))); + // Restrict this preparation to nonrecursive lambdas. Keeping a + // recursive source binding is an explicit layout obligation. + let recursive = + dependencies + .get(&binding.binder.id) + .is_some_and(|dependencies_from_binding| { + dependencies_from_binding.iter().any(|id| { + reaches(*id, binding.binder.id, &dependencies, &mut HashSet::new()) + }) + }); + if !recursive + && !quantifiers(module, binding).is_empty() + && matches!(binding.value.kind, ExprKind::Lambda { .. }) + { + scope.insert(binding.binder.id, binding.clone()); + } + } + for binding in bindings.iter_mut() { + rewrite(&mut binding.value, module, fresh, scope); + } + rewrite(body, module, fresh, scope); + let removable = bindings + .iter() + .filter(|binding| { + scope.contains_key(&binding.binder.id) + && !uses(body, binding.binder.id) + && !bindings + .iter() + .any(|other| uses(&other.value, binding.binder.id)) + }) + .map(|binding| binding.binder.id) + .collect::>(); + bindings.retain(|binding| !removable.contains(&binding.binder.id)); + for (id, binding) in previous { + scope.remove(&id); + if let Some(binding) = binding { + scope.insert(id, binding); + } + } + } + ExprKind::Case { + scrutinee, + branches, + } => { + rewrite(scrutinee, module, fresh, scope); + for branch in branches { + let mut ids = Vec::new(); + pattern_ids(&branch.pattern, &mut ids); + let previous = ids + .into_iter() + .map(|id| (id, scope.remove(&id))) + .collect::>(); + rewrite(&mut branch.value, module, fresh, scope); + for (id, binding) in previous { + if let Some(binding) = binding { + scope.insert(id, binding); + } + } + } + } + _ => { + for child in children(expr) { + rewrite(child, module, fresh, scope); + } + } + } +} + +fn instantiate( + module: &mut Module, + binding: &Binding, + use_type: TypeId, + fresh: &mut FreshLocals, +) -> Option { + let rows = { + let quantified = quantifiers(module, binding); + let proof = module.checked_instantiation(binding.binder.ty, &quantified, use_type)?; + quantified + .iter() + .map(|variable| { + let row = proof.row(*variable)?; + if row.tail.is_some() { + return None; + } + Some((*variable, row.fields)) + }) + .collect::>>()? + }; + let mut replacements = HashMap::new(); + let empty = module + .types + .iter() + .position(|ty| matches!(ty, Type::RowEmpty))?; + for (variable, fields) in rows { + let mut tail = TypeId(empty as u32); + for (label, ty) in fields.into_iter().rev() { + let node = Type::RowExtend { label, ty, tail }; + tail = if let Some(index) = module.types.iter().position(|ty| *ty == node) { + TypeId(index as u32) + } else { + let id = TypeId(u32::try_from(module.types.len()).ok()?); + module.types.push(node); + id + }; + } + replacements.insert(variable, tail); + } + let value = super::substitution::instantiate_expression(module, &binding.value, &replacements)?; + clone_with_fresh_locals(&value, fresh) +} + +fn quantifiers(module: &Module, binding: &Binding) -> Vec { + let mut variables = binding.quantified.clone(); + let mut ty = binding.binder.ty; + while let Some(Type::ForAll { + variables: bound, + body, + }) = module.types.get(ty.0 as usize) + { + variables.extend(bound); + ty = *body; + } + variables +} + +fn children(expr: &mut Expr) -> Vec<&mut Expr> { + match &mut expr.kind { + ExprKind::Constructor { arguments, .. } + | ExprKind::IntrinsicCall { arguments, .. } + | ExprKind::Array { + elements: arguments, + } => arguments.iter_mut().collect(), + ExprKind::Record { fields } => fields.iter_mut().map(|(_, value)| value).collect(), + ExprKind::RecordUpdate { record, fields } => std::iter::once(record.as_mut()) + .chain(fields.iter_mut().map(|(_, value)| value)) + .collect(), + ExprKind::FieldAccess { record, .. } + | ExprKind::RepresentationCast { value: record, .. } => vec![record], + ExprKind::Application(function, argument) => vec![function, argument], + ExprKind::Lambda { body, .. } => vec![body], + ExprKind::Let { bindings, body } => bindings + .iter_mut() + .map(|binding| &mut binding.value) + .chain(std::iter::once(body.as_mut())) + .collect(), + ExprKind::If { + condition, + then_branch, + else_branch, + } => vec![condition, then_branch, else_branch], + ExprKind::Case { + scrutinee, + branches, + } => std::iter::once(scrutinee.as_mut()) + .chain(branches.iter_mut().map(|branch| &mut branch.value)) + .collect(), + ExprKind::Local(_) + | ExprKind::Global(_) + | ExprKind::Integer(_) + | ExprKind::Number(_) + | ExprKind::Boolean(_) + | ExprKind::String(_) + | ExprKind::Char(_) + | ExprKind::Unit + | ExprKind::Trap => Vec::new(), + } +} + +fn uses(expr: &Expr, id: LocalId) -> bool { + // The traversal is scope aware, including malformed/reused local identities + // accepted as lexical shadowing by Core's verifier. + match &expr.kind { + ExprKind::Local(found) => *found == id, + ExprKind::Lambda { binder, .. } if binder.id == id => false, + ExprKind::Let { bindings, .. } + if bindings.iter().any(|binding| binding.binder.id == id) => + { + false + } + ExprKind::Case { + scrutinee, + branches, + } => { + uses(scrutinee, id) + || branches.iter().any(|branch| { + let mut ids = Vec::new(); + pattern_ids(&branch.pattern, &mut ids); + !ids.contains(&id) && uses(&branch.value, id) + }) + } + _ => children_ref(expr).into_iter().any(|child| uses(child, id)), + } +} + +fn reaches( + current: LocalId, + target: LocalId, + dependencies: &HashMap>, + seen: &mut HashSet, +) -> bool { + current == target + || (seen.insert(current) + && dependencies.get(¤t).is_some_and(|next| { + next.iter() + .any(|id| reaches(*id, target, dependencies, seen)) + })) +} + +fn children_ref(expr: &Expr) -> Vec<&Expr> { + match &expr.kind { + ExprKind::Constructor { arguments, .. } + | ExprKind::IntrinsicCall { arguments, .. } + | ExprKind::Array { + elements: arguments, + } => arguments.iter().collect(), + ExprKind::Record { fields } => fields.iter().map(|(_, value)| value).collect(), + ExprKind::RecordUpdate { record, fields } => std::iter::once(record.as_ref()) + .chain(fields.iter().map(|(_, value)| value)) + .collect(), + ExprKind::FieldAccess { record, .. } + | ExprKind::RepresentationCast { value: record, .. } => vec![record], + ExprKind::Application(function, argument) => vec![function, argument], + ExprKind::Lambda { body, .. } => vec![body], + ExprKind::Let { bindings, body } => bindings + .iter() + .map(|binding| &binding.value) + .chain(std::iter::once(body.as_ref())) + .collect(), + ExprKind::If { + condition, + then_branch, + else_branch, + } => vec![condition, then_branch, else_branch], + ExprKind::Case { + scrutinee, + branches, + } => std::iter::once(scrutinee.as_ref()) + .chain(branches.iter().map(|branch| &branch.value)) + .collect(), + ExprKind::Local(_) + | ExprKind::Global(_) + | ExprKind::Integer(_) + | ExprKind::Number(_) + | ExprKind::Boolean(_) + | ExprKind::String(_) + | ExprKind::Char(_) + | ExprKind::Unit + | ExprKind::Trap => Vec::new(), + } +} + +fn pattern_ids(pattern: &Pattern, ids: &mut Vec) { + match &pattern.kind { + PatternKind::Var { id, .. } => ids.push(*id), + PatternKind::Named { id, pattern } => { + ids.push(*id); + pattern_ids(pattern, ids); + } + PatternKind::Array { elements } + | PatternKind::Constructor { + arguments: elements, + .. + } => { + for pattern in elements { + pattern_ids(pattern, ids); + } + } + PatternKind::Record { fields } => { + for (_, pattern) in fields { + pattern_ids(pattern, ids); + } + } + PatternKind::Wildcard | PatternKind::Literal { .. } => {} + } +} diff --git a/crates/psrs-core/src/instantiation/mod.rs b/crates/psrs-core/src/instantiation/mod.rs new file mode 100644 index 00000000..e94f9f5c --- /dev/null +++ b/crates/psrs-core/src/instantiation/mod.rs @@ -0,0 +1,86 @@ +//! Read-only instantiation evidence and checked materialization. +mod local_rows; +pub(crate) mod substitution; +pub use local_rows::instantiate_local_rows; + +use crate::{Module, Type, TypeConstructor, TypeId}; +use psrs_hir::TypeVariableId; +use std::collections::{HashMap, HashSet}; + +/// A successful declaration-scheme/use check. Borrowing the source module keeps +/// its type arena immutable for the lifetime of the evidence. +pub struct Instantiation<'a> { + pub(crate) module: &'a Module, + pub(crate) replacements: HashMap, + pub(crate) rows: HashMap, +} + +/// A checked row residual, including solutions with no existing arena node. +/// Field and tail ids refer to the immutable source module of the evidence. +#[derive(Clone, Debug)] +pub struct RowInstantiation { + pub fields: Vec<(String, TypeId)>, + pub tail: Option, +} + +impl std::fmt::Debug for Instantiation<'_> { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("Instantiation") + .field("replacements", &self.replacements) + .field("rows", &self.rows) + .finish() + } +} + +impl Instantiation<'_> { + /// Returns the checking owner's solution for a quantified row variable. + /// A residual is retained even when its fields have no arena row node. + pub fn row(&self, variable: TypeVariableId) -> Option { + if let Some(row) = self.rows.get(&variable) { + return Some(row.clone()); + } + let mut current = *self.replacements.get(&variable)?; + let mut seen = HashSet::new(); + loop { + if !seen.insert(current) { + return None; + } + if let Some(Type::Variable(variable)) = self.module.types.get(current.0 as usize) { + if let Some(row) = self.rows.get(variable) { + return Some(row.clone()); + } + current = *self.replacements.get(variable)?; + } else { + let (fields, tail) = crate::row_fields(&self.module.types, current)?; + return Some(RowInstantiation { fields, tail }); + } + } + } + + /// Resolves an abstract constructor to its checked constructor application. + /// Fixed arguments belong to the borrowed source arena. An unsolved head, + /// row solution or substitution cycle is not a constructor identity. + pub fn constructor(&self, variable: TypeVariableId) -> Option<(TypeConstructor, Vec)> { + let mut current = *self.replacements.get(&variable)?; + let mut arguments = Vec::new(); + let mut seen = HashSet::new(); + loop { + if !seen.insert(current) { + return None; + } + match self.module.types.get(current.0 as usize)? { + Type::Variable(variable) => current = *self.replacements.get(variable)?, + Type::Application(function, argument) => { + arguments.push(*argument); + current = *function; + } + Type::Constructor(constructor) => { + arguments.reverse(); + return Some((*constructor, arguments)); + } + _ => return None, + } + } + } +} diff --git a/crates/psrs-core/src/opt/specialize/types/substitution.rs b/crates/psrs-core/src/instantiation/substitution.rs similarity index 90% rename from crates/psrs-core/src/opt/specialize/types/substitution.rs rename to crates/psrs-core/src/instantiation/substitution.rs index aa6d1629..685e1fe7 100644 --- a/crates/psrs-core/src/opt/specialize/types/substitution.rs +++ b/crates/psrs-core/src/instantiation/substitution.rs @@ -4,7 +4,7 @@ use crate::{ use psrs_hir::{SymbolId, TypeVariableId}; use std::collections::{HashMap, HashSet}; -pub(in crate::opt::specialize) fn instantiate_declaration( +pub(crate) fn instantiate_declaration( module: &mut Module, declaration: &Declaration, replacements: &HashMap, @@ -13,13 +13,7 @@ pub(in crate::opt::specialize) fn instantiate_declaration( ) -> Option { let mut replacement_free_variables = HashSet::new(); for id in replacements.values() { - free_variables( - module, - *id, - &mut HashSet::new(), - &mut HashSet::new(), - &mut replacement_free_variables, - ); + replacement_free_variables.extend(crate::types::free_variables(module, *id)); } let used_variables = all_variables(module); let mut substitution = TypeSubstitution { @@ -30,6 +24,7 @@ pub(in crate::opt::specialize) fn instantiate_declaration( shadowed: HashSet::new(), renamed: HashMap::new(), active: HashSet::new(), + discharged: HashSet::new(), }; let ty = substitution.type_id(declaration.ty)?; let value = substitution.expression(&declaration.value)?; @@ -44,6 +39,29 @@ pub(in crate::opt::specialize) fn instantiate_declaration( }) } +pub(super) fn instantiate_expression( + module: &mut Module, + expression: &Expr, + replacements: &HashMap, +) -> Option { + let mut replacement_free_variables = HashSet::new(); + for id in replacements.values() { + replacement_free_variables.extend(crate::types::free_variables(module, *id)); + } + let used_variables = all_variables(module); + TypeSubstitution { + module, + replacements, + replacement_free_variables, + used_variables, + shadowed: HashSet::new(), + renamed: HashMap::new(), + active: HashSet::new(), + discharged: replacements.keys().copied().collect(), + } + .expression(expression) +} + struct TypeSubstitution<'a> { module: &'a mut Module, replacements: &'a HashMap, @@ -52,6 +70,7 @@ struct TypeSubstitution<'a> { shadowed: HashSet, renamed: HashMap, active: HashSet, + discharged: HashSet, } impl TypeSubstitution<'_> { @@ -77,6 +96,13 @@ impl TypeSubstitution<'_> { let argument = self.type_id(argument)?; self.intern(Type::Application(function, argument))? } + Type::ForAll { variables, body } + if variables + .iter() + .all(|variable| self.discharged.contains(variable)) => + { + self.type_id(body)? + } Type::ForAll { variables, body } => { let mut renamed_variables = variables.clone(); let mut previous_renamings = Vec::new(); @@ -160,6 +186,10 @@ impl TypeSubstitution<'_> { } fn expression(&mut self, expression: &Expr) -> Option { + psrs_span::with_sufficient_stack(|| self.expression_inner(expression)) + } + + fn expression_inner(&mut self, expression: &Expr) -> Option { let kind = match &expression.kind { ExprKind::Local(id) => ExprKind::Local(*id), ExprKind::Global(symbol) => ExprKind::Global(*symbol), @@ -331,44 +361,6 @@ impl TypeSubstitution<'_> { } } -fn free_variables( - module: &Module, - id: TypeId, - bound: &mut HashSet, - active: &mut HashSet, - out: &mut HashSet, -) { - if !active.insert(id) { - return; - } - match module.types.get(id.0 as usize) { - Some(Type::Variable(variable)) if !bound.contains(variable) => { - out.insert(*variable); - } - Some(Type::Application(function, argument)) => { - free_variables(module, *function, bound, active, out); - free_variables(module, *argument, bound, active, out); - } - Some(Type::ForAll { variables, body }) => { - let inserted = variables - .iter() - .copied() - .filter(|variable| bound.insert(*variable)) - .collect::>(); - free_variables(module, *body, bound, active, out); - for variable in inserted { - bound.remove(&variable); - } - } - Some(Type::RowExtend { ty, tail, .. }) => { - free_variables(module, *ty, bound, active, out); - free_variables(module, *tail, bound, active, out); - } - _ => {} - } - active.remove(&id); -} - fn all_variables(module: &Module) -> HashSet { let mut variables = HashSet::new(); for ty in &module.types { diff --git a/crates/psrs-core/src/lib.rs b/crates/psrs-core/src/lib.rs index 54d64aec..d6507c71 100644 --- a/crates/psrs-core/src/lib.rs +++ b/crates/psrs-core/src/lib.rs @@ -1,17 +1,30 @@ +pub mod command; +mod control; +pub use control::{CheckedControlContract, ControlDomain, checked_control_contract}; +/// The Core owner's rigid type relation, including lexical alpha-equivalence. +pub fn equivalent_types(left: TypeId, right: TypeId, module: &Module) -> bool { + verify::equivalent_types(left, right, module) +} pub mod dictionary; -pub mod effect; +mod instantiation; mod link; +mod locals; mod lower; pub mod opt; mod pattern; +pub mod primitive; mod records; +pub use instantiation::{Instantiation, RowInstantiation, instantiate_local_rows}; mod types; mod verify; pub use link::{link, prune_unreachable}; pub use pattern::{Literal, Pattern, PatternKind}; pub use records::{record_row, row_fields}; -pub use types::{Type, TypeConstructor, TypeId, arrow_parts, closure_parts, forall_parts}; +pub use types::{ + Type, TypeConstructor, TypeId, arrow_parts, call_parts, closure_parts, forall_parts, + scheme_parts, +}; use psrs_hir::{ CaseBranchCoverage, ExternalSymbol, Intrinsic, LocalId, ModuleId, SymbolId, @@ -36,8 +49,9 @@ pub struct ConstructorInfo { pub parameters: Vec, } -/// The checked, synonym-expanded signature of a WIT value import. Compiler -/// intrinsics use registry-owned contracts and do not appear in this table. +/// The checked, synonym-expanded signature of a source foreign value import. +/// Bootstrap intrinsics use registry-owned contracts and do not appear here; +/// explicit primitive bindings retain their source signature until linking. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ExternalType { pub symbol: SymbolId, @@ -53,7 +67,7 @@ pub struct Module { pub id: ModuleId, pub name: String, pub externals: Vec, - /// Checked WIT import signatures projected from THIR. Backend binding and + /// Checked source foreign signatures projected from THIR. Backend binding and /// representation lowering must consume these schemes rather than /// reconstructing types from raw HIR annotations. pub external_types: Vec, @@ -111,7 +125,7 @@ pub struct Binding { pub span: TextRange, } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Debug, Eq)] pub struct Expr { pub kind: ExprKind, pub ty: TypeId, @@ -191,6 +205,32 @@ pub enum ExprKind { }, } +impl Clone for Expr { + fn clone(&self) -> Self { + psrs_span::with_sufficient_stack(|| clone_expr(self)) + } +} + +impl PartialEq for Expr { + fn eq(&self, other: &Self) -> bool { + psrs_span::with_sufficient_stack(|| expr_eq(self, other)) + } +} + +#[inline(never)] +fn clone_expr(expression: &Expr) -> Expr { + Expr { + kind: expression.kind.clone(), + ty: expression.ty, + span: expression.span, + } +} + +#[inline(never)] +fn expr_eq(left: &Expr, right: &Expr) -> bool { + left.ty == right.ty && left.span == right.span && left.kind == right.kind +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct CaseBranch { pub pattern: Pattern, @@ -225,7 +265,35 @@ pub fn lower_module_unverified(module: psrs_thir::Module) -> Result Result<(), Vec> { - verify::module(self) + verify::module(self, None) + } + + /// Verifies this physical module while checking declaration instantiation + /// against `source` whenever both type ids still exist there. + /// + /// Representation lowering may replace a source application with a closure + /// at the same type id. Those relations stay on the immutable source + /// module. Types allocated only in this module, including synthesized + /// operation closures, are checked against this module's own type table. + pub fn verify_with_source(&self, source: &Module) -> Result<(), Vec> { + verify::module(self, Some(source)) + } + + /// Checks a declaration use with the same relation as Core verification, + /// retaining its solved constructor bindings without changing acceptance. + pub fn checked_instantiation( + &self, + scheme: TypeId, + quantified: &[TypeVariableId], + instance: TypeId, + ) -> Option> { + verify::instantiation(self, scheme, quantified, instance) + } + + /// Compares types with the same semantic relation used by Core verification, + /// including separately interned alpha-equivalent quantified types. + pub fn types_equivalent(&self, left: TypeId, right: TypeId) -> bool { + verify::equivalent_types(left, right, self) } /// The hidden calling-convention parameter count registered for a callable @@ -242,7 +310,9 @@ impl Module { /// applied to it, in order. A non-constructor head yields `None`. pub fn applied_constructor(&self, mut id: TypeId) -> Option<(TypeConstructor, Vec)> { let mut arguments = Vec::new(); + let mut remaining = self.types.len(); while let Some(Type::Application(function, argument)) = self.types.get(id.0 as usize) { + remaining = remaining.checked_sub(1)?; arguments.push(*argument); id = *function; } diff --git a/crates/psrs-core/src/link/mod.rs b/crates/psrs-core/src/link/mod.rs index ca4fdcae..d911bacf 100644 --- a/crates/psrs-core/src/link/mod.rs +++ b/crates/psrs-core/src/link/mod.rs @@ -184,6 +184,10 @@ fn shift_declaration(declaration: Declaration, offset: u32, variable_offset: u32 } fn shift_expr(expression: Expr, offset: u32, variable_offset: u32) -> Expr { + psrs_span::with_sufficient_stack(|| shift_expr_inner(expression, offset, variable_offset)) +} + +fn shift_expr_inner(expression: Expr, offset: u32, variable_offset: u32) -> Expr { Expr { kind: shift::shift_kind(expression.kind, offset, variable_offset), ty: shift_id(expression.ty, offset), @@ -235,6 +239,8 @@ fn shift_pattern(pattern: crate::Pattern, offset: u32) -> crate::Pattern { /// library declarations it reaches. pub fn prune_unreachable(module: &mut Module, root: SymbolId) { let mut reachable = HashSet::new(); + let mut used_types = HashSet::new(); + let mut visited_types = HashSet::new(); let mut work = vec![root]; while let Some(symbol) = work.pop() { if !reachable.insert(symbol) { @@ -245,27 +251,52 @@ pub fn prune_unreachable(module: &mut Module, root: SymbolId) { .iter() .find(|declaration| declaration.symbol == symbol) { - collect_references(&declaration.value, &mut work); + collect_core_type_ids(declaration.ty, module, &mut used_types, &mut visited_types); + collect_references( + &declaration.value, + &mut work, + module, + &mut used_types, + &mut visited_types, + ); } } module .declarations .retain(|declaration| reachable.contains(&declaration.symbol)); - // A reachable constructor keeps every case of its type so the variant - // layout stays complete. Unused library types drop out with their cases. - let mut used_types = module - .constructors + // Static library imports follow the same executable dependency graph as + // library declarations. Target protocols (WIT and explicit primitives) + // retain their source-wide conformance obligations even when unused. + let discarded_imports = module + .externals .iter() - .filter(|constructor| reachable.contains(&constructor.symbol)) - .map(|constructor| constructor.type_id) + .filter(|external| { + matches!(external.kind, psrs_hir::ExternalKind::Library { .. }) + && !reachable.contains(&external.symbol) + }) + .map(|external| external.symbol) .collect::>(); - // A foreign import's declared signature can name a library type the program - // never constructs or matches, such as a newtype resource wrapper. Its - // constructors are still needed to resolve and lay out the binding, so keep - // every type the external signatures mention. - let mut visited = HashSet::new(); + module + .externals + .retain(|external| !discarded_imports.contains(&external.symbol)); + module + .external_types + .retain(|external| !discarded_imports.contains(&external.symbol)); + // A reachable constructor keeps every case of its type so the variant + // layout stays complete. Unused library types drop out with their cases. + used_types.extend( + module + .constructors + .iter() + .filter(|constructor| reachable.contains(&constructor.symbol)) + .map(|constructor| constructor.type_id), + ); + // Reachable declaration and foreign-import signatures can mention library + // types that the program never constructs or matches directly. Their + // constructors are still needed to lay out those signatures (for example, + // the nullary `Proxy` constructor in a class method type). for external in &module.external_types { - collect_core_type_ids(external.ty, module, &mut used_types, &mut visited); + collect_core_type_ids(external.ty, module, &mut used_types, &mut visited_types); } loop { let before = used_types.len(); @@ -276,7 +307,7 @@ pub fn prune_unreachable(module: &mut Module, root: SymbolId) { .flat_map(|constructor| constructor.field_types.iter().copied()) .collect::>(); for field_type in field_types { - collect_core_type_ids(field_type, module, &mut used_types, &mut visited); + collect_core_type_ids(field_type, module, &mut used_types, &mut visited_types); } if used_types.len() == before { break; @@ -321,13 +352,32 @@ fn collect_core_type_ids( } } -fn collect_references(expression: &Expr, out: &mut Vec) { +fn collect_references( + expression: &Expr, + out: &mut Vec, + module: &Module, + used_types: &mut HashSet, + visited_types: &mut HashSet, +) { + psrs_span::with_sufficient_stack(|| { + collect_references_inner(expression, out, module, used_types, visited_types) + }) +} + +fn collect_references_inner( + expression: &Expr, + out: &mut Vec, + module: &Module, + used_types: &mut HashSet, + visited_types: &mut HashSet, +) { + collect_core_type_ids(expression.ty, module, used_types, visited_types); match &expression.kind { ExprKind::Global(symbol) => out.push(*symbol), ExprKind::Constructor { symbol, arguments } => { out.push(*symbol); for argument in arguments { - collect_references(argument, out); + collect_references(argument, out, module, used_types, visited_types); } } ExprKind::Local(_) @@ -335,83 +385,107 @@ fn collect_references(expression: &Expr, out: &mut Vec) { | ExprKind::Number(_) | ExprKind::Boolean(_) | ExprKind::String(_) - | ExprKind::Char(_) => {} - ExprKind::Unit | ExprKind::Trap => {} + | ExprKind::Char(_) + | ExprKind::Unit + | ExprKind::Trap => {} ExprKind::Array { elements } => { for element in elements { - collect_references(element, out); + collect_references(element, out, module, used_types, visited_types); } } ExprKind::Record { fields } => { for (_, value) in fields { - collect_references(value, out); + collect_references(value, out, module, used_types, visited_types); } } ExprKind::RecordUpdate { record, fields } => { - collect_references(record, out); + collect_references(record, out, module, used_types, visited_types); for (_, value) in fields { - collect_references(value, out); + collect_references(value, out, module, used_types, visited_types); } } - ExprKind::FieldAccess { record, .. } => collect_references(record, out), - ExprKind::RepresentationCast { value, .. } => collect_references(value, out), + ExprKind::FieldAccess { record, .. } => { + collect_references(record, out, module, used_types, visited_types) + } + ExprKind::RepresentationCast { + value, + source_type, + target_type, + } => { + collect_core_type_ids(*source_type, module, used_types, visited_types); + collect_core_type_ids(*target_type, module, used_types, visited_types); + collect_references(value, out, module, used_types, visited_types); + } ExprKind::IntrinsicCall { arguments, .. } => { for argument in arguments { - collect_references(argument, out); + collect_references(argument, out, module, used_types, visited_types); } } ExprKind::Application(left, right) => { - collect_references(left, out); - collect_references(right, out); + collect_references(left, out, module, used_types, visited_types); + collect_references(right, out, module, used_types, visited_types); + } + ExprKind::Lambda { binder, body } => { + collect_core_type_ids(binder.ty, module, used_types, visited_types); + collect_references(body, out, module, used_types, visited_types); } - ExprKind::Lambda { body, .. } => collect_references(body, out), ExprKind::Let { bindings, body } => { for binding in bindings { - collect_references(&binding.value, out); + collect_core_type_ids(binding.binder.ty, module, used_types, visited_types); + collect_references(&binding.value, out, module, used_types, visited_types); } - collect_references(body, out); + collect_references(body, out, module, used_types, visited_types); } ExprKind::If { condition, then_branch, else_branch, } => { - collect_references(condition, out); - collect_references(then_branch, out); - collect_references(else_branch, out); + collect_references(condition, out, module, used_types, visited_types); + collect_references(then_branch, out, module, used_types, visited_types); + collect_references(else_branch, out, module, used_types, visited_types); } ExprKind::Case { scrutinee, branches, } => { - collect_references(scrutinee, out); + collect_references(scrutinee, out, module, used_types, visited_types); for branch in branches { - collect_pattern(&branch.pattern, out); - collect_references(&branch.value, out); + collect_pattern(&branch.pattern, out, module, used_types, visited_types); + collect_references(&branch.value, out, module, used_types, visited_types); } } } } -fn collect_pattern(pattern: &crate::Pattern, out: &mut Vec) { +fn collect_pattern( + pattern: &crate::Pattern, + out: &mut Vec, + module: &Module, + used_types: &mut HashSet, + visited_types: &mut HashSet, +) { + collect_core_type_ids(pattern.ty, module, used_types, visited_types); match &pattern.kind { PatternKind::Constructor { symbol, arguments } => { out.push(*symbol); for argument in arguments { - collect_pattern(argument, out); + collect_pattern(argument, out, module, used_types, visited_types); } } PatternKind::Record { fields } => { for (_, field) in fields { - collect_pattern(field, out); + collect_pattern(field, out, module, used_types, visited_types); } } PatternKind::Array { elements } => { for element in elements { - collect_pattern(element, out); + collect_pattern(element, out, module, used_types, visited_types); } } - PatternKind::Named { pattern, .. } => collect_pattern(pattern, out), + PatternKind::Named { pattern, .. } => { + collect_pattern(pattern, out, module, used_types, visited_types) + } _ => {} } } diff --git a/crates/psrs-core/src/locals/alpha.rs b/crates/psrs-core/src/locals/alpha.rs new file mode 100644 index 00000000..a48a1f3a --- /dev/null +++ b/crates/psrs-core/src/locals/alpha.rs @@ -0,0 +1,237 @@ +use super::FreshLocals; +use crate::{Binding, CaseBranch, Expr, ExprKind, Pattern, PatternKind}; +use psrs_hir::LocalId; +use std::collections::HashMap; + +pub(crate) fn clone_with_fresh_locals(expression: &Expr, fresh: &mut FreshLocals) -> Option { + clone_expr(expression, fresh, &mut HashMap::new()) +} + +fn clone_expr( + expression: &Expr, + fresh: &mut FreshLocals, + locals: &mut HashMap, +) -> Option { + let kind = match &expression.kind { + ExprKind::Local(id) => ExprKind::Local(locals.get(id).copied().unwrap_or(*id)), + ExprKind::Global(symbol) => ExprKind::Global(*symbol), + ExprKind::Constructor { symbol, arguments } => ExprKind::Constructor { + symbol: *symbol, + arguments: arguments + .iter() + .map(|argument| clone_expr(argument, fresh, locals)) + .collect::>>()?, + }, + ExprKind::IntrinsicCall { + intrinsic, + arguments, + } => ExprKind::IntrinsicCall { + intrinsic: *intrinsic, + arguments: arguments + .iter() + .map(|argument| clone_expr(argument, fresh, locals)) + .collect::>>()?, + }, + ExprKind::Integer(value) => ExprKind::Integer(*value), + ExprKind::Number(value) => ExprKind::Number(value.clone()), + ExprKind::Boolean(value) => ExprKind::Boolean(*value), + ExprKind::String(value) => ExprKind::String(value.clone()), + ExprKind::Char(value) => ExprKind::Char(*value), + ExprKind::Unit => ExprKind::Unit, + ExprKind::Trap => ExprKind::Trap, + ExprKind::Array { elements } => ExprKind::Array { + elements: elements + .iter() + .map(|element| clone_expr(element, fresh, locals)) + .collect::>>()?, + }, + ExprKind::Record { fields } => ExprKind::Record { + fields: fields + .iter() + .map(|(label, value)| Some((label.clone(), clone_expr(value, fresh, locals)?))) + .collect::>>()?, + }, + ExprKind::RecordUpdate { record, fields } => ExprKind::RecordUpdate { + record: Box::new(clone_expr(record, fresh, locals)?), + fields: fields + .iter() + .map(|(label, value)| Some((label.clone(), clone_expr(value, fresh, locals)?))) + .collect::>>()?, + }, + ExprKind::FieldAccess { record, field } => ExprKind::FieldAccess { + record: Box::new(clone_expr(record, fresh, locals)?), + field: field.clone(), + }, + ExprKind::RepresentationCast { + value, + source_type, + target_type, + } => ExprKind::RepresentationCast { + value: Box::new(clone_expr(value, fresh, locals)?), + source_type: *source_type, + target_type: *target_type, + }, + ExprKind::Application(function, argument) => ExprKind::Application( + Box::new(clone_expr(function, fresh, locals)?), + Box::new(clone_expr(argument, fresh, locals)?), + ), + ExprKind::Lambda { binder, body } => { + let renamed = fresh.fresh()?; + let previous = locals.insert(binder.id, renamed); + let body = clone_expr(body, fresh, locals)?; + restore_local(locals, binder.id, previous); + ExprKind::Lambda { + binder: crate::Binder { + id: renamed, + name: binder.name.clone(), + ty: binder.ty, + span: binder.span, + }, + body: Box::new(body), + } + } + ExprKind::Let { bindings, body } => { + let mut renamed = Vec::with_capacity(bindings.len()); + let mut previous = Vec::with_capacity(bindings.len()); + for binding in bindings { + let id = fresh.fresh()?; + previous.push((binding.binder.id, locals.insert(binding.binder.id, id))); + renamed.push(id); + } + let bindings = bindings + .iter() + .zip(renamed) + .map(|(binding, id)| { + Some(Binding { + binder: crate::Binder { + id, + name: binding.binder.name.clone(), + ty: binding.binder.ty, + span: binding.binder.span, + }, + quantified: binding.quantified.clone(), + value: clone_expr(&binding.value, fresh, locals)?, + span: binding.span, + }) + }) + .collect::>>()?; + let body = clone_expr(body, fresh, locals)?; + restore_locals(locals, previous); + ExprKind::Let { + bindings, + body: Box::new(body), + } + } + ExprKind::If { + condition, + then_branch, + else_branch, + } => ExprKind::If { + condition: Box::new(clone_expr(condition, fresh, locals)?), + then_branch: Box::new(clone_expr(then_branch, fresh, locals)?), + else_branch: Box::new(clone_expr(else_branch, fresh, locals)?), + }, + ExprKind::Case { + scrutinee, + branches, + } => ExprKind::Case { + scrutinee: Box::new(clone_expr(scrutinee, fresh, locals)?), + branches: branches + .iter() + .map(|branch| { + let mut previous = Vec::new(); + let pattern = clone_pattern(&branch.pattern, fresh, locals, &mut previous)?; + let value = clone_expr(&branch.value, fresh, locals)?; + restore_locals(locals, previous); + Some(CaseBranch { + pattern, + value, + span: branch.span, + coverage: branch.coverage, + }) + }) + .collect::>>()?, + }, + }; + Some(Expr { + kind, + ty: expression.ty, + span: expression.span, + }) +} + +fn clone_pattern( + pattern: &Pattern, + fresh: &mut FreshLocals, + locals: &mut HashMap, + previous: &mut Vec<(LocalId, Option)>, +) -> Option { + let kind = match &pattern.kind { + PatternKind::Wildcard => PatternKind::Wildcard, + PatternKind::Literal { value } => PatternKind::Literal { + value: value.clone(), + }, + PatternKind::Array { elements } => PatternKind::Array { + elements: elements + .iter() + .map(|element| clone_pattern(element, fresh, locals, previous)) + .collect::>>()?, + }, + PatternKind::Named { id, pattern } => { + let renamed = fresh.fresh()?; + previous.push((*id, locals.insert(*id, renamed))); + PatternKind::Named { + id: renamed, + pattern: Box::new(clone_pattern(pattern, fresh, locals, previous)?), + } + } + PatternKind::Var { id, ty } => { + let renamed = fresh.fresh()?; + previous.push((*id, locals.insert(*id, renamed))); + PatternKind::Var { + id: renamed, + ty: *ty, + } + } + PatternKind::Constructor { symbol, arguments } => PatternKind::Constructor { + symbol: *symbol, + arguments: arguments + .iter() + .map(|argument| clone_pattern(argument, fresh, locals, previous)) + .collect::>>()?, + }, + PatternKind::Record { fields } => PatternKind::Record { + fields: fields + .iter() + .map(|(label, pattern)| { + Some(( + label.clone(), + clone_pattern(pattern, fresh, locals, previous)?, + )) + }) + .collect::>>()?, + }, + }; + Some(Pattern { + kind, + ty: pattern.ty, + span: pattern.span, + }) +} + +fn restore_local(locals: &mut HashMap, id: LocalId, previous: Option) { + if let Some(previous) = previous { + locals.insert(id, previous); + } else { + locals.remove(&id); + } +} + +fn restore_locals( + locals: &mut HashMap, + previous: Vec<(LocalId, Option)>, +) { + for (id, old) in previous.into_iter().rev() { + restore_local(locals, id, old); + } +} diff --git a/crates/psrs-core/src/locals/mod.rs b/crates/psrs-core/src/locals/mod.rs new file mode 100644 index 00000000..1c278ace --- /dev/null +++ b/crates/psrs-core/src/locals/mod.rs @@ -0,0 +1,129 @@ +//! Shared collision-free local allocation for Core rewrites. +mod alpha; +pub(crate) use alpha::clone_with_fresh_locals; + +use crate::{Expr, ExprKind, LocalId, Pattern, PatternKind}; +use std::collections::HashSet; + +pub(crate) struct FreshLocals { + next: Option, +} + +impl FreshLocals { + pub fn for_declaration(expression: &Expr) -> Self { + let mut ids = HashSet::new(); + collect_ids(expression, &mut ids); + Self { + next: ids + .iter() + .map(|id| id.0) + .max() + .map_or(Some(0), |value| value.checked_add(1)), + } + } + + pub fn fresh(&mut self) -> Option { + let id = LocalId(self.next?); + self.next = id.0.checked_add(1); + Some(id) + } +} + +fn collect_ids(expression: &Expr, ids: &mut HashSet) { + match &expression.kind { + ExprKind::Local(id) => { + ids.insert(*id); + } + ExprKind::Lambda { binder, body } => { + ids.insert(binder.id); + collect_ids(body, ids); + } + ExprKind::Let { bindings, body } => { + for binding in bindings { + ids.insert(binding.binder.id); + collect_ids(&binding.value, ids); + } + collect_ids(body, ids); + } + ExprKind::Case { + scrutinee, + branches, + } => { + collect_ids(scrutinee, ids); + for branch in branches { + collect_pattern_ids(&branch.pattern, ids); + collect_ids(&branch.value, ids); + } + } + ExprKind::Constructor { arguments, .. } + | ExprKind::IntrinsicCall { arguments, .. } + | ExprKind::Array { + elements: arguments, + } => { + for argument in arguments { + collect_ids(argument, ids); + } + } + ExprKind::Record { fields } => { + for (_, value) in fields { + collect_ids(value, ids); + } + } + ExprKind::RecordUpdate { record, fields } => { + collect_ids(record, ids); + for (_, value) in fields { + collect_ids(value, ids); + } + } + ExprKind::FieldAccess { record, .. } + | ExprKind::RepresentationCast { value: record, .. } => collect_ids(record, ids), + ExprKind::Application(left, right) => { + collect_ids(left, ids); + collect_ids(right, ids); + } + ExprKind::If { + condition, + then_branch, + else_branch, + } => { + collect_ids(condition, ids); + collect_ids(then_branch, ids); + collect_ids(else_branch, ids); + } + ExprKind::Global(_) + | ExprKind::Integer(_) + | ExprKind::Number(_) + | ExprKind::Boolean(_) + | ExprKind::String(_) + | ExprKind::Char(_) => {} + ExprKind::Unit | ExprKind::Trap => {} + } +} + +fn collect_pattern_ids(pattern: &Pattern, ids: &mut HashSet) { + match &pattern.kind { + PatternKind::Var { id, .. } => { + ids.insert(*id); + } + PatternKind::Named { id, pattern } => { + ids.insert(*id); + collect_pattern_ids(pattern, ids); + } + PatternKind::Array { elements } => { + for element in elements { + collect_pattern_ids(element, ids); + } + } + PatternKind::Constructor { arguments, .. } => { + for argument in arguments { + collect_pattern_ids(argument, ids); + } + } + PatternKind::Record { fields } => { + for (_, field) in fields { + collect_pattern_ids(field, ids); + } + } + PatternKind::Wildcard | PatternKind::Literal { .. } => {} + } +} diff --git a/crates/psrs-core/src/lower/dictionary.rs b/crates/psrs-core/src/lower/dictionary.rs index 63b6b2cc..ee2e6c31 100644 --- a/crates/psrs-core/src/lower/dictionary.rs +++ b/crates/psrs-core/src/lower/dictionary.rs @@ -2,10 +2,25 @@ use super::{Expr, ExprKind, LowerError, TypeId}; use psrs_thir::{Evidence, EvidenceKind, Type}; /// Erases checked class evidence into the existing Core value and call forms. -pub(super) fn lower_evidence(evidence: &Evidence, types: &[Type]) -> Result { +pub(super) fn lower_evidence( + evidence: &Evidence, + types: &[Type], + externals: &std::collections::HashMap, + constructors: &std::collections::HashMap, + context: &mut super::module::LowerContext, +) -> Result { let span = evidence.span; let ty = TypeId(evidence.ty.0); let kind = match &evidence.kind { + EvidenceKind::DictionaryValue(value) => { + return super::lower_expr( + value.as_ref().clone(), + externals, + constructors, + types, + context, + ); + } EvidenceKind::Given(id) => ExprKind::Local(*id), EvidenceKind::Global(symbol) => ExprKind::Global(*symbol), EvidenceKind::Coercible { .. } | EvidenceKind::Primitive { .. } => { @@ -14,14 +29,50 @@ pub(super) fn lower_evidence(evidence: &Evidence, types: &[Type]) -> Result ExprKind::FieldAccess { - record: Box::new(lower_evidence(parent, types)?), - field: field.clone(), - }, + EvidenceKind::Superclass { parent, field } => { + let fields = psrs_thir::record_fields(types, parent.ty).ok_or(LowerError { + span, + message: "superclass evidence parent is not a dictionary record", + })?; + let field_type = fields + .iter() + .find(|(label, _)| label == field) + .map(|(_, ty)| *ty) + .ok_or(LowerError { + span, + message: "superclass dictionary has no selected field", + })?; + let (parameter, _) = psrs_thir::arrow_parts(types, field_type).ok_or(LowerError { + span, + message: "superclass dictionary field is not a thunk", + })?; + let function = Expr { + kind: ExprKind::FieldAccess { + record: Box::new(lower_evidence( + parent, + types, + externals, + constructors, + context, + )?), + field: field.clone(), + }, + ty: TypeId(field_type.0), + span, + }; + ExprKind::Application( + Box::new(function), + Box::new(Expr { + kind: ExprKind::Unit, + ty: TypeId(parameter.0), + span, + }), + ) + } EvidenceKind::Instance { constructor, constructor_type, - context, + context: arguments, } => { let mut function_type = *constructor_type; let mut function = Expr { @@ -29,20 +80,18 @@ pub(super) fn lower_evidence(evidence: &Evidence, types: &[Type]) -> Result Result, constructors: &HashMap, source_types: &[psrs_thir::Type], + context: &mut module::LowerContext, +) -> Result { + psrs_span::with_sufficient_stack(|| { + lower_expr_inner(expression, externals, constructors, source_types, context) + }) +} + +fn lower_expr_inner( + expression: TypedExpr, + externals: &HashMap, + constructors: &HashMap, + source_types: &[psrs_thir::Type], + context: &mut module::LowerContext, ) -> Result { let span = expression.span; let ty = TypeId(expression.ty.0); if let Some((symbol, arguments)) = constructor_application(&expression, constructors) { let arguments = arguments .into_iter() - .map(|argument| lower_expr(argument.clone(), externals, constructors, source_types)) + .map(|argument| { + lower_expr( + argument.clone(), + externals, + constructors, + source_types, + context, + ) + }) .collect::, _>>()?; return Ok(Expr { kind: ExprKind::Constructor { symbol, arguments }, @@ -79,10 +102,14 @@ fn lower_expr( } if let Some(constructor) = constructors.get(&id) { if constructor.field_count != 0 { - return Err(LowerError { + return lower_partial_constructor( + id, + expression.ty, + constructor.field_count, span, - message: "partially applied field constructors require closure conversion", - }); + source_types, + context, + ); } ExprKind::Constructor { symbol: id, @@ -100,7 +127,7 @@ fn lower_expr( TypedExprKind::Array(elements) => ExprKind::Array { elements: elements .into_iter() - .map(|element| lower_expr(element, externals, constructors, source_types)) + .map(|element| lower_expr(element, externals, constructors, source_types, context)) .collect::, _>>()?, }, TypedExprKind::Record(fields) => ExprKind::Record { @@ -109,7 +136,7 @@ fn lower_expr( .map(|(label, value)| { Ok(( label, - lower_expr(value, externals, constructors, source_types)?, + lower_expr(value, externals, constructors, source_types, context)?, )) }) .collect::, LowerError>>()?, @@ -120,13 +147,14 @@ fn lower_expr( externals, constructors, source_types, + context, )?), fields: fields .into_iter() .map(|(label, value)| { Ok(( label, - lower_expr(value, externals, constructors, source_types)?, + lower_expr(value, externals, constructors, source_types, context)?, )) }) .collect::, LowerError>>()?, @@ -137,11 +165,18 @@ fn lower_expr( externals, constructors, source_types, + context, )?), field, }, TypedExprKind::Evidence(evidence) => { - return dictionary::lower_evidence(&evidence, source_types); + return dictionary::lower_evidence( + &evidence, + source_types, + externals, + constructors, + context, + ); } TypedExprKind::Coerce { value, @@ -159,25 +194,55 @@ fn lower_expr( }); } ExprKind::RepresentationCast { - value: Box::new(lower_expr(*value, externals, constructors, source_types)?), + value: Box::new(lower_expr( + *value, + externals, + constructors, + source_types, + context, + )?), + source_type: TypeId(source_type.0), + target_type: TypeId(target_type.0), + } + } + TypedExprKind::UnsafeCoerce { + value, + source_type, + target_type, + .. + } => { + if source_type != value.ty || target_type.0 != ty.0 { + return Err(LowerError { + span, + message: "unsafe coercion boundary does not match its value and result", + }); + } + ExprKind::RepresentationCast { + value: Box::new(lower_expr( + *value, + externals, + constructors, + source_types, + context, + )?), source_type: TypeId(source_type.0), target_type: TypeId(target_type.0), } } TypedExprKind::Application(function, argument) => { - let function = lower_expr(*function, externals, constructors, source_types)?; - let argument = lower_expr(*argument, externals, constructors, source_types)?; + let function = lower_expr(*function, externals, constructors, source_types, context)?; + let argument = lower_expr(*argument, externals, constructors, source_types, context)?; // A saturated intrinsic application becomes one IntrinsicCall. The // registry's arity decides saturation, and the per-intrinsic // handling lives in the intrinsic module rather than here. - if let Some((symbol, args)) = flatten_intrinsic(&function, argument.clone(), externals) - && let Some(ExternalKind::Intrinsic(intrinsic)) = externals.get(&symbol) - && args.len() == intrinsic.descriptor().arity as usize - { + // The right argument is already the lowered remainder of the spine. + // Move that spine into the intrinsic call; cloning it recurses while + // these lowering frames are still live. + if let Some(intrinsic) = saturated_intrinsic(&function, externals) { return Ok(Expr { kind: ExprKind::IntrinsicCall { - intrinsic: *intrinsic, - arguments: args, + intrinsic, + arguments: unfold_application(function, argument), }, ty, span, @@ -192,7 +257,13 @@ fn lower_expr( ty: TypeId(binder.ty.0), span: binder.span, }, - body: Box::new(lower_expr(*body, externals, constructors, source_types)?), + body: Box::new(lower_expr( + *body, + externals, + constructors, + source_types, + context, + )?), }, TypedExprKind::Let { bindings, body } => ExprKind::Let { bindings: bindings @@ -206,12 +277,24 @@ fn lower_expr( span: binding.binder.span, }, quantified: binding.quantified, - value: lower_expr(binding.value, externals, constructors, source_types)?, + value: lower_expr( + binding.value, + externals, + constructors, + source_types, + context, + )?, span: binding.span, }) }) .collect::, LowerError>>()?, - body: Box::new(lower_expr(*body, externals, constructors, source_types)?), + body: Box::new(lower_expr( + *body, + externals, + constructors, + source_types, + context, + )?), }, TypedExprKind::If { condition, @@ -223,18 +306,21 @@ fn lower_expr( externals, constructors, source_types, + context, )?), then_branch: Box::new(lower_expr( *then_branch, externals, constructors, source_types, + context, )?), else_branch: Box::new(lower_expr( *else_branch, externals, constructors, source_types, + context, )?), }, TypedExprKind::Case { @@ -246,13 +332,20 @@ fn lower_expr( externals, constructors, source_types, + context, )?), branches: branches .into_iter() .map(|branch| { Ok(crate::CaseBranch { pattern: lower_pattern(branch.pattern)?, - value: lower_expr(branch.value, externals, constructors, source_types)?, + value: lower_expr( + branch.value, + externals, + constructors, + source_types, + context, + )?, span: branch.span, coverage: branch.coverage, }) @@ -263,54 +356,76 @@ fn lower_expr( Ok(Expr { kind, ty, span }) } -fn lower_pattern(pattern: psrs_thir::Pattern) -> Result { - let span = pattern.span; - let kind = match pattern.kind { - psrs_thir::PatternKind::Wildcard => crate::PatternKind::Wildcard, - psrs_thir::PatternKind::Literal { literal } => crate::PatternKind::Literal { - value: match literal { - psrs_thir::PatternLiteral::Integer(value) => crate::Literal::Integer(value), - psrs_thir::PatternLiteral::Number(value) => crate::Literal::Number(value), - psrs_thir::PatternLiteral::String(value) => crate::Literal::String(value), - psrs_thir::PatternLiteral::Char(value) => crate::Literal::Char(value), - psrs_thir::PatternLiteral::Boolean(value) => crate::Literal::Boolean(value), +fn lower_partial_constructor( + symbol: SymbolId, + function_type: psrs_thir::TypeId, + field_count: usize, + span: psrs_span::TextRange, + source_types: &[psrs_thir::Type], + context: &mut module::LowerContext, +) -> Result { + let mut current_type = function_type; + let mut binders = Vec::with_capacity(field_count); + for index in 0..field_count { + let Some((parameter, result)) = arrow_parts_after_foralls(source_types, current_type) + else { + return Err(LowerError { + span, + message: "constructor type has fewer arguments than its declaration", + }); + }; + let Some(id) = context.fresh_local() else { + return Err(LowerError { + span, + message: "cannot allocate a local for a constructor function", + }); + }; + binders.push(( + Binder { + id, + name: format!("__partial_constructor_{index}"), + ty: TypeId(parameter.0), + span, }, - }, - psrs_thir::PatternKind::Array { elements } => crate::PatternKind::Array { - elements: elements - .into_iter() - .map(lower_pattern) - .collect::, _>>()?, - }, - psrs_thir::PatternKind::Named { id, pattern } => crate::PatternKind::Named { - id, - pattern: Box::new(lower_pattern(*pattern)?), - }, - psrs_thir::PatternKind::Var { id, ty } => crate::PatternKind::Var { - id, - ty: TypeId(ty.0), - }, - psrs_thir::PatternKind::Constructor { symbol, arguments } => { - crate::PatternKind::Constructor { - symbol, - arguments: arguments - .into_iter() - .map(lower_pattern) - .collect::, _>>()?, - } - } - psrs_thir::PatternKind::Record { fields } => crate::PatternKind::Record { - fields: fields - .into_iter() - .map(|(label, pattern)| Ok((label, lower_pattern(pattern)?))) - .collect::, LowerError>>()?, - }, - }; - Ok(crate::Pattern { - kind, - ty: TypeId(pattern.ty.0), + TypeId(current_type.0), + )); + current_type = result; + } + + let arguments = binders + .iter() + .map(|(binder, _)| Expr { + kind: ExprKind::Local(binder.id), + ty: binder.ty, + span, + }) + .collect(); + let mut body = Expr { + kind: ExprKind::Constructor { symbol, arguments }, + ty: TypeId(current_type.0), span, - }) + }; + for (binder, function_type) in binders.into_iter().rev() { + body = Expr { + kind: ExprKind::Lambda { + binder, + body: Box::new(body), + }, + ty: function_type, + span, + }; + } + Ok(body) +} + +fn arrow_parts_after_foralls( + types: &[psrs_thir::Type], + mut function_type: psrs_thir::TypeId, +) -> Option<(psrs_thir::TypeId, psrs_thir::TypeId)> { + while let Some((_, body)) = psrs_thir::forall_parts(types, function_type) { + function_type = body; + } + psrs_thir::arrow_parts(types, function_type) } fn constructor_application<'a>( @@ -333,23 +448,36 @@ fn constructor_application<'a>( }) } -fn flatten_intrinsic( +fn saturated_intrinsic( function: &Expr, - final_argument: Expr, externals: &HashMap, -) -> Option<(SymbolId, Vec)> { - let mut arguments = vec![final_argument]; +) -> Option { + let mut count = 1usize; let mut head = function; - while let ExprKind::Application(next, argument) = &head.kind { - arguments.push((**argument).clone()); + while let ExprKind::Application(next, _) = &head.kind { + count += 1; head = next; } let ExprKind::Global(symbol) = head.kind else { return None; }; - if !matches!(externals.get(&symbol), Some(ExternalKind::Intrinsic(_))) { - return None; + match externals.get(&symbol) { + Some(ExternalKind::Intrinsic(intrinsic)) + if count == intrinsic.descriptor().arity as usize => + { + Some(*intrinsic) + } + _ => None, + } +} + +fn unfold_application(function: Expr, final_argument: Expr) -> Vec { + let mut arguments = vec![final_argument]; + let mut head = function; + while let ExprKind::Application(next, argument) = head.kind { + arguments.push(*argument); + head = *next; } arguments.reverse(); - Some((symbol, arguments)) + arguments } diff --git a/crates/psrs-core/src/lower/module.rs b/crates/psrs-core/src/lower/module.rs index 9af7177a..c153c440 100644 --- a/crates/psrs-core/src/lower/module.rs +++ b/crates/psrs-core/src/lower/module.rs @@ -1,6 +1,148 @@ use crate::{Declaration, LowerError, Module, Type, TypeId}; use std::collections::HashMap; +pub(super) struct LowerContext { + next_local_id: u64, +} + +impl LowerContext { + fn new(module: &psrs_thir::Module) -> Self { + let mut max_local_id = None; + for declaration in &module.declarations { + scan_expr_locals(&declaration.value, &mut max_local_id); + } + Self { + next_local_id: max_local_id.map_or(0, |id| u64::from(id) + 1), + } + } + + pub(super) fn fresh_local(&mut self) -> Option { + let id = u32::try_from(self.next_local_id).ok()?; + self.next_local_id += 1; + Some(psrs_hir::LocalId(id)) + } +} + +fn scan_expr_locals(expression: &psrs_thir::Expr, max: &mut Option) { + let note = |id: psrs_hir::LocalId, max: &mut Option| { + *max = Some(max.map_or(id.0, |current| current.max(id.0))); + }; + match &expression.kind { + psrs_thir::ExprKind::Local(id) => note(*id, max), + psrs_thir::ExprKind::Global(_) + | psrs_thir::ExprKind::Integer(_) + | psrs_thir::ExprKind::Number(_) + | psrs_thir::ExprKind::Boolean(_) + | psrs_thir::ExprKind::String(_) + | psrs_thir::ExprKind::Char(_) => {} + psrs_thir::ExprKind::Array(elements) => { + for element in elements { + scan_expr_locals(element, max); + } + } + psrs_thir::ExprKind::Record(fields) => { + for (_, value) in fields { + scan_expr_locals(value, max); + } + } + psrs_thir::ExprKind::RecordUpdate { expression, fields } => { + scan_expr_locals(expression, max); + for (_, value) in fields { + scan_expr_locals(value, max); + } + } + psrs_thir::ExprKind::FieldAccess { expression, .. } => scan_expr_locals(expression, max), + psrs_thir::ExprKind::Evidence(evidence) => scan_evidence_locals(evidence, max), + psrs_thir::ExprKind::Coerce { + value, evidence, .. + } => { + scan_expr_locals(value, max); + scan_evidence_locals(evidence, max); + } + psrs_thir::ExprKind::UnsafeCoerce { value, .. } => scan_expr_locals(value, max), + psrs_thir::ExprKind::Application(function, argument) => { + scan_expr_locals(function, max); + scan_expr_locals(argument, max); + } + psrs_thir::ExprKind::Lambda { binder, body } => { + note(binder.id, max); + scan_expr_locals(body, max); + } + psrs_thir::ExprKind::Let { bindings, body } => { + for binding in bindings { + note(binding.binder.id, max); + scan_expr_locals(&binding.value, max); + } + scan_expr_locals(body, max); + } + psrs_thir::ExprKind::If { + condition, + then_branch, + else_branch, + } => { + scan_expr_locals(condition, max); + scan_expr_locals(then_branch, max); + scan_expr_locals(else_branch, max); + } + psrs_thir::ExprKind::Case { + scrutinee, + branches, + } => { + scan_expr_locals(scrutinee, max); + for branch in branches { + scan_pattern_locals(&branch.pattern, max); + scan_expr_locals(&branch.value, max); + } + } + } +} + +fn scan_pattern_locals(pattern: &psrs_thir::Pattern, max: &mut Option) { + match &pattern.kind { + psrs_thir::PatternKind::Wildcard | psrs_thir::PatternKind::Literal { .. } => {} + psrs_thir::PatternKind::Array { elements } => { + for element in elements { + scan_pattern_locals(element, max); + } + } + psrs_thir::PatternKind::Named { id, pattern } => { + *max = Some(max.map_or(id.0, |current| current.max(id.0))); + scan_pattern_locals(pattern, max); + } + psrs_thir::PatternKind::Var { id, .. } => { + *max = Some(max.map_or(id.0, |current| current.max(id.0))); + } + psrs_thir::PatternKind::Constructor { arguments, .. } => { + for argument in arguments { + scan_pattern_locals(argument, max); + } + } + psrs_thir::PatternKind::Record { fields } => { + for (_, pattern) in fields { + scan_pattern_locals(pattern, max); + } + } + } +} + +fn scan_evidence_locals(evidence: &psrs_thir::Evidence, max: &mut Option) { + match &evidence.kind { + psrs_thir::EvidenceKind::DictionaryValue(value) => scan_expr_locals(value, max), + psrs_thir::EvidenceKind::Given(id) => { + *max = Some(max.map_or(id.0, |current| current.max(id.0))); + } + psrs_thir::EvidenceKind::Superclass { parent, .. } => scan_evidence_locals(parent, max), + psrs_thir::EvidenceKind::Instance { context, .. } => { + for evidence in context { + scan_evidence_locals(evidence, max); + } + } + psrs_thir::EvidenceKind::Global(_) + | psrs_thir::EvidenceKind::Coercible { .. } + | psrs_thir::EvidenceKind::Primitive { .. } => {} + } +} + fn lower_type_constructor(constructor: psrs_thir::TypeConstructor) -> crate::TypeConstructor { match constructor { psrs_thir::TypeConstructor::Function => crate::TypeConstructor::Function, @@ -40,6 +182,7 @@ pub(super) fn lower_module_inner(module: psrs_thir::Module) -> Result>(); + let mut context = LowerContext::new(&module); let source_types = module.types; let types = source_types .iter() @@ -68,8 +211,14 @@ pub(super) fn lower_module_inner(module: psrs_thir::Module) -> Result Result { + let span = pattern.span; + let kind = match pattern.kind { + psrs_thir::PatternKind::Wildcard => crate::PatternKind::Wildcard, + psrs_thir::PatternKind::Literal { literal } => crate::PatternKind::Literal { + value: match literal { + psrs_thir::PatternLiteral::Integer(value) => crate::Literal::Integer(value), + psrs_thir::PatternLiteral::Number(value) => crate::Literal::Number(value), + psrs_thir::PatternLiteral::String(value) => crate::Literal::String(value), + psrs_thir::PatternLiteral::Char(value) => crate::Literal::Char(value), + psrs_thir::PatternLiteral::Boolean(value) => crate::Literal::Boolean(value), + }, + }, + psrs_thir::PatternKind::Array { elements } => crate::PatternKind::Array { + elements: elements + .into_iter() + .map(lower_pattern) + .collect::, _>>()?, + }, + psrs_thir::PatternKind::Named { id, pattern } => crate::PatternKind::Named { + id, + pattern: Box::new(lower_pattern(*pattern)?), + }, + psrs_thir::PatternKind::Var { id, ty } => crate::PatternKind::Var { + id, + ty: TypeId(ty.0), + }, + psrs_thir::PatternKind::Constructor { symbol, arguments } => { + crate::PatternKind::Constructor { + symbol, + arguments: arguments + .into_iter() + .map(lower_pattern) + .collect::, _>>()?, + } + } + psrs_thir::PatternKind::Record { fields } => crate::PatternKind::Record { + fields: fields + .into_iter() + .map(|(label, pattern)| Ok((label, lower_pattern(pattern)?))) + .collect::, LowerError>>()?, + }, + }; + Ok(crate::Pattern { + kind, + ty: TypeId(pattern.ty.0), + span, + }) +} diff --git a/crates/psrs-core/src/opt/dead.rs b/crates/psrs-core/src/opt/dead.rs index e1bc734d..4c2deb55 100644 --- a/crates/psrs-core/src/opt/dead.rs +++ b/crates/psrs-core/src/opt/dead.rs @@ -11,7 +11,11 @@ pub(super) fn run(mut module: Module) -> Module { module } -fn eliminate_expr(mut expression: Expr) -> Expr { +fn eliminate_expr(expression: Expr) -> Expr { + psrs_span::with_sufficient_stack(|| eliminate_expr_inner(expression)) +} + +fn eliminate_expr_inner(mut expression: Expr) -> Expr { expression.kind = match expression.kind { ExprKind::Constructor { symbol, arguments } => ExprKind::Constructor { symbol, @@ -159,6 +163,10 @@ fn live_bindings(bindings: &[Binding], body: &Expr) -> Vec { } fn collect_refs(expression: &Expr, references: &mut HashSet) { + psrs_span::with_sufficient_stack(|| collect_refs_inner(expression, references)) +} + +fn collect_refs_inner(expression: &Expr, references: &mut HashSet) { match &expression.kind { ExprKind::Local(id) => { references.insert(*id); diff --git a/crates/psrs-core/src/opt/effects.rs b/crates/psrs-core/src/opt/effects.rs index 280ca8e2..af2b2c6a 100644 --- a/crates/psrs-core/src/opt/effects.rs +++ b/crates/psrs-core/src/opt/effects.rs @@ -1,11 +1,11 @@ use crate::{Expr, ExprKind}; -use psrs_hir::Intrinsic; /// Conservative evaluation effects relevant to call-by-value rewrites. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub(super) struct Effects { pub may_call: bool, pub may_trap: bool, + pub may_write: bool, } impl Effects { @@ -13,15 +13,20 @@ impl Effects { Self { may_call: self.may_call || other.may_call, may_trap: self.may_trap || other.may_trap, + may_write: self.may_write || other.may_write, } } pub fn inert(self) -> bool { - !self.may_call && !self.may_trap + !self.may_call && !self.may_trap && !self.may_write } } pub(super) fn summarize(expression: &Expr) -> Effects { + psrs_span::with_sufficient_stack(|| summarize_inner(expression)) +} + +fn summarize_inner(expression: &Expr) -> Effects { match &expression.kind { ExprKind::Local(_) | ExprKind::Integer(_) @@ -43,6 +48,7 @@ pub(super) fn summarize(expression: &Expr) -> Effects { ExprKind::Global(_) => Effects { may_call: true, may_trap: true, + may_write: true, }, // Creating a closure does not run its body. Function identity is not // observable in Core, and capture reads are inert local lookups. @@ -58,8 +64,10 @@ pub(super) fn summarize(expression: &Expr) -> Effects { arguments, } => { let arguments = combine_all(arguments.iter().map(summarize)); + let effects = intrinsic.descriptor().effects; Effects { - may_trap: intrinsic_may_trap(*intrinsic) || arguments.may_trap, + may_trap: effects.may_trap || arguments.may_trap, + may_write: effects.may_write || arguments.may_write, ..arguments } } @@ -74,6 +82,7 @@ pub(super) fn summarize(expression: &Expr) -> Effects { ExprKind::Application(_, _) => Effects { may_call: true, may_trap: true, + may_write: true, }, ExprKind::Let { bindings, body } => combine_all( bindings @@ -109,27 +118,11 @@ fn combine_all(effects: impl IntoIterator) -> Effects { .fold(Effects::default(), Effects::combine) } -/// Whether an intrinsic can trap. The byte conversions validate their input, -/// the array operations can trap on a missing or out-of-range index, and the -/// truncating and Euclidean division operations trap on a zero divisor. -fn intrinsic_may_trap(intrinsic: Intrinsic) -> bool { - matches!( - intrinsic, - Intrinsic::ArrayIndex - | Intrinsic::ArrayUpdate - | Intrinsic::StringToBytes - | Intrinsic::BytesToString - | Intrinsic::I32DivS - | Intrinsic::I32RemS - | Intrinsic::IntDiv - | Intrinsic::IntMod - ) -} - #[cfg(test)] mod tests { use super::*; use crate::TypeId; + use psrs_hir::Intrinsic; fn intrinsic(intrinsic: Intrinsic, arguments: Vec) -> Expr { Expr { diff --git a/crates/psrs-core/src/opt/inline/alpha.rs b/crates/psrs-core/src/opt/inline/alpha.rs index 10af43ff..375f6a04 100644 --- a/crates/psrs-core/src/opt/inline/alpha.rs +++ b/crates/psrs-core/src/opt/inline/alpha.rs @@ -1,237 +1 @@ -use super::super::util::FreshLocals; -use crate::{Binding, CaseBranch, Expr, ExprKind, Pattern, PatternKind}; -use psrs_hir::LocalId; -use std::collections::HashMap; - -pub(super) fn clone_with_fresh_locals(expression: &Expr, fresh: &mut FreshLocals) -> Option { - clone_expr(expression, fresh, &mut HashMap::new()) -} - -fn clone_expr( - expression: &Expr, - fresh: &mut FreshLocals, - locals: &mut HashMap, -) -> Option { - let kind = match &expression.kind { - ExprKind::Local(id) => ExprKind::Local(locals.get(id).copied().unwrap_or(*id)), - ExprKind::Global(symbol) => ExprKind::Global(*symbol), - ExprKind::Constructor { symbol, arguments } => ExprKind::Constructor { - symbol: *symbol, - arguments: arguments - .iter() - .map(|argument| clone_expr(argument, fresh, locals)) - .collect::>>()?, - }, - ExprKind::IntrinsicCall { - intrinsic, - arguments, - } => ExprKind::IntrinsicCall { - intrinsic: *intrinsic, - arguments: arguments - .iter() - .map(|argument| clone_expr(argument, fresh, locals)) - .collect::>>()?, - }, - ExprKind::Integer(value) => ExprKind::Integer(*value), - ExprKind::Number(value) => ExprKind::Number(value.clone()), - ExprKind::Boolean(value) => ExprKind::Boolean(*value), - ExprKind::String(value) => ExprKind::String(value.clone()), - ExprKind::Char(value) => ExprKind::Char(*value), - ExprKind::Unit => ExprKind::Unit, - ExprKind::Trap => ExprKind::Trap, - ExprKind::Array { elements } => ExprKind::Array { - elements: elements - .iter() - .map(|element| clone_expr(element, fresh, locals)) - .collect::>>()?, - }, - ExprKind::Record { fields } => ExprKind::Record { - fields: fields - .iter() - .map(|(label, value)| Some((label.clone(), clone_expr(value, fresh, locals)?))) - .collect::>>()?, - }, - ExprKind::RecordUpdate { record, fields } => ExprKind::RecordUpdate { - record: Box::new(clone_expr(record, fresh, locals)?), - fields: fields - .iter() - .map(|(label, value)| Some((label.clone(), clone_expr(value, fresh, locals)?))) - .collect::>>()?, - }, - ExprKind::FieldAccess { record, field } => ExprKind::FieldAccess { - record: Box::new(clone_expr(record, fresh, locals)?), - field: field.clone(), - }, - ExprKind::RepresentationCast { - value, - source_type, - target_type, - } => ExprKind::RepresentationCast { - value: Box::new(clone_expr(value, fresh, locals)?), - source_type: *source_type, - target_type: *target_type, - }, - ExprKind::Application(function, argument) => ExprKind::Application( - Box::new(clone_expr(function, fresh, locals)?), - Box::new(clone_expr(argument, fresh, locals)?), - ), - ExprKind::Lambda { binder, body } => { - let renamed = fresh.fresh()?; - let previous = locals.insert(binder.id, renamed); - let body = clone_expr(body, fresh, locals)?; - restore_local(locals, binder.id, previous); - ExprKind::Lambda { - binder: crate::Binder { - id: renamed, - name: binder.name.clone(), - ty: binder.ty, - span: binder.span, - }, - body: Box::new(body), - } - } - ExprKind::Let { bindings, body } => { - let mut renamed = Vec::with_capacity(bindings.len()); - let mut previous = Vec::with_capacity(bindings.len()); - for binding in bindings { - let id = fresh.fresh()?; - previous.push((binding.binder.id, locals.insert(binding.binder.id, id))); - renamed.push(id); - } - let bindings = bindings - .iter() - .zip(renamed) - .map(|(binding, id)| { - Some(Binding { - binder: crate::Binder { - id, - name: binding.binder.name.clone(), - ty: binding.binder.ty, - span: binding.binder.span, - }, - quantified: binding.quantified.clone(), - value: clone_expr(&binding.value, fresh, locals)?, - span: binding.span, - }) - }) - .collect::>>()?; - let body = clone_expr(body, fresh, locals)?; - restore_locals(locals, previous); - ExprKind::Let { - bindings, - body: Box::new(body), - } - } - ExprKind::If { - condition, - then_branch, - else_branch, - } => ExprKind::If { - condition: Box::new(clone_expr(condition, fresh, locals)?), - then_branch: Box::new(clone_expr(then_branch, fresh, locals)?), - else_branch: Box::new(clone_expr(else_branch, fresh, locals)?), - }, - ExprKind::Case { - scrutinee, - branches, - } => ExprKind::Case { - scrutinee: Box::new(clone_expr(scrutinee, fresh, locals)?), - branches: branches - .iter() - .map(|branch| { - let mut previous = Vec::new(); - let pattern = clone_pattern(&branch.pattern, fresh, locals, &mut previous)?; - let value = clone_expr(&branch.value, fresh, locals)?; - restore_locals(locals, previous); - Some(CaseBranch { - pattern, - value, - span: branch.span, - coverage: branch.coverage, - }) - }) - .collect::>>()?, - }, - }; - Some(Expr { - kind, - ty: expression.ty, - span: expression.span, - }) -} - -fn clone_pattern( - pattern: &Pattern, - fresh: &mut FreshLocals, - locals: &mut HashMap, - previous: &mut Vec<(LocalId, Option)>, -) -> Option { - let kind = match &pattern.kind { - PatternKind::Wildcard => PatternKind::Wildcard, - PatternKind::Literal { value } => PatternKind::Literal { - value: value.clone(), - }, - PatternKind::Array { elements } => PatternKind::Array { - elements: elements - .iter() - .map(|element| clone_pattern(element, fresh, locals, previous)) - .collect::>>()?, - }, - PatternKind::Named { id, pattern } => { - let renamed = fresh.fresh()?; - previous.push((*id, locals.insert(*id, renamed))); - PatternKind::Named { - id: renamed, - pattern: Box::new(clone_pattern(pattern, fresh, locals, previous)?), - } - } - PatternKind::Var { id, ty } => { - let renamed = fresh.fresh()?; - previous.push((*id, locals.insert(*id, renamed))); - PatternKind::Var { - id: renamed, - ty: *ty, - } - } - PatternKind::Constructor { symbol, arguments } => PatternKind::Constructor { - symbol: *symbol, - arguments: arguments - .iter() - .map(|argument| clone_pattern(argument, fresh, locals, previous)) - .collect::>>()?, - }, - PatternKind::Record { fields } => PatternKind::Record { - fields: fields - .iter() - .map(|(label, pattern)| { - Some(( - label.clone(), - clone_pattern(pattern, fresh, locals, previous)?, - )) - }) - .collect::>>()?, - }, - }; - Some(Pattern { - kind, - ty: pattern.ty, - span: pattern.span, - }) -} - -fn restore_local(locals: &mut HashMap, id: LocalId, previous: Option) { - if let Some(previous) = previous { - locals.insert(id, previous); - } else { - locals.remove(&id); - } -} - -fn restore_locals( - locals: &mut HashMap, - previous: Vec<(LocalId, Option)>, -) { - for (id, old) in previous.into_iter().rev() { - restore_local(locals, id, old); - } -} +pub(super) use crate::locals::clone_with_fresh_locals; diff --git a/crates/psrs-core/src/opt/inline/global/analysis.rs b/crates/psrs-core/src/opt/inline/global/analysis.rs index 7ce35a4b..d4ea2d8b 100644 --- a/crates/psrs-core/src/opt/inline/global/analysis.rs +++ b/crates/psrs-core/src/opt/inline/global/analysis.rs @@ -39,7 +39,14 @@ fn type_has_forall(id: TypeId, types: &[Type], seen: &mut HashSet) -> bo } } -fn expr_introduces_type_binders(expression: &Expr, types: &[Type]) -> bool { +pub(in crate::opt::inline) fn expr_introduces_type_binders( + expression: &Expr, + types: &[Type], +) -> bool { + psrs_span::with_sufficient_stack(|| expr_introduces_type_binders_inner(expression, types)) +} + +fn expr_introduces_type_binders_inner(expression: &Expr, types: &[Type]) -> bool { if type_has_forall(expression.ty, types, &mut HashSet::new()) { return true; } @@ -154,6 +161,10 @@ pub(super) fn is_recursive(root: SymbolId, graph: &HashMap bool { + psrs_span::with_sufficient_stack(|| contains_case_inner(expression)) +} + +fn contains_case_inner(expression: &Expr) -> bool { match &expression.kind { ExprKind::Case { .. } => true, ExprKind::Constructor { arguments, .. } @@ -190,6 +201,10 @@ pub(super) fn contains_case(expression: &Expr) -> bool { } pub(super) fn collect_globals(expression: &Expr, out: &mut Vec) { + psrs_span::with_sufficient_stack(|| collect_globals_inner(expression, out)) +} + +fn collect_globals_inner(expression: &Expr, out: &mut Vec) { match &expression.kind { ExprKind::Global(symbol) => out.push(*symbol), ExprKind::Constructor { arguments, .. } diff --git a/crates/psrs-core/src/opt/inline/global/mod.rs b/crates/psrs-core/src/opt/inline/global/mod.rs index f8f32e7a..7d3851bf 100644 --- a/crates/psrs-core/src/opt/inline/global/mod.rs +++ b/crates/psrs-core/src/opt/inline/global/mod.rs @@ -1,4 +1,4 @@ -mod analysis; +pub(super) mod analysis; mod call; use super::super::util::{FreshLocals, next_locals}; @@ -53,6 +53,29 @@ pub(super) fn run(mut module: Module, max_body_nodes: usize, sites_left: &mut us #[allow(clippy::too_many_arguments)] fn inline_expr( + expression: Expr, + fresh: &mut FreshLocals, + sites_left: &mut usize, + max_body_nodes: usize, + declarations: &HashMap, + recursive: &HashSet, + types: &[Type], +) -> Expr { + psrs_span::with_sufficient_stack(|| { + inline_expr_inner( + expression, + fresh, + sites_left, + max_body_nodes, + declarations, + recursive, + types, + ) + }) +} + +#[allow(clippy::too_many_arguments)] +fn inline_expr_inner( mut expression: Expr, fresh: &mut FreshLocals, sites_left: &mut usize, diff --git a/crates/psrs-core/src/opt/inline/local.rs b/crates/psrs-core/src/opt/inline/local.rs index 6b042fbc..ff653f21 100644 --- a/crates/psrs-core/src/opt/inline/local.rs +++ b/crates/psrs-core/src/opt/inline/local.rs @@ -1,5 +1,6 @@ use super::super::util::{FreshLocals, count_nodes, next_locals, substitute_locals}; -use crate::{Binding, Expr, ExprKind, Module}; +use super::global::analysis::expr_introduces_type_binders; +use crate::{Binding, Expr, ExprKind, Module, Type}; use std::collections::HashMap; pub(super) fn run(mut module: Module, max_inline_nodes: usize, sites_left: &mut usize) -> Module { @@ -7,29 +8,47 @@ pub(super) fn run(mut module: Module, max_inline_nodes: usize, sites_left: &mut return module; } let mut fresh = next_locals(&module); + // Split the type table out so each declaration body can be rewritten + // while the binder check still reads the types those bodies mention. + let types = std::mem::take(&mut module.types); for (declaration, fresh) in module.declarations.iter_mut().zip(&mut fresh) { declaration.value = inline_expr( declaration.value.clone(), fresh, sites_left, max_inline_nodes, + &types, ); } + module.types = types; module } fn inline_expr( + expression: Expr, + fresh: &mut FreshLocals, + sites_left: &mut usize, + max_body_nodes: usize, + types: &[Type], +) -> Expr { + psrs_span::with_sufficient_stack(|| { + inline_expr_inner(expression, fresh, sites_left, max_body_nodes, types) + }) +} + +fn inline_expr_inner( mut expression: Expr, fresh: &mut FreshLocals, sites_left: &mut usize, max_body_nodes: usize, + types: &[Type], ) -> Expr { expression.kind = match expression.kind { ExprKind::Constructor { symbol, arguments } => ExprKind::Constructor { symbol, arguments: arguments .into_iter() - .map(|argument| inline_expr(argument, fresh, sites_left, max_body_nodes)) + .map(|argument| inline_expr(argument, fresh, sites_left, max_body_nodes, types)) .collect(), }, ExprKind::IntrinsicCall { @@ -39,34 +58,52 @@ fn inline_expr( intrinsic, arguments: arguments .into_iter() - .map(|argument| inline_expr(argument, fresh, sites_left, max_body_nodes)) + .map(|argument| inline_expr(argument, fresh, sites_left, max_body_nodes, types)) .collect(), }, ExprKind::Array { elements } => ExprKind::Array { elements: elements .into_iter() - .map(|element| inline_expr(element, fresh, sites_left, max_body_nodes)) + .map(|element| inline_expr(element, fresh, sites_left, max_body_nodes, types)) .collect(), }, ExprKind::Record { fields } => ExprKind::Record { fields: fields .into_iter() .map(|(label, value)| { - (label, inline_expr(value, fresh, sites_left, max_body_nodes)) + ( + label, + inline_expr(value, fresh, sites_left, max_body_nodes, types), + ) }) .collect(), }, ExprKind::RecordUpdate { record, fields } => ExprKind::RecordUpdate { - record: Box::new(inline_expr(*record, fresh, sites_left, max_body_nodes)), + record: Box::new(inline_expr( + *record, + fresh, + sites_left, + max_body_nodes, + types, + )), fields: fields .into_iter() .map(|(label, value)| { - (label, inline_expr(value, fresh, sites_left, max_body_nodes)) + ( + label, + inline_expr(value, fresh, sites_left, max_body_nodes, types), + ) }) .collect(), }, ExprKind::FieldAccess { record, field } => ExprKind::FieldAccess { - record: Box::new(inline_expr(*record, fresh, sites_left, max_body_nodes)), + record: Box::new(inline_expr( + *record, + fresh, + sites_left, + max_body_nodes, + types, + )), field, }, ExprKind::RepresentationCast { @@ -74,14 +111,24 @@ fn inline_expr( source_type, target_type, } => ExprKind::RepresentationCast { - value: Box::new(inline_expr(*value, fresh, sites_left, max_body_nodes)), + value: Box::new(inline_expr( + *value, + fresh, + sites_left, + max_body_nodes, + types, + )), source_type, target_type, }, ExprKind::Application(function, argument) => { - let function = inline_expr(*function, fresh, sites_left, max_body_nodes); - let argument = inline_expr(*argument, fresh, sites_left, max_body_nodes); + let function = inline_expr(*function, fresh, sites_left, max_body_nodes, types); + let argument = inline_expr(*argument, fresh, sites_left, max_body_nodes, types); + // A lambda whose type binds quantifiers is the scope of those + // variables. Copying its body into the call replaces that scope + // with the instantiated result type. let can_inline = *sites_left > 0 + && !expr_introduces_type_binders(&function, types) && matches!( &function.kind, ExprKind::Lambda { body, .. } if count_nodes(body) <= max_body_nodes @@ -97,6 +144,8 @@ fn inline_expr( span: expression.span, }; let body = substitute_locals(&body, &HashMap::from([(binder.id, replacement)])); + // The enclosing let's result quantifiers scope both its + // argument binding and body; the binding stays monomorphic. ExprKind::Let { bindings: vec![Binding { binder: crate::Binder { @@ -117,36 +166,62 @@ fn inline_expr( } ExprKind::Lambda { binder, body } => ExprKind::Lambda { binder, - body: Box::new(inline_expr(*body, fresh, sites_left, max_body_nodes)), + body: Box::new(inline_expr(*body, fresh, sites_left, max_body_nodes, types)), }, ExprKind::Let { bindings, body } => ExprKind::Let { bindings: bindings .into_iter() .map(|mut binding| { - binding.value = inline_expr(binding.value, fresh, sites_left, max_body_nodes); + binding.value = + inline_expr(binding.value, fresh, sites_left, max_body_nodes, types); binding }) .collect(), - body: Box::new(inline_expr(*body, fresh, sites_left, max_body_nodes)), + body: Box::new(inline_expr(*body, fresh, sites_left, max_body_nodes, types)), }, ExprKind::If { condition, then_branch, else_branch, } => ExprKind::If { - condition: Box::new(inline_expr(*condition, fresh, sites_left, max_body_nodes)), - then_branch: Box::new(inline_expr(*then_branch, fresh, sites_left, max_body_nodes)), - else_branch: Box::new(inline_expr(*else_branch, fresh, sites_left, max_body_nodes)), + condition: Box::new(inline_expr( + *condition, + fresh, + sites_left, + max_body_nodes, + types, + )), + then_branch: Box::new(inline_expr( + *then_branch, + fresh, + sites_left, + max_body_nodes, + types, + )), + else_branch: Box::new(inline_expr( + *else_branch, + fresh, + sites_left, + max_body_nodes, + types, + )), }, ExprKind::Case { scrutinee, branches, } => ExprKind::Case { - scrutinee: Box::new(inline_expr(*scrutinee, fresh, sites_left, max_body_nodes)), + scrutinee: Box::new(inline_expr( + *scrutinee, + fresh, + sites_left, + max_body_nodes, + types, + )), branches: branches .into_iter() .map(|mut branch| { - branch.value = inline_expr(branch.value, fresh, sites_left, max_body_nodes); + branch.value = + inline_expr(branch.value, fresh, sites_left, max_body_nodes, types); branch }) .collect(), diff --git a/crates/psrs-core/src/opt/mod.rs b/crates/psrs-core/src/opt/mod.rs index d5d4d6f3..6c0a4dbe 100644 --- a/crates/psrs-core/src/opt/mod.rs +++ b/crates/psrs-core/src/opt/mod.rs @@ -9,6 +9,12 @@ mod util; use crate::{Module, VerifyError}; +/// Whether evaluating an expression can be deferred without moving a call, +/// trap or write. Consumers share the optimizer's conservative effect summary. +pub fn evaluation_is_inert(expression: &crate::Expr) -> bool { + effects::summarize(expression).inert() +} + /// Resource limits for deterministic P7 optimization. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Budget { diff --git a/crates/psrs-core/src/opt/simplify/mod.rs b/crates/psrs-core/src/opt/simplify/mod.rs index cad65d91..7d5c31d1 100644 --- a/crates/psrs-core/src/opt/simplify/mod.rs +++ b/crates/psrs-core/src/opt/simplify/mod.rs @@ -15,7 +15,11 @@ pub(super) fn run(mut module: Module) -> Module { module } -fn simplify_expr(mut expression: Expr, fresh: &mut FreshLocals) -> Expr { +fn simplify_expr(expression: Expr, fresh: &mut FreshLocals) -> Expr { + psrs_span::with_sufficient_stack(|| simplify_expr_inner(expression, fresh)) +} + +fn simplify_expr_inner(mut expression: Expr, fresh: &mut FreshLocals) -> Expr { expression.kind = match expression.kind { ExprKind::Constructor { symbol, arguments } => ExprKind::Constructor { symbol, @@ -308,19 +312,19 @@ fn fold_intrinsic( return None; }; let folded = match intrinsic { - Intrinsic::I32Add => ExprKind::Integer(left.wrapping_add(*right)), - Intrinsic::I32Sub => ExprKind::Integer(left.wrapping_sub(*right)), - Intrinsic::I32Mul => ExprKind::Integer(left.wrapping_mul(*right)), + Intrinsic::IntAdd => ExprKind::Integer(left.wrapping_add(*right)), + Intrinsic::IntSub => ExprKind::Integer(left.wrapping_sub(*right)), + Intrinsic::IntMul => ExprKind::Integer(left.wrapping_mul(*right)), // checked_{div,rem} returns None for both trapping cases: zero divisor // and signed overflow. Leaving the operation intact preserves the trap. - Intrinsic::I32DivS => ExprKind::Integer(left.checked_div(*right)?), - Intrinsic::I32RemS => ExprKind::Integer(left.checked_rem(*right)?), - Intrinsic::I32Eq => ExprKind::Boolean(left == right), - Intrinsic::I32Ne => ExprKind::Boolean(left != right), - Intrinsic::I32LtS => ExprKind::Boolean(left < right), - Intrinsic::I32LeS => ExprKind::Boolean(left <= right), - Intrinsic::I32GtS => ExprKind::Boolean(left > right), - Intrinsic::I32GeS => ExprKind::Boolean(left >= right), + Intrinsic::IntQuot => ExprKind::Integer(left.checked_div(*right)?), + Intrinsic::IntRem => ExprKind::Integer(left.checked_rem(*right)?), + Intrinsic::IntEq => ExprKind::Boolean(left == right), + Intrinsic::IntNe => ExprKind::Boolean(left != right), + Intrinsic::IntLt => ExprKind::Boolean(left < right), + Intrinsic::IntLe => ExprKind::Boolean(left <= right), + Intrinsic::IntGt => ExprKind::Boolean(left > right), + Intrinsic::IntGe => ExprKind::Boolean(left >= right), _ => return None, }; Some(Expr { @@ -342,15 +346,15 @@ fn intrinsic_identity( span, }; match (intrinsic, &left.kind, &right.kind) { - (Intrinsic::I32Add, _, ExprKind::Integer(0)) - | (Intrinsic::I32Sub, _, ExprKind::Integer(0)) - | (Intrinsic::I32Mul, _, ExprKind::Integer(1)) => Some(with_span(left.clone(), span)), - (Intrinsic::I32Add, ExprKind::Integer(0), _) - | (Intrinsic::I32Mul, ExprKind::Integer(1), _) => Some(with_span(right.clone(), span)), - (Intrinsic::I32Mul, ExprKind::Integer(0), _) if effects::summarize(right).inert() => { + (Intrinsic::IntAdd, _, ExprKind::Integer(0)) + | (Intrinsic::IntSub, _, ExprKind::Integer(0)) + | (Intrinsic::IntMul, _, ExprKind::Integer(1)) => Some(with_span(left.clone(), span)), + (Intrinsic::IntAdd, ExprKind::Integer(0), _) + | (Intrinsic::IntMul, ExprKind::Integer(1), _) => Some(with_span(right.clone(), span)), + (Intrinsic::IntMul, ExprKind::Integer(0), _) if effects::summarize(right).inert() => { Some(zero(left.ty)) } - (Intrinsic::I32Mul, _, ExprKind::Integer(0)) if effects::summarize(left).inert() => { + (Intrinsic::IntMul, _, ExprKind::Integer(0)) if effects::summarize(left).inert() => { Some(zero(right.ty)) } _ => None, diff --git a/crates/psrs-core/src/opt/specialize/callable_fields.rs b/crates/psrs-core/src/opt/specialize/callable_fields.rs new file mode 100644 index 00000000..4a712694 --- /dev/null +++ b/crates/psrs-core/src/opt/specialize/callable_fields.rs @@ -0,0 +1,67 @@ +//! Eligibility at declaration-owned callable storage boundaries. +//! +//! Specializing a generic body which opens or produces such a field changes +//! its use signature while the field's declared convention stays generic. +//! Until lowering can eliminate that round trip, retain the generic entry: +//! added result adapters can turn a constant-stack invocation into recursion +//! with post-call work. This is independent of State and library names. + +use crate::{Declaration, Module, Type, TypeConstructor, TypeId, arrow_parts}; +use std::collections::HashSet; + +pub(super) fn crosses_callable_field(module: &Module, declaration: &Declaration) -> bool { + // Examine each ordinary argument and the final result, rather than + // classifying the declaration's own outer arrows as stored callables. + let mut result = declaration.ty; + while let Some((parameter, next)) = arrow_parts(&module.types, result) { + if contains_callable_field(module, parameter, &mut HashSet::new()) { + return true; + } + result = next; + } + contains_callable_field(module, result, &mut HashSet::new()) +} + +fn contains_callable_field(module: &Module, ty: TypeId, seen: &mut HashSet) -> bool { + if !seen.insert(ty) { + return false; + } + if let Some((head, arguments)) = module.applied_constructor(ty) { + if let TypeConstructor::User(owner) = head { + for constructor in &module.constructors { + if constructor.type_id == owner + && constructor.field_types.iter().any(|field| { + arrow_parts(&module.types, *field).is_some() + || contains_callable_field(module, *field, seen) + }) + { + return true; + } + } + } + if arguments + .iter() + .any(|argument| contains_callable_field(module, *argument, seen)) + { + return true; + } + } + match module.types.get(ty.0 as usize) { + Some(Type::Application(function, argument)) => { + contains_callable_field(module, *function, seen) + || contains_callable_field(module, *argument, seen) + } + Some(Type::RowExtend { ty, tail, .. }) => { + contains_callable_field(module, *ty, seen) + || contains_callable_field(module, *tail, seen) + } + Some(Type::ForAll { body, .. }) => contains_callable_field(module, *body, seen), + Some(Type::Closure { parameters, result }) => { + parameters + .iter() + .any(|parameter| contains_callable_field(module, *parameter, seen)) + || contains_callable_field(module, *result, seen) + } + _ => false, + } +} diff --git a/crates/psrs-core/src/opt/specialize/calls.rs b/crates/psrs-core/src/opt/specialize/calls.rs index 812b786f..f35e6b24 100644 --- a/crates/psrs-core/src/opt/specialize/calls.rs +++ b/crates/psrs-core/src/opt/specialize/calls.rs @@ -9,6 +9,18 @@ pub(super) fn rewrite( declarations: &HashMap, state: &mut State, pending: &mut Vec, +) -> Expr { + psrs_span::with_sufficient_stack(|| { + rewrite_inner(expression, module, declarations, state, pending) + }) +} + +fn rewrite_inner( + expression: Expr, + module: &mut Module, + declarations: &HashMap, + state: &mut State, + pending: &mut Vec, ) -> Expr { let mut expression = expression; expression.kind = match expression.kind { diff --git a/crates/psrs-core/src/opt/specialize/mod.rs b/crates/psrs-core/src/opt/specialize/mod.rs index 4f3ec83f..777a8960 100644 --- a/crates/psrs-core/src/opt/specialize/mod.rs +++ b/crates/psrs-core/src/opt/specialize/mod.rs @@ -1,3 +1,4 @@ +mod callable_fields; mod calls; mod types; @@ -63,6 +64,9 @@ impl State { call_type: crate::TypeId, pending: &mut Vec, ) -> Option { + if callable_fields::crosses_callable_field(module, declaration) { + return None; + } let (replacements, arguments) = match_instantiation(module, declaration, call_type)?; let key = Key { declaration: declaration.symbol, @@ -160,6 +164,10 @@ pub(super) fn retain_live(mut module: Module, generated: &HashSet) -> } fn collect_references(expression: &crate::Expr, out: &mut Vec) { + psrs_span::with_sufficient_stack(|| collect_references_inner(expression, out)) +} + +fn collect_references_inner(expression: &crate::Expr, out: &mut Vec) { match &expression.kind { crate::ExprKind::Global(symbol) => out.push(*symbol), crate::ExprKind::Constructor { arguments, .. } diff --git a/crates/psrs-core/src/opt/specialize/types/mod.rs b/crates/psrs-core/src/opt/specialize/types/mod.rs index 17601f3a..ac373c5c 100644 --- a/crates/psrs-core/src/opt/specialize/types/mod.rs +++ b/crates/psrs-core/src/opt/specialize/types/mod.rs @@ -1,5 +1,3 @@ -mod substitution; - use crate::{Declaration, Module, Type, TypeConstructor, TypeId}; use psrs_hir::TypeVariableId; use std::collections::{HashMap, HashSet}; @@ -52,7 +50,7 @@ pub(super) fn match_instantiation( Some((replacements, key)) } -pub(super) use substitution::instantiate_declaration; +pub(super) use crate::instantiation::substitution::instantiate_declaration; fn type_key(module: &Module, id: TypeId, active: &mut HashSet) -> Option { if !active.insert(id) { diff --git a/crates/psrs-core/src/opt/tests/global_inline.rs b/crates/psrs-core/src/opt/tests/global_inline.rs index 1a2e1114..9462cd83 100644 --- a/crates/psrs-core/src/opt/tests/global_inline.rs +++ b/crates/psrs-core/src/opt/tests/global_inline.rs @@ -40,7 +40,7 @@ fn named_global_inlining_binds_arguments_once_before_effects_and_preserves_spans }], body: Box::new(expression( ExprKind::IntrinsicCall { - intrinsic: Intrinsic::I32Add, + intrinsic: Intrinsic::IntAdd, arguments: vec![call, expression(ExprKind::Local(LocalId(0)), 0, 40, 45)], }, int_type.0, @@ -80,7 +80,7 @@ fn named_global_inlining_binds_arguments_once_before_effects_and_preserves_spans quantified: Vec::new(), value: expression( ExprKind::IntrinsicCall { - intrinsic: Intrinsic::IntDiv, + intrinsic: Intrinsic::IntQuot, arguments: vec![ expression(ExprKind::Integer(1), 0, 70, 71), expression(ExprKind::Integer(0), 0, 72, 73), @@ -117,7 +117,7 @@ fn named_global_inlining_binds_arguments_once_before_effects_and_preserves_spans panic!("the caller binding should remain in scope") }; let ExprKind::IntrinsicCall { - intrinsic: Intrinsic::I32Add, + intrinsic: Intrinsic::IntAdd, arguments, .. } = &body.kind @@ -146,7 +146,7 @@ fn named_global_inlining_binds_arguments_once_before_effects_and_preserves_spans assert!(matches!( callee_bindings[0].value.kind, ExprKind::IntrinsicCall { - intrinsic: Intrinsic::IntDiv, + intrinsic: Intrinsic::IntQuot, .. } )); @@ -257,11 +257,11 @@ fn evaluate( return Err(()); }; match intrinsic { - Intrinsic::I32Add => Ok(Value::Integer(left.wrapping_add(right))), - Intrinsic::IntDiv if right != 0 => { - left.checked_div_euclid(right).map(Value::Integer).ok_or(()) + Intrinsic::IntAdd => Ok(Value::Integer(left.wrapping_add(right))), + Intrinsic::IntQuot if right != 0 => { + left.checked_div(right).map(Value::Integer).ok_or(()) } - Intrinsic::IntDiv => Err(()), + Intrinsic::IntQuot => Err(()), _ => Err(()), } } diff --git a/crates/psrs-core/src/opt/tests/mod.rs b/crates/psrs-core/src/opt/tests/mod.rs index 71139148..99a73499 100644 --- a/crates/psrs-core/src/opt/tests/mod.rs +++ b/crates/psrs-core/src/opt/tests/mod.rs @@ -93,6 +93,7 @@ fn with_trace(mut module: Module) -> Module { ty, }); module.externals.push(ExternalSymbol { + source_module: None, symbol: SymbolId::new(ModuleId(0), 0), name: "trace".into(), kind: ExternalKind::Wit { @@ -130,7 +131,7 @@ fn trace_call(function_type: u32, int_type: u32, argument: i32, start: u32) -> E fn folds_wrapping_integer_arithmetic_and_keeps_the_operation_span() { let value = expression( ExprKind::IntrinsicCall { - intrinsic: Intrinsic::I32Add, + intrinsic: Intrinsic::IntAdd, arguments: vec![ expression(ExprKind::Integer(i32::MAX), 0, 5, 6), expression(ExprKind::Integer(1), 0, 9, 10), @@ -160,7 +161,7 @@ fn folds_wrapping_integer_arithmetic_and_keeps_the_operation_span() { fn leaves_constant_division_that_would_trap() { let value = expression( ExprKind::IntrinsicCall { - intrinsic: Intrinsic::I32DivS, + intrinsic: Intrinsic::IntQuot, arguments: vec![ expression(ExprKind::Integer(1), 0, 5, 6), expression(ExprKind::Integer(0), 0, 9, 10), @@ -182,7 +183,7 @@ fn leaves_constant_division_that_would_trap() { assert!(matches!( result.declarations[0].value.kind, ExprKind::IntrinsicCall { - intrinsic: Intrinsic::I32DivS, + intrinsic: Intrinsic::IntQuot, .. } )); @@ -201,7 +202,7 @@ fn retains_an_unused_euclidean_division_that_may_trap() { quantified: Vec::new(), value: expression( ExprKind::IntrinsicCall { - intrinsic: Intrinsic::IntDiv, + intrinsic: Intrinsic::IntQuot, arguments: vec![ expression(ExprKind::Integer(1), 0, 13, 14), expression(ExprKind::Integer(0), 0, 17, 18), @@ -237,7 +238,7 @@ fn retains_an_unused_euclidean_division_that_may_trap() { assert!(matches!( bindings[0].value.kind, ExprKind::IntrinsicCall { - intrinsic: Intrinsic::IntDiv, + intrinsic: Intrinsic::IntQuot, .. } )); @@ -250,7 +251,7 @@ fn algebraic_zero_does_not_remove_an_effectful_operand() { let function_type = arrow_type(&mut types, int_type, int_type); let value = expression( ExprKind::IntrinsicCall { - intrinsic: Intrinsic::I32Mul, + intrinsic: Intrinsic::IntMul, arguments: vec![ trace_call(function_type.0, int_type.0, 4, 5), expression(ExprKind::Integer(0), int_type.0, 14, 15), @@ -268,7 +269,7 @@ fn algebraic_zero_does_not_remove_an_effectful_operand() { assert!(matches!( result.declarations[0].value.kind, ExprKind::IntrinsicCall { - intrinsic: Intrinsic::I32Mul, + intrinsic: Intrinsic::IntMul, .. } )); diff --git a/crates/psrs-core/src/opt/tests/rank_inline.rs b/crates/psrs-core/src/opt/tests/rank_inline.rs index 9e557181..3df81edc 100644 --- a/crates/psrs-core/src/opt/tests/rank_inline.rs +++ b/crates/psrs-core/src/opt/tests/rank_inline.rs @@ -175,3 +175,59 @@ fn global_inline_keeps_a_forall_signature_with_an_empty_quantified_list() { if matches!(&head.kind, ExprKind::Global(symbol) if *symbol == function) )); } + +#[test] +fn local_inline_keeps_a_result_quantifier_on_the_enclosing_let() { + let variable = TypeId(0); + let int_type = TypeId(1); + let mut types = vec![ + Type::Variable(TypeVariableId(0)), + Type::Constructor(TypeConstructor::Int), + ]; + let function_type = arrow_type(&mut types, variable, variable); + let quantified = TypeId(types.len() as u32); + types.push(Type::ForAll { + variables: vec![TypeVariableId(0)], + body: variable, + }); + let argument = expression( + ExprKind::RepresentationCast { + value: Box::new(expression(ExprKind::Integer(1), int_type.0, 1, 2)), + source_type: int_type, + target_type: variable, + }, + variable.0, + 1, + 2, + ); + let lambda = expression( + ExprKind::Lambda { + binder: Binder { + id: LocalId(0), + name: "value".into(), + ty: variable, + span: span(3, 8), + }, + body: Box::new(expression(ExprKind::Local(LocalId(0)), variable.0, 9, 14)), + }, + function_type.0, + 3, + 14, + ); + let input = module( + types, + quantified.0, + expression( + ExprKind::Application(Box::new(lambda), Box::new(argument)), + quantified.0, + 1, + 14, + ), + ); + + let optimized = optimize(input, Budget::default()).expect("the cast variable stays bound"); + let ExprKind::Let { bindings, .. } = &optimized.declarations[0].value.kind else { + panic!("the coercion lambda should beta-reduce: {optimized:?}"); + }; + assert!(bindings[0].quantified.is_empty()); +} diff --git a/crates/psrs-core/src/opt/util.rs b/crates/psrs-core/src/opt/util.rs index d502789b..f4afc800 100644 --- a/crates/psrs-core/src/opt/util.rs +++ b/crates/psrs-core/src/opt/util.rs @@ -1,31 +1,13 @@ use crate::{Binding, Expr, ExprKind, LocalId, Module, Pattern, PatternKind}; use std::collections::{HashMap, HashSet}; -pub(super) struct FreshLocals { - next: Option, -} - -impl FreshLocals { - pub fn for_declaration(expression: &Expr) -> Self { - let mut ids = HashSet::new(); - collect_ids(expression, &mut ids); - Self { - next: ids - .iter() - .map(|id| id.0) - .max() - .map_or(Some(0), |value| value.checked_add(1)), - } - } +pub(super) use crate::locals::FreshLocals; - pub fn fresh(&mut self) -> Option { - let id = LocalId(self.next?); - self.next = id.0.checked_add(1); - Some(id) - } +pub(super) fn count_nodes(expression: &Expr) -> usize { + psrs_span::with_sufficient_stack(|| count_nodes_inner(expression)) } -pub(super) fn count_nodes(expression: &Expr) -> usize { +fn count_nodes_inner(expression: &Expr) -> usize { 1 + match &expression.kind { ExprKind::Local(_) | ExprKind::Global(_) @@ -98,6 +80,14 @@ fn substitute_inner( expression: &Expr, substitutions: &HashMap, shadowed: &mut HashSet, +) -> Expr { + psrs_span::with_sufficient_stack(|| substitute_inner_walk(expression, substitutions, shadowed)) +} + +fn substitute_inner_walk( + expression: &Expr, + substitutions: &HashMap, + shadowed: &mut HashSet, ) -> Expr { if let ExprKind::Local(id) = &expression.kind && !shadowed.contains(id) @@ -289,102 +279,3 @@ fn pattern_ids(pattern: &Pattern) -> Vec { collect(pattern, &mut ids); ids } - -fn collect_ids(expression: &Expr, ids: &mut HashSet) { - match &expression.kind { - ExprKind::Local(id) => { - ids.insert(*id); - } - ExprKind::Lambda { binder, body } => { - ids.insert(binder.id); - collect_ids(body, ids); - } - ExprKind::Let { bindings, body } => { - for binding in bindings { - ids.insert(binding.binder.id); - collect_ids(&binding.value, ids); - } - collect_ids(body, ids); - } - ExprKind::Case { - scrutinee, - branches, - } => { - collect_ids(scrutinee, ids); - for branch in branches { - collect_pattern_ids(&branch.pattern, ids); - collect_ids(&branch.value, ids); - } - } - ExprKind::Constructor { arguments, .. } - | ExprKind::IntrinsicCall { arguments, .. } - | ExprKind::Array { - elements: arguments, - } => { - for argument in arguments { - collect_ids(argument, ids); - } - } - ExprKind::Record { fields } => { - for (_, value) in fields { - collect_ids(value, ids); - } - } - ExprKind::RecordUpdate { record, fields } => { - collect_ids(record, ids); - for (_, value) in fields { - collect_ids(value, ids); - } - } - ExprKind::FieldAccess { record, .. } - | ExprKind::RepresentationCast { value: record, .. } => collect_ids(record, ids), - ExprKind::Application(left, right) => { - collect_ids(left, ids); - collect_ids(right, ids); - } - ExprKind::If { - condition, - then_branch, - else_branch, - } => { - collect_ids(condition, ids); - collect_ids(then_branch, ids); - collect_ids(else_branch, ids); - } - ExprKind::Global(_) - | ExprKind::Integer(_) - | ExprKind::Number(_) - | ExprKind::Boolean(_) - | ExprKind::String(_) - | ExprKind::Char(_) => {} - ExprKind::Unit | ExprKind::Trap => {} - } -} - -fn collect_pattern_ids(pattern: &Pattern, ids: &mut HashSet) { - match &pattern.kind { - PatternKind::Var { id, .. } => { - ids.insert(*id); - } - PatternKind::Named { id, pattern } => { - ids.insert(*id); - collect_pattern_ids(pattern, ids); - } - PatternKind::Array { elements } => { - for element in elements { - collect_pattern_ids(element, ids); - } - } - PatternKind::Constructor { arguments, .. } => { - for argument in arguments { - collect_pattern_ids(argument, ids); - } - } - PatternKind::Record { fields } => { - for (_, field) in fields { - collect_pattern_ids(field, ids); - } - } - PatternKind::Wildcard | PatternKind::Literal { .. } => {} - } -} diff --git a/crates/psrs-core/src/primitive.rs b/crates/psrs-core/src/primitive.rs new file mode 100644 index 00000000..32489215 --- /dev/null +++ b/crates/psrs-core/src/primitive.rs @@ -0,0 +1,191 @@ +//! Instantiate checked primitive callable values before runtime ABI erasure. +//! +//! This is mandatory elaboration, not budgeted optimization. Primitive globals +//! must use the checked type at each occurrence; adapting a polymorphic array +//! wrapper by copying its inputs would change mutation and reference identity. +use crate::locals::FreshLocals; +use crate::{Binder, Expr, ExprKind, Module, Type, TypeId, VerifyError, arrow_parts, scheme_parts}; +use psrs_hir::{Intrinsic, SymbolId}; +use psrs_span::TextRange; +use std::collections::HashMap; + +/// The value operation of a checked explicit primitive. Unsafe coercion is a +/// representation boundary, matching source coercion lowering, not a target +/// arithmetic instruction. The caller validates its signature and operands. +pub fn primitive_value( + intrinsic: Intrinsic, + arguments: Vec, + result: TypeId, + span: TextRange, +) -> Expr { + let kind = if intrinsic == Intrinsic::UnsafeCoerce && arguments.len() == 1 { + let value = arguments.into_iter().next().expect("one checked operand"); + ExprKind::RepresentationCast { + source_type: value.ty, + target_type: result, + value: Box::new(value), + } + } else { + ExprKind::IntrinsicCall { + intrinsic, + arguments, + } + }; + Expr { + kind, + ty: result, + span, + } +} + +/// The caller supplies verified Core and validated operation identities. Work +/// on a candidate and verify the complete result before publishing it; failures +/// may leave this candidate partially rewritten. +pub fn expand_primitive_globals( + module: &mut Module, + primitives: &HashMap, +) -> Result<(), Vec> { + for declaration in &mut module.declarations { + let mut fresh = FreshLocals::for_declaration(&declaration.value); + rewrite( + &mut declaration.value, + &module.types, + primitives, + &mut fresh, + ) + .map_err(|message| { + vec![VerifyError { + module: declaration.symbol.module, + span: declaration.span, + message, + }] + })?; + } + Ok(()) +} + +fn rewrite( + expression: &mut Expr, + types: &[Type], + primitives: &HashMap, + fresh: &mut FreshLocals, +) -> Result<(), &'static str> { + psrs_span::with_sufficient_stack(|| rewrite_inner(expression, types, primitives, fresh)) +} + +fn rewrite_inner( + expression: &mut Expr, + types: &[Type], + primitives: &HashMap, + fresh: &mut FreshLocals, +) -> Result<(), &'static str> { + match &mut expression.kind { + ExprKind::Global(symbol) => { + if let Some(intrinsic) = primitives.get(symbol) { + let (_, mut cursor) = scheme_parts(types, expression.ty) + .ok_or("primitive value has an invalid checked scheme")?; + let mut parameters = Vec::new(); + for index in 0..intrinsic.descriptor().arity { + let (parameter, result) = arrow_parts(types, cursor) + .ok_or("primitive value has an invalid checked function type")?; + let binder = Binder { + id: fresh + .fresh() + .ok_or("cannot allocate a primitive parameter")?, + name: format!("primitive_argument_{index}"), + ty: parameter, + span: expression.span, + }; + parameters.push((binder, cursor)); + cursor = result; + } + let mut body = primitive_value( + *intrinsic, + parameters + .iter() + .map(|(binder, _)| Expr { + kind: ExprKind::Local(binder.id), + ty: binder.ty, + span: expression.span, + }) + .collect(), + cursor, + expression.span, + ); + for (binder, ty) in parameters.into_iter().rev() { + body = Expr { + kind: ExprKind::Lambda { + binder, + body: Box::new(body), + }, + ty, + span: expression.span, + }; + } + // Keep the occurrence's outer forall and its checked identities. + body.ty = expression.ty; + *expression = body; + } + } + ExprKind::Constructor { arguments, .. } + | ExprKind::IntrinsicCall { arguments, .. } + | ExprKind::Array { + elements: arguments, + } => { + for argument in arguments { + rewrite(argument, types, primitives, fresh)?; + } + } + ExprKind::Record { fields } => { + for (_, value) in fields { + rewrite(value, types, primitives, fresh)?; + } + } + ExprKind::RecordUpdate { record, fields } => { + rewrite(record, types, primitives, fresh)?; + for (_, value) in fields { + rewrite(value, types, primitives, fresh)?; + } + } + ExprKind::FieldAccess { record, .. } + | ExprKind::RepresentationCast { value: record, .. } + | ExprKind::Lambda { body: record, .. } => rewrite(record, types, primitives, fresh)?, + ExprKind::Application(function, argument) => { + rewrite(function, types, primitives, fresh)?; + rewrite(argument, types, primitives, fresh)?; + } + ExprKind::Let { bindings, body } => { + for binding in bindings { + rewrite(&mut binding.value, types, primitives, fresh)?; + } + rewrite(body, types, primitives, fresh)?; + } + ExprKind::If { + condition, + then_branch, + else_branch, + } => { + rewrite(condition, types, primitives, fresh)?; + rewrite(then_branch, types, primitives, fresh)?; + rewrite(else_branch, types, primitives, fresh)?; + } + ExprKind::Case { + scrutinee, + branches, + } => { + rewrite(scrutinee, types, primitives, fresh)?; + for branch in branches { + rewrite(&mut branch.value, types, primitives, fresh)?; + } + } + ExprKind::Local(_) + | ExprKind::Integer(_) + | ExprKind::Number(_) + | ExprKind::Boolean(_) + | ExprKind::String(_) + | ExprKind::Char(_) + | ExprKind::Unit + | ExprKind::Trap => {} + } + Ok(()) +} diff --git a/crates/psrs-core/src/records.rs b/crates/psrs-core/src/records.rs index 0ae1bebb..7c1f9136 100644 --- a/crates/psrs-core/src/records.rs +++ b/crates/psrs-core/src/records.rs @@ -22,7 +22,11 @@ pub fn record_row(types: &[Type], id: TypeId) -> Option { /// reaches a node that is neither a row constructor nor a row variable. pub fn row_fields(types: &[Type], mut row: TypeId) -> Option { let mut fields = Vec::new(); + let mut seen = std::collections::HashSet::new(); loop { + if !seen.insert(row) { + return None; + } match types.get(row.0 as usize)? { Type::RowEmpty => return Some((fields, None)), Type::RowExtend { label, ty, tail } => { diff --git a/crates/psrs-core/src/tests/command.rs b/crates/psrs-core/src/tests/command.rs new file mode 100644 index 00000000..5a08d907 --- /dev/null +++ b/crates/psrs-core/src/tests/command.rs @@ -0,0 +1,64 @@ +use super::*; +use psrs_hir::TypeVariableId; + +fn fixture(entry: TypeId) -> Module { + let mut types = vec![ + Type::Constructor(TypeConstructor::Int), + Type::Constructor(TypeConstructor::Unit), + Type::Variable(TypeVariableId(7)), + ]; + let runner_type = arrow_type(&mut types, TypeId(1), TypeId(0)); + let span = TextRange::new(0, 1); + let mut module = single_declaration( + types, + entry, + Expr { + kind: ExprKind::Trap, + ty: entry, + span, + }, + ); + module.declarations[0].quantified = vec![TypeVariableId(7)]; + let symbol = SymbolId::new(module.id, 1); + module.declarations.push(Declaration { + symbol, + name: "runner".into(), + name_span: span, + quantified: vec![TypeVariableId(8)], + ty: runner_type, + value: Expr { + kind: ExprKind::Trap, + ty: runner_type, + span, + }, + span, + }); + module +} + +#[test] +fn closed_command_types_allow_quantifiers_used_only_inside_declarations() { + for ty in [TypeId(0), TypeId(1)] { + let mut module = fixture(ty); + let entry = module.declarations[0].symbol; + let runner = module.declarations[1].symbol; + let normalized = crate::command::normalize_entry(&mut module, entry, runner).unwrap(); + assert_eq!(normalized == entry, ty == TypeId(0)); + module.verify().unwrap(); + } +} + +#[test] +fn open_command_types_are_rejected_without_changing_the_module() { + let mut module = fixture(TypeId(2)); + let entry = module.declarations[0].symbol; + let runner = module.declarations[1].symbol; + let before = module.clone(); + let errors = crate::command::normalize_entry(&mut module, entry, runner).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message == "command entry must be monomorphic") + ); + assert_eq!(module, before); +} diff --git a/crates/psrs-core/src/tests/control.rs b/crates/psrs-core/src/tests/control.rs new file mode 100644 index 00000000..1aa7bcba --- /dev/null +++ b/crates/psrs-core/src/tests/control.rs @@ -0,0 +1,154 @@ +//! Malformed control calls must fail before their domains are erased. +use super::*; + +fn append(types: &mut Vec, ty: Type) -> TypeId { + let id = TypeId(types.len() as u32); + types.push(ty); + id +} + +fn application(types: &mut Vec, head: TypeId, argument: TypeId) -> TypeId { + append(types, Type::Application(head, argument)) +} + +fn fixture(wrong_raw: bool, wrong_target: bool) -> Module { + let variable = psrs_hir::TypeVariableId(42); + let mut types = vec![ + Type::Constructor(TypeConstructor::Int), + Type::Constructor(TypeConstructor::Boolean), + Type::Constructor(TypeConstructor::User(psrs_hir::TypeId::CONTROL)), + Type::Constructor(TypeConstructor::User(psrs_hir::TypeId::new(ModuleId(0), 7))), + Type::Constructor(TypeConstructor::User(psrs_hir::TypeId::new(ModuleId(0), 8))), + Type::Variable(variable), + ]; + let head = application(&mut types, TypeId(2), TypeId(3)); + let source = application(&mut types, head, TypeId(0)); + let head = application(&mut types, TypeId(2), TypeId(4)); + let target = application(&mut types, head, TypeId(1)); + let ret = arrow_type(&mut types, TypeId(0), target); + let request = application(&mut types, TypeId(3), TypeId(5)); + let raw = arrow_type( + &mut types, + if wrong_raw { TypeId(1) } else { TypeId(5) }, + source, + ); + let result = if wrong_target { source } else { target }; + let op = arrow_type(&mut types, raw, result); + let op = arrow_type(&mut types, request, op); + let op = append( + &mut types, + Type::ForAll { + variables: vec![variable], + body: op, + }, + ); + let trap = |ty| Expr { + kind: ExprKind::Trap, + ty, + span: TextRange::new(3, 9), + }; + single_declaration( + types, + target, + Expr { + kind: ExprKind::IntrinsicCall { + intrinsic: Intrinsic::ControlHandle, + arguments: vec![trap(ret), trap(op), trap(source)], + }, + ty: target, + span: TextRange::new(3, 9), + }, + ) +} + +#[test] +fn verifier_accepts_distinct_control_source_and_target_answers() { + fixture(false, false).verify().unwrap(); +} + +#[test] +fn verifier_rejects_control_domain_and_raw_answer_mismatches() { + for (raw, target) in [(true, false), (false, true)] { + let errors = fixture(raw, target).verify().unwrap_err(); + assert!(errors.iter().any(|error| error.message + == "control handle clauses do not preserve their checked source and target contracts")); + assert!(errors.iter().all(|error| error.module == ModuleId(0))); + } +} + +#[test] +fn verifier_rejects_old_control_arity_without_indexing_invalid_arguments() { + for count in [0, 1, 2, 4, 5] { + let mut module = fixture(false, false); + let ExprKind::IntrinsicCall { arguments, .. } = &mut module.declarations[0].value.kind + else { + unreachable!() + }; + arguments.resize(count, arguments[0].clone()); + assert!(has_message( + &module, + "intrinsic call does not have its registered saturated arity" + )); + } +} + +#[test] +fn verifier_rejects_a_forged_source_payload_at_the_common_constructor() { + let mut module = fixture(false, false); + let ExprKind::IntrinsicCall { arguments, .. } = &mut module.declarations[0].value.kind else { + unreachable!() + }; + arguments[2].kind = ExprKind::IntrinsicCall { + intrinsic: Intrinsic::ControlReturn, + arguments: vec![Expr { + kind: ExprKind::Boolean(true), + ty: TypeId(1), + span: TextRange::new(3, 9), + }], + }; + assert!(has_message( + &module, + "control primitive does not preserve its checked carrier and callable contracts" + )); +} + +#[test] +fn checked_control_proof_retains_both_domains_and_the_full_clause_type() { + let module = fixture(false, false); + let expression = &module.declarations[0].value; + let ExprKind::IntrinsicCall { + intrinsic, + arguments, + } = &expression.kind + else { + unreachable!() + }; + let arguments = arguments + .iter() + .map(|argument| argument.ty) + .collect::>(); + let proof = + crate::checked_control_contract(&module, *intrinsic, expression.ty, &arguments).unwrap(); + assert_eq!(proof.operation(), Intrinsic::ControlHandle); + assert_eq!(proof.result(), expression.ty); + assert_eq!(proof.arguments(), arguments); + assert_eq!( + proof.source(), + Some(crate::ControlDomain { + request: TypeId(3), + answer: TypeId(0) + }) + ); + assert_eq!( + proof.target(), + Some(crate::ControlDomain { + request: TypeId(4), + answer: TypeId(1) + }) + ); + assert!(matches!( + module.types[proof.arguments()[1].0 as usize], + Type::ForAll { .. } + )); + assert!(crate::checked_control_contract(&module, *intrinsic, expression.ty, &[]).is_err()); +} diff --git a/crates/psrs-core/src/tests/deep_expr.rs b/crates/psrs-core/src/tests/deep_expr.rs new file mode 100644 index 00000000..50371b6e --- /dev/null +++ b/crates/psrs-core/src/tests/deep_expr.rs @@ -0,0 +1,30 @@ +use super::*; + +/// Clone and equality follow a deep application spine on a heap stack. +/// Ordinary recursive destruction also fits the worker stack at this depth. +#[test] +fn deep_application_clone_equality_and_drop_stay_within_a_worker_stack() { + let mut expression = Expr { + kind: ExprKind::Boolean(true), + ty: TypeId(0), + span: TextRange::default(), + }; + for _ in 0..800 { + expression = Expr { + kind: ExprKind::Application( + Box::new(Expr { + kind: ExprKind::Boolean(false), + ty: TypeId(0), + span: TextRange::default(), + }), + Box::new(expression), + ), + ty: TypeId(0), + span: TextRange::default(), + }; + } + let cloned = expression.clone(); + assert_eq!(expression, cloned); + drop(cloned); + drop(expression); +} diff --git a/crates/psrs-core/src/tests/dictionaries.rs b/crates/psrs-core/src/tests/dictionaries.rs index 1cbf9d06..782d3e0b 100644 --- a/crates/psrs-core/src/tests/dictionaries.rs +++ b/crates/psrs-core/src/tests/dictionaries.rs @@ -56,9 +56,12 @@ fn lowering_erases_instance_and_superclass_evidence_to_calls_and_projections() { ]; let i32_to_boolean = push_arrow(&mut types, thir::TypeId(0), thir::TypeId(1)); let dictionary = push_record(&mut types, vec![("isPositive", i32_to_boolean)]); + let unit = thir::TypeId(types.len() as u32); + types.push(thir::Type::Constructor(thir::TypeConstructor::Unit)); + let superclass_thunk = push_arrow(&mut types, unit, dictionary); let parent_dictionary = push_record( &mut types, - vec![("rank", thir::TypeId(0)), ("super", dictionary)], + vec![("rank", thir::TypeId(0)), ("super", superclass_thunk)], ); let identity_type = push_arrow(&mut types, parent_dictionary, parent_dictionary); let main_type = push_arrow(&mut types, parent_dictionary, thir::TypeId(1)); @@ -183,8 +186,12 @@ fn lowering_erases_instance_and_superclass_evidence_to_calls_and_projections() { panic!("expected method selection to be an ordinary field projection"); }; assert_eq!(field, "isPositive"); - let ExprKind::FieldAccess { record, field } = &record.kind else { - panic!("expected superclass evidence to be an ordinary field projection"); + let ExprKind::Application(thunk, unit) = &record.kind else { + panic!("expected superclass selection to force its thunk"); + }; + assert!(matches!(unit.kind, ExprKind::Unit)); + let ExprKind::FieldAccess { record, field } = &thunk.kind else { + panic!("expected superclass selection to project its thunk"); }; assert_eq!(field, "super"); assert!(matches!(record.kind, ExprKind::Application(_, _))); diff --git a/crates/psrs-core/src/tests/effects.rs b/crates/psrs-core/src/tests/effects.rs deleted file mode 100644 index 9f90da3b..00000000 --- a/crates/psrs-core/src/tests/effects.rs +++ /dev/null @@ -1,268 +0,0 @@ -//! Fixtures for the effect representation closures written by `lower_effects`. -//! -//! The pass is the only one that recognizes the opaque `Prelude.Effect` -//! application, so it also owns the check that every closure it wrote takes the -//! runtime token alone and returns the lowered effect result. - -use super::*; -use crate::effect::{EffectClosure, TrustedEffect, classify_effect_import, lower_effects}; -use psrs_hir::{ModuleId, SymbolId, TypeId as HirTypeId, TypeVariableId}; - -const SPAN: TextRange = TextRange::new(0, 8); - -const TOKEN: TypeId = TypeId(0); -const A: TypeId = TypeId(1); -const B: TypeId = TypeId(2); -const ARROW: TypeId = TypeId(5); -const EFFECT: TypeId = TypeId(6); -const EFFECT_APPLICATION: TypeId = TypeId(7); -const EFFECT_HIR: HirTypeId = HirTypeId::new(ModuleId(0), 3); - -fn trusted_effect() -> TrustedEffect { - TrustedEffect { - effect_type: EFFECT_HIR, - operations: Vec::new(), - } -} - -/// A linked module whose only effect is `Effect (a -> b)`. -/// -/// Its type table is -/// -/// | id | node | -/// | --- | --- | -/// | 0 | `Int`, the token representation lowering chose | -/// | 1, 2 | `a`, `b` | -/// | 3, 4, 5 | `a -> b` | -/// | 6 | `Prelude.Effect` | -/// | 7 | `Effect (a -> b)`, the node lowering replaces | -fn effect_module() -> Module { - let effect = EFFECT_HIR; - Module { - id: ModuleId(0), - name: "Main".into(), - externals: Vec::new(), - external_types: Vec::new(), - types: vec![ - Type::Constructor(TypeConstructor::Int), - Type::Variable(TypeVariableId(0)), - Type::Variable(TypeVariableId(1)), - Type::Constructor(TypeConstructor::Function), - Type::Application(TypeId(3), A), - Type::Application(TypeId(4), B), - Type::Constructor(TypeConstructor::User(effect)), - Type::Application(EFFECT, ARROW), - ], - newtype_ids: Vec::new(), - opaque_ids: vec![effect], - callable_types: Vec::new(), - constructors: Vec::new(), - declarations: Vec::new(), - type_names: vec![(effect, "Prelude.Effect".into())], - entry: Some(SymbolId::new(ModuleId(0), 0)), - span: SPAN, - } -} - -/// `Effect (a -> b)` lowers to one token parameter and keeps `a -> b` whole as -/// its result, so the accepted path stays accepted. -#[test] -fn lowered_effect_closures_are_one_token_closure_over_the_effect_result() { - let mut module = effect_module(); - let lowering = - lower_effects(&mut module, &trusted_effect()).expect("the written closure keeps its shape"); - assert_eq!( - lowering.closures, - vec![EffectClosure { - ty: EFFECT_APPLICATION, - token: TOKEN, - result: ARROW, - }] - ); - assert_eq!( - closure_parts(&module.types, EFFECT_APPLICATION), - Some(([TOKEN].as_slice(), ARROW)), - "Effect (a -> b) takes the token alone and returns the function whole" - ); - assert!(lowering.verify(&module).is_ok()); - assert!( - module.verify().is_ok(), - "{:?}", - module.verify().unwrap_err() - ); -} - -/// The negative fixture the obligation names: the closure for `Effect (a -> b)` -/// flattened to two parameters `[Token, a]` is not a representation closure. -/// The general Core verifier accepts that type table, so the rejection is this -/// pass's own check and it happens before closure conversion. -#[test] -fn a_lowered_effect_closure_flattened_to_arity_two_is_rejected() { - let mut module = effect_module(); - let lowering = lower_effects(&mut module, &trusted_effect()) - .expect("the written closure starts in its shape"); - module.types[EFFECT_APPLICATION.0 as usize] = Type::Closure { - parameters: vec![TOKEN, A], - result: ARROW, - }; - assert!( - module.verify().is_ok(), - "{:?}", - module.verify().unwrap_err() - ); - assert_eq!( - messages(lowering.verify(&module)), - ["a lowered effect closure must take only the runtime token"] - ); -} - -/// A closure that takes the token but returns something other than the lowered -/// effect result is not a representation closure either. -#[test] -fn a_lowered_effect_closure_with_the_wrong_result_is_rejected() { - let mut module = effect_module(); - let lowering = lower_effects(&mut module, &trusted_effect()) - .expect("the written closure starts in its shape"); - module.types[EFFECT_APPLICATION.0 as usize] = Type::Closure { - parameters: vec![TOKEN], - result: B, - }; - assert_eq!( - messages(lowering.verify(&module)), - ["a lowered effect closure must return the lowered effect result"] - ); -} - -/// A node that is not a closure at all cannot be the representation of an -/// effect either. -#[test] -fn a_lowered_effect_that_is_not_a_closure_is_rejected() { - let mut module = effect_module(); - let lowering = lower_effects(&mut module, &trusted_effect()) - .expect("the written closure starts in its shape"); - module.types[EFFECT_APPLICATION.0 as usize] = Type::Application(EFFECT, ARROW); - let errors = lowering - .verify(&module) - .expect_err("an application is not a closure representation"); - assert_eq!( - errors[0].message, - "a lowered effect application is not a closure" - ); - assert_eq!(errors[0].module, module.id); - assert_eq!(errors[0].span, module.span); -} - -/// A module without the opaque library type is left alone and passes the check. -#[test] -fn lowering_rejects_missing_trusted_effect_identity() { - let mut module = effect_module(); - module.type_names.clear(); - module.opaque_ids.clear(); - let errors = lower_effects(&mut module, &trusted_effect()) - .expect_err("missing trusted identity metadata is an error"); - assert_eq!( - errors[0].message, - "trusted Effect identity is missing or is not an opaque type" - ); -} - -#[test] -fn effect_import_classification_rejects_a_cyclic_arrow_result() { - let mut module = effect_module(); - module.types = vec![ - Type::Constructor(TypeConstructor::Function), - Type::Constructor(TypeConstructor::Int), - Type::Application(TypeId(0), TypeId(1)), - Type::Application(TypeId(2), TypeId(3)), - ]; - let error = classify_effect_import(&module, TypeId(3), EFFECT_HIR) - .expect_err("a recursive arrow spine is malformed"); - assert_eq!(error, "cyclic type spine in effect import signature"); -} - -#[test] -fn a_checked_foreign_effect_signature_must_quantify_its_type_variables() { - let mut module = effect_module(); - let symbol = SymbolId::new(ModuleId(0), 9); - let free = TypeId(module.types.len() as u32); - module.types.push(Type::Variable(TypeVariableId(99))); - module.externals.push(psrs_hir::ExternalSymbol { - symbol, - name: "foreignEffect".into(), - kind: psrs_hir::ExternalKind::Wit { - interface: "test:effect".into(), - function: "foreign-effect".into(), - }, - signature: None, - }); - module.external_types.push(crate::ExternalType { - symbol, - source_module: module.id, - ty: free, - }); - - let errors = module - .verify() - .expect_err("free external variables are invalid"); - assert!( - errors - .iter() - .any(|error| { error.message == "type variable is outside its quantifier scope" }) - ); -} - -#[test] -fn another_foreign_effect_identity_is_not_the_trusted_effect_constructor() { - let mut module = effect_module(); - let other_effect = HirTypeId::new(ModuleId(1), 3); - let other = TypeId(module.types.len() as u32); - module - .types - .push(Type::Constructor(TypeConstructor::User(other_effect))); - let application = TypeId(module.types.len() as u32); - module.types.push(Type::Application(other, A)); - module.opaque_ids.push(other_effect); - module - .type_names - .push((other_effect, "Prelude.Effect".into())); - - assert_eq!( - classify_effect_import(&module, application, EFFECT_HIR).unwrap(), - None, - "matching spelling and opacity do not substitute for trusted identity" - ); - - lower_effects(&mut module, &trusted_effect()).unwrap(); - assert!(matches!( - module.types.get(application.0 as usize), - Some(Type::Application(_, _)) - )); - assert!(matches!( - module.types.get(EFFECT_APPLICATION.0 as usize), - Some(Type::Closure { .. }) - )); -} - -#[test] -fn an_ordinary_closure_shaped_import_is_not_an_effect_import() { - let mut module = effect_module(); - let ordinary = TypeId(module.types.len() as u32); - module.types.push(Type::Closure { - parameters: vec![A], - result: B, - }); - - assert_eq!( - classify_effect_import(&module, ordinary, EFFECT_HIR).unwrap(), - None, - "suspension is planned from the checked abstract Effect result, not closure shape" - ); -} - -fn messages(result: Result<(), Vec>) -> Vec<&'static str> { - result - .expect_err("the malformed closure must be rejected") - .iter() - .map(|error| error.message) - .collect() -} diff --git a/crates/psrs-core/src/tests/external_types.rs b/crates/psrs-core/src/tests/external_types.rs index 731fed88..a797a7b0 100644 --- a/crates/psrs-core/src/tests/external_types.rs +++ b/crates/psrs-core/src/tests/external_types.rs @@ -8,6 +8,7 @@ fn fixture() -> Module { id: ModuleId(2), name: "ExternalScheme".into(), externals: vec![ExternalSymbol { + source_module: None, symbol, name: "clock".into(), kind: ExternalKind::Wit { diff --git a/crates/psrs-core/src/tests/instantiation/mod.rs b/crates/psrs-core/src/tests/instantiation/mod.rs new file mode 100644 index 00000000..88db4ca1 --- /dev/null +++ b/crates/psrs-core/src/tests/instantiation/mod.rs @@ -0,0 +1,402 @@ +//! Checked instantiation evidence. These cases cover the relation P8 reads +//! before representation lowering rewrites a constructor application. + +use super::*; + +mod polymorphic; +use psrs_hir::{ModuleId, TypeId as HirTypeId, TypeVariableId}; +use std::collections::HashMap; + +const SPAN: TextRange = TextRange::new(0, 1); + +fn bare(types: Vec) -> Module { + Module { + type_names: Vec::new(), + id: ModuleId(0), + name: "Instantiation".into(), + externals: Vec::new(), + external_types: Vec::new(), + types, + newtype_ids: Vec::new(), + opaque_ids: Vec::new(), + callable_types: Vec::new(), + constructors: Vec::new(), + declarations: Vec::new(), + entry: None, + span: SPAN, + } +} + +fn apply(types: &mut Vec, function: TypeId, argument: TypeId) -> TypeId { + let id = TypeId(types.len() as u32); + types.push(Type::Application(function, argument)); + id +} + +fn nominal(types: &mut Vec, id: u32, argument: TypeId) -> TypeId { + let constructor = TypeId(types.len() as u32); + types.push(Type::Constructor(TypeConstructor::User(HirTypeId::new( + ModuleId(0), + id, + )))); + apply(types, constructor, argument) +} + +#[test] +fn distinct_nominal_applications_are_rejected() { + let mut types = vec![Type::Constructor(TypeConstructor::Int)]; + let maybe = nominal(&mut types, 1, TypeId(0)); + let either = nominal(&mut types, 2, TypeId(0)); + let module = bare(types); + assert!(module.checked_instantiation(maybe, &[], either).is_none()); +} + +#[test] +fn flexible_variables_match_from_either_side() { + let variable = TypeVariableId(7); + let mut types = vec![ + Type::Constructor(TypeConstructor::Int), + Type::Variable(variable), + ]; + let head = TypeId(types.len() as u32); + types.push(Type::Constructor(TypeConstructor::User(HirTypeId::new( + ModuleId(0), + 1, + )))); + let concrete = apply(&mut types, head, TypeId(0)); + let abstract_application = apply(&mut types, TypeId(1), TypeId(0)); + let module = bare(types); + + let from_scheme = module + .checked_instantiation(abstract_application, &[variable], concrete) + .expect("a flexible head instantiates to the nominal constructor"); + assert_eq!( + from_scheme.constructor(variable), + Some(( + TypeConstructor::User(HirTypeId::new(ModuleId(0), 1)), + Vec::new() + )) + ); + + let from_target = module + .checked_instantiation(concrete, &[variable], abstract_application) + .expect("a flexible target head binds to the concrete constructor"); + assert_eq!( + from_target.constructor(variable), + Some(( + TypeConstructor::User(HirTypeId::new(ModuleId(0), 1)), + Vec::new() + )) + ); +} + +#[test] +fn dictionary_fields_alpha_rename_their_quantifiers() { + let mut types = vec![ + Type::Constructor(TypeConstructor::Int), + Type::Variable(TypeVariableId(1)), + Type::Variable(TypeVariableId(2)), + Type::RowEmpty, + ]; + let left_body = arrow(&mut types, TypeId(0), TypeId(1)); + let left_scheme = forall(&mut types, vec![TypeVariableId(1)], left_body); + let right_body = arrow(&mut types, TypeId(0), TypeId(2)); + let right_scheme = forall(&mut types, vec![TypeVariableId(2)], right_body); + let left = record(&mut types, left_scheme); + let right = record(&mut types, right_scheme); + let module = bare(types); + assert!( + module.checked_instantiation(left, &[], right).is_some(), + "a dictionary field quantifier is alpha-equivalent under a renamed binder" + ); +} + +#[test] +fn constructor_binding_rejects_unsolved_variables_and_cycles() { + let variable = TypeVariableId(3); + let mut types = vec![ + Type::Constructor(TypeConstructor::Int), + Type::Variable(variable), + Type::Constructor(TypeConstructor::Function), + ]; + let function_int = apply(&mut types, TypeId(2), TypeId(0)); + let solved_module = bare(types); + let solved = Instantiation { + module: &solved_module, + rows: HashMap::new(), + replacements: HashMap::from([(variable, function_int)]), + }; + assert_eq!( + solved.constructor(variable), + Some((TypeConstructor::Function, vec![TypeId(0)])) + ); + + let unsolved = Instantiation { + module: &solved_module, + rows: HashMap::new(), + replacements: HashMap::from([(variable, TypeId(1))]), + }; + assert_eq!(unsolved.constructor(variable), None); + + let cyclic_module = bare(vec![Type::Application(TypeId(0), TypeId(0))]); + let cyclic = Instantiation { + module: &cyclic_module, + rows: HashMap::new(), + replacements: HashMap::from([(variable, TypeId(0))]), + }; + assert_eq!(cyclic.constructor(variable), None); +} + +fn arrow(types: &mut Vec, parameter: TypeId, result: TypeId) -> TypeId { + let function = TypeId(types.len() as u32); + types.push(Type::Constructor(TypeConstructor::Function)); + let applied = apply(types, function, parameter); + apply(types, applied, result) +} + +fn forall(types: &mut Vec, variables: Vec, body: TypeId) -> TypeId { + let id = TypeId(types.len() as u32); + types.push(Type::ForAll { variables, body }); + id +} + +fn record(types: &mut Vec, field: TypeId) -> TypeId { + let row = TypeId(types.len() as u32); + types.push(Type::RowExtend { + label: "chain".into(), + ty: field, + tail: TypeId(3), + }); + let head = TypeId(types.len() as u32); + types.push(Type::Constructor(TypeConstructor::Record)); + apply(types, head, row) +} + +#[test] +fn type_level_literals_match_by_value_in_invariant_applications() { + let mut types = vec![ + Type::TypeLevelString("Pair".into()), + Type::TypeLevelString("Pair".into()), + Type::TypeLevelString("Single".into()), + Type::TypeLevelInt(42), + Type::TypeLevelInt(42), + Type::TypeLevelInt(43), + ]; + for (first, equal, different) in [(0, 1, 2), (3, 4, 5)] { + let first = nominal(&mut types, 0, TypeId(first)); + let equal = nominal(&mut types, 0, TypeId(equal)); + let different = nominal(&mut types, 0, TypeId(different)); + let module = bare(types.clone()); + assert!(module.checked_instantiation(first, &[], equal).is_some()); + assert!( + module + .checked_instantiation(first, &[], different) + .is_none() + ); + } +} + +#[test] +fn row_evidence_retains_residuals_without_an_arena_node() { + let variable = TypeVariableId(40); + let mut types = vec![ + Type::Constructor(TypeConstructor::Int), + Type::RowEmpty, + Type::Variable(variable), + Type::Constructor(TypeConstructor::Record), + ]; + let generic_row = TypeId(types.len() as u32); + types.push(Type::RowExtend { + label: "b".into(), + ty: TypeId(0), + tail: TypeId(2), + }); + let generic = apply(&mut types, TypeId(3), generic_row); + let mut row = TypeId(1); + for label in ["c", "b", "a"] { + let id = TypeId(types.len() as u32); + types.push(Type::RowExtend { + label: label.into(), + ty: TypeId(0), + tail: row, + }); + row = id; + } + let concrete = apply(&mut types, TypeId(3), row); + let module = bare(types); + let evidence = module + .checked_instantiation(generic, &[variable], concrete) + .unwrap(); + let residual = evidence.row(variable).unwrap(); + assert_eq!( + residual.fields, + vec![("a".into(), TypeId(0)), ("c".into(), TypeId(0))] + ); + assert_eq!(residual.tail, None); + assert_eq!(evidence.constructor(variable), None); + assert_eq!(evidence.row(TypeVariableId(41)).map(|row| row.fields), None); +} + +fn row(types: &mut Vec, fields: &[(&str, TypeId)], tail: TypeId) -> TypeId { + fields.iter().rev().fold(tail, |tail, (label, ty)| { + let id = TypeId(types.len() as u32); + types.push(Type::RowExtend { + label: (*label).into(), + ty: *ty, + tail, + }); + id + }) +} + +#[test] +fn nominal_row_arguments_preserve_checked_residuals_and_label_order_independence() { + let variable = TypeVariableId(40); + let mut types = vec![ + Type::Constructor(TypeConstructor::Int), + Type::Constructor(TypeConstructor::Boolean), + Type::RowEmpty, + Type::Variable(variable), + ]; + let generic_row = row(&mut types, &[("y", TypeId(0))], TypeId(3)); + let generic = nominal(&mut types, 1, generic_row); + let concrete_row = row( + &mut types, + &[("a", TypeId(0)), ("b", TypeId(1)), ("y", TypeId(0))], + TypeId(2), + ); + let concrete = nominal(&mut types, 1, concrete_row); + let reordered_row = row( + &mut types, + &[("y", TypeId(0)), ("b", TypeId(1)), ("a", TypeId(0))], + TypeId(2), + ); + let reordered = nominal(&mut types, 1, reordered_row); + let generic_arrow = arrow(&mut types, generic, generic); + let consistent_arrow = arrow(&mut types, concrete, reordered); + let different_row = row( + &mut types, + &[("y", TypeId(0)), ("b", TypeId(0)), ("a", TypeId(0))], + TypeId(2), + ); + let different = nominal(&mut types, 1, different_row); + let inconsistent_arrow = arrow(&mut types, concrete, different); + let missing_row = row(&mut types, &[("a", TypeId(0))], TypeId(2)); + let missing = nominal(&mut types, 1, missing_row); + let duplicate_row = row(&mut types, &[("y", TypeId(0)), ("y", TypeId(0))], TypeId(2)); + let duplicate = nominal(&mut types, 1, duplicate_row); + let module = bare(types); + let evidence = module + .checked_instantiation(generic_arrow, &[variable], consistent_arrow) + .unwrap(); + let mut residual = evidence.row(variable).unwrap(); + residual.fields.sort_by(|left, right| left.0.cmp(&right.0)); + assert_eq!( + residual.fields, + vec![("a".into(), TypeId(0)), ("b".into(), TypeId(1))] + ); + assert_eq!(residual.tail, None); + assert!( + module + .checked_instantiation(concrete, &[], reordered) + .is_some() + ); + assert!( + module + .checked_instantiation(generic_arrow, &[variable], inconsistent_arrow) + .is_none() + ); + assert!( + module + .checked_instantiation(generic, &[], concrete) + .is_none(), + "rigid tails cannot absorb fields" + ); + assert!( + module + .checked_instantiation(generic, &[variable], missing) + .is_none() + ); + let duplicate_evidence = module + .checked_instantiation(generic, &[variable], duplicate) + .unwrap(); + assert_eq!( + duplicate_evidence.row(variable).unwrap().fields, + vec![("y".into(), TypeId(0))] + ); +} + +#[test] +fn nominal_row_arguments_preserve_duplicate_label_occurrence_order() { + let variable = TypeVariableId(41); + let mut types = vec![ + Type::Constructor(TypeConstructor::Int), + Type::Constructor(TypeConstructor::Boolean), + Type::RowEmpty, + Type::Variable(variable), + ]; + let scheme_row = row(&mut types, &[("a", TypeId(0)), ("a", TypeId(1))], TypeId(3)); + let scheme = nominal(&mut types, 1, scheme_row); + let matching_row = row( + &mut types, + &[("z", TypeId(0)), ("a", TypeId(0)), ("a", TypeId(1))], + TypeId(2), + ); + let matching = nominal(&mut types, 1, matching_row); + let reversed_row = row( + &mut types, + &[("a", TypeId(1)), ("a", TypeId(0)), ("z", TypeId(0))], + TypeId(2), + ); + let reversed = nominal(&mut types, 1, reversed_row); + let module = bare(types); + let evidence = module + .checked_instantiation(scheme, &[variable], matching) + .unwrap(); + assert_eq!( + evidence.row(variable).unwrap().fields, + vec![("z".into(), TypeId(0))] + ); + assert!( + module + .checked_instantiation(scheme, &[variable], reversed) + .is_none() + ); +} + +#[test] +fn repeated_residual_fields_use_the_checked_relation_for_nested_row_arguments() { + let variable = TypeVariableId(42); + let mut types = vec![ + Type::Constructor(TypeConstructor::Int), + Type::Constructor(TypeConstructor::Boolean), + Type::RowEmpty, + Type::Variable(variable), + ]; + let nested_left = row(&mut types, &[("a", TypeId(0)), ("b", TypeId(1))], TypeId(2)); + let nested_left = nominal(&mut types, 2, nested_left); + let nested_right = row(&mut types, &[("b", TypeId(1)), ("a", TypeId(0))], TypeId(2)); + let nested_right = nominal(&mut types, 2, nested_right); + let scheme_row = row(&mut types, &[("y", TypeId(0))], TypeId(3)); + let scheme = nominal(&mut types, 1, scheme_row); + let scheme = arrow(&mut types, scheme, scheme); + let domain_row = row( + &mut types, + &[("x", nested_left), ("y", TypeId(0))], + TypeId(2), + ); + let domain = nominal(&mut types, 1, domain_row); + let codomain_row = row( + &mut types, + &[("y", TypeId(0)), ("x", nested_right)], + TypeId(2), + ); + let codomain = nominal(&mut types, 1, codomain_row); + let instance = arrow(&mut types, domain, codomain); + let module = bare(types); + assert!( + module + .checked_instantiation(scheme, &[variable], instance) + .is_some() + ); +} diff --git a/crates/psrs-core/src/tests/instantiation/polymorphic.rs b/crates/psrs-core/src/tests/instantiation/polymorphic.rs new file mode 100644 index 00000000..982e0a52 --- /dev/null +++ b/crates/psrs-core/src/tests/instantiation/polymorphic.rs @@ -0,0 +1,139 @@ +//! Explicit quantified arguments survive checked nominal instantiation. + +use super::*; + +#[test] +fn a_polymorphic_nominal_argument_cannot_contain_the_parameter_being_solved() { + let parameter = TypeVariableId(10); + let bound = TypeVariableId(11); + let mut types = vec![Type::Variable(parameter), Type::Variable(bound)]; + let body = arrow(&mut types, TypeId(0), TypeId(1)); + let poly = forall(&mut types, vec![bound], body); + let scheme = nominal(&mut types, 1, TypeId(0)); + let instance = nominal(&mut types, 1, poly); + let module = bare(types); + assert!( + module + .checked_instantiation(scheme, &[parameter], instance) + .is_none() + ); + assert!( + module + .checked_instantiation(instance, &[parameter], scheme) + .is_none() + ); +} + +#[test] +fn nominal_parameters_retain_explicit_polymorphic_arguments() { + let parameter = TypeVariableId(10); + let bound = TypeVariableId(11); + let renamed = TypeVariableId(12); + let mut types = vec![ + Type::Variable(parameter), + Type::Variable(bound), + Type::Variable(renamed), + Type::Constructor(TypeConstructor::Int), + ]; + let body = arrow(&mut types, TypeId(1), TypeId(1)); + let poly = forall(&mut types, vec![bound], body); + let body = arrow(&mut types, TypeId(2), TypeId(2)); + let alpha = forall(&mut types, vec![renamed], body); + let mono = arrow(&mut types, TypeId(3), TypeId(3)); + let abstract_box = nominal(&mut types, 1, TypeId(0)); + let poly_box = nominal(&mut types, 1, poly); + let mono_box = nominal(&mut types, 1, mono); + let scheme = arrow(&mut types, abstract_box, abstract_box); + let alpha_box = nominal(&mut types, 1, alpha); + let instance = arrow(&mut types, poly_box, alpha_box); + let inconsistent = arrow(&mut types, poly_box, mono_box); + let module = bare(types); + + let evidence = module + .checked_instantiation(scheme, &[parameter], instance) + .expect("an explicitly polymorphic argument is retained under a nominal constructor"); + assert!(module.types_equivalent(evidence.replacements[¶meter], poly)); + assert!( + module + .checked_instantiation(scheme, &[parameter], inconsistent) + .is_none() + ); + assert!( + module + .checked_instantiation(abstract_box, &[], poly_box) + .is_none() + ); + assert!( + module + .checked_instantiation(poly_box, &[], mono_box) + .is_none() + ); + assert!( + module + .checked_instantiation(mono_box, &[], poly_box) + .is_none() + ); +} + +#[test] +fn constructor_fields_preserve_an_explicit_universal_argument() { + let parameter = TypeVariableId(10); + let bound = TypeVariableId(11); + let mut types = vec![ + Type::Variable(parameter), + Type::Variable(bound), + Type::Constructor(TypeConstructor::Int), + ]; + let body = arrow(&mut types, TypeId(1), TypeId(1)); + let poly = forall(&mut types, vec![bound], body); + let mono = arrow(&mut types, TypeId(2), TypeId(2)); + let boxed = nominal(&mut types, 1, poly); + let constructor = SymbolId::new(ModuleId(0), 1); + for (field, accepted) in [(poly, true), (mono, false)] { + let mut types = types.clone(); + let ty = arrow(&mut types, field, boxed); + let mut module = bare(types); + module.constructors.push(ConstructorInfo { + symbol: constructor, + name: "Box".into(), + type_id: HirTypeId::new(ModuleId(0), 1), + tag: 0, + field_count: 1, + field_types: vec![TypeId(0)], + parameters: vec![parameter], + }); + module.declarations.push(Declaration { + symbol: SymbolId::new(ModuleId(0), 0), + name: "box".into(), + name_span: SPAN, + quantified: Vec::new(), + ty, + span: SPAN, + value: Expr { + ty, + span: SPAN, + kind: ExprKind::Lambda { + binder: Binder { + id: LocalId(0), + name: "value".into(), + ty: field, + span: SPAN, + }, + body: Box::new(Expr { + ty: boxed, + span: SPAN, + kind: ExprKind::Constructor { + symbol: constructor, + arguments: vec![Expr { + kind: ExprKind::Local(LocalId(0)), + ty: field, + span: SPAN, + }], + }, + }), + }, + }, + }); + assert_eq!(module.verify().is_ok(), accepted, "{module:?}"); + } +} diff --git a/crates/psrs-core/src/tests/link.rs b/crates/psrs-core/src/tests/link.rs index 84b0d7cf..f6b3c9ed 100644 --- a/crates/psrs-core/src/tests/link.rs +++ b/crates/psrs-core/src/tests/link.rs @@ -207,6 +207,7 @@ fn pruning_keeps_nominal_types_through_a_checked_wit_alias_and_nested_fields() { ); module.entry = Some(SymbolId::new(ModuleId(0), 0)); module.externals.push(psrs_hir::ExternalSymbol { + source_module: None, symbol: import, name: "read".into(), kind: psrs_hir::ExternalKind::Wit { diff --git a/crates/psrs-core/src/tests/mod.rs b/crates/psrs-core/src/tests/mod.rs index 0ed11b44..c0d7aaee 100644 --- a/crates/psrs-core/src/tests/mod.rs +++ b/crates/psrs-core/src/tests/mod.rs @@ -1,11 +1,15 @@ use super::*; use psrs_hir::{Intrinsic, LocalId, ModuleId, SymbolId}; -mod effects; +mod command; +mod control; +mod deep_expr; mod external_types; +mod instantiation; mod link; mod patterns; mod rank_n; +mod record_scope; mod rows; /// Appends an arrow `parameter -> result` as the application spine and returns diff --git a/crates/psrs-core/src/tests/rank_n.rs b/crates/psrs-core/src/tests/rank_n/mod.rs similarity index 79% rename from crates/psrs-core/src/tests/rank_n.rs rename to crates/psrs-core/src/tests/rank_n/mod.rs index 766ec086..81f116ff 100644 --- a/crates/psrs-core/src/tests/rank_n.rs +++ b/crates/psrs-core/src/tests/rank_n/mod.rs @@ -2,6 +2,8 @@ use super::*; use psrs_hir::{LocalId, ModuleId, SymbolId, TypeVariableId}; use psrs_span::TextRange; +mod universals; + const SPAN: TextRange = TextRange::new(0, 1); fn arrow(types: &mut Vec, parameter: TypeId, result: TypeId) -> TypeId { @@ -350,3 +352,76 @@ fn verifier_rejects_an_empty_forall() { "forall binders must be non-empty, unique, and lexically distinct", ); } + +#[test] +fn quantified_let_scopes_its_locals_and_rejects_unbound_local_variables() { + for unbound in [false, true] { + let outer = TypeVariableId(10); + let inner = if unbound { TypeVariableId(11) } else { outer }; + let mut types = vec![Type::Variable(outer), Type::Variable(inner)]; + let outer_arrow = arrow(&mut types, TypeId(0), TypeId(0)); + let inner_arrow = arrow(&mut types, TypeId(1), TypeId(1)); + let quantified = TypeId(types.len() as u32); + types.push(Type::ForAll { + variables: vec![outer], + body: outer_arrow, + }); + let binder = Binder { + id: LocalId(0), + name: "ident".into(), + ty: inner_arrow, + span: SPAN, + }; + let local_value = Expr { + kind: ExprKind::Lambda { + binder: Binder { + id: LocalId(1), + name: "x".into(), + ty: TypeId(1), + span: SPAN, + }, + body: Box::new(Expr { + kind: ExprKind::Local(LocalId(1)), + ty: TypeId(1), + span: SPAN, + }), + }, + ty: inner_arrow, + span: SPAN, + }; + let value = Expr { + kind: ExprKind::Let { + bindings: vec![Binding { + binder, + quantified: Vec::new(), + value: local_value, + span: SPAN, + }], + body: Box::new(Expr { + kind: ExprKind::Local(LocalId(0)), + ty: outer_arrow, + span: SPAN, + }), + }, + ty: quantified, + span: SPAN, + }; + let module = module( + types, + vec![declaration(0, "main", Vec::new(), quantified, value)], + ); + if unbound { + assert!( + module + .verify() + .unwrap_err() + .iter() + .any(|error| error.message == "type variable is outside its quantifier scope") + ); + } else { + module + .verify() + .expect("the enclosing forall scopes monomorphic local definitions"); + } + } +} diff --git a/crates/psrs-core/src/tests/rank_n/universals.rs b/crates/psrs-core/src/tests/rank_n/universals.rs new file mode 100644 index 00000000..2986059b --- /dev/null +++ b/crates/psrs-core/src/tests/rank_n/universals.rs @@ -0,0 +1,86 @@ +use super::super::{ + Expr, ExprKind, Module, TextRange, Type, TypeConstructor, TypeId, arrow_type, + single_declaration, +}; +use psrs_hir::TypeVariableId; + +fn projected_identity(reverse: bool, inconsistent: bool) -> Module { + let mut types = vec![ + Type::Variable(TypeVariableId(0)), + Type::Variable(TypeVariableId(1)), + Type::Constructor(TypeConstructor::Array), + Type::Constructor(TypeConstructor::Int), + ]; + let general_body = arrow_type( + &mut types, + if inconsistent { TypeId(3) } else { TypeId(0) }, + TypeId(0), + ); + let general = TypeId(types.len() as u32); + types.push(Type::ForAll { + variables: vec![TypeVariableId(0)], + body: general_body, + }); + let array = TypeId(types.len() as u32); + types.push(Type::Application(TypeId(2), TypeId(1))); + let array_body = arrow_type(&mut types, array, array); + let specialized = TypeId(types.len() as u32); + types.push(Type::ForAll { + variables: vec![TypeVariableId(1)], + body: array_body, + }); + let (field, selected) = if reverse { + (specialized, general) + } else { + (general, specialized) + }; + let record = TypeId(types.len() as u32); + types.push(Type::Constructor(TypeConstructor::Record)); + let empty = TypeId(types.len() as u32); + types.push(Type::RowEmpty); + let row = TypeId(types.len() as u32); + types.push(Type::RowExtend { + label: "identity".into(), + ty: field, + tail: empty, + }); + let dictionary = TypeId(types.len() as u32); + types.push(Type::Application(record, row)); + let span = TextRange::new(0, 1); + single_declaration( + types, + selected, + Expr { + kind: ExprKind::FieldAccess { + record: Box::new(Expr { + kind: ExprKind::Trap, + ty: dictionary, + span, + }), + field: "identity".into(), + }, + ty: selected, + span, + }, + ) +} + +#[test] +fn a_universal_dictionary_method_can_specialize_under_another_universal() { + projected_identity(false, false).verify().unwrap(); +} + +#[test] +fn universal_specialization_cannot_generalize_or_change_its_input_relation() { + for (reverse, inconsistent) in [(true, false), (false, true)] { + let errors = projected_identity(reverse, inconsistent) + .verify() + .unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message + == "Core expression type is inconsistent with its context") + ); + } +} diff --git a/crates/psrs-core/src/tests/record_scope.rs b/crates/psrs-core/src/tests/record_scope.rs new file mode 100644 index 00000000..48b390cd --- /dev/null +++ b/crates/psrs-core/src/tests/record_scope.rs @@ -0,0 +1,235 @@ +use super::*; +use psrs_hir::{LocalId, ModuleId, SymbolId, TypeVariableId}; +use psrs_span::TextRange; + +fn fixture(unrelated: bool, sibling: bool) -> Module { + let span = TextRange::new(0, 1); + let variable = TypeVariableId(0); + let used_variable = TypeVariableId(u32::from(unrelated)); + let mut types = vec![ + Type::Variable(variable), + Type::Variable(used_variable), + Type::Constructor(TypeConstructor::Function), + Type::Constructor(TypeConstructor::Int), + ]; + let mut arrow = |argument| { + let head = TypeId(types.len() as u32); + types.push(Type::Application(TypeId(2), argument)); + let result = TypeId(types.len() as u32); + types.push(Type::Application(head, argument)); + result + }; + let value_type = arrow(TypeId(1)); + let field_body = arrow(TypeId(0)); + let int_function = arrow(TypeId(3)); + let field = TypeId(types.len() as u32); + types.push(Type::ForAll { + variables: vec![variable], + body: field_body, + }); + let mut row = TypeId(types.len() as u32); + types.push(Type::RowEmpty); + if sibling { + let tail = row; + row = TypeId(types.len() as u32); + types.push(Type::RowExtend { + label: "sibling".into(), + ty: int_function, + tail, + }); + } + let tail = row; + row = TypeId(types.len() as u32); + types.push(Type::RowExtend { + label: "method".into(), + ty: field, + tail, + }); + let record_head = TypeId(types.len() as u32); + types.push(Type::Constructor(TypeConstructor::Record)); + let record = TypeId(types.len() as u32); + types.push(Type::Application(record_head, row)); + let declaration = |index, name: &str, quantified, ty, value| Declaration { + symbol: SymbolId::new(ModuleId(0), index), + name: name.into(), + name_span: span, + quantified, + ty, + value, + span, + }; + let identity = declaration( + 0, + "identity", + vec![used_variable], + value_type, + Expr { + kind: ExprKind::Lambda { + binder: Binder { + id: LocalId(0), + name: "value".into(), + ty: TypeId(1), + span, + }, + body: Box::new(Expr { + kind: ExprKind::Local(LocalId(0)), + ty: TypeId(1), + span, + }), + }, + ty: value_type, + span, + }, + ); + let value = || Expr { + kind: ExprKind::Global(identity.symbol), + ty: value_type, + span, + }; + let mut fields = vec![("method".into(), value())]; + if sibling { + fields.push(("sibling".into(), value())); + } + let dictionary = declaration( + 1, + "dictionary", + vec![], + record, + Expr { + kind: ExprKind::Record { fields }, + ty: record, + span, + }, + ); + Module { + id: ModuleId(0), + name: "RecordScope".into(), + types, + type_names: vec![], + externals: vec![], + external_types: vec![], + newtype_ids: vec![], + opaque_ids: vec![], + callable_types: vec![], + constructors: vec![], + declarations: vec![identity, dictionary], + entry: None, + span, + } +} + +#[test] +fn quantified_record_field_scopes_its_instantiated_method() { + fixture(false, false).verify().unwrap(); +} + +#[test] +fn record_field_quantifier_does_not_bind_an_unrelated_variable() { + assert_scope_error(fixture(true, false)); +} + +#[test] +fn record_field_quantifier_does_not_leak_to_sibling_fields() { + assert_scope_error(fixture(false, true)); +} + +fn projection(unrelated: bool, polymorphic: bool) -> Module { + let mut module = fixture(unrelated, false); + let field = module + .record_field(module.declarations[1].ty, "method") + .unwrap(); + let Type::ForAll { body, .. } = module.types[field.0 as usize] else { + panic!("quantified method expected"); + }; + for ty in &mut module.types { + if let Type::RowExtend { label, ty, .. } = ty + && label == "method" + { + *ty = body; + } + } + let selected = if polymorphic { field } else { body }; + let declaration = &mut module.declarations[1]; + let record = declaration.value.clone(); + declaration.ty = selected; + declaration.value = Expr { + kind: ExprKind::FieldAccess { + record: Box::new(record), + field: "method".into(), + }, + ty: selected, + span: declaration.span, + }; + module +} + +#[test] +fn quantified_projection_scopes_the_record_instantiation() { + projection(false, true).verify().unwrap(); +} + +#[test] +fn projection_quantifier_does_not_bind_an_unrelated_variable() { + assert_scope_error(projection(true, true)); +} + +#[test] +fn a_monomorphic_projection_does_not_introduce_type_variables() { + assert_scope_error(projection(false, false)); +} + +fn array(unrelated: bool, empty: bool) -> Module { + let mut module = fixture(unrelated, false); + let dictionary = &module.declarations[1]; + let field = module.record_field(dictionary.ty, "method").unwrap(); + let Type::ForAll { body, .. } = module.types[field.0 as usize] else { + panic!("quantified method expected"); + }; + let head = TypeId(module.types.len() as u32); + module.types.push(Type::Constructor(TypeConstructor::Array)); + let applied = TypeId(module.types.len() as u32); + module.types.push(Type::Application(head, body)); + let quantified = TypeId(module.types.len() as u32); + module.types.push(Type::ForAll { + variables: vec![TypeVariableId(0)], + body: applied, + }); + let elements = if empty { + vec![] + } else { + vec![Expr { + kind: ExprKind::Global(module.declarations[0].symbol), + ty: module.declarations[0].ty, + span: module.span, + }] + }; + let declaration = &mut module.declarations[1]; + declaration.ty = quantified; + declaration.value = Expr { + kind: ExprKind::Array { elements }, + ty: quantified, + span: module.span, + }; + module +} + +#[test] +fn quantified_array_values_scope_their_elements() { + array(false, false).verify().unwrap(); + array(false, true).verify().unwrap(); +} + +#[test] +fn an_array_quantifier_does_not_bind_an_unrelated_variable() { + assert_scope_error(array(true, false)); +} + +fn assert_scope_error(module: Module) { + let errors = module.verify().unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message == "type variable is outside its quantifier scope"), + "{errors:?}" + ); +} diff --git a/crates/psrs-core/src/types/call.rs b/crates/psrs-core/src/types/call.rs new file mode 100644 index 00000000..0c5f7917 --- /dev/null +++ b/crates/psrs-core/src/types/call.rs @@ -0,0 +1,38 @@ +//! The checked calling boundary of a Core value, before representation lowering. +use super::{TypeId, arrow_parts, closure_parts, forall_parts, scheme_parts}; +use crate::Type; +use std::collections::HashSet; + +/// A fixed-arity closure retains its result as a value. Ordinary arrows flatten +/// only up to a quantified or explicit-closure result boundary. Non-callable +/// values have no parameters; malformed spines are errors, never empty calls. +pub fn call_parts(types: &[Type], id: TypeId) -> Result<(Vec, TypeId), &'static str> { + let (_, id) = scheme_parts(types, id).ok_or("callable has a dangling or cyclic scheme")?; + if let Some((parameters, result)) = closure_parts(types, id) { + if parameters + .iter() + .chain(std::iter::once(&result)) + .any(|ty| ty.0 as usize >= types.len()) + { + return Err("closure signature has a dangling type"); + } + return Ok((parameters.to_vec(), result)); + } + let mut parameters = Vec::new(); + let mut current = id; + let mut visited = HashSet::new(); + while let Some((parameter, result)) = arrow_parts(types, current) { + if !visited.insert(current) { + return Err("callable has a cyclic arrow spine"); + } + if parameter.0 as usize >= types.len() || result.0 as usize >= types.len() { + return Err("function signature has a dangling type"); + } + parameters.push(parameter); + current = result; + if forall_parts(types, current).is_some() { + break; + } + } + Ok((parameters, current)) +} diff --git a/crates/psrs-core/src/types.rs b/crates/psrs-core/src/types/mod.rs similarity index 65% rename from crates/psrs-core/src/types.rs rename to crates/psrs-core/src/types/mod.rs index c14f48f1..023f56cc 100644 --- a/crates/psrs-core/src/types.rs +++ b/crates/psrs-core/src/types/mod.rs @@ -8,6 +8,11 @@ use psrs_hir::{TypeId as HirTypeId, TypeVariableId}; +mod call; +mod variables; +pub use call::call_parts; +pub(crate) use variables::{collect_free_variables, free_variables}; + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct TypeId(pub u32); @@ -58,7 +63,7 @@ pub enum Type { }, /// A closure created with a fixed parameter list. The result is a value, /// even when that value is a function or another closure. Representation - /// lowering emits this for an effect; source arrows stay [`TypeConstructor::Function`]. + /// lowering emits this for runtime callables; source arrows stay [`TypeConstructor::Function`]. Closure { parameters: Vec, result: TypeId, @@ -113,3 +118,58 @@ pub fn forall_parts(types: &[Type], id: TypeId) -> Option<(&[TypeVariableId], Ty _ => None, } } + +/// Reads a binding's leading lexical quantifiers without crossing an arrow. +/// Returns `None` for a dangling or cyclic spine; callers must report invalid IR. +/// The variable identities and their order are preserved for declaration scope. +pub fn scheme_parts(types: &[Type], mut id: TypeId) -> Option<(Vec, TypeId)> { + let mut quantified = Vec::new(); + let mut seen = std::collections::HashSet::new(); + while seen.insert(id) { + match types.get(id.0 as usize)? { + Type::ForAll { variables, body } => { + quantified.extend_from_slice(variables); + id = *body; + } + _ => return Some((quantified, id)), + } + } + None +} + +#[cfg(test)] +mod scheme_tests { + use super::*; + + #[test] + fn binding_scheme_preserves_variables_and_stops_before_a_result_quantifier() { + let a = TypeVariableId(7); + let b = TypeVariableId(8); + let types = vec![ + Type::Constructor(TypeConstructor::Function), + Type::Variable(a), + Type::Variable(b), + Type::ForAll { + variables: vec![b], + body: TypeId(2), + }, + Type::Application(TypeId(0), TypeId(1)), + Type::Application(TypeId(4), TypeId(3)), + Type::ForAll { + variables: vec![a], + body: TypeId(5), + }, + ]; + assert_eq!(scheme_parts(&types, TypeId(6)), Some((vec![a], TypeId(5)))); + } + + #[test] + fn invalid_scheme_spines_are_rejected_without_fresh_unknowns() { + let types = vec![Type::ForAll { + variables: vec![TypeVariableId(0)], + body: TypeId(0), + }]; + assert!(scheme_parts(&types, TypeId(0)).is_none()); + assert!(scheme_parts(&types, TypeId(1)).is_none()); + } +} diff --git a/crates/psrs-core/src/types/variables.rs b/crates/psrs-core/src/types/variables.rs new file mode 100644 index 00000000..658d9629 --- /dev/null +++ b/crates/psrs-core/src/types/variables.rs @@ -0,0 +1,65 @@ +use crate::{Module, Type, TypeId}; +use psrs_hir::TypeVariableId; +use std::collections::{HashMap, HashSet}; + +/// Free variables of a type in a verified module, respecting lexical forall +/// scope. Declaration quantifiers may also bind variables used only in bodies. +pub(crate) fn free_variables(module: &Module, id: TypeId) -> HashSet { + let mut free = HashSet::new(); + collect_free_variables( + id, + &module.types, + &mut HashMap::new(), + &mut HashSet::new(), + &mut free, + ); + free +} + +pub(crate) fn collect_free_variables( + id: TypeId, + types: &[Type], + bound: &mut HashMap, + active: &mut HashSet, + free: &mut HashSet, +) { + if !active.insert(id) { + return; + } + match types.get(id.0 as usize) { + Some(Type::Variable(variable)) if !bound.contains_key(variable) => { + free.insert(*variable); + } + Some(Type::Application(function, argument)) => { + collect_free_variables(*function, types, bound, active, free); + collect_free_variables(*argument, types, bound, active, free); + } + Some(Type::ForAll { variables, body }) => { + for variable in variables { + *bound.entry(*variable).or_default() += 1; + } + collect_free_variables(*body, types, bound, active, free); + for variable in variables { + let count = bound + .get_mut(variable) + .expect("the forall binder was entered"); + *count -= 1; + if *count == 0 { + bound.remove(variable); + } + } + } + Some(Type::RowExtend { ty, tail, .. }) => { + collect_free_variables(*ty, types, bound, active, free); + collect_free_variables(*tail, types, bound, active, free); + } + Some(Type::Closure { parameters, result }) => { + for parameter in parameters { + collect_free_variables(*parameter, types, bound, active, free); + } + collect_free_variables(*result, types, bound, active, free); + } + _ => {} + } + active.remove(&id); +} diff --git a/crates/psrs-core/src/verify/expr/arrays.rs b/crates/psrs-core/src/verify/expr/arrays.rs index 31225591..c17bd4a9 100644 --- a/crates/psrs-core/src/verify/expr/arrays.rs +++ b/crates/psrs-core/src/verify/expr/arrays.rs @@ -58,6 +58,22 @@ impl Context<'_> { ); } + pub(super) fn verify_array_fill(&mut self, expression: &Expr, length: &Expr, value: &Expr) { + self.expr( + length, + Some(primitive_type_id(self.module, TypeConstructor::Int)), + ); + let Some(element) = array_element(expression.ty, self.module) else { + self.errors.push(error( + self.owner, + expression.span, + "arrayFill must return an Array", + )); + return; + }; + self.expr(value, Some(element)); + } + pub(super) fn verify_array_index(&mut self, expression: &Expr, array: &Expr, index: &Expr) { let Some(element_type) = array_element(array.ty, self.module) else { self.errors.push(error( diff --git a/crates/psrs-core/src/verify/expr/control.rs b/crates/psrs-core/src/verify/expr/control.rs new file mode 100644 index 00000000..0f2398ef --- /dev/null +++ b/crates/psrs-core/src/verify/expr/control.rs @@ -0,0 +1,30 @@ +//! Consume the control owner's checked contract before payload erasure. +use super::{Context, error, viewed}; +use crate::Expr; + +impl Context<'_> { + pub(super) fn verify_control( + &mut self, + expression: &Expr, + intrinsic: psrs_hir::Intrinsic, + arguments: &[Expr], + ) { + for argument in arguments { + self.expr(argument, None); + } + let argument_types = arguments + .iter() + .map(|argument| argument.ty) + .collect::>(); + let ids = std::iter::once(expression.ty) + .chain(argument_types.iter().copied()) + .collect::>(); + let module = viewed(self.module, self.source, &ids); + if let Err(message) = + crate::checked_control_contract(module, intrinsic, expression.ty, &argument_types) + { + self.errors + .push(error(self.owner, expression.span, message)); + } + } +} diff --git a/crates/psrs-core/src/verify/expr/entry.rs b/crates/psrs-core/src/verify/expr/entry.rs index ff2c5ea0..58fe4e54 100644 --- a/crates/psrs-core/src/verify/expr/entry.rs +++ b/crates/psrs-core/src/verify/expr/entry.rs @@ -4,10 +4,12 @@ use crate::{Expr, Module, TypeId, VerifyError}; use psrs_hir::{ModuleId, SymbolId}; use std::collections::HashMap; +#[allow(clippy::too_many_arguments)] pub(in crate::verify) fn verify_expr( expression: &Expr, expected: Option, module: &Module, + source: Option<&Module>, owner: ModuleId, globals: &HashMap>, locals: &mut Locals, @@ -15,6 +17,7 @@ pub(in crate::verify) fn verify_expr( ) { let mut context = Context { module, + source, owner, globals, locals, diff --git a/crates/psrs-core/src/verify/expr/intrinsic.rs b/crates/psrs-core/src/verify/expr/intrinsic.rs index 56d5b854..d33b81be 100644 --- a/crates/psrs-core/src/verify/expr/intrinsic.rs +++ b/crates/psrs-core/src/verify/expr/intrinsic.rs @@ -5,9 +5,10 @@ //! rules as the expressions they replaced. A mismatch is a lowering bug rather //! than a source error, so every check is an internal verification error. -use super::super::types::{primitive_types, unary_primitive_types}; -use super::{Context, compatible}; -use crate::Expr; +use super::super::types::{primitive_type_id, primitive_types, unary_primitive_types}; +use super::super::{compatible, error}; +use super::Context; +use crate::{Expr, TypeConstructor}; use psrs_hir::{Intrinsic, IntrinsicCategory}; impl Context<'_> { @@ -17,12 +18,29 @@ impl Context<'_> { intrinsic: Intrinsic, arguments: &[Expr], ) { + if arguments.len() != intrinsic.descriptor().arity as usize { + self.errors.push(error( + self.owner, + expression.span, + "intrinsic call does not have its registered saturated arity", + )); + for argument in arguments { + self.expr(argument, None); + } + return; + } match intrinsic { + intrinsic if intrinsic.descriptor().category == IntrinsicCategory::Control => { + self.verify_control(expression, intrinsic, arguments) + } Intrinsic::ArrayLength => self.verify_array_length(expression, &arguments[0]), Intrinsic::ArrayIndex => { self.verify_array_index(expression, &arguments[0], &arguments[1]) } - Intrinsic::ArrayUpdate => { + Intrinsic::ArrayFill => { + self.verify_array_fill(expression, &arguments[0], &arguments[1]) + } + Intrinsic::ArrayWrite | Intrinsic::ArrayUpdate => { self.verify_array_update(expression, &arguments[0], &arguments[1], &arguments[2]) } Intrinsic::ArrayAppend => { @@ -30,6 +48,24 @@ impl Context<'_> { } Intrinsic::StringToBytes => self.verify_string_to_bytes(expression, &arguments[0]), Intrinsic::BytesToString => self.verify_bytes_to_string(expression, &arguments[0]), + Intrinsic::NumberNaN | Intrinsic::NumberInfinity => { + if !arguments.is_empty() { + self.errors.push(error( + self.owner, + expression.span, + "a numeric constant has no arguments", + )); + } + let result = primitive_type_id(self.module, TypeConstructor::Number); + compatible( + result, + expression.ty, + self.module, + self.owner, + expression.span, + self.errors, + ); + } _ => match intrinsic.descriptor().category { IntrinsicCategory::BinaryScalar => { let (operand, result) = primitive_types(intrinsic, self.module); @@ -44,7 +80,7 @@ impl Context<'_> { self.errors, ); } - IntrinsicCategory::UnaryScalar => { + IntrinsicCategory::Unary => { let (operand, result) = unary_primitive_types(intrinsic, self.module); self.expr(&arguments[0], Some(operand)); compatible( diff --git a/crates/psrs-core/src/verify/expr/mod.rs b/crates/psrs-core/src/verify/expr/mod.rs index 42d0cd8c..01403af2 100644 --- a/crates/psrs-core/src/verify/expr/mod.rs +++ b/crates/psrs-core/src/verify/expr/mod.rs @@ -7,6 +7,7 @@ use psrs_hir::{ModuleId, SymbolId}; use std::collections::HashMap; mod arrays; +mod control; mod entry; mod helpers; mod intrinsic; @@ -17,14 +18,31 @@ use helpers::{closure_call, strip_leading_foralls}; struct Context<'a> { module: &'a Module, + /// Immutable source module used for declaration instantiation. `None` + /// checks every relation against `module`. + source: Option<&'a Module>, owner: ModuleId, globals: &'a HashMap>, locals: &'a mut Locals, errors: &'a mut Vec, } +/// Source relations apply only when every type id still addresses the +/// immutable source table. A newer id belongs to a representation closure +/// and is checked on the physical module. +fn viewed<'a>(physical: &'a Module, source: Option<&'a Module>, ids: &[TypeId]) -> &'a Module { + match source { + Some(source) if ids.iter().all(|id| (id.0 as usize) < source.types.len()) => source, + _ => physical, + } +} + impl Context<'_> { fn expr(&mut self, expression: &Expr, expected: Option) { + psrs_span::with_sufficient_stack(|| self.expr_inner(expression, expected)) + } + + fn expr_inner(&mut self, expression: &Expr, expected: Option) { verify_type( expression.ty, self.module, @@ -33,10 +51,11 @@ impl Context<'_> { self.errors, ); if let Some(expected) = expected { + let module = viewed(self.module, self.source, &[expression.ty, expected]); compatible( expression.ty, expected, - self.module, + module, self.owner, expression.span, self.errors, @@ -52,11 +71,12 @@ impl Context<'_> { )); return; }; + let module = viewed(self.module, self.source, &[local_type.ty, expression.ty]); if !super::types::scheme_instance( local_type.ty, &local_type.quantified, expression.ty, - self.module, + module, ) { self.errors.push(error( self.owner, @@ -67,11 +87,12 @@ impl Context<'_> { } ExprKind::Global(id) => match self.globals.get(id) { Some(Some(global_type)) => { + let module = viewed(self.module, self.source, &[global_type.ty, expression.ty]); if !super::types::scheme_instance( global_type.ty, &global_type.quantified, expression.ty, - self.module, + module, ) { self.errors.push(error( self.owner, @@ -101,7 +122,8 @@ impl Context<'_> { arguments, } => self.verify_intrinsic(expression, *intrinsic, arguments), ExprKind::Array { elements } => { - let Some(element_type) = array_element(expression.ty, self.module) else { + let body_type = strip_leading_foralls(self.module, expression.ty); + let Some(element_type) = array_element(body_type, self.module) else { self.errors.push(error( self.owner, expression.span, @@ -148,10 +170,11 @@ impl Context<'_> { ExprKind::FieldAccess { record, field } => { self.expr(record, None); if let Some(field_type) = record_field(record.ty, field, self.module) { + let module = viewed(self.module, self.source, &[field_type, expression.ty]); compatible( field_type, expression.ty, - self.module, + module, self.owner, expression.span, self.errors, @@ -244,18 +267,20 @@ impl Context<'_> { self.expr(argument, None); let function_body = strip_leading_foralls(self.module, function.ty); if let Some((parameter, result)) = closure_call(self.module, function_body) { + let module = viewed(self.module, self.source, &[argument.ty, parameter]); compatible( argument.ty, parameter, - self.module, + module, self.owner, argument.span, self.errors, ); + let module = viewed(self.module, self.source, &[result, expression.ty]); compatible( result, expression.ty, - self.module, + module, self.owner, expression.span, self.errors, @@ -266,26 +291,34 @@ impl Context<'_> { function.span, "application target is not a function", )); - } else if !super::types::application_matches( - function.ty, - argument.ty, - expression.ty, - self.module, - ) { - self.errors.push(error( - self.owner, - function.span, - "Core expression type is inconsistent with its context", - )); + } else { + let module = viewed( + self.module, + self.source, + &[function.ty, argument.ty, expression.ty], + ); + if !super::types::application_matches( + function.ty, + argument.ty, + expression.ty, + module, + ) { + self.errors.push(error( + self.owner, + function.span, + "Core expression type is inconsistent with its context", + )); + } } } ExprKind::Lambda { binder, body } => { let function_type = strip_leading_foralls(self.module, expression.ty); if let Some((parameter, result)) = closure_call(self.module, function_type) { + let module = viewed(self.module, self.source, &[binder.ty, parameter]); compatible( binder.ty, parameter, - self.module, + module, self.owner, binder.span, self.errors, @@ -311,10 +344,11 @@ impl Context<'_> { )); return; }; + let module = viewed(self.module, self.source, &[binder.ty, parameter]); compatible( binder.ty, parameter, - self.module, + module, self.owner, binder.span, self.errors, @@ -390,6 +424,7 @@ impl Context<'_> { ); let mut branch_context = Context { module: self.module, + source: self.source, owner: self.owner, globals: self.globals, locals: &mut branch_locals, diff --git a/crates/psrs-core/src/verify/mod.rs b/crates/psrs-core/src/verify/mod.rs index 1cd9c6f4..b9788703 100644 --- a/crates/psrs-core/src/verify/mod.rs +++ b/crates/psrs-core/src/verify/mod.rs @@ -1,5 +1,5 @@ use crate::{Module, Type, TypeId, VerifyError}; -use psrs_hir::{ExternalKind, LocalId}; +use psrs_hir::LocalId; use std::collections::HashMap; mod expr; @@ -8,6 +8,7 @@ mod scopes; mod types; pub(crate) use types::equivalent_types; +pub(crate) use types::instantiation; use expr::verify_expr; use patterns::verify_pattern; @@ -23,7 +24,7 @@ pub(super) struct SchemeType { type Locals = HashMap; -pub(crate) fn module(module: &Module) -> Result<(), Vec> { +pub(crate) fn module(module: &Module, source: Option<&Module>) -> Result<(), Vec> { let globals = module .declarations .iter() @@ -81,7 +82,7 @@ pub(crate) fn module(module: &Module) -> Result<(), Vec> { ); } for external in &module.externals { - if matches!(&external.kind, ExternalKind::Wit { .. }) + if external.kind.requires_checked_signature() && !external_type_symbols.contains(&external.symbol) { errors.push(error( @@ -145,6 +146,7 @@ pub(crate) fn module(module: &Module) -> Result<(), Vec> { &declaration.value, Some(declaration.ty), module, + source, owner, &globals, &mut locals, @@ -159,18 +161,6 @@ pub(crate) fn module(module: &Module) -> Result<(), Vec> { } fn external_scheme(module: &Module, ty: TypeId) -> Option { - let mut current = ty; - let mut quantified = Vec::new(); - let mut seen = std::collections::HashSet::new(); - while seen.insert(current) { - let Some((variables, body)) = crate::forall_parts(&module.types, current) else { - return Some(SchemeType { - ty: current, - quantified, - }); - }; - quantified.extend_from_slice(variables); - current = body; - } - None + let (quantified, ty) = crate::scheme_parts(&module.types, ty)?; + Some(SchemeType { ty, quantified }) } diff --git a/crates/psrs-core/src/verify/scopes/expr.rs b/crates/psrs-core/src/verify/scopes/expr.rs index 050b1b6a..e41b8ec1 100644 --- a/crates/psrs-core/src/verify/scopes/expr.rs +++ b/crates/psrs-core/src/verify/scopes/expr.rs @@ -1,13 +1,22 @@ use super::{enter, error, leading_foralls, types::scoped_type}; -use crate::{Expr, ExprKind, Module, Pattern, PatternKind, Type, TypeId, VerifyError}; +use crate::{Expr, ExprKind, Module, Pattern, PatternKind, VerifyError}; use psrs_hir::TypeVariableId; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; pub(super) fn scoped_expr( expression: &Expr, module: &Module, scope: &mut HashSet, errors: &mut Vec, +) { + psrs_span::with_sufficient_stack(|| scoped_expr_inner(expression, module, scope, errors)) +} + +fn scoped_expr_inner( + expression: &Expr, + module: &Module, + scope: &mut HashSet, + errors: &mut Vec, ) { scoped_type( expression.ty, @@ -29,8 +38,14 @@ pub(super) fn scoped_expr( // check, and neither mentions a binder. ExprKind::Unit | ExprKind::Trap => {} ExprKind::Constructor { arguments, .. } => { + // Constructor lowering collapses a THIR application spine. Its + // result quantifiers still bind the corresponding free variables + // in instantiated constructor fields, just as for Application. + let binders = leading_foralls(module, expression.ty); for argument in arguments { - scoped_expr(argument, module, scope, errors); + let mut argument_scope = scope.clone(); + open_child_binders(argument, &binders, module, &mut argument_scope, errors); + scoped_expr(argument, module, &mut argument_scope, errors); } } ExprKind::IntrinsicCall { arguments, .. } => { @@ -39,13 +54,24 @@ pub(super) fn scoped_expr( } } ExprKind::Array { elements } => { + let binders = leading_foralls(module, expression.ty); for element in elements { - scoped_expr(element, module, scope, errors); + let mut element_scope = scope.clone(); + open_child_binders(element, &binders, module, &mut element_scope, errors); + scoped_expr(element, module, &mut element_scope, errors); } } ExprKind::Record { fields } => { - for (_, value) in fields { - scoped_expr(value, module, scope, errors); + let field_types = module.record_fields(expression.ty).unwrap_or_default(); + for (label, value) in fields { + let binders = field_types + .iter() + .find(|(field_label, _)| field_label == label) + .map(|(_, ty)| leading_foralls(module, *ty)) + .unwrap_or_default(); + let mut field_scope = scope.clone(); + open_child_binders(value, &binders, module, &mut field_scope, errors); + scoped_expr(value, module, &mut field_scope, errors); } } ExprKind::RecordUpdate { record, fields } => { @@ -54,7 +80,12 @@ pub(super) fn scoped_expr( scoped_expr(value, module, scope, errors); } } - ExprKind::FieldAccess { record, .. } => scoped_expr(record, module, scope, errors), + ExprKind::FieldAccess { record, .. } => { + let binders = leading_foralls(module, expression.ty); + let mut record_scope = scope.clone(); + open_child_binders(record, &binders, module, &mut record_scope, errors); + scoped_expr(record, module, &mut record_scope, errors); + } ExprKind::RepresentationCast { value, source_type, @@ -101,8 +132,10 @@ pub(super) fn scoped_expr( scoped_expr(body, module, &mut body_scope, errors); } ExprKind::Let { bindings, body } => { + let mut body_scope = scope.clone(); + open_expression_binders(expression, module, &mut body_scope, errors); for binding in bindings { - let mut binding_scope = scope.clone(); + let mut binding_scope = body_scope.clone(); enter( &binding.quantified, &mut binding_scope, @@ -121,8 +154,6 @@ pub(super) fn scoped_expr( ); scoped_expr(&binding.value, module, &mut binding_scope, errors); } - let mut body_scope = scope.clone(); - open_expression_binders(expression, module, &mut body_scope, errors); scoped_expr(body, module, &mut body_scope, errors); } ExprKind::If { @@ -182,14 +213,7 @@ fn open_child_binders( scope: &mut HashSet, errors: &mut Vec, ) { - let mut free = HashSet::new(); - free_type_variables( - child.ty, - module, - &mut HashMap::new(), - &mut HashSet::new(), - &mut free, - ); + let free = crate::types::free_variables(module, child.ty); let relevant = candidates .iter() .copied() @@ -212,53 +236,6 @@ fn open_child_binders( scope.extend(local); } -fn free_type_variables( - id: TypeId, - module: &Module, - bound: &mut HashMap, - active: &mut HashSet, - free: &mut HashSet, -) { - if !active.insert(id) { - return; - } - match module.types.get(id.0 as usize) { - Some(Type::Variable(variable)) if !bound.contains_key(variable) => { - free.insert(*variable); - } - Some(Type::Application(function, argument)) => { - free_type_variables(*function, module, bound, active, free); - free_type_variables(*argument, module, bound, active, free); - } - Some(Type::ForAll { variables, body }) => { - for variable in variables { - *bound.entry(*variable).or_default() += 1; - } - free_type_variables(*body, module, bound, active, free); - for variable in variables { - if let Some(count) = bound.get_mut(variable) { - *count -= 1; - if *count == 0 { - bound.remove(variable); - } - } - } - } - Some(Type::RowExtend { ty, tail, .. }) => { - free_type_variables(*ty, module, bound, active, free); - free_type_variables(*tail, module, bound, active, free); - } - Some(Type::Closure { parameters, result }) => { - for parameter in parameters { - free_type_variables(*parameter, module, bound, active, free); - } - free_type_variables(*result, module, bound, active, free); - } - _ => {} - } - active.remove(&id); -} - fn scoped_pattern( pattern: &Pattern, module: &Module, diff --git a/crates/psrs-core/src/verify/types/matching/closure.rs b/crates/psrs-core/src/verify/types/matching/closure.rs index 68c659c7..5e242c74 100644 --- a/crates/psrs-core/src/verify/types/matching/closure.rs +++ b/crates/psrs-core/src/verify/types/matching/closure.rs @@ -1,9 +1,9 @@ -use super::TypeMatcher; +use super::{TypeMatcher, Variance}; impl TypeMatcher<'_> { /// Both types are closures with a fixed parameter list. Parameters are /// invariant and the result follows the surrounding subsumption. - pub(super) fn subsumes_closure( + pub(super) fn closure_subsumption( &mut self, actual: crate::TypeId, expected: crate::TypeId, @@ -19,10 +19,27 @@ impl TypeMatcher<'_> { } let parameters_match = actual_parameters.into_iter().zip(expected_parameters).all( |(actual_parameter, expected_parameter)| { - self.subsumes(expected_parameter, actual_parameter, true) - && self.subsumes(actual_parameter, expected_parameter, true) + self.relate( + expected_parameter, + actual_parameter, + Variance::Subsumption, + true, + ) && self.relate( + actual_parameter, + expected_parameter, + Variance::Subsumption, + true, + ) }, ); - Some(parameters_match && self.subsumes(actual_result, expected_result, instantiate)) + Some( + parameters_match + && self.relate( + actual_result, + expected_result, + Variance::Subsumption, + instantiate, + ), + ) } } diff --git a/crates/psrs-core/src/verify/types/matching/constructors.rs b/crates/psrs-core/src/verify/types/matching/constructors.rs index 25b64ad3..978ff464 100644 --- a/crates/psrs-core/src/verify/types/matching/constructors.rs +++ b/crates/psrs-core/src/verify/types/matching/constructors.rs @@ -1,5 +1,5 @@ -use super::TypeMatcher; -use crate::{Module, Type, TypeId}; +use super::{TypeMatcher, Variance}; +use crate::{Module, TypeId}; use std::collections::{HashMap, HashSet}; pub(in crate::verify) fn constructor_fields_match( @@ -23,12 +23,8 @@ pub(in crate::verify) fn constructor_fields_match( // A parameter may never appear as its own type node. Nullary constructors // still quantify it, and the pattern's type arguments are that instantiation. for (parameter, argument) in parameters.iter().zip(type_arguments) { - if matches!( - module.types.get(argument.0 as usize), - Some(Type::ForAll { .. }) - ) { - return false; - } + // Checked explicit arguments may themselves be universal types. Keep + // those binders intact when checking the constructor's field instances. if !matcher.bind_flexible(*parameter, *argument) { return false; } @@ -36,5 +32,7 @@ pub(in crate::verify) fn constructor_fields_match( field_templates .iter() .zip(field_instances) - .all(|(template, instance)| matcher.matches(*template, *instance, false)) + .all(|(template, instance)| { + matcher.relate(*template, *instance, Variance::Invariant, false) + }) } diff --git a/crates/psrs-core/src/verify/types/matching/evidence.rs b/crates/psrs-core/src/verify/types/matching/evidence.rs new file mode 100644 index 00000000..db40af70 --- /dev/null +++ b/crates/psrs-core/src/verify/types/matching/evidence.rs @@ -0,0 +1,28 @@ +use super::{TypeMatcher, Variance}; +use crate::{Instantiation, Module, TypeId}; +use psrs_hir::TypeVariableId; +use std::collections::{HashMap, HashSet}; + +pub(crate) fn instantiation<'a>( + module: &'a Module, + scheme: TypeId, + quantified: &[TypeVariableId], + instance: TypeId, +) -> Option> { + let mut matcher = TypeMatcher { + module, + flexible: quantified.iter().copied().collect(), + replacements: HashMap::new(), + row_forms: HashMap::new(), + alpha: HashMap::new(), + active: HashSet::new(), + }; + if !matcher.relate(scheme, instance, Variance::Subsumption, true) { + return None; + } + Some(Instantiation { + module, + replacements: matcher.replacements, + rows: matcher.row_forms, + }) +} diff --git a/crates/psrs-core/src/verify/types/matching/helpers.rs b/crates/psrs-core/src/verify/types/matching/helpers.rs index 95eb6b50..3ff68d04 100644 --- a/crates/psrs-core/src/verify/types/matching/helpers.rs +++ b/crates/psrs-core/src/verify/types/matching/helpers.rs @@ -1,53 +1,6 @@ -use crate::{Module, Type, TypeId, record_row}; +use crate::{Module, TypeId, record_row}; use std::collections::{HashMap, HashSet}; -pub(super) fn collect_free_variables( - id: TypeId, - types: &[Type], - bound: &mut HashMap, - active: &mut HashSet, - free: &mut HashSet, -) { - if !active.insert(id) { - return; - } - match types.get(id.0 as usize) { - Some(Type::Variable(variable)) if !bound.contains_key(variable) => { - free.insert(*variable); - } - Some(Type::Application(function, argument)) => { - collect_free_variables(*function, types, bound, active, free); - collect_free_variables(*argument, types, bound, active, free); - } - Some(Type::ForAll { variables, body }) => { - for variable in variables { - *bound.entry(*variable).or_default() += 1; - } - collect_free_variables(*body, types, bound, active, free); - for variable in variables { - if let Some(count) = bound.get_mut(variable) { - *count -= 1; - if *count == 0 { - bound.remove(variable); - } - } - } - } - Some(Type::RowExtend { ty, tail, .. }) => { - collect_free_variables(*ty, types, bound, active, free); - collect_free_variables(*tail, types, bound, active, free); - } - Some(Type::Closure { parameters, result }) => { - for parameter in parameters { - collect_free_variables(*parameter, types, bound, active, free); - } - collect_free_variables(*result, types, bound, active, free); - } - _ => {} - } - active.remove(&id); -} - pub(super) fn same_type(left: TypeId, right: TypeId, module: &Module) -> bool { super::super::types_compatible( left, diff --git a/crates/psrs-core/src/verify/types/matching/invariant.rs b/crates/psrs-core/src/verify/types/matching/invariant.rs new file mode 100644 index 00000000..f580d017 --- /dev/null +++ b/crates/psrs-core/src/verify/types/matching/invariant.rs @@ -0,0 +1,181 @@ +//! The rigid (`Invariant`) mode of the single checked type relation. +//! Flexible variables are solved from either side, and constructor identity +//! comes from an explicit binding, never from a closure's parameter count. + +use super::{TypeMatcher, Variance}; +use crate::Type; + +impl TypeMatcher<'_> { + pub(super) fn invariant( + &mut self, + source: crate::TypeId, + target: crate::TypeId, + instantiate: bool, + ) -> bool { + if !self.active.insert((Variance::Invariant, source, target)) { + return true; + } + if let ( + Some((source_parameters, source_result)), + Some((target_parameters, target_result)), + ) = ( + crate::closure_parts(&self.module.types, source), + crate::closure_parts(&self.module.types, target), + ) { + let source_parameters = source_parameters.to_vec(); + let target_parameters = target_parameters.to_vec(); + let result = + source_parameters.len() == target_parameters.len() + && source_parameters.into_iter().zip(target_parameters).all( + |(source, target)| self.relate(source, target, Variance::Invariant, false), + ) + && self.relate( + source_result, + target_result, + Variance::Invariant, + instantiate, + ); + self.active.remove(&(Variance::Invariant, source, target)); + return result; + } + let (Some(source_type), Some(target_type)) = ( + self.module.types.get(source.0 as usize), + self.module.types.get(target.0 as usize), + ) else { + self.active.remove(&(Variance::Invariant, source, target)); + return false; + }; + if let Type::Variable(variable) = source_type { + let result = if self.alpha.contains_key(variable) + || self.alpha.values().any(|bound| bound == variable) + || matches!(target_type, Type::Variable(other) if self.alpha.contains_key(other) || self.alpha.values().any(|bound| bound == other)) + { + matches!(target_type, Type::Variable(other) if self.alpha_variables_match(*variable, *other)) + } else if self.flexible.contains(variable) { + // Replay an explicit checked type argument as a whole, including + // its quantifiers. Nominal invariance still checks every later + // occurrence against this same binding. + self.bind_flexible(*variable, target) + } else { + match target_type { + Type::Variable(actual) if self.flexible.contains(actual) => { + self.bind_flexible(*actual, source) + } + Type::Variable(actual) => actual == variable, + _ => false, + } + }; + self.active.remove(&(Variance::Invariant, source, target)); + return result; + } + if let Type::Variable(variable) = target_type { + let result = !self.alpha.values().any(|bound| bound == variable) + && self.flexible.contains(variable) + && self.bind_flexible(*variable, source); + self.active.remove(&(Variance::Invariant, source, target)); + return result; + } + let result = match (source_type, target_type) { + ( + Type::ForAll { + variables: source_variables, + body: source_body, + }, + Type::ForAll { + variables: target_variables, + body: target_body, + }, + ) if source_variables.len() == target_variables.len() => { + if source_variables + .iter() + .any(|variable| self.alpha.contains_key(variable)) + { + false + } else { + for (source, target) in source_variables.iter().zip(target_variables) { + self.alpha.insert(*source, *target); + } + let related = + self.relate(*source_body, *target_body, Variance::Invariant, instantiate); + for variable in source_variables { + self.alpha.remove(variable); + } + related + } + } + (Type::ForAll { variables, body }, _) if instantiate => { + let added = variables + .iter() + .copied() + .filter(|variable| self.flexible.insert(*variable)) + .collect::>(); + let related = self.relate(*body, target, Variance::Invariant, true); + for variable in added { + self.flexible.remove(&variable); + self.replacements.remove(&variable); + } + related + } + (Type::ForAll { .. }, _) | (_, Type::ForAll { .. }) => false, + (Type::Constructor(left), Type::Constructor(right)) => left == right, + (Type::Application(_, _), Type::Application(_, _)) => { + if let ( + Some((source_parameter, source_result)), + Some((target_parameter, target_result)), + ) = ( + crate::arrow_parts(&self.module.types, source), + crate::arrow_parts(&self.module.types, target), + ) { + self.relate( + source_parameter, + target_parameter, + Variance::Invariant, + false, + ) && self.relate( + source_result, + target_result, + Variance::Invariant, + instantiate, + ) + } else if super::is_record_type(self.module, source) + && super::is_record_type(self.module, target) + { + self.invariant_record(source, target) + } else { + let ( + Type::Application(source_function, source_argument), + Type::Application(target_function, target_argument), + ) = (source_type, target_type) + else { + unreachable!() + }; + self.relate( + *source_function, + *target_function, + Variance::Invariant, + false, + ) && self.relate( + *source_argument, + *target_argument, + Variance::Invariant, + false, + ) + } + } + (Type::RowEmpty | Type::RowExtend { .. }, Type::RowEmpty | Type::RowExtend { .. }) => { + self.relate_rows(source, target, Variance::Invariant) + } + _ => false, + }; + self.active.remove(&(Variance::Invariant, source, target)); + result + } + + pub(super) fn invariant_record( + &mut self, + source: crate::TypeId, + target: crate::TypeId, + ) -> bool { + self.relate_records(source, target, Variance::Invariant) + } +} diff --git a/crates/psrs-core/src/verify/types/matching/mod.rs b/crates/psrs-core/src/verify/types/matching/mod.rs index 5022bd44..5cec08dc 100644 --- a/crates/psrs-core/src/verify/types/matching/mod.rs +++ b/crates/psrs-core/src/verify/types/matching/mod.rs @@ -1,3 +1,13 @@ +//! The one checked type relation. +//! +//! `TypeMatcher::relate` is the single entry point, parameterized by +//! [`Variance`]: `Subsumption` checks that an actual value type may be consumed +//! where an expected one is required, and `Invariant` checks rigid equality for +//! the positions where the type system allows no variance. Every public entry +//! point and every recursive step goes through it, so acceptance and the +//! substitution evidence it solves have one owner. This module is checking's +//! (P5/P6); no backend stage recomputes the relation. + use super::error; use crate::{Module, Type, TypeId, VerifyError}; use psrs_hir::ModuleId; @@ -6,11 +16,15 @@ use std::collections::{HashMap, HashSet}; mod closure; mod constructors; +mod evidence; +mod invariant; +pub(crate) use evidence::instantiation; mod helpers; mod rows; +use crate::types::collect_free_variables; pub(in crate::verify) use constructors::constructor_fields_match; pub(crate) use helpers::equivalent_types; -use helpers::{collect_free_variables, is_record_type, same_type}; +use helpers::{is_record_type, same_type}; use rows::RowForm; pub(in crate::verify) fn compatible( @@ -29,7 +43,7 @@ pub(in crate::verify) fn compatible( alpha: HashMap::new(), active: HashSet::new(), }; - if !matcher.subsumes(actual, expected, true) { + if !matcher.relate(actual, expected, Variance::Subsumption, true) { errors.push(error( owner, span, @@ -55,7 +69,7 @@ pub(in crate::verify) fn scheme_instance( alpha: HashMap::new(), active: HashSet::new(), }; - matcher.subsumes(scheme, instance, true) + matcher.relate(scheme, instance, Variance::Subsumption, true) } /// Checks an application after transparently instantiating leading type-level @@ -89,7 +103,20 @@ pub(in crate::verify) fn application_matches( else { return false; }; - matcher.subsumes(argument, parameter, true) && matcher.subsumes(function_result, result, true) + matcher.relate(argument, parameter, Variance::Subsumption, true) + && matcher.relate(function_result, result, Variance::Subsumption, true) +} + +/// The variance mode of the single checked type relation. `Subsumption` is the +/// value-level relation a declaration or local scheme is checked against at its +/// use: function parameters are contravariant, immutable record fields are +/// covariant, and an actual universal may be instantiated. `Invariant` is the +/// rigid mode used where no variance applies — nominal and higher-kinded +/// applications, closed and rigid rows, and constructor field templates. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub(super) enum Variance { + Subsumption, + Invariant, } struct TypeMatcher<'a> { @@ -98,24 +125,58 @@ struct TypeMatcher<'a> { replacements: HashMap, row_forms: HashMap, alpha: HashMap, - active: HashSet<(TypeId, TypeId)>, + active: HashSet<(Variance, TypeId, TypeId)>, } impl TypeMatcher<'_> { + /// The one checked type relation. Every entry point and every recursive + /// position goes through it; `variance` selects the mode. Checking is the + /// sole owner of this relation, and its consumers read only its result. + pub(super) fn relate( + &mut self, + actual: TypeId, + expected: TypeId, + variance: Variance, + instantiate: bool, + ) -> bool { + // Literal identity is by value in every mode, independent of the + // arena IDs assigned to occurrences of the same type-level literal. + match ( + self.module.types.get(actual.0 as usize), + self.module.types.get(expected.0 as usize), + ) { + (Some(Type::TypeLevelString(left)), Some(Type::TypeLevelString(right))) => { + return left == right; + } + (Some(Type::TypeLevelInt(left)), Some(Type::TypeLevelInt(right))) => { + return left == right; + } + _ => {} + } + match variance { + Variance::Subsumption => self.subsumption(actual, expected, instantiate), + Variance::Invariant => self.invariant(actual, expected, instantiate), + } + } + /// Checks value subsumption: `actual` may be consumed wherever `expected` /// is required. Function parameters are contravariant, while immutable /// record fields are covariant. An actual universal may be instantiated; - /// an expected universal remains rigid and therefore requires an actual - /// universal with alpha-equivalent binders. - fn subsumes(&mut self, actual: TypeId, expected: TypeId, instantiate: bool) -> bool { - if !self.active.insert((actual, expected)) { + /// an expected universal remains rigid. An actual universal may specialize + /// its body even when both sides quantify the same number of variables. + fn subsumption(&mut self, actual: TypeId, expected: TypeId, instantiate: bool) -> bool { + if !self + .active + .insert((Variance::Subsumption, actual, expected)) + { return true; } let (Some(actual_type), Some(expected_type)) = ( self.module.types.get(actual.0 as usize), self.module.types.get(expected.0 as usize), ) else { - self.active.remove(&(actual, expected)); + self.active + .remove(&(Variance::Subsumption, actual, expected)); return false; }; @@ -127,7 +188,8 @@ impl TypeMatcher<'_> { } else { self.bind_flexible(*variable, actual) }; - self.active.remove(&(actual, expected)); + self.active + .remove(&(Variance::Subsumption, actual, expected)); return result; } @@ -138,11 +200,12 @@ impl TypeMatcher<'_> { && self.flexible.contains(variable) { let result = if let Some(previous) = self.replacements.get(variable) { - self.subsumes(*previous, expected, true) + self.relate(*previous, expected, Variance::Subsumption, true) } else { self.bind_flexible(*variable, expected) }; - self.active.remove(&(actual, expected)); + self.active + .remove(&(Variance::Subsumption, actual, expected)); return result; } let Type::ForAll { variables, body } = expected_type else { @@ -152,13 +215,14 @@ impl TypeMatcher<'_> { .iter() .map(|variable| (*variable, self.flexible.remove(variable))) .collect::>(); - let result = self.subsumes(actual, *body, instantiate); + let result = self.relate(actual, *body, Variance::Subsumption, instantiate); for (variable, was_flexible) in was_flexible { if was_flexible { self.flexible.insert(variable); } } - self.active.remove(&(actual, expected)); + self.active + .remove(&(Variance::Subsumption, actual, expected)); return result; } @@ -171,40 +235,56 @@ impl TypeMatcher<'_> { variables: expected_variables, body: expected_body, } = expected_type + && actual_variables.len() == expected_variables.len() + && !instantiate { - if actual_variables.len() != expected_variables.len() - || actual_variables - .iter() - .any(|variable| self.alpha.contains_key(variable)) + if actual_variables + .iter() + .any(|variable| self.alpha.contains_key(variable)) { - self.active.remove(&(actual, expected)); + self.active + .remove(&(Variance::Subsumption, actual, expected)); return false; } for (actual, expected) in actual_variables.iter().zip(expected_variables) { self.alpha.insert(*actual, *expected); } - let result = self.subsumes(*actual_body, *expected_body, instantiate); + let result = self.relate( + *actual_body, + *expected_body, + Variance::Subsumption, + instantiate, + ); for variable in actual_variables { self.alpha.remove(variable); } - self.active.remove(&(actual, expected)); + self.active + .remove(&(Variance::Subsumption, actual, expected)); return result; } if !instantiate { - self.active.remove(&(actual, expected)); + self.active + .remove(&(Variance::Subsumption, actual, expected)); return false; } + // A more general actual value can instantiate only some of its + // binders, retaining the expected rank-N universal. For example, + // `forall a b. (a -> b -> b) -> b -> f a -> b` can be used at + // `forall b. (Int -> b -> b) -> b -> f Int -> b`. Expected binders + // remain rigid in the recursive relation; their count is not an + // arity restriction on value subsumption. let added = actual_variables .iter() .copied() .filter(|variable| self.flexible.insert(*variable)) .collect::>(); - let result = self.subsumes(*actual_body, expected, true); + let result = self.relate(*actual_body, expected, Variance::Subsumption, true); for variable in added { self.flexible.remove(&variable); self.replacements.remove(&variable); } - self.active.remove(&(actual, expected)); + self.active + .remove(&(Variance::Subsumption, actual, expected)); return result; } if let Type::Variable(variable) = actual_type { @@ -231,7 +311,8 @@ impl TypeMatcher<'_> { } else { false }; - self.active.remove(&(actual, expected)); + self.active + .remove(&(Variance::Subsumption, actual, expected)); return result; } if let Type::Variable(variable) = expected_type { @@ -242,12 +323,14 @@ impl TypeMatcher<'_> { } else { false }; - self.active.remove(&(actual, expected)); + self.active + .remove(&(Variance::Subsumption, actual, expected)); return result; } - if let Some(result) = self.subsumes_closure(actual, expected, instantiate) { - self.active.remove(&(actual, expected)); + if let Some(result) = self.closure_subsumption(actual, expected, instantiate) { + self.active + .remove(&(Variance::Subsumption, actual, expected)); return result; } @@ -261,38 +344,34 @@ impl TypeMatcher<'_> { crate::arrow_parts(&self.module.types, actual), crate::arrow_parts(&self.module.types, expected), ) { - self.subsumes(expected_parameter, actual_parameter, true) - && self.subsumes(actual_result, expected_result, instantiate) + self.relate( + expected_parameter, + actual_parameter, + Variance::Subsumption, + true, + ) && self.relate( + actual_result, + expected_result, + Variance::Subsumption, + instantiate, + ) } else if is_record_type(self.module, actual) && is_record_type(self.module, expected) { - self.subsumes_record(actual, expected) + self.record_subsumption(actual, expected) } else { // No variance metadata is carried for nominal and // higher-kinded applications, so keep them invariant. - self.matches(actual, expected, false) + self.relate(actual, expected, Variance::Invariant, false) } } - (Type::RowEmpty, Type::RowEmpty) => true, - ( - Type::RowExtend { - label: actual_label, - ty: actual_ty, - tail: actual_tail, - }, - Type::RowExtend { - label: expected_label, - ty: expected_ty, - tail: expected_tail, - }, - ) => { - actual_label == expected_label - && self.subsumes(*actual_ty, *expected_ty, true) - && self.matches(*actual_tail, *expected_tail, false) + (Type::RowEmpty | Type::RowExtend { .. }, Type::RowEmpty | Type::RowExtend { .. }) => { + self.relate_rows(actual, expected, Variance::Invariant) } _ => false, }; - self.active.remove(&(actual, expected)); + self.active + .remove(&(Variance::Subsumption, actual, expected)); result } @@ -344,145 +423,7 @@ impl TypeMatcher<'_> { true } - fn subsumes_record(&mut self, actual: TypeId, expected: TypeId) -> bool { - self.relate_records(actual, expected, true) - } - - fn matches(&mut self, source: TypeId, target: TypeId, instantiate: bool) -> bool { - if !self.active.insert((source, target)) { - return true; - } - if let ( - Some((source_parameters, source_result)), - Some((target_parameters, target_result)), - ) = ( - crate::closure_parts(&self.module.types, source), - crate::closure_parts(&self.module.types, target), - ) { - let source_parameters = source_parameters.to_vec(); - let target_parameters = target_parameters.to_vec(); - let result = source_parameters.len() == target_parameters.len() - && source_parameters - .into_iter() - .zip(target_parameters) - .all(|(source, target)| self.matches(source, target, false)) - && self.matches(source_result, target_result, instantiate); - self.active.remove(&(source, target)); - return result; - } - let (Some(source_type), Some(target_type)) = ( - self.module.types.get(source.0 as usize), - self.module.types.get(target.0 as usize), - ) else { - self.active.remove(&(source, target)); - return false; - }; - if let Type::Variable(variable) = source_type { - let result = if let Some(mapped) = self.alpha.get(variable) { - matches!(target_type, Type::Variable(actual) if actual == mapped) - } else if self.flexible.contains(variable) { - if matches!(target_type, Type::ForAll { .. }) { - false - } else { - self.bind_flexible(*variable, target) - } - } else { - matches!(target_type, Type::Variable(actual) if actual == variable) - }; - self.active.remove(&(source, target)); - return result; - } - let result = match (source_type, target_type) { - ( - Type::ForAll { - variables: source_variables, - body: source_body, - }, - Type::ForAll { - variables: target_variables, - body: target_body, - }, - ) if source_variables.len() == target_variables.len() => { - if source_variables - .iter() - .any(|variable| self.alpha.contains_key(variable)) - { - false - } else { - for (source, target) in source_variables.iter().zip(target_variables) { - self.alpha.insert(*source, *target); - } - let matches = self.matches(*source_body, *target_body, instantiate); - for variable in source_variables { - self.alpha.remove(variable); - } - matches - } - } - (Type::ForAll { variables, body }, _) if instantiate => { - let added = variables - .iter() - .copied() - .filter(|variable| self.flexible.insert(*variable)) - .collect::>(); - let matches = self.matches(*body, target, true); - for variable in added { - self.flexible.remove(&variable); - self.replacements.remove(&variable); - } - matches - } - (Type::ForAll { .. }, _) | (_, Type::ForAll { .. }) => false, - (Type::Constructor(left), Type::Constructor(right)) => left == right, - (Type::Application(_, _), Type::Application(_, _)) => { - if let ( - Some((source_parameter, source_result)), - Some((target_parameter, target_result)), - ) = ( - crate::arrow_parts(&self.module.types, source), - crate::arrow_parts(&self.module.types, target), - ) { - self.matches(source_parameter, target_parameter, false) - && self.matches(source_result, target_result, instantiate) - } else if is_record_type(self.module, source) && is_record_type(self.module, target) - { - self.matches_record(source, target) - } else { - let ( - Type::Application(source_function, source_argument), - Type::Application(target_function, target_argument), - ) = (source_type, target_type) - else { - unreachable!() - }; - self.matches(*source_function, *target_function, false) - && self.matches(*source_argument, *target_argument, false) - } - } - (Type::RowEmpty, Type::RowEmpty) => true, - ( - Type::RowExtend { - label: left_label, - ty: left_ty, - tail: left_tail, - }, - Type::RowExtend { - label: right_label, - ty: right_ty, - tail: right_tail, - }, - ) => { - left_label == right_label - && self.matches(*left_ty, *right_ty, false) - && self.matches(*left_tail, *right_tail, false) - } - _ => false, - }; - self.active.remove(&(source, target)); - result - } - - fn matches_record(&mut self, source: TypeId, target: TypeId) -> bool { - self.relate_records(source, target, false) + fn record_subsumption(&mut self, actual: TypeId, expected: TypeId) -> bool { + self.relate_records(actual, expected, Variance::Subsumption) } } diff --git a/crates/psrs-core/src/verify/types/matching/rows.rs b/crates/psrs-core/src/verify/types/matching/rows.rs index ac0ea073..827489c6 100644 --- a/crates/psrs-core/src/verify/types/matching/rows.rs +++ b/crates/psrs-core/src/verify/types/matching/rows.rs @@ -1,16 +1,10 @@ -use super::super::types_compatible; -use super::TypeMatcher; -use super::helpers::collect_free_variables; +use super::{TypeMatcher, Variance}; +use crate::types::collect_free_variables; use crate::{Type, TypeId, record_row}; use psrs_hir::TypeVariableId; use std::collections::{HashMap, HashSet}; -/// A row variable instantiated to a residual that has no single type-table node. -#[derive(Clone, Debug)] -pub(super) struct RowForm { - fields: Vec<(String, TypeId)>, - tail: Option, -} +pub(super) use crate::instantiation::RowInstantiation as RowForm; enum TailKind { Closed, @@ -21,14 +15,14 @@ enum TailKind { type RowShape = (Vec<(String, TypeId)>, Option); impl TypeMatcher<'_> { - /// Record subsumption (`covariant`) or invariant matching. Closed rows and - /// rigid open rows must agree exactly. A flexible tail is a quantifier - /// being instantiated and may absorb the other side's residual row. + /// Record relation in the given [`Variance`]. Closed rows and rigid open + /// rows must agree exactly. A flexible tail is a quantifier being + /// instantiated and may absorb the other side's residual row. pub(super) fn relate_records( &mut self, actual: TypeId, expected: TypeId, - covariant: bool, + variance: Variance, ) -> bool { let (Some(actual_row), Some(expected_row)) = ( record_row(&self.module.types, actual), @@ -36,34 +30,68 @@ impl TypeMatcher<'_> { ) else { return false; }; + for row in [actual_row, expected_row] { + let Some((fields, _)) = self.flatten_row(row) else { + return false; + }; + let mut labels = HashSet::new(); + if fields.iter().any(|(label, _)| !labels.insert(label)) { + return false; + } + } + self.relate_rows(actual_row, expected_row, variance) + } + + /// Bare rows in nominal arguments use the same checked residual relation as + /// records. The caller owns variance; nominal row arguments are invariant. + pub(super) fn relate_rows( + &mut self, + actual: TypeId, + expected: TypeId, + variance: Variance, + ) -> bool { let (Some((actual_fields, actual_tail)), Some((expected_fields, expected_tail))) = - (self.flatten_row(actual_row), self.flatten_row(expected_row)) + (self.flatten_row(actual), self.flatten_row(expected)) else { return false; }; - let Some(mut expected_fields) = index_fields(expected_fields) else { - return false; - }; + let mut expected_fields = expected_fields; + let mut actual_fields = actual_fields; + // Preserve duplicate-label occurrence order while ignoring the order + // of distinct labels. Bare rows may contain duplicates (e.g. Union). + actual_fields.sort_by(|left, right| left.0.cmp(&right.0)); + expected_fields.sort_by(|left, right| left.0.cmp(&right.0)); + let (mut actual_index, mut expected_index) = (0, 0); let mut actual_rest = Vec::new(); - let mut seen = HashSet::new(); - for (label, actual_ty) in actual_fields { - if !seen.insert(label.clone()) { - return false; - } - if let Some(expected_ty) = expected_fields.remove(&label) { - let agrees = if covariant { - self.subsumes(actual_ty, expected_ty, true) - } else { - self.matches(actual_ty, expected_ty, false) - }; - if !agrees { - return false; + let mut expected_rest = Vec::new(); + while actual_index < actual_fields.len() && expected_index < expected_fields.len() { + let actual_field = &actual_fields[actual_index]; + let expected_field = &expected_fields[expected_index]; + match actual_field.0.cmp(&expected_field.0) { + std::cmp::Ordering::Equal => { + if !self.relate( + actual_field.1, + expected_field.1, + variance, + variance == Variance::Subsumption, + ) { + return false; + } + actual_index += 1; + expected_index += 1; + } + std::cmp::Ordering::Less => { + actual_rest.push(actual_field.clone()); + actual_index += 1; + } + std::cmp::Ordering::Greater => { + expected_rest.push(expected_field.clone()); + expected_index += 1; } - } else { - actual_rest.push((label, actual_ty)); } } - let expected_rest = expected_fields.into_iter().collect::>(); + actual_rest.extend(actual_fields[actual_index..].iter().cloned()); + expected_rest.extend(expected_fields[expected_index..].iter().cloned()); self.finish_row(actual_rest, actual_tail, expected_rest, expected_tail) } @@ -198,15 +226,7 @@ impl TypeMatcher<'_> { } fn types_equal(&mut self, left: TypeId, right: TypeId) -> bool { - let mut alpha = self.alpha.clone(); - types_compatible(left, right, self.module, &mut HashSet::new(), &mut alpha) - && types_compatible( - right, - left, - self.module, - &mut HashSet::new(), - &mut self.alpha.clone(), - ) + self.relate(left, right, Variance::Invariant, false) } fn flatten_row(&self, mut row: TypeId) -> Option { @@ -266,16 +286,6 @@ impl TypeMatcher<'_> { } } -fn index_fields(fields: Vec<(String, TypeId)>) -> Option> { - let mut indexed = HashMap::new(); - for (label, ty) in fields { - if indexed.insert(label, ty).is_some() { - return None; - } - } - Some(indexed) -} - fn same_labels(left: &[(String, TypeId)], right: &[(String, TypeId)]) -> bool { let mut left = left.to_vec(); let mut right = right.to_vec(); diff --git a/crates/psrs-core/src/verify/types/mod.rs b/crates/psrs-core/src/verify/types/mod.rs index 90eba305..a75007b1 100644 --- a/crates/psrs-core/src/verify/types/mod.rs +++ b/crates/psrs-core/src/verify/types/mod.rs @@ -23,25 +23,23 @@ pub(super) fn verify_type( pub(super) fn primitive_types(intrinsic: Intrinsic, module: &Module) -> (TypeId, TypeId) { use TypeConstructor::{Boolean, Char, Int, Number}; let (operand_type, result_type) = match intrinsic { - Intrinsic::I32Add - | Intrinsic::I32Sub - | Intrinsic::I32Mul - | Intrinsic::I32DivS - | Intrinsic::I32RemS - | Intrinsic::IntDiv - | Intrinsic::IntMod + Intrinsic::IntAdd + | Intrinsic::IntSub + | Intrinsic::IntMul + | Intrinsic::IntQuot + | Intrinsic::IntRem | Intrinsic::IntAnd | Intrinsic::IntOr | Intrinsic::IntXor | Intrinsic::IntShl | Intrinsic::IntShr | Intrinsic::IntZshr => (Int, Int), - Intrinsic::I32Eq - | Intrinsic::I32Ne - | Intrinsic::I32LtS - | Intrinsic::I32LeS - | Intrinsic::I32GtS - | Intrinsic::I32GeS => (Int, Boolean), + Intrinsic::IntEq + | Intrinsic::IntNe + | Intrinsic::IntLt + | Intrinsic::IntLe + | Intrinsic::IntGt + | Intrinsic::IntGe => (Int, Boolean), Intrinsic::CharEq | Intrinsic::CharNe | Intrinsic::CharLt @@ -51,7 +49,12 @@ pub(super) fn primitive_types(intrinsic: Intrinsic, module: &Module) -> (TypeId, Intrinsic::NumberAdd | Intrinsic::NumberSub | Intrinsic::NumberMul - | Intrinsic::NumberDiv => (Number, Number), + | Intrinsic::NumberDiv + | Intrinsic::NumberAtan2 + | Intrinsic::NumberPow + | Intrinsic::NumberMin + | Intrinsic::NumberMax + | Intrinsic::NumberRemainder => (Number, Number), Intrinsic::NumberEq | Intrinsic::NumberNe | Intrinsic::NumberLt @@ -73,13 +76,30 @@ pub(super) fn primitive_types(intrinsic: Intrinsic, module: &Module) -> (TypeId, } pub(super) fn unary_primitive_types(intrinsic: Intrinsic, module: &Module) -> (TypeId, TypeId) { - use TypeConstructor::{Boolean, Char, Int, Number}; + use TypeConstructor::{Boolean, Char, Int, Number, String}; let (operand, result) = match intrinsic { Intrinsic::IntNeg | Intrinsic::IntComplement => (Int, Int), - Intrinsic::NumberNeg => (Number, Number), + Intrinsic::NumberNeg + | Intrinsic::NumberTrunc + | Intrinsic::NumberFloor + | Intrinsic::NumberCeil + | Intrinsic::NumberAbs + | Intrinsic::NumberSqrt + | Intrinsic::NumberAcos + | Intrinsic::NumberAsin + | Intrinsic::NumberAtan + | Intrinsic::NumberSin + | Intrinsic::NumberCos + | Intrinsic::NumberTan + | Intrinsic::NumberExp + | Intrinsic::NumberLog + | Intrinsic::NumberSign => (Number, Number), + Intrinsic::NumberIsNaN => (Number, Boolean), Intrinsic::BooleanNot => (Boolean, Boolean), Intrinsic::IntToNumber => (Int, Number), Intrinsic::NumberToInt => (Number, Int), + Intrinsic::NumberToString => (Number, String), + Intrinsic::NumberFromDecimal => (String, Number), Intrinsic::BooleanToInt => (Boolean, Int), Intrinsic::IntToBoolean => (Int, Boolean), Intrinsic::CharToInt => (Char, Int), @@ -126,6 +146,7 @@ pub(super) fn record_field(id: TypeId, label: &str, module: &Module) -> Option, pub span: TextRange, } diff --git a/crates/psrs-cst/src/expr.rs b/crates/psrs-cst/src/expr.rs index 6b882ae9..6fb047c0 100644 --- a/crates/psrs-cst/src/expr.rs +++ b/crates/psrs-cst/src/expr.rs @@ -48,6 +48,10 @@ pub enum ExprKind { dot_span: TextRange, field: CstName, }, + RecordAccessor { + marker_span: TextRange, + fields: Vec, + }, Operator { operator: CstName, left: Box, @@ -112,6 +116,12 @@ pub enum ExprKind { }, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RecordAccessorField { + pub dot_span: TextRange, + pub field: CstName, +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum OperatorSectionSide { /// The section supplies the left operand: `(expression operator _)`. diff --git a/crates/psrs-desugar/src/alpha.rs b/crates/psrs-desugar/src/alpha.rs index 45da7bbc..f14e6502 100644 --- a/crates/psrs-desugar/src/alpha.rs +++ b/crates/psrs-desugar/src/alpha.rs @@ -106,7 +106,7 @@ fn collect_expr_ids(expression: &Expr, ids: &mut HashSet) { collect_expr_ids(element, ids); } } - ExprKind::Record(fields) => { + ExprKind::Record(fields) | ExprKind::MatchProduct(fields) => { for (_, value) in fields { collect_expr_ids(value, ids); } @@ -236,6 +236,12 @@ fn rename_expr(expression: Expr, mapping: &HashMap) -> Expr { .map(|(label, value)| (label, rename_expr(value, mapping))) .collect(), ), + ExprKind::MatchProduct(fields) => ExprKind::MatchProduct( + fields + .into_iter() + .map(|(label, value)| (label, rename_expr(value, mapping))) + .collect(), + ), ExprKind::RecordUpdate { expression, fields } => ExprKind::RecordUpdate { expression: Box::new(rename_expr(*expression, mapping)), fields: fields diff --git a/crates/psrs-desugar/src/case_helpers.rs b/crates/psrs-desugar/src/case_helpers.rs index 8ae30d1e..efb7c9d9 100644 --- a/crates/psrs-desugar/src/case_helpers.rs +++ b/crates/psrs-desugar/src/case_helpers.rs @@ -31,7 +31,7 @@ pub(super) fn wrap_lambdas(parameters: Vec, body: Expr, span: TextR pub(super) fn product_expression(value: Expr) -> Expr { let span = value.span; Expr { - kind: ExprKind::Record(vec![("_1".into(), value)]), + kind: ExprKind::MatchProduct(vec![("_1".into(), value)]), span, } } diff --git a/crates/psrs-desugar/src/expr.rs b/crates/psrs-desugar/src/expr.rs index 06b264eb..8f5cf830 100644 --- a/crates/psrs-desugar/src/expr.rs +++ b/crates/psrs-desugar/src/expr.rs @@ -4,7 +4,6 @@ use crate::{ apply, guarded_rhs_exhaustive, is_guarded_rhs, product_expression, product_pattern, wrap_lambdas, }, - free_vars, }; use psrs_hir::{ self as hir, CaseBranch, CaseBranchCoverage, Expr, ExprKind, LocalBinder, LocalBinding, @@ -33,6 +32,10 @@ impl Desugarer { } pub(super) fn expr(&mut self, expression: Expr) -> Expr { + psrs_span::with_sufficient_stack(|| self.expr_inner(expression)) + } + + fn expr_inner(&mut self, expression: Expr) -> Expr { let span = expression.span; let kind = match expression.kind { ExprKind::OperatorChain { .. } | ExprKind::OperatorSection { .. } => { @@ -73,6 +76,12 @@ impl Desugarer { .map(|(label, value)| (label, self.expr(value))) .collect(), ), + ExprKind::MatchProduct(fields) => ExprKind::MatchProduct( + fields + .into_iter() + .map(|(label, value)| (label, self.expr(value))) + .collect(), + ), ExprKind::RecordUpdate { expression, fields } => ExprKind::RecordUpdate { expression: Box::new(self.expr(*expression)), fields: fields @@ -136,8 +145,10 @@ impl Desugarer { fn case(&mut self, scrutinee: Expr, mut branches: Vec, span: TextRange) -> Expr { let mut scrutinee = self.expr(scrutinee); - let has_guards = branches.iter().any(|branch| is_guarded_rhs(&branch.value)); - if !has_guards { + let first_guard = branches + .iter() + .position(|branch| is_guarded_rhs(&branch.value)); + let Some(first_guard) = first_guard else { return Expr { kind: ExprKind::Case { scrutinee: Box::new(scrutinee), @@ -151,7 +162,7 @@ impl Desugarer { }, span, }; - } + }; if !branches .iter() @@ -163,39 +174,38 @@ impl Desugarer { } } - let captures = free_vars::captures(&branches); + // Fallthrough helpers stay in this `let`, so they see the same outer + // locals as the source branch. Passing those locals in as arguments + // would instantiate a polymorphic scheme once, before the guard body + // applies the arguments that determine its constraints. The scrutinee + // product is captured too: passing it would instantiate polymorphic + // fields across fallthrough rows. An empty token keeps helpers lazy. let temp = self.local_binder("case_scrutinee", span); - let helper_binders = (1..=branches.len() + 1) + // Only rows after the first guarded row can be entered through a + // fallthrough helper. Earlier rows are checked directly at the case's + // expected result type, and must not acquire unused inferred copies. + let first_helper_row = first_guard + 1; + let helper_binders = (first_helper_row..=branches.len()) .map(|index| self.local_binder(&format!("guard_fallthrough_{index}"), span)) .collect::>(); - let mut bindings = Vec::with_capacity(helper_binders.len() + 1); - bindings.push(LocalBinding { + let mut bindings = Vec::with_capacity(helper_binders.len()); + let scrutinee_binding = LocalBinding { binder: temp.clone(), value: scrutinee, span, - }); + }; - for row_index in 0..=branches.len() { - let next_functions = helper_binders[row_index + 1..] + for (helper_index, row_index) in (first_helper_row..=branches.len()).enumerate() { + let next_functions = helper_binders[helper_index + 1..] .iter() .map(|_| self.local_binder("next_guard", span)) .collect::>(); - let capture_parameters = captures - .iter() - .map(|_| self.local_binder("guard_capture", span)) - .collect::>(); - let temp_parameter = self.local_binder("guard_scrutinee", span); - let mut local_mapping = captures - .iter() - .zip(&capture_parameters) - .map(|(id, binder)| (*id, binder.id)) - .collect::>(); - local_mapping.insert(temp.id, temp_parameter.id); + let temp_parameter = self.local_binder("guard_token", span); let body = if row_index == branches.len() { Expr { kind: ExprKind::Case { - scrutinee: Box::new(self.local_expr(&temp_parameter, span)), + scrutinee: Box::new(self.local_expr(&temp, span)), branches: Vec::new(), }, span, @@ -203,18 +213,15 @@ impl Desugarer { } else { let mut source = alpha::clone_branch(&branches[row_index], &mut self.fresh); source.coverage = CaseBranchCoverage::Generated; - source.value = free_vars::rebind(source.value, &local_mapping); let failure = apply( self.local_expr(&next_functions[0], span), next_functions[1..] .iter() .map(|binder| self.local_expr(binder, span)) - .chain( - capture_parameters - .iter() - .map(|binder| self.local_expr(binder, span)), - ) - .chain(std::iter::once(self.local_expr(&temp_parameter, span))), + .chain(std::iter::once(Expr { + kind: ExprKind::Record(Vec::new()), + span, + })), span, ); let guard_failure = self.clone_expression(&failure); @@ -230,7 +237,7 @@ impl Desugarer { }; Expr { kind: ExprKind::Case { - scrutinee: Box::new(self.local_expr(&temp_parameter, span)), + scrutinee: Box::new(self.local_expr(&temp, span)), branches: vec![source, wildcard], }, span, @@ -238,12 +245,11 @@ impl Desugarer { }; let parameters = next_functions .into_iter() - .chain(capture_parameters) .chain(std::iter::once(temp_parameter)) .collect::>(); let value = wrap_lambdas(parameters, body, span); bindings.push(LocalBinding { - binder: helper_binders[row_index].clone(), + binder: helper_binders[helper_index].clone(), value, span, }); @@ -259,17 +265,16 @@ impl Desugarer { branch.coverage = CaseBranchCoverage::Source; } if is_guarded_rhs(&branch.value) { - let start = index + 2; + let next_helper = index - first_guard; let failure = apply( - self.local_expr(&helper_binders[start - 1], branch.span), - helper_binders[start..] + self.local_expr(&helper_binders[next_helper], branch.span), + helper_binders[next_helper + 1..] .iter() .map(|binder| self.local_expr(binder, branch.span)) - .chain(captures.iter().map(|id| Expr { - kind: ExprKind::Local(*id), + .chain(std::iter::once(Expr { + kind: ExprKind::Record(Vec::new()), span: branch.span, - })) - .chain(std::iter::once(self.local_expr(&temp, branch.span))), + })), branch.span, ); branch.value = self.branch_value(branch.value, failure); @@ -288,8 +293,14 @@ impl Desugarer { }; Expr { kind: ExprKind::Let { - bindings, - body: Box::new(body), + bindings: vec![scrutinee_binding], + body: Box::new(Expr { + kind: ExprKind::Let { + bindings, + body: Box::new(body), + }, + span, + }), }, span, } diff --git a/crates/psrs-desugar/src/fixity.rs b/crates/psrs-desugar/src/fixity.rs index 0e233294..396ddfeb 100644 --- a/crates/psrs-desugar/src/fixity.rs +++ b/crates/psrs-desugar/src/fixity.rs @@ -63,7 +63,7 @@ fn validate_expr(expression: &Expr, errors: &mut Vec) { validate_expr(element, errors); } } - ExprKind::Record(fields) => { + ExprKind::Record(fields) | ExprKind::MatchProduct(fields) => { for (_, value) in fields { validate_expr(value, errors); } @@ -350,15 +350,25 @@ fn reduce(values: &mut Vec, operators: &mut Vec) { let right = values.pop().expect("operator has a right operand"); let left = values.pop().expect("operator has a left operand"); let span = TextRange::new(left.span.start, right.span.end); - values.push(Expr { - kind: ExprKind::Operator { + let kind = if let Some(local) = operator.local { + let function = Expr { + kind: ExprKind::Local(local), + span: operator.operator_span, + }; + let partial = Expr { + kind: ExprKind::Application(Box::new(function), Box::new(left)), + span, + }; + ExprKind::Application(Box::new(partial), Box::new(right)) + } else { + ExprKind::Operator { operator: operator.symbol, operator_span: operator.operator_span, left: Box::new(left), right: Box::new(right), - }, - span, - }); + } + }; + values.push(Expr { kind, span }); } pub(super) fn reassociate_pattern( diff --git a/crates/psrs-desugar/src/free_vars.rs b/crates/psrs-desugar/src/free_vars.rs deleted file mode 100644 index 6ad4cebc..00000000 --- a/crates/psrs-desugar/src/free_vars.rs +++ /dev/null @@ -1,377 +0,0 @@ -use psrs_hir::{CaseBranch, Expr, ExprKind, Guard, GuardedExpr, LocalId, Pattern, PatternKind}; -use std::collections::{HashMap, HashSet}; - -pub(super) fn captures(branches: &[CaseBranch]) -> Vec { - let mut free = HashSet::new(); - for branch in branches { - let mut bound = HashSet::new(); - pattern_ids(&branch.pattern, &mut bound); - collect(&branch.value, &mut bound, &mut free); - } - let mut captures = free.into_iter().collect::>(); - captures.sort_by_key(|id| id.0); - captures -} - -pub(super) fn rebind(expression: Expr, mapping: &HashMap) -> Expr { - let span = expression.span; - let kind = match expression.kind { - ExprKind::Local(id) => ExprKind::Local(mapping.get(&id).copied().unwrap_or(id)), - ExprKind::Array(items) => ExprKind::Array( - items - .into_iter() - .map(|item| rebind(item, mapping)) - .collect(), - ), - ExprKind::Record(fields) => ExprKind::Record( - fields - .into_iter() - .map(|(label, value)| (label, rebind(value, mapping))) - .collect(), - ), - ExprKind::RecordUpdate { expression, fields } => ExprKind::RecordUpdate { - expression: Box::new(rebind(*expression, mapping)), - fields: fields - .into_iter() - .map(|(label, value)| (label, rebind(value, mapping))) - .collect(), - }, - ExprKind::FieldAccess { expression, field } => ExprKind::FieldAccess { - expression: Box::new(rebind(*expression, mapping)), - field, - }, - ExprKind::Application(function, argument) => ExprKind::Application( - Box::new(rebind(*function, mapping)), - Box::new(rebind(*argument, mapping)), - ), - ExprKind::Typed { expression, ty } => ExprKind::Typed { - expression: Box::new(rebind(*expression, mapping)), - ty, - }, - ExprKind::TypeApplication { expression, ty } => ExprKind::TypeApplication { - expression: Box::new(rebind(*expression, mapping)), - ty, - }, - ExprKind::Operator { - operator, - operator_span, - left, - right, - } => ExprKind::Operator { - operator, - operator_span, - left: Box::new(rebind(*left, mapping)), - right: Box::new(rebind(*right, mapping)), - }, - ExprKind::Negate { - function, - minus_span, - expression, - } => ExprKind::Negate { - function: Box::new(rebind(*function, mapping)), - minus_span, - expression: Box::new(rebind(*expression, mapping)), - }, - ExprKind::OperatorChain { - operands, - operators, - } => ExprKind::OperatorChain { - operands: operands - .into_iter() - .map(|operand| rebind(operand, mapping)) - .collect(), - operators, - }, - ExprKind::OperatorSection { - operator, - operand, - binder, - side, - } => ExprKind::OperatorSection { - operator, - operand: Box::new(rebind(*operand, mapping)), - binder, - side, - }, - ExprKind::Lambda { binder, body } => ExprKind::Lambda { - binder, - body: Box::new(rebind(*body, mapping)), - }, - ExprKind::Let { bindings, body } => ExprKind::Let { - bindings: bindings - .into_iter() - .map(|binding| LocalBinding { - value: rebind(binding.value, mapping), - ..binding - }) - .collect(), - body: Box::new(rebind(*body, mapping)), - }, - ExprKind::If { - condition, - then_branch, - else_branch, - } => ExprKind::If { - condition: Box::new(rebind(*condition, mapping)), - then_branch: Box::new(rebind(*then_branch, mapping)), - else_branch: Box::new(rebind(*else_branch, mapping)), - }, - ExprKind::Case { - scrutinee, - branches, - } => ExprKind::Case { - scrutinee: Box::new(rebind(*scrutinee, mapping)), - branches: branches - .into_iter() - .map(|branch| CaseBranch { - value: rebind(branch.value, mapping), - ..branch - }) - .collect(), - }, - ExprKind::Guarded(clauses) => ExprKind::Guarded( - clauses - .into_iter() - .map(|clause| rebind_clause(clause, mapping)) - .collect(), - ), - leaf @ (ExprKind::Global(_) - | ExprKind::Integer(_) - | ExprKind::Number(_) - | ExprKind::String(_) - | ExprKind::Char(_)) => leaf, - }; - Expr { kind, span } -} - -use psrs_hir::LocalBinding; - -fn rebind_clause(clause: GuardedExpr, mapping: &HashMap) -> GuardedExpr { - GuardedExpr { - guards: clause - .guards - .into_iter() - .map(|guard| match guard { - Guard::Boolean(value) => Guard::Boolean(rebind(value, mapping)), - Guard::Pattern { pattern, value } => Guard::Pattern { - pattern, - value: rebind(value, mapping), - }, - Guard::Let { bindings, span } => Guard::Let { - bindings: bindings - .into_iter() - .map(|binding| LocalBinding { - value: rebind(binding.value, mapping), - ..binding - }) - .collect(), - span, - }, - }) - .collect(), - value: rebind(clause.value, mapping), - where_bindings: clause - .where_bindings - .into_iter() - .map(|binding| LocalBinding { - value: rebind(binding.value, mapping), - ..binding - }) - .collect(), - ..clause - } -} - -fn collect(expression: &Expr, bound: &mut HashSet, free: &mut HashSet) { - match &expression.kind { - ExprKind::Local(id) => { - if !bound.contains(id) { - free.insert(*id); - } - } - ExprKind::Array(items) => { - for item in items { - collect(item, bound, free); - } - } - ExprKind::Record(fields) => { - for (_, value) in fields { - collect(value, bound, free); - } - } - ExprKind::RecordUpdate { expression, fields } => { - collect(expression, bound, free); - for (_, value) in fields { - collect(value, bound, free); - } - } - ExprKind::FieldAccess { expression, .. } - | ExprKind::Typed { expression, .. } - | ExprKind::TypeApplication { expression, .. } => { - collect(expression, bound, free); - } - ExprKind::Application(function, argument) => { - collect(function, bound, free); - collect(argument, bound, free); - } - ExprKind::Operator { left, right, .. } => { - collect(left, bound, free); - collect(right, bound, free); - } - ExprKind::Negate { - function, - expression, - .. - } => { - collect(function, bound, free); - collect(expression, bound, free); - } - ExprKind::OperatorChain { operands, .. } => { - for operand in operands { - collect(operand, bound, free); - } - } - ExprKind::OperatorSection { operand, .. } => collect(operand, bound, free), - ExprKind::Lambda { binder, body } => { - let inserted = bound.insert(binder.id); - collect(body, bound, free); - if inserted { - bound.remove(&binder.id); - } - } - ExprKind::Let { bindings, body } => { - let mut inserted = Vec::new(); - for binding in bindings { - if bound.insert(binding.binder.id) { - inserted.push(binding.binder.id); - } - } - for binding in bindings { - collect(&binding.value, bound, free); - } - collect(body, bound, free); - for id in inserted { - bound.remove(&id); - } - } - ExprKind::If { - condition, - then_branch, - else_branch, - } => { - collect(condition, bound, free); - collect(then_branch, bound, free); - collect(else_branch, bound, free); - } - ExprKind::Case { - scrutinee, - branches, - } => { - collect(scrutinee, bound, free); - for branch in branches { - let mut inserted = Vec::new(); - let mut ids = HashSet::new(); - pattern_ids(&branch.pattern, &mut ids); - for id in ids { - if bound.insert(id) { - inserted.push(id); - } - } - collect(&branch.value, bound, free); - for id in inserted { - bound.remove(&id); - } - } - } - ExprKind::Guarded(clauses) => { - for clause in clauses { - collect_clause(clause, bound, free); - } - } - ExprKind::Global(_) - | ExprKind::Integer(_) - | ExprKind::Number(_) - | ExprKind::String(_) - | ExprKind::Char(_) => {} - } -} - -fn collect_clause(clause: &GuardedExpr, bound: &mut HashSet, free: &mut HashSet) { - let mut inserted = Vec::new(); - for binding in &clause.where_bindings { - if bound.insert(binding.binder.id) { - inserted.push(binding.binder.id); - } - } - for binding in &clause.where_bindings { - collect(&binding.value, bound, free); - } - for guard in &clause.guards { - match guard { - Guard::Boolean(value) => collect(value, bound, free), - Guard::Pattern { pattern, value } => { - collect(value, bound, free); - let mut ids = HashSet::new(); - pattern_ids(pattern, &mut ids); - for id in ids { - if bound.insert(id) { - inserted.push(id); - } - } - } - Guard::Let { bindings, .. } => { - for binding in bindings { - if bound.insert(binding.binder.id) { - inserted.push(binding.binder.id); - } - } - for binding in bindings { - collect(&binding.value, bound, free); - } - } - } - } - collect(&clause.value, bound, free); - for id in inserted { - bound.remove(&id); - } -} - -fn pattern_ids(pattern: &Pattern, ids: &mut HashSet) { - match &pattern.kind { - PatternKind::Var(binder) => { - ids.insert(binder.id); - } - PatternKind::Named { binder, pattern } => { - ids.insert(binder.id); - pattern_ids(pattern, ids); - } - PatternKind::Array(elements) => { - for element in elements { - pattern_ids(element, ids); - } - } - PatternKind::Typed { pattern, .. } => pattern_ids(pattern, ids), - PatternKind::Constructor { arguments, .. } => { - for argument in arguments { - pattern_ids(argument, ids); - } - } - PatternKind::Record { fields, .. } => { - for (_, field) in fields { - pattern_ids(field, ids); - } - } - PatternKind::OperatorChain { operands, .. } => { - for operand in operands { - pattern_ids(operand, ids); - } - } - PatternKind::Wildcard - | PatternKind::Boolean(_) - | PatternKind::Integer(_) - | PatternKind::Number(_) - | PatternKind::String(_) - | PatternKind::Char(_) => {} - } -} diff --git a/crates/psrs-desugar/src/lib.rs b/crates/psrs-desugar/src/lib.rs index 31f93252..bae54518 100644 --- a/crates/psrs-desugar/src/lib.rs +++ b/crates/psrs-desugar/src/lib.rs @@ -22,7 +22,6 @@ mod case_helpers; mod constant_truth; mod expr; mod fixity; -mod free_vars; mod guards; mod types; @@ -111,6 +110,10 @@ pub fn desugar_module_with_true_symbols( } fn desugar_expr(expression: Expr) -> Expr { + psrs_span::with_sufficient_stack(|| desugar_expr_inner(expression)) +} + +fn desugar_expr_inner(expression: Expr) -> Expr { let span = expression.span; let kind = match expression.kind { ExprKind::Operator { @@ -158,7 +161,10 @@ fn desugar_expr(expression: Expr) -> Expr { span: binder.span, }; let function = Expr { - kind: ExprKind::Global(operator.symbol), + kind: match operator.local { + Some(local) => ExprKind::Local(local), + None => ExprKind::Global(operator.symbol), + }, span: operator.operator_span, }; let (left, right) = match side { @@ -191,6 +197,12 @@ fn desugar_expr(expression: Expr) -> Expr { .map(|(label, value)| (label, desugar_expr(value))) .collect(), ), + ExprKind::MatchProduct(fields) => ExprKind::MatchProduct( + fields + .into_iter() + .map(|(label, value)| (label, desugar_expr(value))) + .collect(), + ), ExprKind::RecordUpdate { expression, fields } => ExprKind::RecordUpdate { expression: Box::new(desugar_expr(*expression)), fields: fields @@ -204,7 +216,11 @@ fn desugar_expr(expression: Expr) -> Expr { }, ExprKind::Typed { expression, ty } => ExprKind::Typed { expression: Box::new(desugar_expr(*expression)), - ty, + ty: types::desugar_type(ty), + }, + ExprKind::TypeApplication { expression, ty } => ExprKind::TypeApplication { + expression: Box::new(desugar_expr(*expression)), + ty: types::desugar_type(ty), }, ExprKind::Lambda { binder, body } => ExprKind::Lambda { binder, diff --git a/crates/psrs-desugar/src/tests.rs b/crates/psrs-desugar/src/tests.rs index 3598370d..32adea92 100644 --- a/crates/psrs-desugar/src/tests.rs +++ b/crates/psrs-desugar/src/tests.rs @@ -5,14 +5,15 @@ use psrs_span::TextRange; #[test] fn lowers_operator_to_applications_and_preserves_source_ranges() { let module_id = ModuleId(0); - let operator_id = Intrinsic::I32Add.symbol(); + let operator_id = Intrinsic::IntAdd.symbol(); let module = hir::Module { id: module_id, name: "Main".into(), externals: vec![hir::ExternalSymbol { + source_module: None, symbol: operator_id, name: "+".into(), - kind: ExternalKind::Intrinsic(Intrinsic::I32Add), + kind: ExternalKind::Intrinsic(Intrinsic::IntAdd), signature: None, }], imports: Vec::new(), diff --git a/crates/psrs-driver/Cargo.toml b/crates/psrs-driver/Cargo.toml index a14281fa..d005bfa2 100644 --- a/crates/psrs-driver/Cargo.toml +++ b/crates/psrs-driver/Cargo.toml @@ -24,3 +24,9 @@ psrs-span.workspace = true psrs-syntax.workspace = true psrs-thir.workspace = true psrs-typecheck.workspace = true +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[dev-dependencies] +wasm-encoder.workspace = true +wasmparser.workspace = true diff --git a/crates/psrs-driver/src/diagnostics.rs b/crates/psrs-driver/src/diagnostics.rs new file mode 100644 index 00000000..507bb63c --- /dev/null +++ b/crates/psrs-driver/src/diagnostics.rs @@ -0,0 +1,60 @@ +use crate::{Artifact, ProgramDiagnostic}; +use psrs_backend::{CompileTrace, TraceArtifactId, TracePassStatus}; + +/// IR dumps retained when a program reaches only part of the compile pipeline. +/// The boundary label identifies the last pass completed or input boundary. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct PartialIrDumps { + pub core_stage: Option<&'static str>, + pub core: Option, + pub cc_stage: Option<&'static str>, + pub cc: Option, + pub mir_stage: Option<&'static str>, + pub mir: Option, +} + +/// Result of one whole-program compile attempt, including partial IR on failure. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CompilationReport { + pub artifact: Option, + pub diagnostics: Vec, + pub dumps: PartialIrDumps, + /// Backend pass/artifact events, populated only by the diagnosis entrypoint. + pub backend_trace: Option, + /// The driver-owned frontend boundary for a diagnosis compile attempt. + pub frontend_trace: Option, + /// Exact trace artifacts represented by the retained Core/CC/MIR dumps. + pub dump_artifacts: IrDumpArtifacts, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FrontendPassTrace { + pub contract_version: u32, + pub pass_key: &'static str, + /// Number of leading entries owned by the trusted library. + pub trusted_prefix: usize, + pub source_names: Vec, + pub status: TracePassStatus, + pub output_core: Option, + pub diagnostic_indices: Vec, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct IrDumpArtifacts { + pub core: Option, + pub cc: Option, + pub mir: Option, +} + +impl CompilationReport { + pub(crate) fn failed(diagnostics: Vec, dumps: PartialIrDumps) -> Self { + Self { + artifact: None, + diagnostics, + dumps, + backend_trace: None, + frontend_trace: None, + dump_artifacts: IrDumpArtifacts::default(), + } + } +} diff --git a/crates/psrs-driver/src/lib.rs b/crates/psrs-driver/src/lib.rs index e74874c1..6da45c19 100644 --- a/crates/psrs-driver/src/lib.rs +++ b/crates/psrs-driver/src/lib.rs @@ -1,16 +1,25 @@ use psrs_span::{SourceFile, TextRange}; +mod diagnostics; mod loader; mod prelude; +pub use prelude::{CommandRunner, StandardLibraryInfo, standard_library_info}; mod program; -pub use loader::load_program_files; +pub use diagnostics::{CompilationReport, FrontendPassTrace, IrDumpArtifacts, PartialIrDumps}; +pub use psrs_backend::TargetCapabilities; +pub use psrs_backend::trace::*; + +pub use loader::{ + ProgramCaseSources, collect_purs_files, load_program_case_sources, load_program_files, +}; pub use program::{ check_program, check_program_kinds_lenient, check_program_kinds_lenient_with_prelude, check_program_lenient, check_program_lenient_with_prelude, check_program_types_lenient, check_program_types_lenient_with_prelude, check_program_with_warnings, compile_program_sources, - compile_program_sources_with_prelude, resolve_program_sources, typecheck_program_sources, - typecheck_program_sources_with_warnings, + compile_program_sources_with_prelude, compile_program_sources_with_prelude_diagnosis, + compile_program_sources_with_prelude_report, resolve_program_sources, + typecheck_program_sources, typecheck_program_sources_with_warnings, }; #[derive(Clone, Debug, PartialEq, Eq)] @@ -114,7 +123,6 @@ pub fn compile_source(source_name: &str, source_text: &str) -> Result, - effect_context: Option, } /// Parses the standard library and a program module and links both into one @@ -141,14 +148,12 @@ fn lower_source_with_prelude_to_core( source_text: &str, ) -> Result> { let (sources, trusted_prefix) = prepend_stdlib(&[(source_name, source_text)])?; - let (core, warnings, effect_context) = - program::lower_program_to_core_and_effect_context(&sources, trusted_prefix) - .map_err(program_diagnostics_from_hidden_prelude)?; + let (core, warnings) = program::lower_program_to_core_and_warnings(&sources, trusted_prefix) + .map_err(program_diagnostics_from_hidden_prelude)?; Ok(LoweredSource { core, trusted_prefix, warnings: program::typecheck_warnings(warnings, trusted_prefix), - effect_context, }) } @@ -186,7 +191,6 @@ pub(crate) fn lower_source_to_core( #[cfg(test)] pub(crate) struct PreparedSource { pub core: psrs_core::Module, - pub effect_context: Option, } #[cfg(test)] @@ -197,13 +201,9 @@ pub(crate) fn prepare_main(source: &str) -> Result Result> { let (sources, trusted_prefix) = prepend_stdlib(sources)?; - let (core, _warnings, effect_context) = - program::lower_program_to_core_and_effect_context(&sources, trusted_prefix) - .map_err(program_diagnostics_from_hidden_prelude)?; - Ok(PreparedSource { - core, - effect_context, - }) + let (core, _warnings) = program::lower_program_to_core_and_warnings(&sources, trusted_prefix) + .map_err(program_diagnostics_from_hidden_prelude)?; + Ok(PreparedSource { core }) } #[cfg(test)] @@ -217,8 +217,7 @@ pub(crate) fn compile_main_with_target( target: psrs_backend::TargetCapabilities, ) -> Result> { let prepared = prepare_main(source)?; - psrs_backend::compile_with_context(prepared.core, prepared.effect_context, target) - .map_err(backend_diagnostics) + psrs_backend::compile_with_context(prepared.core, target).map_err(backend_diagnostics) } #[cfg(test)] @@ -226,8 +225,7 @@ pub(crate) fn lower_main_to_cc( source: &str, ) -> Result> { let prepared = prepare_main(source)?; - psrs_backend::lower_cc_with_context(prepared.core, prepared.effect_context.as_ref()) - .map_err(backend_diagnostics) + psrs_backend::cc::lower_module(prepared.core).map_err(backend_diagnostics) } pub fn compile_source_with_dumps( @@ -238,7 +236,6 @@ pub fn compile_source_with_dumps( let mut warnings = lowered.warnings; let stages = psrs_backend::compile_with_context( lowered.core, - lowered.effect_context, psrs_backend::TargetCapabilities::default(), ) .map_err(backend_diagnostics)?; diff --git a/crates/psrs-driver/src/loader.rs b/crates/psrs-driver/src/loader.rs index 5f31b474..d92c375d 100644 --- a/crates/psrs-driver/src/loader.rs +++ b/crates/psrs-driver/src/loader.rs @@ -1,6 +1,6 @@ //! Filesystem discovery for the transitive source graph. //! -//! The standard library is read from `stdlib/lib`; user modules are discovered +//! The standard library is read from the locked `psrs-stdlib` package; user modules are discovered //! from the filesystem. Given the entry files, this loader scans their //! directories for `.purs` files, indexes them by declared module name, and //! follows the `import` graph until it closes. Modules supplied by the @@ -11,6 +11,81 @@ use crate::lower_source_to_ast; use std::collections::{HashMap, HashSet, VecDeque}; use std::path::{Path, PathBuf}; +/// Source files that make up a corpus case: the main file, its optional +/// same-stem support directory, and the transitive imports the normal loader +/// discovers for those entry files. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ProgramCaseSources { + pub own: Vec<(String, String)>, + pub loaded: Vec<(String, String)>, +} + +impl ProgramCaseSources { + pub fn inputs(&self) -> Vec<(&str, &str)> { + self.own + .iter() + .chain(&self.loaded) + .map(|(path, text)| (path.as_str(), text.as_str())) + .collect() + } +} + +/// Assembles one suite case using the compiler's regular import loader. +/// Files beside a category root are independent; a same-stem subdirectory is +/// searched only when the case itself lives below that root. +pub fn load_program_case_sources( + path: &Path, + category_dir: &Path, + main_text: &str, +) -> Result { + let mut own = vec![(path.to_string_lossy().into_owned(), main_text.to_owned())]; + let support_dir = path.with_extension(""); + if support_dir.is_dir() { + let mut support = Vec::new(); + collect_purs_files(&support_dir, &mut support); + support.sort(); + for support_path in support { + let text = std::fs::read_to_string(&support_path) + .map_err(|error| format!("{}: {error}", support_path.display()))?; + own.push((support_path.to_string_lossy().into_owned(), text)); + } + } + + let own_directory = path.parent() != Some(category_dir); + let loaded = if own_directory { + let entry_paths = own + .iter() + .skip(1) + .map(|(path, _)| path.clone()) + .chain(std::iter::once(path.to_string_lossy().into_owned())) + .collect::>(); + load_program_files(&entry_paths)? + .into_iter() + .filter(|(loaded, _)| !own.iter().any(|(own, _)| own == loaded)) + .collect() + } else { + Vec::new() + }; + Ok(ProgramCaseSources { own, loaded }) +} + +pub fn collect_purs_files(dir: &Path, out: &mut Vec) { + let Ok(entries) = std::fs::read_dir(dir) else { + return; + }; + for entry in entries.filter_map(Result::ok) { + let path = entry.path(); + if path.is_dir() { + collect_purs_files(&path, out); + } else if path + .extension() + .is_some_and(|extension| extension == "purs") + { + out.push(path); + } + } +} + /// Loads the entry files plus every transitively imported user module found on /// disk. Returns `(path, text)` pairs; the order is discovery order, and P3 /// orders modules by dependency. An unreadable entry file is an error. diff --git a/crates/psrs-driver/src/prelude.rs b/crates/psrs-driver/src/prelude/mod.rs similarity index 54% rename from crates/psrs-driver/src/prelude.rs rename to crates/psrs-driver/src/prelude/mod.rs index b3ceed55..b0c55f1c 100644 --- a/crates/psrs-driver/src/prelude.rs +++ b/crates/psrs-driver/src/prelude/mod.rs @@ -1,11 +1,13 @@ //! The on-disk PureScript standard library. //! -//! Sources are read from `stdlib/lib` at runtime (not embedded). `lib/trusted` -//! names the modules that form the trusted prefix, in order. The directory is -//! resolved from the crate location first, so `cargo test` and the CLI do not -//! depend on the process current directory. +//! Sources come from the locked external `psrs-stdlib` package at runtime. +//! `PSRS_STDLIB_ROOT` explicitly selects an unlocked development package. + +mod package; +pub use package::{CommandRunner, StandardLibraryInfo}; use std::collections::HashSet; +use std::collections::VecDeque; use std::path::{Path, PathBuf}; use std::sync::OnceLock; @@ -16,12 +18,18 @@ pub(crate) struct ModuleSource { pub path: String, pub module_name: String, pub text: String, + imports: Vec, } struct Library { + info: StandardLibraryInfo, modules: Vec, } +pub fn standard_library_info() -> Result { + Ok(load()?.info.clone()) +} + pub(crate) fn sources() -> Result<&'static [ModuleSource], String> { Ok(&load()?.modules) } @@ -33,14 +41,63 @@ pub(crate) fn module_names() -> Result, String> { .collect()) } -/// Prepends the trusted standard-library modules to `sources`. +/// Prepends the trusted standard-library modules reachable from `sources`. pub(crate) fn prepend<'a>( user_sources: &[(&'a str, &'a str)], +) -> Result, String> { + prepend_selected(user_sources, false) +} + +pub(crate) fn prepend_for_command<'a>( + user_sources: &[(&'a str, &'a str)], +) -> Result, String> { + prepend_selected(user_sources, true) +} + +fn prepend_selected<'a>( + user_sources: &[(&'a str, &'a str)], + command: bool, ) -> Result, String> { let library = sources()?; - let trusted_prefix = library.len(); + let by_name = library + .iter() + .enumerate() + .map(|(index, module)| (module.module_name.as_str(), index)) + .collect::>(); + let mut needed = HashSet::new(); + let mut pending = VecDeque::new(); + if command && let Some(runner) = &load()?.info.command_runner { + if !by_name.contains_key(runner.module.as_str()) { + return Err(format!( + "configured command runner module `{}` is absent from the trusted package inventory", + runner.module + )); + } + pending.push_back(runner.module.clone()); + } + for (name, text) in user_sources { + if let Ok(parsed) = crate::lower_source_to_ast(name, text) { + pending.extend(parsed.imports.into_iter().map(|import| import.module.text)); + } + } + while let Some(name) = pending.pop_front() { + let Some(&index) = by_name.get(name.as_str()) else { + continue; + }; + if needed.insert(index) { + pending.extend(library[index].imports.iter().cloned()); + } + } + + let selected = library + .iter() + .enumerate() + .filter(|(index, _)| needed.contains(index)) + .map(|(_, module)| module) + .collect::>(); + let trusted_prefix = selected.len(); let mut all = Vec::with_capacity(trusted_prefix + user_sources.len()); - for module in library { + for module in selected { all.push((module.path.as_str(), module.text.as_str())); } all.extend_from_slice(user_sources); @@ -57,11 +114,18 @@ fn load() -> Result<&'static Library, String> { } fn read_library() -> Result { - let root = find_stdlib_root()?; - let lib = root.join("lib"); + let info = package::select()?; + let lib = info.root.join("lib"); let names = read_trusted_names(&lib.join("trusted"))?; let mut modules = Vec::with_capacity(names.len()); for name in names { + // A compiler-provided module (for example `Safe.Coerce`) resolves + // through its primitive interface. Loading the vendored file as well + // would shadow that interface with the official `unsafeCoerce` body, + // which the project cannot compile. + if psrs_resolve::compiler_provided_module(&name) { + continue; + } let path = module_file(&lib, &name); let text = std::fs::read_to_string(&path) .map_err(|error| format!("{}: {error}", path.display()))?; @@ -71,7 +135,10 @@ fn read_library() -> Result { .first() .map(|error| error.message.as_str()) .unwrap_or("could not parse the standard-library module"); - format!("{display}: {message}") + format!( + "{display}:{}: {message}", + errors.first().map(|error| error.span.start).unwrap_or(0) + ) })?; if parsed.name.text != name { return Err(format!( @@ -83,9 +150,17 @@ fn read_library() -> Result { path: display, module_name: name, text, + imports: parsed + .imports + .into_iter() + .map(|import| import.module.text) + .collect(), }); } - Ok(Library { modules }) + if package::fingerprint(&info.root)? != info.source_fingerprint { + return Err("standard-library package changed during loading".into()); + } + Ok(Library { modules, info }) } fn read_trusted_names(path: &Path) -> Result, String> { @@ -127,56 +202,3 @@ fn module_file(lib: &Path, module_name: &str) -> PathBuf { path.push(format!("{file_stem}.purs")); path } - -fn find_stdlib_root() -> Result { - let mut tried = Vec::new(); - for candidate in stdlib_candidates() { - if is_stdlib_root(&candidate) { - return candidate - .canonicalize() - .map_err(|error| format!("{}: {error}", candidate.display())); - } - if !tried.iter().any(|existing| existing == &candidate) { - tried.push(candidate); - } - } - Err(format!( - "could not find the standard library (expected stdlib/lib/trusted and stdlib/lib/Prelude.purs); looked in {}", - tried - .iter() - .map(|path| path.display().to_string()) - .collect::>() - .join(", ") - )) -} - -fn is_stdlib_root(path: &Path) -> bool { - path.join("lib/trusted").is_file() && path.join("lib/Prelude.purs").is_file() -} - -fn stdlib_candidates() -> Vec { - let mut candidates = Vec::new(); - let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - // Anchored at this crate: `crates/psrs-driver` -> repository `stdlib/`. - candidates.push(crate_dir.join("../../stdlib")); - push_ancestor_stdlibs(&crate_dir, &mut candidates); - if let Ok(current) = std::env::current_dir() { - push_ancestor_stdlibs(¤t, &mut candidates); - } - if let Ok(executable) = std::env::current_exe() - && let Some(parent) = executable.parent() - { - push_ancestor_stdlibs(parent, &mut candidates); - } - candidates -} - -fn push_ancestor_stdlibs(start: &Path, candidates: &mut Vec) { - let mut directory = start.to_path_buf(); - loop { - candidates.push(directory.join("stdlib")); - if !directory.pop() { - break; - } - } -} diff --git a/crates/psrs-driver/src/prelude/package.rs b/crates/psrs-driver/src/prelude/package.rs new file mode 100644 index 00000000..ba881a2a --- /dev/null +++ b/crates/psrs-driver/src/prelude/package.rs @@ -0,0 +1,365 @@ +//! Package selection and portable content identity for the external stdlib. +use std::path::{Path, PathBuf}; + +use serde::Deserialize; + +#[derive(Clone, Debug)] +pub struct StandardLibraryInfo { + pub root: PathBuf, + pub source_fingerprint: String, + /// Absent for an explicit development override. + pub locked_revision: Option, + pub command_runner: Option, +} + +/// An ordinary package-owned declaration used to adapt command entries. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct CommandRunner { + pub module: String, + pub function: String, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct Lock { + schema_version: u32, + path: PathBuf, + revision: String, + source_fingerprint: String, +} + +pub(super) fn select() -> Result { + if let Some(root) = std::env::var_os("PSRS_STDLIB_ROOT") { + return inspect(Path::new(&root), None); + } + let lock_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../stdlib.lock.json"); + select_locked(&lock_path) +} + +fn select_locked(lock_path: &Path) -> Result { + let lock: Lock = serde_json::from_slice(&read(lock_path)?) + .map_err(|error| format!("{}: {error}", lock_path.display()))?; + if lock.schema_version != 1 + || lock.revision.len() != 40 + || !lock.revision.bytes().all(|byte| byte.is_ascii_hexdigit()) + { + return Err(format!( + "{}: unsupported or invalid stdlib lock", + lock_path.display() + )); + } + let info = inspect( + &lock_path.parent().unwrap().join(lock.path), + Some(lock.revision), + )?; + if info.source_fingerprint != lock.source_fingerprint { + return Err(format!( + "{}: standard-library content differs from the lock; use PSRS_STDLIB_ROOT for an explicit development override", + info.root.display() + )); + } + Ok(info) +} + +fn inspect(root: &Path, revision: Option) -> Result { + let root = root + .canonicalize() + .map_err(|error| format!("{}: {error}", root.display()))?; + let manifest: serde_json::Value = serde_json::from_slice(&read(&root.join("manifest.json"))?) + .map_err(|error| format!("{}: {error}", root.display()))?; + // Protocol versions are compiler contracts, not assumptions inferred from source names. + for (pointer, expected) in [ + ("/schema_version", serde_json::json!(1)), + ("/name", serde_json::json!("psrs-stdlib")), + ("/source_root", serde_json::json!("lib")), + ("/trusted_modules", serde_json::json!("lib/trusted")), + ("/upstream_lock", serde_json::json!("upstream-lock.json")), + ( + "/compiler_contract/primitive_binding_protocol", + serde_json::json!(1), + ), + ( + "/compiler_contract/string_semantics", + serde_json::json!("unicode-scalar-utf8"), + ), + ( + "/compiler_contract/int_semantics", + serde_json::json!("signed-i32"), + ), + ] { + if manifest.pointer(pointer) != Some(&expected) { + return Err(format!( + "{}: unsupported stdlib manifest field {pointer}", + root.display() + )); + } + } + // Packages using the draft control surface declare its checked API contract. + // Absence preserves packages that do not depend on control (including the + // older locked package); source is always rechecked against the registry. + if let Some(protocol) = manifest.pointer("/compiler_contract/control_protocol") + && protocol != &serde_json::json!(2) + { + return Err(format!( + "{}: unsupported stdlib control_protocol; expected 2", + root.display() + )); + } + let command_runner = manifest + .pointer("/compiler_contract/command_runner") + .map(|value| { + serde_json::from_value::(value.clone()) + .map_err(|error| format!("{}: invalid command_runner: {error}", root.display())) + }) + .transpose()?; + if command_runner.as_ref().is_some_and(|runner| { + runner.module.is_empty() + || runner.function.is_empty() + || runner.module.trim() != runner.module + || runner.function.trim() != runner.function + }) { + return Err(format!( + "{}: command_runner requires nonempty module and function names", + root.display() + )); + } + // Required metadata must exist even in development mode. + read(&root.join("lib/trusted"))?; + read(&root.join("lib/Prelude.purs"))?; + read(&root.join("upstream-lock.json"))?; + let source_fingerprint = fingerprint(&root)?; + Ok(StandardLibraryInfo { + root, + source_fingerprint, + locked_revision: revision, + command_runner, + }) +} + +/// FNV-1a content identity, versioned and independent of checkout paths. +/// Covers all files under lib/ and conformance/, plus both package manifests. +/// This is a reproducibility identifier, not a cryptographic integrity claim. +pub(super) fn fingerprint(root: &Path) -> Result { + let mut paths = vec![ + PathBuf::from("manifest.json"), + PathBuf::from("upstream-lock.json"), + ]; + collect(root, Path::new("lib"), &mut paths)?; + collect(root, Path::new("conformance"), &mut paths)?; + paths.sort(); + let mut hash = 0xcbf29ce484222325_u64; + let mut feed = |bytes: &[u8]| { + for byte in bytes { + hash = (hash ^ u64::from(*byte)).wrapping_mul(0x100000001b3); + } + }; + feed(b"psrs-stdlib-content-v1\0"); + for path in paths { + let bytes = read(&root.join(&path))?; + let name = path + .components() + .map(|part| { + part.as_os_str() + .to_str() + .ok_or("stdlib contains a non-UTF-8 path") + }) + .collect::, _>>()? + .join("/"); + feed(name.as_bytes()); + feed(&[0]); + feed(&(bytes.len() as u64).to_le_bytes()); + feed(&bytes); + } + Ok(format!("fnv1a64-v1:{hash:016x}")) +} + +fn collect(root: &Path, relative: &Path, paths: &mut Vec) -> Result<(), String> { + let path = root.join(relative); + if std::fs::symlink_metadata(&path) + .map_err(|error| error.to_string())? + .file_type() + .is_symlink() + { + return Err(format!( + "{}: stdlib package symlinks are unsupported", + path.display() + )); + } + for entry in std::fs::read_dir(&path).map_err(|error| format!("{}: {error}", path.display()))? { + let entry = entry.map_err(|error| error.to_string())?; + let kind = entry.file_type().map_err(|error| error.to_string())?; + let relative = relative.join(entry.file_name()); + if kind.is_symlink() { + return Err(format!( + "{}: stdlib package symlinks are unsupported", + entry.path().display() + )); + } else if kind.is_dir() { + collect(root, &relative, paths)?; + } else if kind.is_file() { + paths.push(relative); + } else { + return Err(format!( + "{}: expected a regular package file", + entry.path().display() + )); + } + } + Ok(()) +} + +fn read(path: &Path) -> Result, String> { + if !std::fs::symlink_metadata(path) + .map_err(|error| format!("{}: {error}", path.display()))? + .file_type() + .is_file() + { + return Err(format!( + "{}: expected a regular package file", + path.display() + )); + } + std::fs::read(path).map_err(|error| format!("{}: {error}", path.display())) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicU64, Ordering}; + + struct Fixture(PathBuf); + impl Fixture { + fn new() -> Self { + static NEXT: AtomicU64 = AtomicU64::new(0); + let root = std::env::temp_dir().join(format!( + "psrs-package-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + )); + std::fs::create_dir_all(root.join("lib")).unwrap(); + std::fs::create_dir_all(root.join("conformance")).unwrap(); + let manifest = serde_json::json!({ + "schema_version": 1, "name": "psrs-stdlib", "source_root": "lib", + "trusted_modules": "lib/trusted", "upstream_lock": "upstream-lock.json", + "compiler_contract": {"primitive_binding_protocol": 1, + "string_semantics": "unicode-scalar-utf8", "int_semantics": "signed-i32"} + }); + std::fs::write( + root.join("manifest.json"), + serde_json::to_vec(&manifest).unwrap(), + ) + .unwrap(); + std::fs::write(root.join("upstream-lock.json"), "{}").unwrap(); + std::fs::write(root.join("lib/trusted"), "Prelude\n").unwrap(); + std::fs::write(root.join("lib/Prelude.purs"), "module Prelude where\n").unwrap(); + Self(root) + } + } + impl Drop for Fixture { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } + } + + #[test] + fn package_identity_is_portable_and_covers_cases_and_extra_modules() { + let a = Fixture::new(); + let b = Fixture::new(); + let original = fingerprint(&a.0).unwrap(); + assert_eq!(original, fingerprint(&b.0).unwrap()); + std::fs::write(b.0.join("lib/Unused.purs"), "module Unused where").unwrap(); + assert_ne!(original, fingerprint(&b.0).unwrap()); + std::fs::remove_file(b.0.join("lib/Unused.purs")).unwrap(); + std::fs::write(b.0.join("conformance/cases.json"), "[]").unwrap(); + assert_ne!(original, fingerprint(&b.0).unwrap()); + } + + #[test] + fn package_without_required_protocol_is_rejected() { + let fixture = Fixture::new(); + std::fs::write(fixture.0.join("manifest.json"), b"{}").unwrap(); + let error = inspect(&fixture.0, None).unwrap_err(); + assert!( + error.contains("unsupported stdlib manifest field"), + "{error}" + ); + } + #[test] + fn declared_control_contract_rejects_incompatible_packages() { + let fixture = Fixture::new(); + let path = fixture.0.join("manifest.json"); + let mut manifest: serde_json::Value = + serde_json::from_slice(&read(&path).unwrap()).unwrap(); + for protocol in [ + serde_json::Value::Null, + serde_json::json!(1), + serde_json::json!(3), + serde_json::json!("2"), + ] { + manifest["compiler_contract"]["control_protocol"] = protocol; + std::fs::write(&path, serde_json::to_vec(&manifest).unwrap()).unwrap(); + assert!( + inspect(&fixture.0, None) + .unwrap_err() + .contains("control_protocol") + ); + } + manifest["compiler_contract"]["control_protocol"] = serde_json::json!(2); + std::fs::write(&path, serde_json::to_vec(&manifest).unwrap()).unwrap(); + assert!(inspect(&fixture.0, None).is_ok()); + } + + #[test] + fn locked_package_rejects_dirty_content_and_invalid_dependency_paths() { + let fixture = Fixture::new(); + let lock_path = fixture.0.join("consumer.lock.json"); + let lock = serde_json::json!({"schema_version": 1, "path": fixture.0, + "revision": "0000000000000000000000000000000000000000", + "source_fingerprint": fingerprint(&fixture.0).unwrap()}); + std::fs::write(&lock_path, serde_json::to_vec(&lock).unwrap()).unwrap(); + let selected = select_locked(&lock_path).unwrap(); + assert_eq!(selected.root, fixture.0.canonicalize().unwrap()); + assert!(selected.locked_revision.is_some()); + std::fs::write(fixture.0.join("lib/Prelude.purs"), "changed").unwrap(); + assert!( + select_locked(&lock_path) + .unwrap_err() + .contains("differs from the lock") + ); + std::fs::remove_dir_all(fixture.0.join("lib")).unwrap(); + assert!(select_locked(&lock_path).is_err()); + } + #[test] + fn manifest_command_runner_is_explicit_and_rejects_malformed_configuration() { + let fixture = Fixture::new(); + assert!(inspect(&fixture.0, None).unwrap().command_runner.is_none()); + let path = fixture.0.join("manifest.json"); + let mut manifest: serde_json::Value = + serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap(); + for value in [ + serde_json::Value::Null, + serde_json::json!({"module":"Runner"}), + serde_json::json!({"module":"Runner", "function":""}), + serde_json::json!({"module":"Runner", "function":"run", "extra":true}), + ] { + manifest["compiler_contract"]["command_runner"] = value; + std::fs::write(&path, serde_json::to_vec(&manifest).unwrap()).unwrap(); + assert!( + inspect(&fixture.0, None) + .unwrap_err() + .contains("command_runner") + ); + } + manifest["compiler_contract"]["command_runner"] = + serde_json::json!({"module":"Runner", "function":"run"}); + std::fs::write(&path, serde_json::to_vec(&manifest).unwrap()).unwrap(); + assert_eq!( + inspect(&fixture.0, None).unwrap().command_runner, + Some(CommandRunner { + module: "Runner".into(), + function: "run".into() + }) + ); + } +} diff --git a/crates/psrs-driver/src/program/compilation.rs b/crates/psrs-driver/src/program/compilation.rs new file mode 100644 index 00000000..b4883c61 --- /dev/null +++ b/crates/psrs-driver/src/program/compilation.rs @@ -0,0 +1,243 @@ +use super::{lower_program_to_core_and_warnings, typecheck_warnings}; +use crate::{ + Artifact, CompilationReport, Diagnostic, DiagnosticOrigin, FrontendPassTrace, IrDumpArtifacts, + PartialIrDumps, ProgramDiagnostic, backend_warnings, +}; +use psrs_backend::{CompileTrace, TraceArtifactId, TracePassStatus}; + +/// Compiles a whole program to a single Wasm component. +pub fn compile_program_sources( + sources: &[(&str, &str)], +) -> Result> { + compile_program_sources_with_trusted_prefix(sources, 0) +} + +pub(crate) fn compile_program_sources_with_trusted_prefix( + sources: &[(&str, &str)], + trusted_prefix: usize, +) -> Result> { + let report = compile_attempt(sources, trusted_prefix, false, false, None); + match report.artifact { + Some(artifact) => Ok(artifact), + None => Err(report.diagnostics), + } +} + +pub(super) fn compile_attempt( + sources: &[(&str, &str)], + trusted_prefix: usize, + capture_dumps: bool, + trace_enabled: bool, + runner: Option<&crate::prelude::CommandRunner>, +) -> CompilationReport { + let (core, source_warnings) = match if let Some(runner) = runner { + super::lowering::lower_program_inner( + sources, + trusted_prefix, + Some((&runner.module, &runner.function)), + ) + } else { + lower_program_to_core_and_warnings(sources, trusted_prefix) + } { + Ok(lowered) => lowered, + Err(diagnostics) => { + let mut report = CompilationReport::failed(diagnostics, PartialIrDumps::default()); + if trace_enabled { + report.frontend_trace = Some(FrontendPassTrace { + contract_version: 1, + pass_key: "driver.frontend.lower_program_to_core", + trusted_prefix, + source_names: sources.iter().map(|(name, _)| (*name).to_owned()).collect(), + status: TracePassStatus::Rejected, + output_core: None, + diagnostic_indices: (0..report.diagnostics.len()).collect(), + }); + } + return report; + } + }; + + let mut frontend_trace = trace_enabled.then(|| FrontendPassTrace { + contract_version: 1, + pass_key: "driver.frontend.lower_program_to_core", + trusted_prefix, + source_names: sources.iter().map(|(name, _)| (*name).to_owned()).collect(), + status: TracePassStatus::Completed, + output_core: None, + diagnostic_indices: Vec::new(), + }); + + if trace_enabled { + let linked_core = capture_dumps.then(|| core.clone()); + let traced = psrs_backend::compile_with_context_traced( + core, + psrs_backend::TargetCapabilities::default(), + capture_dumps, + ); + if let Some(frontend) = &mut frontend_trace { + frontend.output_core = Some(traced.trace.initial_core); + } + return match traced.result { + Ok(stages) => success_report( + stages, + source_warnings, + trusted_prefix, + capture_dumps, + Some(traced.trace), + frontend_trace, + ), + Err(failure) => { + let (dumps, dump_artifacts) = if capture_dumps { + partial_dumps( + *failure.partial, + linked_core.expect("dump capture retained linked Core"), + Some(&traced.trace), + ) + } else { + (PartialIrDumps::default(), IrDumpArtifacts::default()) + }; + let mut report = + CompilationReport::failed(backend_diagnostics(failure.errors), dumps); + report.backend_trace = Some(traced.trace); + report.frontend_trace = frontend_trace; + report.dump_artifacts = dump_artifacts; + report + } + }; + } + + if capture_dumps { + let linked_core = core.clone(); + let stages = match psrs_backend::compile_with_context_capturing( + core, + psrs_backend::TargetCapabilities::default(), + ) { + Ok(stages) => stages, + Err(failure) => { + let (dumps, _) = partial_dumps(*failure.partial, linked_core, None); + return CompilationReport::failed(backend_diagnostics(failure.errors), dumps); + } + }; + return success_report(stages, source_warnings, trusted_prefix, true, None, None); + } + + match psrs_backend::compile_with_context(core, psrs_backend::TargetCapabilities::default()) { + Ok(stages) => success_report(stages, source_warnings, trusted_prefix, false, None, None), + Err(errors) => { + CompilationReport::failed(backend_diagnostics(errors), PartialIrDumps::default()) + } + } +} + +fn success_report( + stages: psrs_backend::Stages, + source_warnings: Vec, + trusted_prefix: usize, + capture: bool, + backend_trace: Option, + frontend_trace: Option, +) -> CompilationReport { + let mut warnings = typecheck_warnings(source_warnings, trusted_prefix); + warnings.extend(backend_warnings(stages.artifact.warnings, trusted_prefix)); + let dumps = if capture { + PartialIrDumps { + core_stage: Some("P7 Core optimization"), + core: Some(format!("{:#?}", stages.core)), + cc_stage: Some("P8 closure conversion (verified)"), + cc: Some(format!("{:#?}", stages.cc)), + mir_stage: Some("P10 MIR optimization"), + mir: Some(format!("{:#?}", stages.mir)), + } + } else { + PartialIrDumps::default() + }; + let dump_artifacts = backend_trace + .as_ref() + .filter(|_| capture) + .map(|trace| IrDumpArtifacts { + core: artifact_from_pass(trace, "backend.core.optimize", 0), + cc: artifact_from_pass(trace, "backend.cc.lower", 0), + mir: artifact_from_pass(trace, "backend.mir.optimize", 0), + }) + .unwrap_or_default(); + CompilationReport { + artifact: Some(Artifact { + wasm: stages.artifact.wasm, + wat: stages.artifact.wat, + warnings, + }), + diagnostics: Vec::new(), + dumps, + backend_trace, + frontend_trace, + dump_artifacts, + } +} + +fn partial_dumps( + partial: psrs_backend::PartialStages, + linked_core: psrs_core::Module, + trace: Option<&CompileTrace>, +) -> (PartialIrDumps, IrDumpArtifacts) { + let mut dumps = PartialIrDumps::default(); + let mut artifacts = IrDumpArtifacts::default(); + let (core, stage, artifact) = match partial.core { + Some(core) => ( + core, + "P7 Core optimization", + trace.and_then(|trace| artifact_from_pass(trace, "backend.core.optimize", 0)), + ), + None => ( + linked_core, + "linked Core input to P7", + trace.map(|trace| trace.initial_core), + ), + }; + dumps.core_stage = Some(stage); + dumps.core = Some(format!("{core:#?}")); + artifacts.core = artifact; + if let Some(cc) = partial.cc { + dumps.cc_stage = Some("P8 closure conversion (verified)"); + dumps.cc = Some(format!("{cc:#?}")); + artifacts.cc = trace.and_then(|trace| artifact_from_pass(trace, "backend.cc.lower", 0)); + } + if let Some(mir) = partial.mir { + dumps.mir_stage = partial.mir_stage; + dumps.mir = Some(format!("{mir:#?}")); + artifacts.mir = trace.and_then(|trace| match partial.mir_stage { + Some("P9 MIR lowering") => artifact_from_pass(trace, "backend.mir.lower", 0), + _ => artifact_from_pass(trace, "backend.mir.optimize", 0), + }); + } + (dumps, artifacts) +} + +fn artifact_from_pass( + trace: &CompileTrace, + pass_key: &str, + output_index: usize, +) -> Option { + trace + .executions + .iter() + .find(|execution| execution.pass_key == pass_key) + .and_then(|execution| execution.outputs.get(output_index).copied()) +} + +fn backend_diagnostics(errors: Vec) -> Vec { + errors + .into_iter() + .map(|error| ProgramDiagnostic { + source: error.module.map_or(DiagnosticOrigin::Program, |module| { + DiagnosticOrigin::Source(module.0 as usize) + }), + diagnostic: Diagnostic { + stage: error.pass, + span: error.span, + message: error.message, + code: None, + kind: Some(error.kind), + }, + }) + .collect() +} diff --git a/crates/psrs-driver/src/program/effects.rs b/crates/psrs-driver/src/program/effects.rs index 5bc52a47..203791f2 100644 --- a/crates/psrs-driver/src/program/effects.rs +++ b/crates/psrs-driver/src/program/effects.rs @@ -1,6 +1,5 @@ use super::{DiagnosticOrigin, ProgramDiagnostic, diagnostic}; -use psrs_core::effect::EffectOperation; -use psrs_hir::{Expr, ExprKind, ExternalKind, Module, SymbolId, TypeDeclarationKind}; +use psrs_hir::{Module, SymbolId}; use psrs_span::TextRange; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -81,269 +80,3 @@ pub(super) fn require_unambiguous_entry( } } } - -pub(super) fn trusted_effect( - modules: &[Module], - trusted_prefix: usize, -) -> Result, Vec> { - let preludes = modules - .iter() - .take(trusted_prefix) - .enumerate() - .filter(|(_, module)| module.name == "Prelude") - .collect::>(); - if preludes.is_empty() { - return Ok(None); - } - if preludes.len() != 1 { - return Err(vec![ProgramDiagnostic { - source: DiagnosticOrigin::Program, - diagnostic: diagnostic( - "P7 Effect contract", - preludes[1].1.span, - "trusted program contains multiple Prelude modules", - ), - }]); - } - let (source, prelude) = preludes[0]; - let Some(effect) = prelude.types.iter().find(|declaration| { - declaration.name == "Effect" && declaration.kind == TypeDeclarationKind::Foreign - }) else { - return Ok(None); - }; - let mut operations = Vec::new(); - for (name, operation, wit_name) in [ - ("pure", EffectOperation::Pure, "pure"), - ("bind", EffectOperation::Bind, "bind"), - ("runEffect", EffectOperation::Run, "run"), - ("trap", EffectOperation::Trap, "trap"), - ] { - let Some(external) = prelude - .externals - .iter() - .find(|external| external.name == name) - else { - return Err(vec![ProgramDiagnostic { - source: DiagnosticOrigin::Source(source), - diagnostic: diagnostic( - "P7 Effect contract", - prelude.span, - "trusted Prelude is missing a required Effect operation binding", - ), - }]); - }; - if !matches!( - &external.kind, - ExternalKind::Wit { interface, function } - if interface == "psrs:effect" && function == wit_name - ) { - return Err(vec![ProgramDiagnostic { - source: DiagnosticOrigin::Source(source), - diagnostic: diagnostic( - "P7 Effect contract", - external - .signature - .as_ref() - .map_or(prelude.span, |ty| ty.span), - "trusted Prelude Effect operation has the wrong WIT identity", - ), - }]); - } - operations.push(psrs_core::effect::EffectOperationBinding { - operation, - symbol: external.symbol, - }); - } - Ok(Some(psrs_core::effect::TrustedEffect { - effect_type: effect.id, - operations, - })) -} - -/// Enforces the source compatibility rule: `runEffect` may be referenced only -/// inside the selected declaration. Passing it from that declaration to a -/// helper is allowed; this is a lexical rule, not capability confinement. -pub(super) fn check_run_effect_scope( - modules: &[Module], - entry: Option, - trusted: Option<&psrs_core::effect::TrustedEffect>, -) -> Result<(), Vec> { - let Some(runner) = trusted - .and_then(|trusted| { - trusted - .operations - .iter() - .find(|operation| operation.operation == EffectOperation::Run) - }) - .map(|operation| operation.symbol) - else { - return Ok(()); - }; - let mut errors = Vec::new(); - for (source, module) in modules.iter().enumerate() { - for declaration in &module.declarations { - let mut references = Vec::new(); - collect_runner_references(&declaration.value, runner, &mut references); - if entry.is_some_and(|entry| declaration.symbol == entry.symbol) { - continue; - } - for span in references { - errors.push(ProgramDiagnostic { - source: DiagnosticOrigin::Source(source), - diagnostic: diagnostic( - "P7 entry selection", - span, - "the trusted `runEffect` binding may only be referenced from the selected command entry `main`", - ), - }); - } - } - for instance in &module.instances { - for member in &instance.members { - let mut references = Vec::new(); - collect_runner_references(&member.value, runner, &mut references); - for span in references { - errors.push(ProgramDiagnostic { - source: DiagnosticOrigin::Source(source), - diagnostic: diagnostic( - "P7 entry selection", - span, - "the trusted `runEffect` binding may only be referenced from the selected command entry `main`", - ), - }); - } - } - } - } - if errors.is_empty() { - Ok(()) - } else { - Err(errors) - } -} - -fn collect_runner_references(expression: &Expr, runner: SymbolId, spans: &mut Vec) { - match &expression.kind { - ExprKind::Global(symbol) if *symbol == runner => spans.push(expression.span), - ExprKind::Array(elements) => { - for element in elements { - collect_runner_references(element, runner, spans); - } - } - ExprKind::Record(fields) => { - for (_, value) in fields { - collect_runner_references(value, runner, spans); - } - } - ExprKind::RecordUpdate { expression, fields } => { - collect_runner_references(expression, runner, spans); - for (_, value) in fields { - collect_runner_references(value, runner, spans); - } - } - ExprKind::FieldAccess { expression, .. } - | ExprKind::Typed { expression, .. } - | ExprKind::TypeApplication { expression, .. } => { - collect_runner_references(expression, runner, spans); - } - ExprKind::Application(function, argument) => { - collect_runner_references(function, runner, spans); - collect_runner_references(argument, runner, spans); - } - ExprKind::Operator { - operator, - operator_span, - left, - right, - } => { - if *operator == runner { - spans.push(*operator_span); - } - collect_runner_references(left, runner, spans); - collect_runner_references(right, runner, spans); - } - ExprKind::Negate { - function, - expression, - .. - } => { - collect_runner_references(function, runner, spans); - collect_runner_references(expression, runner, spans); - } - ExprKind::OperatorChain { - operands, - operators, - } => { - for operator in operators { - if operator.symbol == runner { - spans.push(operator.operator_span); - } - } - for operand in operands { - collect_runner_references(operand, runner, spans); - } - } - ExprKind::OperatorSection { - operator, operand, .. - } => { - if operator.symbol == runner { - spans.push(operator.operator_span); - } - collect_runner_references(operand, runner, spans); - } - ExprKind::Lambda { body, .. } => collect_runner_references(body, runner, spans), - ExprKind::Let { bindings, body } => { - for binding in bindings { - collect_runner_references(&binding.value, runner, spans); - } - collect_runner_references(body, runner, spans); - } - ExprKind::If { - condition, - then_branch, - else_branch, - } => { - collect_runner_references(condition, runner, spans); - collect_runner_references(then_branch, runner, spans); - collect_runner_references(else_branch, runner, spans); - } - ExprKind::Case { - scrutinee, - branches, - } => { - collect_runner_references(scrutinee, runner, spans); - for branch in branches { - collect_runner_references(&branch.value, runner, spans); - } - } - ExprKind::Guarded(clauses) => { - for clause in clauses { - for binding in &clause.where_bindings { - collect_runner_references(&binding.value, runner, spans); - } - for guard in &clause.guards { - match guard { - psrs_hir::Guard::Boolean(value) => { - collect_runner_references(value, runner, spans) - } - psrs_hir::Guard::Pattern { value, .. } => { - collect_runner_references(value, runner, spans) - } - psrs_hir::Guard::Let { bindings, .. } => { - for binding in bindings { - collect_runner_references(&binding.value, runner, spans); - } - } - } - } - collect_runner_references(&clause.value, runner, spans); - } - } - ExprKind::Local(_) - | ExprKind::Global(_) - | ExprKind::Integer(_) - | ExprKind::Number(_) - | ExprKind::String(_) - | ExprKind::Char(_) => {} - } -} diff --git a/crates/psrs-driver/src/program/lenient.rs b/crates/psrs-driver/src/program/lenient.rs index 9b7b4c9e..5015a6f0 100644 --- a/crates/psrs-driver/src/program/lenient.rs +++ b/crates/psrs-driver/src/program/lenient.rs @@ -73,6 +73,10 @@ pub fn check_program_types_lenient(sources: &[(&str, &str)]) -> Result<(), Vec

>(); // Every per-module table below is indexed by the module's own `ModuleId`, // which is its position in `sources`. `resolved` holds only the sources that // reached resolution, so enumerating it would both attribute a diagnostic to @@ -113,6 +117,7 @@ pub fn check_program_types_lenient(sources: &[(&str, &str)]) -> Result<(), Vec

Result> { - let (all_sources, trusted_prefix) = with_prelude(sources)?; - compile_program_sources_with_trusted_prefix(&all_sources, trusted_prefix) - .map_err(|errors| shift(errors, trusted_prefix)) + let report = compile_library(sources, false, false); + match report.artifact { + Some(artifact) => Ok(artifact), + None => Err(report.diagnostics), + } +} + +/// Compiles with the package and retains the last successful IR stages. +pub fn compile_program_sources_with_prelude_report(sources: &[(&str, &str)]) -> CompilationReport { + compile_library(sources, true, false) +} + +/// Compiles with the package and records actual frontend/backend pass events. +pub fn compile_program_sources_with_prelude_diagnosis( + sources: &[(&str, &str)], + capture_dumps: bool, +) -> CompilationReport { + compile_library(sources, capture_dumps, true) +} + +fn compile_library( + sources: &[(&str, &str)], + capture_dumps: bool, + trace: bool, +) -> CompilationReport { + let (all_sources, trusted_prefix) = match prelude::prepend_for_command(sources) { + Ok(sources) => sources, + Err(message) => return package_failure(message), + }; + let info = match prelude::standard_library_info() { + Ok(info) => info, + Err(message) => return package_failure(message), + }; + let mut report = super::compilation::compile_attempt( + &all_sources, + trusted_prefix, + capture_dumps, + trace, + info.command_runner.as_ref(), + ); + report.diagnostics = shift(report.diagnostics, trusted_prefix); + report +} + +fn package_failure(message: String) -> CompilationReport { + CompilationReport::failed( + vec![ProgramDiagnostic { + source: DiagnosticOrigin::Library, + diagnostic: diagnostic("stdlib", psrs_span::TextRange::default(), message), + }], + Default::default(), + ) } /// Resolves user sources leniently together with the on-disk standard library. diff --git a/crates/psrs-driver/src/program/lowering.rs b/crates/psrs-driver/src/program/lowering.rs new file mode 100644 index 00000000..9a5fc0bb --- /dev/null +++ b/crates/psrs-driver/src/program/lowering.rs @@ -0,0 +1,88 @@ +use super::*; + +pub(super) fn lower_program_inner( + sources: &[(&str, &str)], + trusted_prefix: usize, + runner: Option<(&str, &str)>, +) -> Result<(psrs_core::Module, Vec), Vec> { + let resolved = resolve_program_sources(sources)?; + let selected_runner = runner + .map(|(owner, name)| { + let candidates = resolved + .iter() + .take(if trusted_prefix == 0 { + resolved.len() + } else { + trusted_prefix + }) + .filter(|module| module.name == owner) + .flat_map(|module| module.declarations.iter().filter(|decl| decl.name == name)) + .collect::>(); + match candidates.as_slice() { + [runner] => Ok(runner.symbol), + _ => Err(vec![ProgramDiagnostic { + source: DiagnosticOrigin::Program, + diagnostic: diagnostic( + "P7 command runner", + psrs_span::TextRange::new(0, 0), + "configured command runner must resolve to exactly one source declaration", + ), + }]), + } + }) + .transpose()?; + let entry = effects::require_unambiguous_entry(effects::select_entry(&resolved))?; + let (typed, warnings) = typecheck_resolved_program_with_warnings(resolved)?; + let mut modules = Vec::with_capacity(typed.len()); + for (index, module) in typed.into_iter().enumerate() { + match psrs_core::lower_module_unverified(module) { + Ok(core) => modules.push(core), + Err(errors) => { + return Err(errors + .into_iter() + .map(|error| ProgramDiagnostic { + source: DiagnosticOrigin::Source(index), + diagnostic: diagnostic("P6 Core lowering", error.span, error.message), + }) + .collect()); + } + } + } + let mut linked = psrs_core::link(modules); + if let Some(entry) = entry { + let symbol = if let Some(runner) = selected_runner { + psrs_core::command::normalize_entry(&mut linked, entry.symbol, runner).map_err( + |errors| { + errors + .into_iter() + .map(|error| ProgramDiagnostic { + source: DiagnosticOrigin::Source(error.module.0 as usize), + diagnostic: diagnostic("P7 command runner", error.span, error.message), + }) + .collect::>() + }, + )? + } else { + entry.symbol + }; + linked.entry = Some(symbol); + psrs_core::prune_unreachable(&mut linked, symbol); + } + if let Err(errors) = linked.verify() { + return Err(errors + .into_iter() + .map(|error| ProgramDiagnostic { + source: DiagnosticOrigin::Source(error.module.0 as usize), + diagnostic: diagnostic("P7 Core verification", error.span, error.message), + }) + .collect()); + } + Ok((linked, warnings)) +} + +pub(crate) fn lower_program_to_core_and_warnings( + sources: &[(&str, &str)], + trusted_prefix: usize, +) -> Result<(psrs_core::Module, Vec), Vec> { + lower_program_inner(sources, trusted_prefix, None) +} diff --git a/crates/psrs-driver/src/program/mod.rs b/crates/psrs-driver/src/program/mod.rs index e5dcb152..255d0eed 100644 --- a/crates/psrs-driver/src/program/mod.rs +++ b/crates/psrs-driver/src/program/mod.rs @@ -2,23 +2,27 @@ //! checking against imported signatures, Core lowering, and linking. use super::{ - Artifact, DiagnosticOrigin, ProgramDiagnostic, ProgramWarning, Warning, backend_warnings, - coded_diagnostic, diagnostic, lower_source_to_ast, + DiagnosticOrigin, ProgramDiagnostic, ProgramWarning, Warning, coded_diagnostic, diagnostic, + lower_source_to_ast, }; +pub use compilation::compile_program_sources; pub use lenient::{ check_program_kinds_lenient, check_program_lenient, check_program_types_lenient, }; pub use library::{ check_program_kinds_lenient_with_prelude, check_program_lenient_with_prelude, check_program_types_lenient_with_prelude, compile_program_sources_with_prelude, + compile_program_sources_with_prelude_diagnosis, compile_program_sources_with_prelude_report, }; use std::collections::{HashMap, HashSet}; +mod compilation; mod effects; mod graph; mod lenient; mod library; +mod lowering; mod reports; mod signatures; @@ -42,45 +46,6 @@ fn desugar_diagnostic(error: psrs_desugar::DesugarError) -> super::Diagnostic { use graph::{imported_instance_declarations, module_dependencies, module_table, typecheck_order}; -/// Compiles a whole program to a single Wasm component. Every module is type -/// checked in dependency order and lowered to Core; the modules are then linked -/// into one before the backend runs. -pub fn compile_program_sources( - sources: &[(&str, &str)], -) -> Result> { - compile_program_sources_with_trusted_prefix(sources, 0) -} - -fn compile_program_sources_with_trusted_prefix( - sources: &[(&str, &str)], - trusted_prefix: usize, -) -> Result> { - let (core, source_warnings, effect_context) = - lower_program_to_core_and_effect_context(sources, trusted_prefix)?; - let output = match effect_context { - Some(context) => psrs_backend::compile_with_effect_context(core, context), - None => psrs_backend::compile(core), - } - .map_err(|errors| { - errors - .into_iter() - .map(|error| ProgramDiagnostic { - source: error.module.map_or(DiagnosticOrigin::Program, |module| { - DiagnosticOrigin::Source(module.0 as usize) - }), - diagnostic: diagnostic(error.pass, error.span, error.message), - }) - .collect::>() - })?; - let mut warnings = typecheck_warnings(source_warnings, trusted_prefix); - warnings.extend(backend_warnings(output.warnings, trusted_prefix)); - Ok(Artifact { - wasm: output.wasm, - wat: output.wat, - warnings, - }) -} - #[cfg(test)] pub(crate) fn lower_program_to_core( sources: &[(&str, &str)], @@ -102,135 +67,10 @@ pub(crate) fn lower_program_to_core_with_trusted_prefix_and_warnings( sources: &[(&str, &str)], trusted_prefix: usize, ) -> Result<(psrs_core::Module, Vec), Vec> { - lower_program_to_core_and_effect_context(sources, trusted_prefix) - .map(|(core, warnings, _context)| (core, warnings)) -} - -pub(super) fn lower_program_to_core_and_effect_context( - sources: &[(&str, &str)], - trusted_prefix: usize, -) -> Result< - ( - psrs_core::Module, - Vec, - Option, - ), - Vec, -> { - let resolved = resolve_program_sources(sources)?; - let entry = effects::require_unambiguous_entry(effects::select_entry(&resolved))?; - let trusted = effects::trusted_effect(&resolved, trusted_prefix)?; - effects::check_run_effect_scope(&resolved, entry, trusted.as_ref())?; - let (typed, warnings) = typecheck_resolved_program_with_warnings(resolved)?; - let command_entry = classify_command_entry(&typed, entry, trusted.as_ref())?; - let mut modules = Vec::with_capacity(typed.len()); - for (index, module) in typed.into_iter().enumerate() { - match psrs_core::lower_module_unverified(module) { - Ok(core) => modules.push(core), - Err(errors) => { - return Err(errors - .into_iter() - .map(|error| ProgramDiagnostic { - source: DiagnosticOrigin::Source(index), - diagnostic: diagnostic("P6 Core lowering", error.span, error.message), - }) - .collect()); - } - } - } - let mut linked = psrs_core::link(modules); - if let Some(entry) = entry { - linked.entry = Some(entry.symbol); - psrs_core::prune_unreachable(&mut linked, entry.symbol); - } - if let Err(errors) = linked.verify() { - return Err(errors - .into_iter() - .map(|error| ProgramDiagnostic { - source: DiagnosticOrigin::Source(error.module.0 as usize), - diagnostic: diagnostic("P7 Core verification", error.span, error.message), - }) - .collect()); - } - let context = trusted.map(|trusted| psrs_core::effect::EffectCompilation { - trusted, - command_entry, - }); - Ok((linked, warnings, context)) + lower_program_to_core_and_warnings(sources, trusted_prefix) } -fn classify_command_entry( - typed: &[psrs_thir::Module], - selected: Option, - trusted: Option<&psrs_core::effect::TrustedEffect>, -) -> Result, Vec> { - let Some(selected) = selected else { - return Ok(None); - }; - let Some(module) = typed.get(selected.source) else { - return Err(vec![ProgramDiagnostic { - source: DiagnosticOrigin::Source(selected.source), - diagnostic: diagnostic( - "P7 entry selection", - selected.span, - "selected command entry was not type checked", - ), - }]); - }; - let Some(main) = module - .declarations - .iter() - .find(|declaration| declaration.symbol == selected.symbol) - else { - return Err(vec![ProgramDiagnostic { - source: DiagnosticOrigin::Source(selected.source), - diagnostic: diagnostic( - "P7 entry selection", - selected.span, - "selected command entry has no typed declaration", - ), - }]); - }; - if matches!( - module.types.get(main.ty.0 as usize), - Some(psrs_thir::Type::Constructor( - psrs_thir::TypeConstructor::Int - )) - ) { - return Ok(None); - } - let is_effect_unit = trusted.is_some_and(|trusted| { - let Some(psrs_thir::Type::Application(function, payload)) = - module.types.get(main.ty.0 as usize) - else { - return false; - }; - matches!( - module.types.get(function.0 as usize), - Some(psrs_thir::Type::Constructor(psrs_thir::TypeConstructor::User(id))) - if *id == trusted.effect_type - ) && matches!( - module.types.get(payload.0 as usize), - Some(psrs_thir::Type::Constructor( - psrs_thir::TypeConstructor::Unit - )) - ) && main.quantified.is_empty() - }); - if is_effect_unit { - Ok(Some(psrs_core::effect::EffectCommandEntry { - symbol: selected.symbol, - })) - } else { - Err(vec![ProgramDiagnostic { - source: DiagnosticOrigin::Source(selected.source), - diagnostic: diagnostic( - "P7 entry selection", - main.name_span, - "command entry must have type `Int` or `Effect Unit`", - ), - }]) - } -} +pub(super) use lowering::lower_program_to_core_and_warnings; /// Resolves a program from a list of `(source_name, source_text)` pairs. Module /// IDs equal each source's index in the list. This is the module-graph entry @@ -281,15 +121,6 @@ pub(crate) fn check_program_with_trusted_prefix_and_warnings( .map(|(_modules, warnings)| warnings) } -#[cfg(test)] -pub(crate) fn typecheck_program_sources_with_trusted_prefix( - sources: &[(&str, &str)], - trusted_prefix: usize, -) -> Result, Vec> { - let modules = resolve_program_sources(sources)?; - typecheck_program_with_warnings(modules, trusted_prefix).map(|(modules, _warnings)| modules) -} - fn typecheck_program_sources_with_trusted_prefix_and_warnings( sources: &[(&str, &str)], trusted_prefix: usize, @@ -300,14 +131,8 @@ fn typecheck_program_sources_with_trusted_prefix_and_warnings( pub(super) fn typecheck_program_with_warnings( modules: Vec, - trusted_prefix: usize, + _trusted_prefix: usize, ) -> Result<(Vec, Vec), Vec> { - let entry = match effects::select_entry(&modules) { - effects::EntryResolution::Selected(entry) => Some(entry), - effects::EntryResolution::Missing | effects::EntryResolution::Ambiguous { .. } => None, - }; - let trusted = effects::trusted_effect(&modules, trusted_prefix)?; - effects::check_run_effect_scope(&modules, entry, trusted.as_ref())?; typecheck_resolved_program_with_warnings(modules) } @@ -368,6 +193,12 @@ fn typecheck_resolved_program_with_warnings( .into_iter() .map(|(_, declaration)| declaration), ); + // Value declarations are program-wide so a deriving rule can pin a core + // library value to its declaring identity. + let known_values = modules + .iter() + .flat_map(|module| module.declarations.iter().cloned()) + .collect::>(); // Instance declarations are threaded per module, like values: a module can // only select an instance declared in a module it imports, directly or // transitively. @@ -423,6 +254,7 @@ fn typecheck_resolved_program_with_warnings( false, psrs_typecheck::TypecheckContext { known_types: &known_types, + known_values: &known_values, imported_instances: &imported_instances, module_names: &module_names, checked_kinds: &checked_kinds, diff --git a/crates/psrs-driver/src/program/signatures.rs b/crates/psrs-driver/src/program/signatures.rs index d2ad0b72..860db646 100644 --- a/crates/psrs-driver/src/program/signatures.rs +++ b/crates/psrs-driver/src/program/signatures.rs @@ -17,11 +17,11 @@ pub(super) fn declared_signatures( .clone() .map(|signature| (declaration.symbol, signature)) }); - // Only a source `foreign import` (a WIT external) is declared in a + // Only a source `foreign import` is declared in a // module; a compiler intrinsic is not, even though its descriptor // gives it a signature. let externals = module.externals.iter().filter_map(|external| { - if matches!(external.kind, psrs_hir::ExternalKind::Wit { .. }) { + if external.kind.requires_checked_signature() { external .signature .clone() diff --git a/crates/psrs-driver/src/tests/assertions.rs b/crates/psrs-driver/src/tests/assertions.rs index 55500844..e4b3b2ce 100644 --- a/crates/psrs-driver/src/tests/assertions.rs +++ b/crates/psrs-driver/src/tests/assertions.rs @@ -25,10 +25,14 @@ fn assert_trapped_after_message(output: &std::process::Output, message: &str) { } /// The one `Unit` value, in both spellings the surface language has for it. +/// +/// The runtime case needs a runner, so it keeps the target `Effect` sequence; +/// the Core-shape case below is deliberately pure and never needs one. const UNIT_SOURCE: &str = r#" module Main where import Prelude +import Effect.Unsafe (unsafePerformEffect) import WASI.Console takesUnit :: Unit -> Int @@ -40,19 +44,33 @@ describe _ = "other" main = let - message = runEffect (log (describe (takesUnit unit))) - alsoUnit = runEffect (log (describe (takesUnit ()))) + message = unsafePerformEffect (log (describe (takesUnit unit))) + alsoUnit = unsafePerformEffect (log (describe (takesUnit ()))) in let ignored = message in let ignoredToo = alsoUnit in 0 "#; +/// A pure use of the one `Unit` value in both spellings. It exercises only +/// type checking and Core lowering, so it does not depend on a runner. +const UNIT_CORE_SOURCE: &str = r#" +module Main where + +import Prelude + +takesUnit :: Unit -> Int +takesUnit _ = 7 + +main :: Int +main = takesUnit unit + takesUnit () +"#; + #[test] fn the_unit_value_is_a_core_expression_rather_than_an_integer_literal() { // `Unit` has no payload and no constructor table here, so the value is a // compiler primitive. Core must name it as itself: lowering it to the // integer `0` would make a unit indistinguishable from an `Int` at every // later stage. - let core = lower_source_to_core("Main.purs", UNIT_SOURCE) + let core = lower_source_to_core("Main.purs", UNIT_CORE_SOURCE) .expect("typechecking and lowering `unit` into Core"); let dump = format!("{core:#?}"); assert!( @@ -76,7 +94,7 @@ fn the_unit_value_executes_when_wasmtime_is_available() { #[test] fn a_held_assertion_lets_the_program_finish_when_wasmtime_is_available() { - let source = "module Main where\nimport Prelude\nimport Test.Assert\nheld :: Effect Unit\nheld = do\n assert true\n assertTrue true\n assertFalse false\n assert' \"unused\" true\n pure unit\nmain = let ignored = runEffect held in 0\n"; + let source = "module Main where\nimport Prelude\nimport Effect (Effect)\nimport Effect.Unsafe (unsafePerformEffect)\nimport Test.Assert\nheld :: Effect Unit\nheld = do\n assert true\n assertTrue true\n assertFalse false\n assert' \"unused\" true\n pure unit\nmain = let ignored = unsafePerformEffect held in 0\n"; let Some(output) = run_with_wasmtime(source) else { eprintln!("skipping: wasmtime is not installed"); return; @@ -87,7 +105,7 @@ fn a_held_assertion_lets_the_program_finish_when_wasmtime_is_available() { #[test] fn a_failed_assertion_traps_with_its_message_when_wasmtime_is_available() { - let source = "module Main where\nimport Prelude\nimport Test.Assert\nmain = let ignored = runEffect (assert false) in 0\n"; + let source = "module Main where\nimport Prelude\nimport Effect.Unsafe (unsafePerformEffect)\nimport Test.Assert\nmain = let ignored = unsafePerformEffect (assert false) in 0\n"; let Some(output) = run_with_wasmtime(source) else { eprintln!("skipping: wasmtime is not installed"); return; @@ -100,7 +118,7 @@ fn a_failed_assertion_writes_the_message_before_it_traps() { // `assert'` carries the diagnostic, and the standard error write must // happen before the trap: the trap ends the guest, so a later write would // never reach the host. - let source = "module Main where\nimport Prelude\nimport Test.Assert\nmain = let ignored = runEffect (assert' \"Expected 42\" false) in 0\n"; + let source = "module Main where\nimport Prelude\nimport Effect.Unsafe (unsafePerformEffect)\nimport Test.Assert\nmain = let ignored = unsafePerformEffect (assert' \"Expected 42\" false) in 0\n"; let Some(output) = run_with_wasmtime(source) else { eprintln!("skipping: wasmtime is not installed"); return; @@ -112,7 +130,7 @@ fn a_failed_assertion_writes_the_message_before_it_traps() { fn a_statement_after_a_failed_assertion_never_runs() { // The escape is the end of the effect chain, not a value that evaluation // continues past, so the statement after the trap never gets to write. - let source = "module Main where\nimport Prelude\nimport Test.Assert\nimport WASI.Console\nfailed :: Effect Unit\nfailed = do\n assert false\n _ <- log \"unreachable\"\n pure unit\nmain = let ignored = runEffect failed in 0\n"; + let source = "module Main where\nimport Prelude\nimport Effect (Effect)\nimport Effect.Unsafe (unsafePerformEffect)\nimport Test.Assert\nimport WASI.Console\nfailed :: Effect Unit\nfailed = do\n assert false\n _ <- log \"unreachable\"\n pure unit\nmain = let ignored = unsafePerformEffect failed in 0\n"; let Some(output) = run_with_wasmtime(source) else { eprintln!("skipping: wasmtime is not installed"); return; @@ -123,17 +141,15 @@ fn a_statement_after_a_failed_assertion_never_runs() { #[test] fn assert_true_and_assert_false_report_the_value_that_did_not_hold() { - let source = "module Main where\nimport Prelude\nimport Test.Assert\nmain = let ignored = runEffect (assertTrue false) in 0\n"; + let source = "module Main where\nimport Prelude\nimport Effect.Unsafe (unsafePerformEffect)\nimport Test.Assert\nmain = let ignored = unsafePerformEffect (assertTrue false) in 0\n"; let Some(output) = run_with_wasmtime(source) else { eprintln!("skipping: wasmtime is not installed"); return; }; // The message names the value that did not hold, the way the official - // module's `assertTrue` does. - assert_trapped_after_message( - &output, - "Assertion failed: Expected: true\nActual: false\n", - ); + // module's `assertTrue` does: `assertEqual'` renders `Expected`/`Actual` + // without an extra prefix. + assert_trapped_after_message(&output, "Expected: true\nActual: false\n"); } /// The renderings `Data.Show` produces, so this fails if `logShow` grows a @@ -148,7 +164,9 @@ fn log_show_writes_the_library_rendering() { module Main where import Prelude +import Effect (Effect) import Effect.Console (logShow) +import Effect.Unsafe (unsafePerformEffect) checks :: Effect Unit checks = do @@ -160,7 +178,7 @@ checks = do logShow [1, 2, 3] pure unit -main = let ignored = runEffect checks in 0 +main = let ignored = unsafePerformEffect checks in 0 "#; let Some(output) = run_with_wasmtime(source) else { eprintln!("skipping: wasmtime is not installed"); diff --git a/crates/psrs-driver/src/tests/cc_ir_audit.rs b/crates/psrs-driver/src/tests/cc_ir_audit.rs index b9b416ee..55bec2c1 100644 --- a/crates/psrs-driver/src/tests/cc_ir_audit.rs +++ b/crates/psrs-driver/src/tests/cc_ir_audit.rs @@ -142,7 +142,7 @@ fn cc_declarations_are_well_formed_and_capture_once() { #[test] fn effects_observe_callee_and_argument_order() { let name = "effects_observe_callee_and_argument_order"; - let source = "module Main where\nimport Prelude\nimport WASI.Console\nfirst :: Unit -> Unit -> Int\nfirst a b = 0\nmain = first (runEffect (log \"a\")) (runEffect (log \"b\"))\n"; + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\nimport Prelude\nimport WASI.Console\nfirst :: Unit -> Unit -> Int\nfirst a b = 0\nmain = first (unsafePerformEffect (log \"a\")) (unsafePerformEffect (log \"b\"))\n"; let artifact = compile_source("Main.purs", source).expect("ordered effects compile"); if let Some(output) = execute_wasm(name, &artifact.wasm).expect("executing ordered effects") { assert_eq!(output.status.code(), Some(0)); @@ -281,54 +281,47 @@ fn a_polymorphic_imported_instance_method_recovers_through_an_adapter() { }), "main must not cast an erased method field straight to a concrete closure" ); - let conversion = main + // A generic method is recovered as its own CC operation. CC exposes a + // dictionary's field conversions as explicit assignments, so the adapter + // that lifts the erased template calling convention is a top-level + // conversion rather than a field nested in a product-map plan. + let (adapted, function, signature) = main .assignments .iter() - .find_map(|assignment| match &assignment.kind { - AssignmentKind::AggregateConvert { conversion, .. } => { - let ValueConversion::ProductMap { fields, .. } = &conversion.plan else { - return None; - }; - let ValueConversion::FunctionAdapter { source, .. } = fields[0] else { - return None; - }; - let ValueShape::Reference(psrs_backend::cc::Reference { - heap: RefShape::Closure(id), + .find_map(|assignment| { + let AssignmentKind::AggregateConvert { + destination, + conversion, + .. + } = &assignment.kind + else { + return None; + }; + let ValueConversion::FunctionAdapter { + function, source, .. + } = &conversion.plan + else { + return None; + }; + let ValueShape::Reference(psrs_backend::cc::Reference { + heap: RefShape::Closure(signature), + .. + }) = source + else { + return None; + }; + matches!( + cc.representations.signatures[signature.0 as usize] + .parameters + .first(), + Some(ValueShape::Reference(psrs_backend::cc::Reference { + heap: RefShape::Erased, .. - }) = source - else { - return None; - }; - matches!( - cc.representations.signatures[id.0 as usize] - .parameters - .first(), - Some(ValueShape::Reference(psrs_backend::cc::Reference { - heap: RefShape::Erased, - .. - })) - ) - .then_some(conversion) - } - _ => None, + })) + ) + .then_some((*destination, *function, *signature)) }) - .expect("dictionary instantiation uses the recursive conversion plan"); - let psrs_backend::cc::ValueConversion::ProductMap { fields, .. } = &conversion.plan else { - panic!("dictionary conversion must reconstruct the product"); - }; - let psrs_backend::cc::ValueConversion::FunctionAdapter { - function, source, .. - } = fields[0] - else { - panic!("the method field must adapt its call signature"); - }; - let ValueShape::Reference(psrs_backend::cc::Reference { - heap: RefShape::Closure(signature), - .. - }) = source - else { - panic!("a generic method retains its template closure signature"); - }; + .expect("the dictionary method recovers through an adapter over its erased template"); assert!( matches!( cc.representations.signatures[signature.0 as usize].parameters[0], @@ -337,9 +330,15 @@ fn a_polymorphic_imported_instance_method_recovers_through_an_adapter() { .. }) ), - "template signature: {:?}; fields: {:?}", - cc.representations.signatures[signature.0 as usize], - fields + "template signature: {:?}", + cc.representations.signatures[signature.0 as usize] + ); + assert!( + main.assignments.iter().any(|assignment| matches!( + &assignment.kind, + AssignmentKind::ProductNew { arguments, .. } if arguments.contains(&adapted) + )), + "the adapted method field is rebuilt into a dictionary product" ); let factory = cc .functions diff --git a/crates/psrs-driver/src/tests/closure_protocol.rs b/crates/psrs-driver/src/tests/closure_protocol.rs new file mode 100644 index 00000000..6be34304 --- /dev/null +++ b/crates/psrs-driver/src/tests/closure_protocol.rs @@ -0,0 +1,161 @@ +//! Value-sensitive evidence for abstract constructor transport. +//! +//! The Reader program does not use Effect. It checks that a dictionary method +//! and its caller adapt a stored function protocol instead of casting one +//! closure signature onto another. + +use super::*; + +#[test] +fn reader_dictionary_returns_the_concrete_result() { + let source = r#" +module Main where + +class Chain f where + chain :: forall a b. f a -> (a -> f b) -> f b + +instance chainReader :: Chain ((->) Int) where + chain m k x = k (m x) x + +repeatAction :: forall f. Chain f => f Int -> f Int +repeatAction action = chain action (\_ -> action) + +action :: Int -> Int +action x = x + +main :: Int +main = repeatAction action 42 +"#; + let Some(output) = run_with_wasmtime(source) else { + eprintln!("skipping: wasmtime is not installed"); + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert_eq!(output.stdout, b"", "{output:?}"); +} + +/// A fixed source payload (`f Unit`) still crosses the generic method's +/// definition ABI. The method returns an `f Unit` whose stored calling +/// convention is its own, so running it executes the action twice without a +/// cast from one closure signature onto another. +#[test] +fn fixed_unit_payload_through_a_bind_constraint_repeats_the_action() { + let source = r#" +module Main where + +import Prelude +import Effect (Effect) +import Effect.Console (log) + +again :: forall f. Bind f => f Unit -> f Unit +again action = bind action (\_ -> action) + +main :: Effect Unit +main = again (log "again") +"#; + let Some(output) = super::run_program_with_prelude_wasmtime(&[("Main.purs", source)]) else { + eprintln!("skipping: wasmtime is not installed"); + return; + }; + assert_eq!(output.status.code(), Some(0), "{output:?}"); + assert_eq!(output.stdout, b"again\nagain\n", "{output:?}"); +} + +/// The abstract callable boundary recovers the producer's stored protocol and +/// then adapts it, rather than casting the erased closure directly onto the +/// consumer's concrete signature. The generated adapter returns the concrete +/// payload and its factory captures the producer protocol closure. +#[test] +fn abstract_callable_transport_emits_a_checked_adapter() { + let source = r#" +module Main where + +class Chain f where + chain :: forall a b. f a -> (a -> f b) -> f b + +instance chainReader :: Chain ((->) Int) where + chain m k x = k (m x) x + +repeatAction :: forall f. Chain f => f Int -> f Int +repeatAction action = chain action (\_ -> action) + +action :: Int -> Int +action x = x + +main :: Int +main = repeatAction action 42 +"#; + let prepared = crate::prepare_main(source).expect("source should lower to Core"); + let cc = psrs_backend::cc::lower_module(prepared.core.clone()) + .expect("the Reader dictionary should lower to CC") + .cc; + let has_adapter = cc + .functions + .iter() + .flat_map(|function| &function.assignments) + .any(|assignment| { + matches!( + &assignment.kind, + psrs_backend::cc::AssignmentKind::AggregateConvert { conversion, .. } + if plan_has_adapter(&conversion.plan) + ) + }); + assert!( + has_adapter, + "the abstract callable boundary must emit a checked adapter" + ); + assert!( + cc.functions + .iter() + .any(|function| function.name.starts_with("protocol_adapter_factory_")), + "the adapter factory must exist and capture the producer protocol closure" + ); + assert!( + cc.functions + .iter() + .filter(|function| function.name.starts_with("protocol_adapter_")) + .any(|function| function.result_type == psrs_backend::cc::ValueShape::Integer), + "the generated adapter must return the concrete payload, not the erased result" + ); +} + +fn plan_has_adapter(plan: &psrs_backend::cc::ValueConversion) -> bool { + use psrs_backend::cc::ValueConversion; + match plan { + ValueConversion::FunctionAdapter { .. } => true, + ValueConversion::Sequence(steps) => steps.iter().any(plan_has_adapter), + ValueConversion::ArrayMap { element, .. } => plan_has_adapter(element), + ValueConversion::ProductMap { fields, .. } => fields.iter().any(plan_has_adapter), + _ => false, + } +} + +#[test] +fn erased_function_slots_preserve_partial_application_and_captures() { + let source = r#" +module Main where + +data Box a = Box a + +store :: forall a. a -> Box a +store value = Box value + +load :: forall a. Box a -> a +load (Box value) = value + +add :: Int -> Int -> Int +add x y = intAdd x y + +capture :: Int -> Int -> Int -> Int +capture offset x y = intAdd offset (intAdd x y) + +main :: Int +main = if intEq (load (store add) 40 2) 42 + then load (store (capture 7)) 20 15 + else 0 +"#; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} diff --git a/crates/psrs-driver/src/tests/coercion.rs b/crates/psrs-driver/src/tests/coercion.rs index ac6041f0..a7a4ec11 100644 --- a/crates/psrs-driver/src/tests/coercion.rs +++ b/crates/psrs-driver/src/tests/coercion.rs @@ -35,6 +35,27 @@ fn expands_type_synonyms_before_coercible_role_matching() { .unwrap_or_else(|errors| panic!("synonym coercion should type check: {errors:?}")); } +#[test] +fn ordinary_coercible_does_not_lift_through_an_unknown_type_constructor() { + // A newtype is coercible to its field, and that conversion lifts through a + // known representational constructor. It does not lift through a quantified + // `f`: the parameter's role is not known. Newtype deriving must not ask + // this rule to prove `Coercible (f (NonEmptyArray a)) (f (Array a))`. + let source = r#"module Main where + +import Safe.Coerce (coerce) + +newtype NonEmptyArray a = NonEmptyArray (Array a) + +bad :: forall f a. f (NonEmptyArray a) -> f (Array a) +bad = coerce + +main :: Int +main = 0 +"#; + rejects(&[("Main.purs", source)], "NoInstanceFound"); +} + #[test] fn a_nominal_data_role_blocks_newtype_coercion_of_its_parameter() { let source = "module Main where\n\ @@ -259,6 +280,36 @@ fn compiler_coercion_intrinsic_is_only_in_scope_through_safe_coerce() { rejects(&[("Main.purs", source)], "UnknownName"); } +#[test] +fn compiler_unsafe_coercion_intrinsic_is_only_in_scope_through_unsafe_coerce() { + let source = "module Main where\n\ + main :: Int\n\ + main = __psrs_unsafe_coerce 42\n"; + rejects(&[("Main.purs", source)], "UnknownName"); +} + +#[test] +fn unsafe_coerce_is_a_compiler_primitive_identity_cast() { + let source = r#" +module Main where + +import Unsafe.Coerce (unsafeCoerce) + +newtype Age = Age Int + +asInt :: Age -> Int +asInt = unsafeCoerce + +main :: Int +main = asInt (Age 42) +"#; + let Some(output) = super::run_with_wasmtime(source) else { + eprintln!("skipping: wasmtime is not installed"); + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + #[test] fn an_imported_newtype_requires_its_constructor_for_unwrapping() { let library = "module Lib (Age, age) where\n\ @@ -378,3 +429,21 @@ fn accepts_a_type_wildcard_in_an_instance_context() { panic!("a wildcard in a constraint should be accepted: {errors:?}") }); } + +#[test] +fn unsafe_coercion_keeps_a_polymorphic_input_boundary() { + let source = r#" +module Main where +import Unsafe.Coerce (unsafeCoerce) + +foreign import data Exists :: (Type -> Type) -> Type + +runExists :: forall f r. (forall a. f a -> r) -> Exists f -> r +runExists = unsafeCoerce + +main :: Int +main = 0 +"#; + crate::check_program(&[("Main.purs", source)]) + .unwrap_or_else(|errors| panic!("rank-N cast boundary should check: {errors:?}")); +} diff --git a/crates/psrs-driver/src/tests/control_boundary.rs b/crates/psrs-driver/src/tests/control_boundary.rs new file mode 100644 index 00000000..00ad5c31 --- /dev/null +++ b/crates/psrs-driver/src/tests/control_boundary.rs @@ -0,0 +1,392 @@ +//! Operand and ordered-effect obligations at the P8 construction boundary. +use psrs_backend::cc::{Assignment, AssignmentKind as A, ControlHelperRole as R, Function}; + +const SOURCE: &str = include_str!("../../tests/control_support/nominal_carriers.purs"); + +fn mutate(body: &mut [Assignment], edit: &mut impl FnMut(&mut A) -> bool) -> bool { + for assignment in body { + if edit(&mut assignment.kind) { + return true; + } + match &mut assignment.kind { + A::TagSwitch { + cases, + default_assignments, + .. + } => { + for case in cases { + if mutate(&mut case.assignments, edit) { + return true; + } + } + if mutate(default_assignments, edit) { + return true; + } + } + A::If { + then_assignments, + else_assignments, + .. + } => { + if mutate(then_assignments, edit) || mutate(else_assignments, edit) { + return true; + } + } + _ => {} + } + } + false +} + +fn rejected(role: R, edit: impl FnOnce(&mut Function)) { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let symbol = input + .cc + .representations + .control_helpers() + .iter() + .find(|h| h.role() == role) + .unwrap() + .function(); + let function = input + .cc + .functions + .iter_mut() + .find(|f| f.symbol == symbol) + .unwrap(); + edit(function); + let errors = + psrs_backend::cc::verify_module(&input.cc).expect_err("mutated machine must fail P8"); + assert!( + errors.iter().any(|error| error + .message + .contains("emitted machine operands or ordered operations")), + "{errors:?}" + ); +} + +#[test] +fn projected_clause_field_must_match_the_plan() { + rejected(R::Executor, |f| { + let callable: std::collections::HashSet<_> = f.values.iter().filter(|v| matches!(v.ty, psrs_backend::cc::ValueShape::Reference(r) if matches!(r.heap, psrs_backend::cc::RefShape::Closure(_)))).map(|v| v.id).collect(); + assert!(mutate(&mut f.assignments, &mut |a| { + if let A::VariantGet { + case: 1, + field, + destination, + .. + } = a + && *field == 0 + && callable.contains(destination) + { + // Scoped return and operation clauses have the same shape. + *field = 1; + return true; + } + false + })); + }); +} + +#[test] +fn replay_projection_index_must_match_the_plan() { + rejected(R::Executor, |f| { + assert!(mutate(&mut f.assignments, &mut |a| { + if let A::ProductGet { field, .. } = a { + *field = 1 - *field; + return true; + } + false + })); + }); +} + +#[test] +fn helper_arguments_must_preserve_source_identity() { + rejected(R::CloneSequence, |f| { + assert!(mutate(&mut f.assignments, &mut |a| { + if let A::DirectCall { arguments, .. } = a + && arguments.len() >= 2 + { + arguments.swap(0, 1); + return true; + } + false + })); + }); +} + +#[test] +fn dispatch_selector_must_preserve_source_identity() { + rejected(R::CloneSequence, |f| { + let other_queue = f.parameters[1]; + assert!(mutate(&mut f.assignments, &mut |a| { + if let A::VariantTag { value, .. } = a { + *value = other_queue; + return true; + } + false + })); + }); +} + +#[test] +fn clause_application_must_consume_its_planned_argument() { + rejected(R::Executor, |f| { + let state = f.parameters[0]; + assert!(mutate(&mut f.assignments, &mut |a| { + if let A::IndirectCall { arguments, .. } = a { + arguments[0] = state; + return true; + } + false + })); + }); +} + +#[test] +fn allocation_fields_must_preserve_source_identity() { + rejected(R::Executor, |f| { + assert!(mutate(&mut f.assignments, &mut |a| { + if let A::VariantNew { + case: 2, fields, .. + } = a + && fields.len() == 2 + { + fields.swap(0, 1); + return true; + } + false + })); + }); +} + +#[test] +fn callback_capture_index_must_match_the_plan() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let symbol = input + .cc + .representations + .control_resumption() + .unwrap() + .function(); + let f = input + .cc + .functions + .iter_mut() + .find(|f| f.symbol == symbol) + .unwrap(); + assert!(mutate(&mut f.assignments, &mut |a| { + if let A::ClosureGetCapture { index, .. } = a { + *index = 1 - *index; + return true; + } + false + })); + let errors = psrs_backend::cc::verify_module(&input.cc).unwrap_err(); + assert!( + errors.iter().any(|e| e.pass == "P8 CC verification"), + "{errors:?}" + ); +} + +#[test] +fn status_write_value_and_index_must_match_the_plan() { + for wrong_index in [false, true] { + rejected(R::Cancel, |f| { + assert!(mutate(&mut f.assignments, &mut |a| { + if let A::ArraySet { + index, new_value, .. + } = a + { + if wrong_index { + *index = *new_value; + } else { + *new_value = *index; + } + return true; + } + false + })); + }); + } +} + +#[test] +fn discarded_close_effect_is_required() { + rejected(R::Close, |f| { + let A::TagSwitch { cases, .. } = &mut f.assignments.last_mut().unwrap().kind else { + panic!("close must dispatch on the extent"); + }; + let mut removed = 0; + for case in cases { + case.assignments.retain(|a| { + if matches!(a.kind, A::ArraySet { .. }) { + removed += 1; + false + } else { + true + } + }); + } + assert!(removed > 0); + }); +} + +#[test] +fn extra_observable_operation_is_rejected() { + rejected(R::Start, |f| { + let allocation = f + .assignments + .iter() + .find(|a| matches!(a.kind, A::ArrayNew { .. })) + .unwrap() + .clone(); + // ArraySet destinations may reuse an existing cell; this adds a legal + // store using the fresh status cell and its already-bound zero value. + let A::ArrayNew { + representation, + elements, + .. + } = allocation.kind + else { + unreachable!() + }; + f.assignments.push(Assignment { + destination: allocation.destination, + kind: A::ArraySet { + destination: allocation.destination, + representation, + value: allocation.destination, + index: elements[0], + new_value: elements[0], + }, + span: allocation.span, + }); + }); +} + +#[test] +fn returned_helper_value_must_match_the_plan() { + rejected(R::Check, |f| f.result = f.parameters[0]); +} + +#[test] +fn unused_narrowing_cast_is_rejected_even_when_its_result_is_erased() { + for erase_result in [false, true] { + rejected(R::Executor, |f| { + let mut declaration = f + .values + .iter() + .find(|v| v.id == f.parameters[2]) + .unwrap() + .clone(); + declaration.id = + psrs_backend::cc::ValueId(f.values.iter().map(|v| v.id.0).max().unwrap() + 1); + let psrs_backend::cc::ValueShape::Reference(reference) = declaration.ty else { + unreachable!() + }; + let narrowed = declaration.id; + f.values.push(declaration.clone()); + f.assignments.push(Assignment { + destination: narrowed, + kind: A::RepresentationCast { + destination: narrowed, + value: f.parameters[0], + reference, + }, + span: f.span, + }); + if erase_result { + declaration.id = psrs_backend::cc::ValueId(narrowed.0 + 1); + let reference = psrs_backend::cc::Reference { + nullable: false, + heap: psrs_backend::cc::RefShape::Erased, + }; + declaration.ty = psrs_backend::cc::ValueShape::Reference(reference); + f.assignments.push(Assignment { + destination: declaration.id, + kind: A::RepresentationCast { + destination: declaration.id, + value: narrowed, + reference, + }, + span: f.span, + }); + f.values.push(declaration); + } + }); + } +} + +#[test] +fn reference_alias_on_the_actual_machine_exit_is_accepted() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let symbol = input + .cc + .representations + .control_helpers() + .iter() + .find(|h| h.role() == R::Start) + .unwrap() + .function(); + let f = input + .cc + .functions + .iter_mut() + .find(|f| f.symbol == symbol) + .unwrap(); + let mut declaration = f.values.iter().find(|v| v.id == f.result).unwrap().clone(); + declaration.id = psrs_backend::cc::ValueId(f.values.iter().map(|v| v.id.0).max().unwrap() + 1); + let psrs_backend::cc::ValueShape::Reference(reference) = declaration.ty else { + unreachable!() + }; + f.assignments.push(Assignment { + destination: declaration.id, + kind: A::RepresentationCast { + destination: declaration.id, + value: f.result, + reference, + }, + span: f.span, + }); + f.result = declaration.id; + f.values.push(declaration); + psrs_backend::cc::verify_module(&input.cc).unwrap(); + psrs_backend::compile_cc(input).unwrap(); +} + +#[test] +fn callback_recovery_cannot_move_before_the_callback_clear() { + rejected(R::Executor, |f| { + let A::TagSwitch { cases, .. } = &mut f.assignments.last_mut().unwrap().kind else { + panic!("executor must dispatch on computation"); + }; + let body = &mut cases + .iter_mut() + .find(|case| case.tag == 5) + .unwrap() + .assignments; + let recovery = body + .iter() + .position(|a| { + matches!( + a.kind, + A::RepresentationCast { + reference: psrs_backend::cc::Reference { + heap: psrs_backend::cc::RefShape::Closure(_), + .. + }, + .. + } + ) + }) + .unwrap(); + let clear = body[..recovery] + .iter() + .rposition(|a| matches!(a.kind, A::ArraySet { .. })) + .unwrap(); + let cast = body.remove(recovery); + body.insert(clear, cast); + }); +} diff --git a/crates/psrs-driver/src/tests/control_carriers.rs b/crates/psrs-driver/src/tests/control_carriers.rs new file mode 100644 index 00000000..0b556195 --- /dev/null +++ b/crates/psrs-driver/src/tests/control_carriers.rs @@ -0,0 +1,305 @@ +//! Source carriers retain nominal shapes before control operand lowering. +use psrs_backend::cc::{ + Assignment, AssignmentKind, ControlHelperRole, Function, RefShape, ValueId, ValueShape, +}; +use psrs_hir::Intrinsic; + +const SOURCE: &str = include_str!("../../tests/control_support/nominal_carriers.purs"); + +fn shape(function: &Function, value: ValueId) -> ValueShape { + function + .values + .iter() + .find(|declaration| declaration.id == value) + .unwrap() + .ty +} + +fn visit(body: &[Assignment], callback: &mut impl FnMut(&Assignment)) { + for assignment in body { + callback(assignment); + match &assignment.kind { + AssignmentKind::If { + then_assignments, + else_assignments, + .. + } => { + visit(then_assignments, callback); + visit(else_assignments, callback); + } + AssignmentKind::TagSwitch { + cases, + default_assignments, + .. + } => { + for case in cases { + visit(&case.assignments, callback); + } + visit(default_assignments, callback); + } + _ => {} + } + } +} + +#[test] +fn all_eight_operations_use_nominal_carriers_and_direct_callable_protocols() { + let input = crate::lower_main_to_cc(SOURCE).unwrap(); + let table = &input.cc.representations; + let signature = |role| { + let helper = table + .control_helpers() + .iter() + .find(|helper| helper.role() == role) + .unwrap(); + table.signature(helper.signature()).unwrap() + }; + let computation = signature(ControlHelperRole::Executor).parameters[2]; + let owner = signature(ControlHelperRole::Once).result; + let callable = signature(ControlHelperRole::Once).parameters[0]; + assert_ne!(computation, owner); + assert!( + matches!(computation, ValueShape::Reference(reference) if matches!(reference.heap, RefShape::Repr(_))) + ); + let erased = ValueShape::Reference(psrs_backend::cc::Reference { + nullable: false, + heap: RefShape::Erased, + }); + let mut operations = std::collections::HashSet::new(); + for function in &input.cc.functions { + visit(&function.assignments, &mut |assignment| { + let AssignmentKind::ControlEvidence { + contract, + arguments, + implementation, + value, + } = &assignment.kind + else { + return; + }; + let operation = table.control_contracts()[contract.0 as usize].operation(); + operations.insert(operation); + let expected = match operation { + Intrinsic::ControlReturn | Intrinsic::ControlPerform => vec![erased], + Intrinsic::ControlBind => vec![computation, callable], + Intrinsic::ControlHandle => vec![callable, callable, computation], + Intrinsic::ControlRun => vec![erased, callable, computation], + Intrinsic::ControlOnce => vec![callable], + Intrinsic::ControlResume => vec![owner, erased], + Intrinsic::ControlCancel => vec![owner], + _ => unreachable!(), + }; + assert_eq!( + arguments + .iter() + .map(|value| shape(function, *value)) + .collect::>(), + expected + ); + // Frames and helpers consume the witnessed operands directly. + let producer = function + .assignments + .iter() + .find(|candidate| candidate.destination == *implementation) + .unwrap(); + let actual = match &producer.kind { + AssignmentKind::VariantNew { fields, .. } => fields, + AssignmentKind::DirectCall { arguments, .. } => arguments, + _ => unreachable!(), + }; + assert_eq!(&actual[..arguments.len()], arguments); + if operation == Intrinsic::ControlOnce { + assert_eq!(value, implementation); + assert_eq!(shape(function, *value), owner); + } + }); + } + assert_eq!(operations.len(), 8); + psrs_backend::compile_cc(input).unwrap(); +} + +#[test] +fn ordinary_parameters_results_and_join_values_keep_the_carrier_shape() { + let input = crate::lower_main_to_cc(SOURCE).unwrap(); + let table = &input.cc.representations; + let expected = |role, parameter: Option| { + let helper = table + .control_helpers() + .iter() + .find(|helper| helper.role() == role) + .unwrap(); + let signature = table.signature(helper.signature()).unwrap(); + parameter.map_or(signature.result, |index| signature.parameters[index]) + }; + for (name, carrier, count) in [ + ("keep", expected(ControlHelperRole::Executor, Some(2)), 1), + ("retain", expected(ControlHelperRole::Once, None), 1), + ("choose", expected(ControlHelperRole::Executor, Some(2)), 3), + ] { + let function = input + .cc + .functions + .iter() + .find(|function| function.name == name) + .unwrap(); + assert_eq!(function.parameters.len(), count); + assert_eq!(function.result_type, carrier); + assert_eq!( + shape(function, *function.parameters.last().unwrap()), + carrier + ); + if name == "choose" { + assert!(function.assignments.iter().any(|assignment| matches!( + assignment.kind, + AssignmentKind::If { .. } + ) && shape( + function, + assignment.destination + ) == carrier)); + } + } +} + +#[test] +fn cc_rejects_weakening_a_source_computation_to_an_abstract_aggregate() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let contracts = &input.cc.representations.control_contracts(); + let (function, value, destination) = input + .cc + .functions + .iter_mut() + .find_map(|function| { + function + .assignments + .iter() + .find_map(|assignment| match &assignment.kind { + AssignmentKind::ControlEvidence { + contract, value, .. + } if contracts[contract.0 as usize].operation() == Intrinsic::ControlReturn => { + Some((*value, assignment.destination)) + } + _ => None, + }) + .map(|(value, destination)| (function, value, destination)) + }) + .unwrap(); + for declaration in &mut function.values { + if declaration.id == value || declaration.id == destination { + declaration.ty = ValueShape::Reference(psrs_backend::cc::Reference { + nullable: false, + heap: RefShape::Aggregate, + }); + } + } + let errors = psrs_backend::cc::verify_module(&input.cc).unwrap_err(); + assert!( + errors.iter().any(|error| error + .message + .contains("control carrier result must retain its nominal representation")), + "{errors:?}" + ); +} + +#[test] +fn cc_boundary_rejects_a_dropped_machine_dispatch_arm() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let symbol = input + .cc + .representations + .control_helpers() + .iter() + .find(|helper| helper.role() == ControlHelperRole::Executor) + .unwrap() + .function(); + let function = input + .cc + .functions + .iter_mut() + .find(|function| function.symbol == symbol) + .unwrap(); + let AssignmentKind::TagSwitch { cases, .. } = + &mut function.assignments.last_mut().unwrap().kind + else { + panic!("executor result must be a computation dispatch"); + }; + cases.retain(|case| case.tag != 1); + let errors = psrs_backend::cc::verify_module(&input.cc).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.pass == "P8 CC verification"), + "boundary check must reject a dropped machine arm: {errors:?}" + ); +} + +#[test] +fn mir_boundary_rejects_a_dropped_machine_dispatch_arm() { + let input = crate::lower_main_to_cc(SOURCE).unwrap(); + let (mut module, _) = psrs_backend::mir::lower_module(input.cc).unwrap(); + let symbol = module + .control_helpers() + .iter() + .find(|helper| helper.role() == ControlHelperRole::Executor) + .unwrap() + .function(); + let function = module + .functions + .iter_mut() + .find(|function| function.symbol == symbol) + .unwrap(); + let mut dropped = false; + for block in &mut function.blocks { + if let Some(psrs_backend::mir::Terminator::Switch { cases, .. }) = &mut block.terminator + && cases.len() > 2 + { + cases.pop(); + dropped = true; + break; + } + } + assert!(dropped, "executor must contain a machine dispatch switch"); + let errors = psrs_backend::mir::verify_module(&module).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.pass == "P9 MIR verification"), + "MIR boundary check must reject a dropped machine arm: {errors:?}" + ); +} + +#[test] +fn mir_boundary_rejects_a_changed_external_tail_target() { + let input = crate::lower_main_to_cc(SOURCE).unwrap(); + let (mut module, _) = psrs_backend::mir::lower_module(input.cc).unwrap(); + let clone = module + .control_helpers() + .iter() + .find(|helper| helper.role() == ControlHelperRole::CloneSequence) + .unwrap() + .function(); + let function = module + .functions + .iter_mut() + .find(|function| function.symbol == clone) + .unwrap(); + let mut changed = false; + for block in &mut function.blocks { + if let Some(psrs_backend::mir::Terminator::ReturnCall { + function: target, .. + }) = &mut block.terminator + && *target != clone + { + *target = clone; + changed = true; + break; + } + } + assert!(changed, "clone must tail-call rebuild"); + let errors = psrs_backend::mir::verify_module(&module).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.pass == "P9 MIR verification"), + "MIR boundary check must reject a changed tail target: {errors:?}" + ); +} diff --git a/crates/psrs-driver/src/tests/control_costs/mod.rs b/crates/psrs-driver/src/tests/control_costs/mod.rs new file mode 100644 index 00000000..8a7de70e --- /dev/null +++ b/crates/psrs-driver/src/tests/control_costs/mod.rs @@ -0,0 +1,168 @@ +//! Cost evidence combines actual ancestry guards with observed GC collections. + +mod probe; +mod replay; +use replay::entry_invocations as replay_invocations; +mod sources; + +use std::process::Command; + +fn run(name: &str, template: &str, steps: i32, depth: i32, expected: i32) -> Option> { + super::wasmtime_available()?; + let source = template.replace("STEPS", &steps.to_string()); + let mut stages = + crate::compile_main_stages(&source).expect("cost source compiles and verifies"); + assert!( + probe::instrument(&mut stages, depth) > 0, + "no extent allocations inspected" + ); + let core = + psrs_backend::wasm::encode_module(&stages.wasm).expect("instrumented target encodes"); + let bytes = probe::component(&stages, &core); + wasmparser::Validator::new_with_features(wasmparser::WasmFeatures::all()) + .validate_all(&bytes) + .expect("instrumented Wasm validates"); + let artifact = psrs_backend::Artifact { + wasm: bytes, + wat: String::new(), + warnings: vec![], + }; + let prefix = std::env::temp_dir().join(format!("psrs-control-cost-{name}-{steps}-{depth}")); + let wasm = prefix.with_extension("wasm"); + std::fs::write(prefix.with_extension("purs"), source).unwrap(); + let identity = artifact + .wasm + .iter() + .fold(0xcbf29ce484222325_u64, |hash, byte| { + (hash ^ u64::from(*byte)).wrapping_mul(0x100000001b3) + }); + std::fs::write(&wasm, artifact.wasm).unwrap(); + let output = Command::new("wasmtime") + .args(["run", "-O", "gc-heap-initial-size=65536"]) + .env("WASMTIME_LOG", "wasmtime::runtime::vm::gc=trace") + .arg(&wasm) + .output() + .unwrap(); + std::fs::write(prefix.with_extension("gc.log"), &output.stderr).unwrap(); + assert_eq!(output.status.code(), Some(expected), "{name}: {output:?}"); + let log = String::from_utf8_lossy(&output.stderr); + if expected == 134 { + assert!( + log.contains("wasm trap:") && !log.contains("cast failure"), + "{log}" + ); + } + let bytes = log + .lines() + .filter_map(|line| { + let (_, after) = line.split_once("After collection, GC heap's allocated bytes = 0x")?; + let hex = after.split_whitespace().next().unwrap(); + Some(u64::from_str_radix(hex, 16).expect("observed GC live bytes")) + }) + .collect::>(); + let report = serde_json::json!({ + "workload": name, + "steps": steps, + "ancestry_bound": depth, + "observed_exit": output.status.code(), + "artifact": wasm, + "artifact_identity": format!("fnv1a64-v1:{identity:016x}"), + "gc_initial_heap_bytes": 65536, + "collections": bytes.len(), + "maximum_post_gc_live_bytes": bytes.iter().max(), + "minimum_post_gc_live_bytes": bytes.iter().min(), + }); + std::fs::write( + prefix.with_extension("json"), + serde_json::to_vec_pretty(&report).unwrap(), + ) + .unwrap(); + eprintln!( + "{name}: steps={steps}, ancestry_bound={depth}, collections={}, maximum_post_gc_live_bytes={}", + bytes.len(), + bytes.iter().max().unwrap_or(&0) + ); + Some(bytes) +} + +#[test] +fn control_tail_handlers_bound_extent_ancestry_and_post_gc_live_heap() { + for (name, source) in [("deep", sources::DEEP), ("forwarded", sources::FORWARDED)] { + let mut previous = 0; + for steps in [1000, 10000, 100000] { + let Some(bytes) = run(name, source, steps, 2, 42) else { + return; + }; + assert!( + !bytes.is_empty(), + "no actual collection at {steps} requests" + ); + assert!( + bytes.len() > previous, + "increased work must exercise collection churn" + ); + previous = bytes.len(); + assert!( + bytes.iter().all(|n| *n < 16384), + "live heap grows despite tail extent coalescing: {bytes:?}" + ); + } + } +} + +#[test] +fn control_ancestry_probe_observes_independent_non_tail_extents() { + // This distinguishes an actual ancestry walk from a dead or ineffective probe. + run("nested", sources::NESTED, 8, 8, 42); + run("nested", sources::NESTED, 8, 7, 134); +} + +#[test] +fn replay_invocations_grow_with_requests_at_fixed_ancestry() { + // `DEEP` replays its saved queue once per request; the ancestry bound stays + // at 2 because tail handlers coalesce. Replay work is therefore measured + // separately from finish/ancestor depth. + let mut previous = 0; + for steps in [1, 2, 4, 8] { + let Some(count) = replay_invocations("deep", sources::DEEP, steps) else { + return; + }; + assert!( + count > previous, + "replay invocations must grow with requests ({previous} -> {count}) at {steps}" + ); + previous = count; + } +} + +#[test] +fn control_cost_pipeline_rejects_a_malformed_compiler_input() { + let mut input = crate::lower_main_to_cc(&sources::DEEP.replace("STEPS", "1")).unwrap(); + let symbol = input + .cc + .representations + .control_helpers() + .iter() + .find(|h| h.role() == psrs_backend::cc::ControlHelperRole::Executor) + .unwrap() + .function(); + let function = input + .cc + .functions + .iter_mut() + .find(|f| f.symbol == symbol) + .unwrap(); + let parameter = function.parameters[2]; + function + .values + .iter_mut() + .find(|v| v.id == parameter) + .unwrap() + .ty = psrs_backend::cc::ValueShape::Integer; + let errors = psrs_backend::compile_cc(input).expect_err("CC must be rechecked before emission"); + assert!( + errors.iter().any(|error| error.pass == "P8 CC verification" + && error.kind == psrs_backend::BackendErrorKind::InvalidCompilerIr), + "{errors:?}" + ); +} diff --git a/crates/psrs-driver/src/tests/control_costs/probe.rs b/crates/psrs-driver/src/tests/control_costs/probe.rs new file mode 100644 index 00000000..71ca96eb --- /dev/null +++ b/crates/psrs-driver/src/tests/control_costs/probe.rs @@ -0,0 +1,380 @@ +//! Observational guards instrument the final Wasm, after compiler verification. +//! The deliberately trapping budget walk is not part of the control protocol. +use psrs_backend::cc::ControlHelperRole as R; +use psrs_backend::wasm::{ + Body, FuncType, Function, Global, GlobalIndex, GlobalInit, Op, TypeIndex, +}; +use psrs_backend::{ + Stages, + types::{CompositeType, HeapType as MirHeap, ValueType}, +}; +use wasm_encoder::{HeapType, Instruction as I, RefType, ValType}; + +pub(super) fn instrument(stages: &mut Stages, limit: i32) -> usize { + let check = stages + .cc + .representations + .control_helpers() + .iter() + .find(|h| h.role() == R::Check) + .unwrap() + .function(); + let helper = stages + .mir + .functions + .iter() + .find(|f| f.symbol == check) + .unwrap(); + let parameter = helper + .values + .iter() + .find(|v| v.id == helper.parameters[0]) + .unwrap() + .ty; + let ValueType::Ref(reference) = parameter else { + panic!("extent reference") + }; + let MirHeap::Index(base) = reference.heap else { + panic!("extent type") + }; + let cases = stages + .wasm + .type_defs + .iter() + .flat_map(|group| &group.0) + .enumerate() + .filter_map(|(index, ty)| { + if ty.supertype != Some(base) { + return None; + } + let CompositeType::Struct(fields) = &ty.composite else { + panic!("extent struct") + }; + Some((fields.len(), index as u32)) + }) + .collect::>(); + let root = cases[&2]; + let child = cases[&3]; + let module = &mut stages.wasm; + let index = module.imports.len() + + module.functions.len() + + usize::from(module.entry.is_some()) + + usize::from(module.realloc.is_some()) + + module.helpers.len(); + let mut count = 0; + for function in &mut module.functions { + let start = function.parameters.len() + function.locals.len(); + // Nullable locals can hold the constructed non-null subtype. local.tee + // retains a nullable static type, so restore the original subtype after + // the observation before the production consumer reads it. + function + .locals + .extend([reference_type(root, true), reference_type(child, true)]); + count += insert( + &mut function.body, + root, + child, + start as u32, + index as u32, + limit, + ); + } + let type_index = TypeIndex(module.defined_type_count() + module.types.len() as u32); + let parameters = vec![reference_type(base.0, false), ValType::I32]; + module.types.push(FuncType { + parameters: parameters.clone(), + results: vec![], + }); + let span = module.span; + let body = vec![Op::Loop { + result: None, + span, + body: vec![ + leaf(I::LocalGet(1)), + leaf(I::I32Const(0)), + leaf(I::I32LtS), + Op::If { + then_body: vec![leaf(I::Unreachable)], + else_body: vec![], + result: None, + span, + }, + leaf(I::LocalGet(0)), + leaf(I::StructGet { + struct_type_index: base.0, + field_index: 0, + }), + leaf(I::I32Eqz), + Op::If { + then_body: vec![leaf(I::Return)], + else_body: vec![], + result: None, + span, + }, + leaf(I::LocalGet(0)), + leaf(I::RefCastNonNull(HeapType::Concrete(child))), + leaf(I::StructGet { + struct_type_index: child, + field_index: 2, + }), + leaf(I::LocalSet(0)), + leaf(I::LocalGet(1)), + leaf(I::I32Const(1)), + leaf(I::I32Sub), + leaf(I::LocalSet(1)), + leaf(I::Br(0)), + ], + }]; + module.helpers.push(Function { + symbol: psrs_hir::SymbolId::new(check.module, u32::MAX - 1), + name: "test_control_ancestry_probe".into(), + type_index, + parameters, + locals: vec![], + body, + span, + }); + count +} + +fn reference_type(index: u32, nullable: bool) -> ValType { + ValType::Ref(RefType { + nullable, + heap_type: HeapType::Concrete(index), + }) +} +fn leaf(instruction: I<'static>) -> Op { + Op::Leaf(instruction) +} + +fn counter(global: u32, budget: i32, span: psrs_span::TextRange) -> Body { + vec![ + leaf(I::GlobalGet(global)), + leaf(I::I32Const(1)), + leaf(I::I32Add), + leaf(I::GlobalSet(global)), + leaf(I::GlobalGet(global)), + leaf(I::I32Const(budget)), + leaf(I::I32GtS), + Op::If { + then_body: vec![leaf(I::Unreachable)], + else_body: vec![], + result: None, + span, + }, + ] +} +fn insert(body: &mut Body, root: u32, child: u32, local: u32, checker: u32, limit: i32) -> usize { + let mut count = 0; + let mut result = Vec::new(); + for mut op in std::mem::take(body) { + match &mut op { + Op::If { + then_body, + else_body, + .. + } => { + count += insert(then_body, root, child, local, checker, limit); + count += insert(else_body, root, child, local, checker, limit); + } + Op::Block { body, .. } | Op::Loop { body, .. } => { + count += insert(body, root, child, local, checker, limit) + } + _ => {} + } + let constructed = match &op { + Op::Leaf(I::StructNew(index)) if *index == root || *index == child => Some(*index), + _ => None, + }; + result.push(op); + if let Some(index) = constructed { + count += 1; + let slot = local + u32::from(index == child); + result.extend([ + leaf(I::LocalTee(slot)), + leaf(I::LocalGet(slot)), + leaf(I::RefCastNonNull(HeapType::Concrete(index))), + leaf(I::I32Const(limit)), + leaf(I::Call(checker)), + leaf(I::RefCastNonNull(HeapType::Concrete(index))), + ]); + } + } + *body = result; + count +} + +/// Count replay invocations by incrementing a private global at each +/// `CloneSequence`/`RebuildSequence` entry and trapping once it exceeds +/// `budget`. The smallest budget that does not trap is therefore the number of +/// replay invocations, measured independently of the extent ancestry bound. +pub(super) fn instrument_replay(stages: &mut Stages, budget: i32) -> usize { + let symbol = |role| { + stages + .cc + .representations + .control_helpers() + .iter() + .find(|helper| helper.role() == role) + .expect("control helper") + .function() + }; + let clone = symbol(R::CloneSequence); + let rebuild = symbol(R::RebuildSequence); + let module = &mut stages.wasm; + let global = module.globals.len() as u32; + module.globals.push(Global { + index: GlobalIndex(global), + mutable: true, + ty: ValType::I32, + init: GlobalInit::I32(0), + }); + let mut count = 0; + for function in &mut module.functions { + if function.symbol != clone && function.symbol != rebuild { + continue; + } + let span = function.span; + let mut prefix = counter(global, budget, span); + prefix.extend(std::mem::take(&mut function.body)); + function.body = prefix; + count += 1; + } + count +} + +/// Observe each loopified dispatch iteration, including self-tail backedges. +/// Optional `ordinal` selects one replay activation by its Clone entry; it +/// remains selected across Clone/Rebuild tail loops and their helper transfer. +pub(super) fn instrument_replay_steps( + stages: &mut Stages, + budget: i32, + ordinal: Option, +) -> usize { + let plan = stages + .cc + .representations + .control_plan() + .expect("planned control machine"); + let clone = plan.helper(R::CloneSequence).unwrap().function(); + let rebuild = plan.helper(R::RebuildSequence).unwrap().function(); + let module = &mut stages.wasm; + let global = module.globals.len() as u32; + for index in [global, global + 1] { + module.globals.push(Global { + index: GlobalIndex(index), + mutable: true, + ty: ValType::I32, + init: GlobalInit::I32(0), + }); + } + let mut helpers = 0; + for function in &mut module.functions { + if function.symbol != clone && function.symbol != rebuild { + continue; + } + let prefix = if let Some(ordinal) = ordinal { + vec![ + leaf(I::GlobalGet(global + 1)), + leaf(I::I32Const(ordinal)), + leaf(I::I32Eq), + Op::If { + then_body: counter(global, budget, function.span), + else_body: vec![], + result: None, + span: function.span, + }, + ] + } else { + counter(global, budget, function.span) + }; + let loops = instrument_loops(&mut function.body, &prefix); + assert_eq!( + loops, 1, + "replay observation requires one loopified dispatch header per helper, got {loops} in {:?}", + function.symbol + ); + if function.symbol == clone { + let mut entry = vec![ + leaf(I::GlobalGet(global + 1)), + leaf(I::I32Const(1)), + leaf(I::I32Add), + leaf(I::GlobalSet(global + 1)), + ]; + entry.extend(std::mem::take(&mut function.body)); + function.body = entry; + } + helpers += 1; + } + assert_eq!( + helpers, 2, + "both replay helpers must survive the verified pipeline" + ); + helpers +} + +fn instrument_loops(body: &mut Body, prefix: &Body) -> usize { + let mut count = 0; + for op in body { + match op { + Op::Loop { body, .. } => { + count += 1 + instrument_loops(body, prefix); + let mut observed = prefix.clone(); + observed.extend(std::mem::take(body)); + *body = observed; + } + Op::Block { body, .. } => count += instrument_loops(body, prefix), + Op::If { + then_body, + else_body, + .. + } => { + count += instrument_loops(then_body, prefix) + instrument_loops(else_body, prefix); + } + _ => {} + } + } + count +} + +/// Replace the program core module in the already assembled component. The +/// observation adds only private locals/functions; imports/exports stay fixed. +pub(super) fn component(stages: &Stages, core: &[u8]) -> Vec { + use wasm_encoder::Encode; + let source = &stages.artifact.wasm; + assert_eq!(&source[..8], b"\0asm\x0d\0\x01\0"); + let mut result = source[..8].to_vec(); + let mut offset = 8; + let mut replaced = 0; + while offset < source.len() { + let start = offset; + let id = source[offset]; + offset += 1; + let mut length = 0usize; + let mut shift = 0; + loop { + let byte = source[offset]; + offset += 1; + length |= usize::from(byte & 127) << shift; + if byte & 128 == 0 { + break; + } + shift += 7; + } + let end = offset + length; + if id == 1 { + replaced += 1; + result.push(id); + (core.len() as u32).encode(&mut result); + result.extend_from_slice(core); + } else { + result.extend_from_slice(&source[start..end]); + } + offset = end; + } + assert_eq!( + replaced, 1, + "cost fixture must have one program core module" + ); + result +} diff --git a/crates/psrs-driver/src/tests/control_costs/replay.rs b/crates/psrs-driver/src/tests/control_costs/replay.rs new file mode 100644 index 00000000..e1ad32ad --- /dev/null +++ b/crates/psrs-driver/src/tests/control_costs/replay.rs @@ -0,0 +1,213 @@ +//! Runtime observations of optimized Wasm. Compile each source once, retain +//! identified artifacts, and require the exact success/trapping budget boundary. +use super::{probe, sources}; +use psrs_backend::Stages; +use std::{path::PathBuf, process::Command}; + +#[derive(Clone, Copy, Debug)] +enum Counter { + Entries, + Steps(Option), +} + +impl Counter { + fn label(self) -> String { + match self { + Self::Entries => "entries".into(), + Self::Steps(None) => "steps-all".into(), + Self::Steps(Some(n)) => format!("steps-replay-{n}"), + } + } +} + +struct Program { + stages: Stages, + directory: PathBuf, + ancestry: i32, +} + +fn identity(bytes: &[u8]) -> String { + let hash = bytes.iter().fold(0xcbf29ce484222325_u64, |hash, byte| { + (hash ^ u64::from(*byte)).wrapping_mul(0x100000001b3) + }); + format!("fnv1a64-v1:{hash:016x}") +} + +impl Program { + fn compile(name: &str, source: &str, ancestry: i32) -> Option { + super::super::wasmtime_available()?; + let mut stages = crate::compile_main_stages(source) + .expect("cost source compiles through the verified pipeline"); + let directory = std::env::temp_dir() + .join(format!("psrs-control-replay-costs-{}", std::process::id())) + .join(name); + std::fs::create_dir_all(&directory).unwrap(); + std::fs::write(directory.join("source.purs"), source).unwrap(); + std::fs::write(directory.join("base.wasm"), &stages.artifact.wasm).unwrap(); + let base_identity = identity(&stages.artifact.wasm); + assert!( + probe::instrument(&mut stages, ancestry) > 0, + "ancestry observation must inspect actual allocations" + ); + let program = Self { + stages, + directory, + ancestry, + }; + assert!( + program.run(&program.stages, "ancestry").0, + "unlimited ancestry-instrumented program must return 42" + ); + let version = Command::new("wasmtime").arg("--version").output().unwrap(); + let report = serde_json::json!({"source":program.directory.join("source.purs"),"source_identity":identity(source.as_bytes()),"base_artifact_identity":base_identity,"ancestry_bound":ancestry,"wasmtime":String::from_utf8_lossy(&version.stdout).trim()}); + std::fs::write( + program.directory.join("input.json"), + serde_json::to_vec_pretty(&report).unwrap(), + ) + .unwrap(); + Some(program) + } + + /// Return success only for the checked value. A non-budget failure must not + /// become a measured count or look like an unavailable runtime skip. + fn run(&self, stages: &Stages, label: &str) -> (bool, String) { + let core = + psrs_backend::wasm::encode_module(&stages.wasm).expect("observed target encodes"); + let bytes = probe::component(stages, &core); + wasmparser::Validator::new_with_features(wasmparser::WasmFeatures::all()) + .validate_all(&bytes) + .expect("observed component validates"); + let hash = identity(&bytes); + let path = self.directory.join(format!("{label}.wasm")); + std::fs::write(&path, bytes).unwrap(); + let output = Command::new("wasmtime") + .arg("run") + .arg(&path) + .output() + .expect("run observed component"); + std::fs::write( + self.directory.join(format!("{label}.stderr")), + &output.stderr, + ) + .unwrap(); + match output.status.code() { + Some(42) => (true, hash), + Some(134) => { + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("wasm trap:") + && stderr.contains("unreachable") + && !stderr.contains("cast failure"), + "unexpected failure while measuring {path:?}: {output:?}" + ); + (false, hash) + } + _ => panic!( + "value/order/request-count assertion failed while measuring {path:?}: {output:?}" + ), + } + } + + fn budget(&self, counter: Counter, budget: i32) -> (bool, String) { + let mut stages = self.stages.clone(); + let inspected = match counter { + Counter::Entries => probe::instrument_replay(&mut stages, budget), + Counter::Steps(ordinal) => probe::instrument_replay_steps(&mut stages, budget, ordinal), + }; + assert_eq!(inspected, 2, "both replay helpers must be observed"); + self.run(&stages, &format!("{}-budget-{budget}", counter.label())) + } + + fn measure(&self, counter: Counter) -> i32 { + let mut high = 1; + while !self.budget(counter, high).0 { + high *= 2; + assert!( + high <= 1 << 20, + "replay did not complete within the observation budget" + ); + } + let mut low = 0; + while low < high { + let middle = low + (high - low) / 2; + if self.budget(counter, middle).0 { + high = middle; + } else { + low = middle + 1; + } + } + assert!(low > 0, "a replay observation must execute"); + let (success, successful_identity) = self.budget(counter, low); + let (under_budget, trapping_identity) = self.budget(counter, low - 1); + assert!( + success && !under_budget, + "counter must distinguish count from count-1" + ); + let report = serde_json::json!({"counter":counter.label(),"count":low,"ancestry_bound":self.ancestry,"successful_budget":low,"successful_exit":42,"successful_artifact_identity":successful_identity,"trapping_budget":low-1,"trapping_exit":134,"trapping_artifact_identity":trapping_identity}); + std::fs::write( + self.directory.join(format!("{}.json", counter.label())), + serde_json::to_vec_pretty(&report).unwrap(), + ) + .unwrap(); + eprintln!( + "{}: {} = {low}, ancestry <= {}", + self.directory.display(), + counter.label(), + self.ancestry + ); + low + } +} + +pub(super) fn entry_invocations(name: &str, template: &str, steps: i32) -> Option { + let source = template.replace("STEPS", &steps.to_string()); + let program = Program::compile(&format!("{name}-requests-{steps}"), &source, 2)?; + Some(program.measure(Counter::Entries)) +} + +#[test] +fn saved_callback_length_changes_actual_loop_work_at_fixed_requests_and_ancestry() { + for length in [0, 1, 4, 16] { + let source = sources::SAVED_LENGTH.replace("LENGTH", &length.to_string()); + let Some(program) = Program::compile(&format!("linear-length-{length}"), &source, 0) else { + return; + }; + assert_eq!( + program.measure(Counter::Entries), + 2, + "one replay has two helper entries regardless of length" + ); + assert_eq!( + program.measure(Counter::Steps(None)), + 2 * length + 2, + "both loops must visit every saved callback and their terminal Empty" + ); + } +} + +#[test] +fn queue_shape_changes_traversal_with_equal_callbacks_requests_and_ancestry() { + for length in [0, 4, 16] { + for (name, source, extra) in [ + ("linear", sources::LINEAR_SECOND, 0), + ("append", sources::APPEND_SECOND, 2), + ] { + let source = source.replace("LENGTH", &length.to_string()); + let Some(program) = + Program::compile(&format!("second-{name}-length-{length}"), &source, 0) + else { + return; + }; + assert_eq!( + program.measure(Counter::Entries), + 4, + "both programs have exactly two replays" + ); + assert_eq!( + program.measure(Counter::Steps(Some(2))), + 2 * length + 2 + extra, + "the second captured sequence has {length} callbacks; Append adds a stack push and empty right traversal" + ); + } + } +} diff --git a/crates/psrs-driver/src/tests/control_costs/sources.rs b/crates/psrs-driver/src/tests/control_costs/sources.rs new file mode 100644 index 00000000..afdea1c3 --- /dev/null +++ b/crates/psrs-driver/src/tests/control_costs/sources.rs @@ -0,0 +1,105 @@ +pub(super) const DEEP: &str = r"module Main where +import Prim.Control as C +data Op x = Op (Int -> x) +deep :: Int -> C.Control Op Int -> C.Control Op { count :: Int, value :: Int } +deep count body = C.handle + (\value -> C.return { count: count, value: value }) + (\(Op k) raw -> deep (intAdd count 1) (raw (k 0))) + body +root :: forall x. Int -> Op x -> (x -> C.Control Op { count :: Int, value :: Int }) -> { _1 :: Int, _2 :: C.Control Op { count :: Int, value :: Int } } +root s (Op k) raw = { _1: s, _2: raw (k 0) } +build :: Int -> C.Control Op Int +build n = if intEq n 0 then C.return 42 else C.bind (C.perform (Op (\x -> x))) (\_ -> build (intSub n 1)) +result = (C.run 0 root (deep 0 (build STEPS)))._2 +main = if intEq result.count STEPS then result.value else 0 +"; + +pub(super) const FORWARDED: &str = r"module Main where +import Prim.Control as C +data F x = F (Int -> x) +data G x = G (Int -> x) +data U x = UF (F x) | UG (G x) +fold :: Int -> C.Control U Int -> C.Control G { count :: Int, value :: Int } +fold count body = C.handle + (\value -> C.return { count: count, value: value }) + (\request raw -> case request of + UF (F k) -> fold (intAdd count 1) (raw (k 0)) + UG op -> C.bind (C.perform op) (\answer -> fold count (raw answer))) + body +root :: forall x. Int -> G x -> (x -> C.Control G { count :: Int, value :: Int }) -> { _1 :: Int, _2 :: C.Control G { count :: Int, value :: Int } } +root s (G k) raw = { _1: intAdd s 1, _2: raw (k 0) } +build :: Int -> C.Control U Int +build n = if intEq n 0 then C.return 42 else C.bind (C.perform (UF (F (\x -> x)))) (\_ -> C.bind (C.perform (UG (G (\x -> x)))) (\_ -> build (intSub n 1))) +result = C.run 0 root (fold 0 (build STEPS)) +main = if intEq result._1 STEPS then if intEq result._2.count STEPS then result._2.value else 0 else 0 +"; + +pub(super) const NESTED: &str = r"module Main where +import Prim.Control as C +data Op x = Op (Int -> x) +clause :: forall x. Op x -> (x -> C.Control Op Int) -> C.Control Op Int +clause (Op k) raw = raw (k 0) +nest :: Int -> C.Control Op Int +nest n = if intEq n 0 then C.return 42 else C.bind (C.handle C.return clause (nest (intSub n 1))) (\n -> C.return n) +root :: forall x. Int -> Op x -> (x -> C.Control Op Int) -> { _1 :: Int, _2 :: C.Control Op Int } +root s (Op k) raw = { _1: s, _2: raw (k 0) } +main = (C.run 0 root (nest STEPS))._2 +"; + +/// One request with LENGTH callbacks already in the captured sequence. +/// Pass the run result once to `check`: separate top-level projections would +/// evaluate this pure definition twice and double the measured machine work. +pub(super) const SAVED_LENGTH: &str = r"module Main where +import Prim.Control as C +data Op x = Op (Int -> x) +step :: Int -> Int -> Int +step index value = intAdd (intMul value 3) index +expected :: Int -> Int +expected n = if intEq n 0 then 42 else step n (expected (intSub n 1)) +build :: Int -> C.Control Op Int +build n = if intEq n 0 then C.perform (Op (\x -> x)) else C.bind (build (intSub n 1)) (\value -> C.return (step n value)) +root :: forall x. Int -> Op x -> (x -> C.Control Op Int) -> { _1 :: Int, _2 :: C.Control Op Int } +root s (Op k) raw = { _1: intAdd s 1, _2: raw (k 42) } +result = C.run 0 root (build LENGTH) +check :: { _1 :: Int, _2 :: Int } -> Int +check result = if intEq result._1 1 then if intEq result._2 (expected LENGTH) then 42 else 0 else 0 +main = check result +"; + +/// The first replay finishes before building the second request's sequence, +/// so the second capture is a linear Cons chain with LENGTH callbacks. +pub(super) const LINEAR_SECOND: &str = r"module Main where +import Prim.Control as C +data Op x = Op (Int -> x) +step :: Int -> Int -> Int +step index value = intAdd (intMul value 3) index +expected :: Int -> Int +expected n = if intEq n 0 then 42 else step n (expected (intSub n 1)) +build :: Int -> C.Control Op Int +build n = if intEq n 0 then C.perform (Op (\x -> x)) else C.bind (build (intSub n 1)) (\value -> C.return (step n value)) +root :: forall x. Int -> Op x -> (x -> C.Control Op Int) -> { _1 :: Int, _2 :: C.Control Op Int } +root s (Op k) raw = if intEq s 0 then { _1: 1, _2: C.bind (raw (k 0)) (\_ -> build LENGTH) } else { _1: intAdd s 1, _2: raw (k 42) } +result = C.run 0 root (C.perform (Op (\x -> x))) +check :: { _1 :: Int, _2 :: Int } -> Int +check result = if intEq result._1 2 then if intEq result._2 (expected LENGTH) then 42 else 0 else 0 +main = check result +"; + +/// After the first replay enters its first callback, the second capture sees +/// Append(remainingCallbacks, Empty): same callbacks as LINEAR_SECOND. +pub(super) const APPEND_SECOND: &str = r"module Main where +import Prim.Control as C +data Op x = Op (Int -> x) +step :: Int -> Int -> Int +step index value = intAdd (intMul value 3) index +expected :: Int -> Int +expected n = if intEq n 0 then 42 else step n (expected (intSub n 1)) +build :: Int -> C.Control Op Int +build n = if intEq n 0 then C.bind (C.perform (Op (\x -> x))) (\_ -> C.perform (Op (\x -> x))) else C.bind (build (intSub n 1)) (\value -> C.return (step n value)) +root :: forall x. Int -> Op x -> (x -> C.Control Op Int) -> { _1 :: Int, _2 :: C.Control Op Int } +root s (Op k) raw = { _1: intAdd s 1, _2: raw (k 42) } +result = C.run 0 root (build LENGTH) +check :: { _1 :: Int, _2 :: Int } -> Int +check result = if intEq result._1 2 then if intEq result._2 (expected LENGTH) then 42 else 0 else 0 +main = check result +"; diff --git a/crates/psrs-driver/src/tests/control_regressions/checkpoints.rs b/crates/psrs-driver/src/tests/control_regressions/checkpoints.rs new file mode 100644 index 00000000..1585270f --- /dev/null +++ b/crates/psrs-driver/src/tests/control_regressions/checkpoints.rs @@ -0,0 +1,267 @@ +//! Checkpoint, raw-resume and factory-status obligations at the P8 boundary. +//! Historical owners: `control_checkpoints`, `control_checkpoint_ordering`, +//! `control_inlined_publication` at `01f7e66c`. +use super::{SOURCE, binding}; +use psrs_backend::cc::{Assignment, AssignmentKind as A, ControlHelperRole as R, Function}; + +fn visit(body: &mut [Assignment], mutation: &mut impl FnMut(&mut A) -> bool) -> bool { + for assignment in body { + if mutation(&mut assignment.kind) { + return true; + } + match &mut assignment.kind { + A::TagSwitch { + cases, + default_assignments, + .. + } => { + for case in cases { + if visit(&mut case.assignments, mutation) { + return true; + } + } + if visit(default_assignments, mutation) { + return true; + } + } + A::If { + then_assignments, + else_assignments, + .. + } => { + if visit(then_assignments, mutation) || visit(else_assignments, mutation) { + return true; + } + } + _ => {} + } + } + false +} + +fn expect_boundary(input: &psrs_backend::BackendInput) { + let errors = psrs_backend::cc::verify_module(&input.cc) + .expect_err("malformed checkpoint construction must fail P8"); + assert!( + errors.iter().any(|error| error + .message + .contains("emitted machine operands or ordered operations")), + "{errors:?}" + ); +} + +fn executor(input: &mut psrs_backend::BackendInput) -> &mut Function { + let (symbol, _) = binding(&input.cc.representations, R::Executor); + input + .cc + .functions + .iter_mut() + .find(|function| function.symbol == symbol) + .expect("Executor") +} + +fn resume_arm(function: &mut Function) -> &mut Vec { + let A::TagSwitch { cases, .. } = &mut function.assignments.last_mut().unwrap().kind else { + panic!("executor dispatches on the computation"); + }; + &mut cases + .iter_mut() + .find(|case| case.tag == 3) + .expect("Resume arm") + .assignments +} + +#[test] +fn cc_perform_cannot_capture_the_execution_extent_as_its_checkpoint() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let function = executor(&mut input); + let execution = function.parameters[4]; + assert!(visit(&mut function.assignments, &mut |kind| { + if let A::FunctionRef { captures, .. } = kind { + assert_ne!(captures[1], execution); + captures[1] = execution; + return true; + } + false + })); + expect_boundary(&input); +} + +#[test] +fn cc_raw_resume_must_check_its_captured_checkpoint_before_the_frame() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let raw = input + .cc + .representations + .control_resumption() + .unwrap() + .function(); + let (check, _) = binding(&input.cc.representations, R::Check); + let function = input + .cc + .functions + .iter_mut() + .find(|function| function.symbol == raw) + .expect("raw resume"); + let before = function.assignments.len(); + function.assignments.retain(|assignment| { + !matches!(assignment.kind, A::DirectCall { function, .. } if function == check) + }); + assert_eq!(function.assignments.len(), before - 1); + expect_boundary(&input); +} + +#[test] +fn cc_resume_cannot_check_the_current_execution_extent() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let (check, _) = binding(&input.cc.representations, R::Check); + let function = executor(&mut input); + let execution = function.parameters[4]; + let body = resume_arm(function); + assert!(visit(body, &mut |kind| { + if let A::DirectCall { + function, + arguments, + .. + } = kind + && *function == check + { + arguments[0] = execution; + return true; + } + false + })); + expect_boundary(&input); +} + +#[test] +fn cc_resume_cannot_start_replay_before_the_checkpoint_check() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let (check, _) = binding(&input.cc.representations, R::Check); + let (clone, _) = binding(&input.cc.representations, R::CloneSequence); + let function = executor(&mut input); + let body = resume_arm(function); + let check_at = body + .iter() + .position(|assignment| { + matches!(assignment.kind, A::DirectCall { function, .. } if function == check) + }) + .expect("Resume checks its checkpoint"); + let check_assignment = body.remove(check_at); + let clone_at = body + .iter() + .position(|assignment| { + matches!(assignment.kind, A::DirectCall { function, .. } if function == clone) + }) + .expect("Resume clones the saved queue"); + body.insert(clone_at + 1, check_assignment); + expect_boundary(&input); +} + +#[test] +fn cc_start_and_once_status_cells_must_be_initialized_live() { + for role in [R::Start, R::Once] { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let (symbol, _) = binding(&input.cc.representations, role); + let function = input + .cc + .functions + .iter_mut() + .find(|function| function.symbol == symbol) + .unwrap(); + assert!(visit(&mut function.assignments, &mut |kind| { + if let A::Constant(value) = kind + && *value == 0 + { + *value = 2; + return true; + } + false + })); + expect_boundary(&input); + } +} + +#[test] +fn cc_handle_cannot_install_its_fresh_extent_as_the_caller_parent() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let function = executor(&mut input); + assert!(visit(&mut function.assignments, &mut |kind| { + if let A::VariantNew { + case: 1, fields, .. + } = kind + && fields.len() == 6 + && fields[3] != fields[5] + { + fields.swap(3, 5); + return true; + } + false + })); + expect_boundary(&input); +} + +#[test] +fn cc_raw_resume_accepts_a_same_shape_alias_of_its_checkpoint() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let raw = input + .cc + .representations + .control_resumption() + .unwrap() + .function(); + let (check, _) = binding(&input.cc.representations, R::Check); + let function = input + .cc + .functions + .iter_mut() + .find(|function| function.symbol == raw) + .unwrap(); + let check_at = function + .assignments + .iter() + .position(|assignment| { + matches!(assignment.kind, A::DirectCall { function, .. } if function == check) + }) + .unwrap(); + let checkpoint = match &function.assignments[check_at].kind { + A::DirectCall { arguments, .. } => arguments[0], + _ => unreachable!(), + }; + let mut declaration = function + .values + .iter() + .find(|value| value.id == checkpoint) + .unwrap() + .clone(); + declaration.id = psrs_backend::cc::ValueId( + function + .values + .iter() + .map(|value| value.id.0) + .max() + .unwrap() + + 1, + ); + let psrs_backend::cc::ValueShape::Reference(reference) = declaration.ty else { + unreachable!() + }; + function.assignments.insert( + check_at, + Assignment { + destination: declaration.id, + kind: A::RepresentationCast { + destination: declaration.id, + value: checkpoint, + reference, + }, + span: function.span, + }, + ); + match &mut function.assignments[check_at + 1].kind { + A::DirectCall { arguments, .. } => arguments[0] = declaration.id, + _ => unreachable!(), + } + function.values.push(declaration); + psrs_backend::cc::verify_module(&input.cc).unwrap(); +} diff --git a/crates/psrs-driver/src/tests/control_regressions/foreign.rs b/crates/psrs-driver/src/tests/control_regressions/foreign.rs new file mode 100644 index 00000000..298997a2 --- /dev/null +++ b/crates/psrs-driver/src/tests/control_regressions/foreign.rs @@ -0,0 +1,67 @@ +//! Host-resource capture and instance-terminal trap boundaries. +use super::SOURCE; + +const HEADER: &str = r"module Main where +import Prim.Control as C +import Unsafe.Coerce (unsafeCoerce) +foreign import data File :: Type +data Op x = Op (Int -> x) +handler :: forall x. Int -> Op x -> (x -> C.Control Op Int) -> { _1 :: Int, _2 :: C.Control Op Int } +handler state (Op k) raw = { _1: state, _2: raw (k 42) } +"; + +#[test] +fn cc_control_callback_cannot_capture_a_host_resource_handle() { + let source = format!( + "{HEADER}\nmain :: Int\nmain = let file = unsafeCoerce 0 :: File\n owner = C.once (\\n -> let _ = file in C.return n)\n in (C.run 0 handler (C.resume owner 1))._2\n" + ); + let errors = crate::lower_main_to_cc(&source).expect_err("a File capture must fail P8"); + assert!( + errors.iter().any(|error| error + .message + .contains("control callback cannot capture a host resource handle")), + "{errors:?}" + ); +} + +#[test] +fn cc_control_callback_may_capture_an_ordinary_integer() { + let source = format!( + "{HEADER}\nmain :: Int\nmain = let n = 1\n owner = C.once (\\x -> C.return (intAdd x n))\n in (C.run 0 handler (C.resume owner 1))._2\n" + ); + crate::lower_main_to_cc(&source).expect("an Int capture is not a host resource"); +} + +#[test] +fn control_artifact_has_no_post_trap_cleanup_export() { + let input = crate::lower_main_to_cc(SOURCE).unwrap(); + let artifact = psrs_backend::compile_cc(input).unwrap(); + for forbidden in [ + "control_cleanup", + "control_recover", + "psrs_control_cleanup", + "cabi_post_control", + ] { + assert!( + !artifact.wat.contains(forbidden), + "instance-terminal traps must not reenter through {forbidden}" + ); + } + assert!( + artifact.wat.contains("wasi:cli/run@0.2.12#run") || artifact.wat.contains("wasi:cli/run"), + "the artifact still exports the ordinary run contract" + ); +} + +#[test] +fn a_trap_during_control_is_instance_terminal() { + let source = + format!("{HEADER}\nmain :: Int\nmain = (C.run 0 handler (C.return (intQuot 1 0)))._2\n"); + let Some(output) = super::super::run_with_wasmtime(&source) else { + return; + }; + assert!( + !output.status.success(), + "a trap must abort the instance: {output:?}" + ); +} diff --git a/crates/psrs-driver/src/tests/control_regressions/lifecycle.rs b/crates/psrs-driver/src/tests/control_regressions/lifecycle.rs new file mode 100644 index 00000000..478912c6 --- /dev/null +++ b/crates/psrs-driver/src/tests/control_regressions/lifecycle.rs @@ -0,0 +1,122 @@ +//! Semantic helper IDs cannot replace proofs of their actual extent behavior. +use super::{SOURCE, binding}; +use psrs_backend::cc::{Assignment, AssignmentKind as A, ControlHelperRole as R}; + +fn visit(body: &mut [Assignment], mutation: &mut impl FnMut(&mut A) -> bool) -> bool { + for assignment in body { + if mutation(&mut assignment.kind) { + return true; + } + match &mut assignment.kind { + A::TagSwitch { + cases, + default_assignments, + .. + } => { + for case in cases { + if visit(&mut case.assignments, mutation) { + return true; + } + } + if visit(default_assignments, mutation) { + return true; + } + } + A::If { + then_assignments, + else_assignments, + .. + } => { + if visit(then_assignments, mutation) || visit(else_assignments, mutation) { + return true; + } + } + _ => {} + } + } + false +} + +#[test] +fn cc_lifecycle_bodies_reject_identity_wrong_guard_write_and_ancestry() { + for role in [R::Check, R::Close] { + for mutation in 0..4 { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let (symbol, _) = binding(&input.cc.representations, role); + let function = input + .cc + .functions + .iter_mut() + .find(|f| f.symbol == symbol) + .unwrap(); + let parameter = function.parameters[0]; + match mutation { + 0 => { + function.assignments.clear(); + function.result = parameter; + } + 1 => assert!(visit(&mut function.assignments, &mut |kind| match kind { + A::Constant(value) if *value == if role == R::Check { 0 } else { 1 } => { + *value = 2; + true + } + _ => false, + })), + 2 if role == R::Check => { + assert!(visit(&mut function.assignments, &mut |kind| match kind { + A::TagSwitch { cases, .. } if cases.len() == 1 => { + cases[0].tag = 1; + true + } + _ => false, + })) + } + 2 => assert!(visit(&mut function.assignments, &mut |kind| match kind { + A::ArraySet { + value, + destination, + representation, + .. + } => { + *kind = A::RepresentationCast { + value: *value, + destination: *destination, + reference: psrs_backend::cc::Reference { + nullable: false, + heap: psrs_backend::cc::RefShape::Repr(*representation), + }, + }; + true + } + _ => false, + })), + _ if role == R::Check => { + assert!(visit(&mut function.assignments, &mut |kind| match kind { + A::DirectCall { + function: target, + arguments, + .. + } if *target == symbol => { + arguments[0] = parameter; + true + } + _ => false, + })) + } + _ => assert!(visit(&mut function.assignments, &mut |kind| match kind { + A::ArraySet { value, .. } => { + *value = parameter; + true + } + _ => false, + })), + } + let errors = + psrs_backend::cc::verify_module(&input.cc).expect_err("malformed lifecycle body"); + assert!( + errors.iter().any(|e| e.pass == "P8 CC verification"), + "{role:?}/{mutation}: {errors:?}" + ); + } + } +} diff --git a/crates/psrs-driver/src/tests/control_regressions/mod.rs b/crates/psrs-driver/src/tests/control_regressions/mod.rs new file mode 100644 index 00000000..86144464 --- /dev/null +++ b/crates/psrs-driver/src/tests/control_regressions/mod.rs @@ -0,0 +1,26 @@ +//! Retired recognizer regressions re-expressed at their P8 construction owner. +//! Original cases: 01f7e66c, control_lifecycle_bodies/control_observable_paths. +mod checkpoints; +mod foreign; +mod lifecycle; +mod observables; +mod root; +mod steps; +mod transforms; + +const SOURCE: &str = include_str!("../../../tests/control_support/nominal_carriers.purs"); + +fn binding( + table: &psrs_backend::cc::RepresentationTable, + role: psrs_backend::cc::ControlHelperRole, +) -> (psrs_hir::SymbolId, psrs_backend::cc::ControlHelperId) { + let index = table + .control_helpers() + .iter() + .position(|helper| helper.role() == role) + .unwrap(); + ( + table.control_helpers()[index].function(), + psrs_backend::cc::ControlHelperId(index as u32), + ) +} diff --git a/crates/psrs-driver/src/tests/control_regressions/observables.rs b/crates/psrs-driver/src/tests/control_regressions/observables.rs new file mode 100644 index 00000000..4c88cff9 --- /dev/null +++ b/crates/psrs-driver/src/tests/control_regressions/observables.rs @@ -0,0 +1,224 @@ +//! Complete helper paths reject extra effects before dispatch and before traps. +use super::{SOURCE, binding}; +use psrs_backend::cc::{ + Assignment, AssignmentKind as A, ControlHelperRole as R, ValueDecl, ValueId, +}; + +#[test] +fn cc_executor_prefix_and_trapping_default_reject_extra_static_checks() { + for prefix in [false, true] { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let (executor, _) = binding(&input.cc.representations, R::Executor); + let (check, contract) = binding(&input.cc.representations, R::Check); + let function = input + .cc + .functions + .iter_mut() + .find(|f| f.symbol == executor) + .unwrap(); + let extent = function.parameters[4]; + let ty = function.values.iter().find(|v| v.id == extent).unwrap().ty; + let destination = ValueId(function.values.iter().map(|v| v.id.0).max().unwrap() + 1); + function.values.push(ValueDecl { + id: destination, + ty, + }); + let extra = Assignment { + destination, + kind: A::DirectCall { + contract: Some(contract), + function: check, + arguments: vec![extent], + }, + span: function.span, + }; + if prefix { + function.assignments.insert(0, extra); + } else { + let A::TagSwitch { + default_assignments, + .. + } = &mut function.assignments.last_mut().unwrap().kind + else { + panic!("dispatch") + }; + default_assignments.insert(0, extra); + } + let errors = + psrs_backend::cc::verify_module(&input.cc).expect_err("unaccounted static check"); + assert!( + errors + .iter() + .any(|e| matches!(e.pass, "P8 CC verification")), + "{prefix}: {errors:?}" + ); + } +} + +#[test] +fn cc_publication_rejects_an_extra_potentially_trapping_check() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let (start, _) = binding(&input.cc.representations, R::Start); + let (check, contract) = binding(&input.cc.representations, R::Check); + let function = input + .cc + .functions + .iter_mut() + .find(|f| f.symbol == start) + .unwrap(); + let destination = ValueId(function.values.iter().map(|v| v.id.0).max().unwrap() + 1); + function.values.push(ValueDecl { + id: destination, + ty: function.result_type, + }); + function.assignments.push(Assignment { + destination, + kind: A::DirectCall { + contract: Some(contract), + function: check, + arguments: vec![function.result], + }, + span: function.span, + }); + let errors = + psrs_backend::cc::verify_module(&input.cc).expect_err("read-only is not unobservable"); + assert!( + errors.iter().any(|e| e.pass == "P8 CC verification"), + "{errors:?}" + ); +} + +#[test] +fn cc_executor_rejects_unused_erased_payload_recovery() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let (executor, _) = binding(&input.cc.representations, R::Executor); + let function = input + .cc + .functions + .iter_mut() + .find(|f| f.symbol == executor) + .unwrap(); + let ty = function + .values + .iter() + .find(|v| v.id == function.parameters[2]) + .unwrap() + .ty; + let psrs_backend::cc::ValueShape::Reference(reference) = ty else { + panic!("computation") + }; + let destination = ValueId(function.values.iter().map(|v| v.id.0).max().unwrap() + 1); + function.values.push(ValueDecl { + id: destination, + ty, + }); + function.assignments.insert( + 0, + Assignment { + destination, + kind: A::RepresentationCast { + destination, + value: function.parameters[0], + reference, + }, + span: function.span, + }, + ); + let errors = + psrs_backend::cc::verify_module(&input.cc).expect_err("unaccounted payload recovery"); + assert!( + errors.iter().any(|e| e + .message + .contains("emitted machine operands or ordered operations")), + "{errors:?}" + ); +} + +#[test] +fn cc_no_write_recursive_calls_are_not_total() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let (executor, _) = binding(&input.cc.representations, R::Executor); + let original = input + .cc + .functions + .iter() + .find(|f| f.symbol == executor) + .unwrap(); + let extent_ty = original + .values + .iter() + .find(|v| v.id == original.parameters[4]) + .unwrap() + .ty; + let symbol = psrs_hir::SymbolId::new( + executor.module, + input + .cc + .functions + .iter() + .filter(|f| f.symbol.module == executor.module) + .map(|f| f.symbol.index) + .max() + .unwrap() + + 1, + ); + let span = original.span; + input.cc.functions.push(psrs_backend::cc::Function { + symbol, + name: "recursive_no_write".into(), + parameters: vec![ValueId(0)], + values: vec![ + ValueDecl { + id: ValueId(0), + ty: extent_ty, + }, + ValueDecl { + id: ValueId(1), + ty: extent_ty, + }, + ], + assignments: vec![Assignment { + destination: ValueId(1), + kind: A::DirectCall { + contract: None, + function: symbol, + arguments: vec![ValueId(0)], + }, + span, + }], + result: ValueId(1), + result_type: extent_ty, + span, + }); + let function = input + .cc + .functions + .iter_mut() + .find(|f| f.symbol == executor) + .unwrap(); + let destination = ValueId(function.values.iter().map(|v| v.id.0).max().unwrap() + 1); + function.values.push(ValueDecl { + id: destination, + ty: extent_ty, + }); + function.assignments.insert( + 0, + Assignment { + destination, + kind: A::DirectCall { + contract: None, + function: symbol, + arguments: vec![function.parameters[4]], + }, + span, + }, + ); + let errors = psrs_backend::cc::verify_module(&input.cc) + .expect_err("an extra recursive call can diverge"); + assert!( + errors + .iter() + .any(|e| matches!(e.pass, "P8 CC verification")), + "{errors:?}" + ); +} diff --git a/crates/psrs-driver/src/tests/control_regressions/root.rs b/crates/psrs-driver/src/tests/control_regressions/root.rs new file mode 100644 index 00000000..c971a4de --- /dev/null +++ b/crates/psrs-driver/src/tests/control_regressions/root.rs @@ -0,0 +1,223 @@ +//! Root lifetime belongs to the same checked construction boundary as replay. +use super::{SOURCE, binding}; +use psrs_backend::cc::{AssignmentKind as A, ControlHelperRole as R}; + +#[test] +fn cc_run_cannot_check_instead_of_closing_its_root() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let (close, _) = binding(&input.cc.representations, R::Close); + let (check, check_id) = binding(&input.cc.representations, R::Check); + let function = input + .cc + .functions + .iter_mut() + .find(|f| { + f.assignments + .iter() + .any(|a| matches!(a.kind, A::DirectCall { function, .. } if function == close)) + }) + .expect("live Run must contain normal root cleanup"); + let assignment = function + .assignments + .iter_mut() + .find(|a| matches!(a.kind, A::DirectCall { function, .. } if function == close)) + .unwrap(); + let A::DirectCall { + function, contract, .. + } = &mut assignment.kind + else { + unreachable!() + }; + *function = check; + *contract = Some(check_id); + let errors = psrs_backend::cc::verify_module(&input.cc) + .expect_err("Run cannot publish a result without closing its own root"); + assert!( + errors.iter().any(|e| e + .message + .contains("emitted machine operands or ordered operations")), + "{errors:?}" + ); +} + +#[test] +fn cc_run_requires_its_fresh_root_empty_queue_ordered_close_and_result() { + use psrs_backend::cc::{Assignment, Function, ValueDecl, ValueId}; + #[derive(Clone, Copy, Debug)] + enum Mutation { + MissingClose, + EarlyClose, + WrongResult, + ExecutionRoot, + DispatchRoot, + CloseRoot, + NonemptyQueue, + } + use Mutation::*; + for mutation in [ + MissingClose, + EarlyClose, + WrongResult, + ExecutionRoot, + DispatchRoot, + CloseRoot, + NonemptyQueue, + ] { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let (run, _) = binding(&input.cc.representations, R::Run); + let (start, start_id) = binding(&input.cc.representations, R::Start); + let (executor, _) = binding(&input.cc.representations, R::Executor); + let (close, _) = binding(&input.cc.representations, R::Close); + let f: &mut Function = input + .cc + .functions + .iter_mut() + .find(|f| f.symbol == run) + .unwrap(); + let call_index = f + .assignments + .iter() + .position(|a| matches!(a.kind, A::DirectCall {function,..} if function == executor)) + .unwrap(); + let A::DirectCall { arguments, .. } = &f.assignments[call_index].kind else { + unreachable!() + }; + let (queue, root, context) = (arguments[3], arguments[4], arguments[5]); + let close_index = f + .assignments + .iter() + .position(|a| matches!(a.kind, A::DirectCall {function,..} if function == close)) + .unwrap(); + match mutation { + MissingClose => { + f.assignments.remove(close_index); + } + EarlyClose => { + let close = f.assignments.remove(close_index); + f.assignments.insert(call_index, close); + } + WrongResult => { + f.result = f.parameters[0]; + } + ExecutionRoot | DispatchRoot | CloseRoot => { + let extra = ValueId(f.values.iter().map(|v| v.id.0).max().unwrap() + 1); + let ty = f.values.iter().find(|v| v.id == root).unwrap().ty; + f.values.push(ValueDecl { id: extra, ty }); + f.assignments.insert( + call_index, + Assignment { + destination: extra, + kind: A::DirectCall { + contract: Some(start_id), + function: start, + arguments: vec![], + }, + span: f.span, + }, + ); + if matches!(mutation, ExecutionRoot) { + let A::DirectCall { arguments, .. } = &mut f.assignments[call_index + 1].kind + else { + unreachable!() + }; + arguments[4] = extra; + } else if matches!(mutation, DispatchRoot) { + // The new root must be bound before the context allocation. + let context_index = f + .assignments + .iter() + .position(|a| a.destination == context) + .unwrap(); + let allocation = f.assignments.remove(call_index); + f.assignments.insert(context_index, allocation); + let A::VariantNew { fields, .. } = &mut f.assignments[context_index + 1].kind + else { + unreachable!() + }; + fields[0] = extra; + } else { + let A::DirectCall { arguments, .. } = &mut f.assignments[close_index + 1].kind + else { + unreachable!() + }; + arguments[0] = extra; + } + } + NonemptyQueue => { + let A::VariantNew { case, fields, .. } = &mut f + .assignments + .iter_mut() + .find(|a| a.destination == queue) + .unwrap() + .kind + else { + unreachable!() + }; + *case = 2; // Append(Empty, Empty) has no callbacks but is not Empty. + fields.extend([queue, queue]); + // Replace self-references with a separate valid Empty, so this + // exercises the machine relation rather than use-before-bind. + let empty = ValueId(f.values.iter().map(|v| v.id.0).max().unwrap() + 1); + let ty = f.values.iter().find(|v| v.id == queue).unwrap().ty; + f.values.push(ValueDecl { id: empty, ty }); + let mut allocation = f + .assignments + .iter() + .find(|a| a.destination == queue) + .unwrap() + .clone(); + let A::VariantNew { + destination, + case, + fields, + .. + } = &mut allocation.kind + else { + unreachable!() + }; + *destination = empty; + *case = 0; + fields.clear(); + allocation.destination = empty; + let index = f + .assignments + .iter() + .position(|a| a.destination == queue) + .unwrap(); + f.assignments.insert(index, allocation); + let A::VariantNew { fields, .. } = &mut f.assignments[index + 1].kind else { + unreachable!() + }; + *fields = vec![empty, empty]; + } + } + let errors = psrs_backend::cc::verify_module(&input.cc) + .expect_err("malformed Run must fail P8 construction verification"); + assert!( + errors.iter().any(|e| e + .message + .contains("emitted machine operands or ordered operations")), + "mutation {mutation:?}: {errors:?}" + ); + } +} + +#[test] +fn sequential_and_exclusive_runs_preserve_independent_invocations_after_optimization() { + const HEADER: &str = r"module Main where +import Prim.Control as C +data Op x = Op (Int -> x) +handler :: forall x. Int -> Op x -> (x -> C.Control Op Int) -> { _1 :: Int, _2 :: C.Control Op Int } +handler state (Op k) raw = { _1: state, _2: raw (k 42) } +"; + for body in [ + "intAdd ((C.run 0 handler (C.return 20))._2) ((C.run 0 handler (C.return 22))._2)", + r"let invoke = \flag -> if flag then (C.run 0 handler (C.return 20))._2 else (C.run 0 handler (C.return 22))._2 in intAdd (invoke true) (invoke false)", + ] { + let source = format!("{HEADER}\nmain :: Int\nmain = {body}\n"); + let Some(output) = super::super::run_with_wasmtime(&source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + } +} diff --git a/crates/psrs-driver/src/tests/control_regressions/steps.rs b/crates/psrs-driver/src/tests/control_regressions/steps.rs new file mode 100644 index 00000000..5522bce5 --- /dev/null +++ b/crates/psrs-driver/src/tests/control_regressions/steps.rs @@ -0,0 +1,337 @@ +//! Dispatch, Bind/Resume, Finish and helper-call obligations at the P8 boundary. +//! Historical owners: control_dispatch, control_clause_steps, control_execution_steps, +//! control_finish_provenance and control_helper_contracts at `01f7e66c`. +use super::{SOURCE, binding}; +use psrs_backend::cc::{Assignment, AssignmentKind as A, ControlHelperRole as R, Function}; + +fn visit(body: &mut [Assignment], mutation: &mut impl FnMut(&mut A) -> bool) -> bool { + for assignment in body { + if mutation(&mut assignment.kind) { + return true; + } + match &mut assignment.kind { + A::TagSwitch { + cases, + default_assignments, + .. + } => { + for case in cases { + if visit(&mut case.assignments, mutation) { + return true; + } + } + if visit(default_assignments, mutation) { + return true; + } + } + A::If { + then_assignments, + else_assignments, + .. + } => { + if visit(then_assignments, mutation) || visit(else_assignments, mutation) { + return true; + } + } + _ => {} + } + } + false +} + +fn expect_boundary(input: &psrs_backend::BackendInput) { + let errors = psrs_backend::cc::verify_module(&input.cc) + .expect_err("malformed machine step must fail P8"); + assert!( + errors.iter().any(|error| error + .message + .contains("emitted machine operands or ordered operations") + || error + .message + .contains("control helper call is missing its semantic contract") + || error + .message + .contains("disagrees with its predeclared semantic helper")), + "{errors:?}" + ); +} + +fn executor(input: &mut psrs_backend::BackendInput) -> &mut Function { + let (symbol, _) = binding(&input.cc.representations, R::Executor); + input + .cc + .functions + .iter_mut() + .find(|function| function.symbol == symbol) + .expect("Executor") +} + +fn arm(function: &mut Function, tag: i32) -> &mut Vec { + let A::TagSwitch { cases, .. } = &mut function.assignments.last_mut().unwrap().kind else { + panic!("executor dispatches on the computation"); + }; + &mut cases + .iter_mut() + .find(|case| case.tag == tag) + .expect("computation arm") + .assignments +} + +fn first_switch_arm(body: &mut [Assignment], tag: i32) -> &mut Vec { + for assignment in body { + if let A::TagSwitch { cases, .. } = &mut assignment.kind { + return &mut cases + .iter_mut() + .find(|case| case.tag == tag) + .unwrap_or_else(|| panic!("dispatch arm {tag}")) + .assignments; + } + } + panic!("no nested dispatch") +} + +#[test] +fn cc_executor_dispatch_default_must_trap() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let function = executor(&mut input); + let A::TagSwitch { + default_assignments, + default_value, + .. + } = &mut function.assignments.last_mut().unwrap().kind + else { + panic!("computation dispatch"); + }; + default_assignments.clear(); + *default_value = function.parameters[0]; + expect_boundary(&input); +} + +#[test] +fn cc_executor_cannot_return_state_instead_of_the_dispatch_result() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let function = executor(&mut input); + function.result = function.parameters[0]; + expect_boundary(&input); +} + +#[test] +fn cc_bind_cannot_reenter_the_current_computation() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let (executor_symbol, _) = binding(&input.cc.representations, R::Executor); + let function = executor(&mut input); + let current = function.parameters[2]; + let body = arm(function, 2); + assert!(visit(body, &mut |kind| { + if let A::DirectCall { + function, + arguments, + .. + } = kind + && *function == executor_symbol + { + arguments[2] = current; + return true; + } + false + })); + expect_boundary(&input); +} + +#[test] +fn cc_bind_cannot_replace_its_callback_with_the_root_handler() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let function = executor(&mut input); + let handler = function.parameters[1]; + let body = arm(function, 2); + assert!(visit(body, &mut |kind| { + if let A::VariantNew { + case: 1, fields, .. + } = kind + && fields.len() == 2 + { + fields[0] = handler; + return true; + } + false + })); + expect_boundary(&input); +} + +#[test] +fn cc_resume_cannot_replace_replay_activation_with_the_current_extent() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let (executor_symbol, _) = binding(&input.cc.representations, R::Executor); + let function = executor(&mut input); + let current = function.parameters[4]; + let body = arm(function, 3); + assert!(visit(body, &mut |kind| { + if let A::DirectCall { + function, + arguments, + .. + } = kind + && *function == executor_symbol + { + arguments[4] = current; + return true; + } + false + })); + expect_boundary(&input); +} + +#[test] +fn cc_finish_cannot_close_the_parent_instead_of_its_extent() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let (close, _) = binding(&input.cc.representations, R::Close); + let function = executor(&mut input); + let body = first_switch_arm(arm(function, 0), 3); + let parent = body + .iter() + .find_map(|assignment| match &assignment.kind { + A::VariantGet { field: 2, .. } => Some(assignment.destination), + _ => None, + }) + .expect("Finish parent projection"); + assert!(visit(body, &mut |kind| { + if let A::DirectCall { + function, + arguments, + .. + } = kind + && *function == close + { + arguments[0] = parent; + return true; + } + false + })); + expect_boundary(&input); +} + +#[test] +fn cc_finish_cannot_restore_the_closed_extent_as_its_parent() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let (executor_symbol, _) = binding(&input.cc.representations, R::Executor); + let (close, _) = binding(&input.cc.representations, R::Close); + let function = executor(&mut input); + let body = first_switch_arm(arm(function, 0), 3); + let closed = body + .iter() + .find_map(|assignment| match &assignment.kind { + A::DirectCall { + function, + arguments, + .. + } if *function == close => Some(arguments[0]), + _ => None, + }) + .expect("Finish close"); + assert!(visit(body, &mut |kind| { + if let A::DirectCall { + function, + arguments, + .. + } = kind + && *function == executor_symbol + { + arguments[4] = closed; + return true; + } + false + })); + expect_boundary(&input); +} + +#[test] +fn cc_scoped_return_cannot_discard_the_clause_result() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let (executor_symbol, _) = binding(&input.cc.representations, R::Executor); + let function = executor(&mut input); + let current = function.parameters[2]; + let empty = first_switch_arm(arm(function, 0), 0); + let body = first_switch_arm(empty, 1); + assert!(visit(body, &mut |kind| { + if let A::DirectCall { + function, + arguments, + .. + } = kind + && *function == executor_symbol + { + arguments[2] = current; + return true; + } + false + })); + expect_boundary(&input); +} + +#[test] +fn cc_helper_call_cannot_drop_its_semantic_contract() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let (check, _) = binding(&input.cc.representations, R::Check); + let function = executor(&mut input); + assert!(visit(&mut function.assignments, &mut |kind| { + if let A::DirectCall { + function, contract, .. + } = kind + && *function == check + { + *contract = None; + return true; + } + false + })); + expect_boundary(&input); +} + +#[test] +fn cc_control_evidence_rejects_a_dangling_contract() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let mut found = false; + for function in &mut input.cc.functions { + if visit(&mut function.assignments, &mut |kind| { + if let A::ControlEvidence { contract, .. } = kind { + *contract = psrs_backend::cc::ControlContractId(u32::MAX); + return true; + } + false + }) { + found = true; + break; + } + } + assert!(found); + let errors = psrs_backend::cc::verify_module(&input.cc) + .expect_err("a dangling source contract must fail CC verification"); + assert!( + errors + .iter() + .any(|error| error.message.contains("no checked source contract")), + "{errors:?}" + ); +} + +#[test] +fn cc_finish_cannot_exchange_close_for_check() { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let (close, _) = binding(&input.cc.representations, R::Close); + let (check, check_id) = binding(&input.cc.representations, R::Check); + let function = executor(&mut input); + let body = first_switch_arm(arm(function, 0), 3); + assert!(visit(body, &mut |kind| { + if let A::DirectCall { + function, contract, .. + } = kind + && *function == close + { + *function = check; + *contract = Some(check_id); + return true; + } + false + })); + expect_boundary(&input); +} diff --git a/crates/psrs-driver/src/tests/control_regressions/transforms.rs b/crates/psrs-driver/src/tests/control_regressions/transforms.rs new file mode 100644 index 00000000..dcbaa359 --- /dev/null +++ b/crates/psrs-driver/src/tests/control_regressions/transforms.rs @@ -0,0 +1,413 @@ +//! Root fixtures whose historical owners were post-construction recognizers. +//! +//! Start's status-cell length is rejected by the P8 boundary. Sharing one fresh +//! root across exclusive successors, distributing Close, excluding a trapping +//! alternative, and copying the root reference are ordinary pre-optimization +//! MIR shapes. Generic MIR verification accepts them. A missing normal Close +//! stays a P8 rejection in `root`, not a new MIR recognizer. + +use super::{SOURCE, binding}; +use psrs_backend::cc::AssignmentKind as A; +use psrs_backend::cc::ControlHelperRole as R; +use psrs_backend::mir::{BasicBlock, BlockId, Instruction as I, Module, Terminator as T}; +use psrs_backend::types::{ValueDecl, ValueId, ValueType}; +use psrs_hir::SymbolId; +use std::collections::HashMap; + +#[test] +fn cc_start_rejects_a_status_cell_whose_length_is_not_one() { + for length in [0, 2] { + let mut input = crate::lower_main_to_cc(SOURCE).unwrap(); + let (start, _) = binding(&input.cc.representations, R::Start); + let function = input + .cc + .functions + .iter_mut() + .find(|function| function.symbol == start) + .expect("Start helper"); + let mut cells = 0; + for assignment in &mut function.assignments { + let A::ArrayNew { elements, .. } = &mut assignment.kind else { + continue; + }; + cells += 1; + if length == 0 { + elements.clear(); + } else { + let element = elements[0]; + elements.push(element); + } + } + assert_eq!(cells, 1, "Start allocates one status cell"); + let errors = psrs_backend::cc::verify_module(&input.cc) + .expect_err("a status cell must contain exactly one live element"); + assert!( + errors.iter().any(|error| error + .message + .contains("emitted machine operands or ordered operations")), + "length {length}: {errors:?}" + ); + } +} + +#[test] +fn pre_optimization_run_accepts_exclusive_sharing_distributed_close_and_copies() { + let (start, close, executor, mut module) = run_module(); + psrs_backend::mir::verify_module(&module).expect("unmodified Run lowers"); + let baseline = calls(&module, start); + assert_eq!(baseline, 1); + assert_eq!(calls(&module, close), 1); + + share_root(&mut module, start); + psrs_backend::mir::verify_module(&module).unwrap_or_else(|errors| { + panic!("exclusive successors may share one fresh root: {errors:?}") + }); + assert_eq!( + calls(&module, start), + baseline, + "Start stays one allocation" + ); + assert_eq!(calls(&module, executor), baseline * 2); + assert_eq!(calls(&module, close), baseline * 2); + + let (_, _, _, mut module) = run_module(); + distribute_close(&mut module, close, false); + psrs_backend::mir::verify_module(&module) + .unwrap_or_else(|errors| panic!("Close may be repeated on normal branches: {errors:?}")); + assert_eq!(calls(&module, close), baseline * 2); + + let (_, _, _, mut module) = run_module(); + distribute_close(&mut module, close, true); + psrs_backend::mir::verify_module(&module).unwrap_or_else(|errors| { + panic!("a trapping alternative is outside the normal close proof: {errors:?}") + }); + assert_eq!(calls(&module, close), baseline); + assert!(module.functions.iter().any(|function| { + function + .blocks + .iter() + .any(|block| matches!(block.terminator, Some(T::Trap { .. }))) + })); + + let (_, _, _, mut module) = run_module(); + copy_root(&mut module, close); + psrs_backend::mir::verify_module(&module) + .unwrap_or_else(|errors| panic!("Close may use a copy of its root reference: {errors:?}")); +} + +fn run_module() -> (SymbolId, SymbolId, SymbolId, Module) { + let input = crate::lower_main_to_cc(SOURCE).unwrap(); + let (start, _) = binding(&input.cc.representations, R::Start); + let (close, _) = binding(&input.cc.representations, R::Close); + let (executor, _) = binding(&input.cc.representations, R::Executor); + let (run, _) = binding(&input.cc.representations, R::Run); + let (module, _) = psrs_backend::mir::lower_module(input.cc).unwrap(); + let function = module + .functions + .iter() + .find(|function| function.symbol == run) + .expect("Run helper"); + assert_eq!( + function.blocks.len(), + 1, + "pre-optimization Run is the straight-line plan" + ); + (start, close, executor, module) +} + +fn run_function(module: &mut Module) -> &mut psrs_backend::mir::Function { + module + .functions + .iter_mut() + .find(|function| function.name == "control_run") + .expect("Run helper name") +} + +fn calls(module: &Module, target: SymbolId) -> usize { + module + .functions + .iter() + .filter(|function| function.name == "control_run") + .flat_map(|function| &function.blocks) + .flat_map(|block| &block.instructions) + .filter( + |instruction| matches!(instruction, I::Call { function, .. } if *function == target), + ) + .count() +} + +fn share_root(module: &mut Module, start: SymbolId) { + let function = run_function(module); + let span = function.span; + let start_at = function.blocks[0] + .instructions + .iter() + .position( + |instruction| matches!(instruction, I::Call { function, .. } if *function == start), + ) + .expect("Start call"); + let tail = function.blocks[0].instructions.split_off(start_at + 1); + let result = match function.blocks[0].terminator.take() { + Some(T::Return { value, .. }) => value, + other => panic!("Run returns normally, got {other:?}"), + }; + assert!( + tail.iter() + .any(|instruction| instruction.destination() == Some(result)), + "the Run result is produced after Start" + ); + let (else_tail, copied) = duplicate(function, &tail); + let else_result = copied[&result]; + let then_block = BlockId( + function + .blocks + .iter() + .map(|block| block.id.0) + .max() + .unwrap() + + 1, + ); + let else_block = BlockId(then_block.0 + 1); + let condition = fresh_value(function, ValueType::Boolean); + function.blocks[0].instructions.push(I::Constant { + destination: condition, + value: 1, + span, + }); + function.blocks[0].terminator = Some(T::Branch { + condition, + then_block, + else_block, + span, + }); + function.blocks.extend([ + block_returning(then_block, tail, result, span), + block_returning(else_block, else_tail, else_result, span), + ]); +} + +fn distribute_close(module: &mut Module, close: SymbolId, trap: bool) { + let function = run_function(module); + let span = function.span; + let close_at = function.blocks[0] + .instructions + .iter() + .position( + |instruction| matches!(instruction, I::Call { function, .. } if *function == close), + ) + .expect("Close call"); + let close_instruction = function.blocks[0].instructions.remove(close_at); + let suffix = function.blocks[0].instructions.split_off(close_at); + let terminator = function.blocks[0].terminator.take(); + let destination = match &close_instruction { + I::Call { destination, .. } => *destination, + _ => unreachable!("Close is a call"), + }; + let result_ty = function + .values + .iter() + .find(|value| value.id == destination) + .unwrap() + .ty; + let alias = fresh_value(function, result_ty); + let mut other = close_instruction.clone(); + let I::Call { destination, .. } = &mut other else { + unreachable!("Close is a call"); + }; + *destination = alias; + let then_block = BlockId( + function + .blocks + .iter() + .map(|block| block.id.0) + .max() + .unwrap() + + 1, + ); + let else_block = BlockId(then_block.0 + 1); + let join = BlockId(then_block.0 + 2); + let condition = fresh_value(function, ValueType::Boolean); + function.blocks[0].instructions.push(I::Constant { + destination: condition, + value: 1, + span, + }); + function.blocks[0].terminator = Some(T::Branch { + condition, + then_block, + else_block, + span, + }); + let jump = Some(T::Jump { + target: join, + arguments: vec![], + span, + }); + function.blocks.extend([ + BasicBlock { + id: then_block, + parameters: vec![], + instructions: vec![close_instruction], + terminator: jump.clone(), + }, + BasicBlock { + id: else_block, + parameters: vec![], + instructions: if trap { vec![] } else { vec![other] }, + terminator: if trap { Some(T::Trap { span }) } else { jump }, + }, + BasicBlock { + id: join, + parameters: vec![], + instructions: suffix, + terminator, + }, + ]); +} + +fn copy_root(module: &mut Module, close: SymbolId) { + let function = run_function(module); + let span = function.span; + let close_at = function.blocks[0] + .instructions + .iter() + .position( + |instruction| matches!(instruction, I::Call { function, .. } if *function == close), + ) + .expect("Close call"); + let root = match &function.blocks[0].instructions[close_at] { + I::Call { arguments, .. } => arguments[0], + _ => unreachable!("Close is a call"), + }; + let ty = function + .values + .iter() + .find(|value| value.id == root) + .unwrap() + .ty; + let alias = fresh_value(function, ty); + match &mut function.blocks[0].instructions[close_at] { + I::Call { arguments, .. } => arguments[0] = alias, + _ => unreachable!("Close is a call"), + } + function.blocks[0].instructions.insert( + close_at, + I::Copy { + destination: alias, + value: root, + span, + }, + ); +} + +fn duplicate( + function: &mut psrs_backend::mir::Function, + tail: &[I], +) -> (Vec, HashMap) { + let mut map = HashMap::new(); + for instruction in tail { + if let Some(destination) = instruction.destination() { + let ty = function + .values + .iter() + .find(|value| value.id == destination) + .expect("defined Run value") + .ty; + map.insert(destination, fresh_value(function, ty)); + } + } + let copied = tail + .iter() + .cloned() + .map(|instruction| retarget(instruction, &map)) + .collect(); + (copied, map) +} + +fn retarget(instruction: I, map: &HashMap) -> I { + let id = |value: ValueId| map.get(&value).copied().unwrap_or(value); + match instruction { + I::Call { + contract, + destination, + function, + arguments, + span, + } => I::Call { + contract, + destination: id(destination), + function, + arguments: arguments.into_iter().map(id).collect(), + span, + }, + I::StructNew { + destination, + type_index, + arguments, + span, + } => I::StructNew { + destination: id(destination), + type_index, + arguments: arguments.into_iter().map(id).collect(), + span, + }, + I::Constant { + destination, + value, + span, + } => I::Constant { + destination: id(destination), + value, + span, + }, + I::RefCast { + destination, + value, + reference, + span, + } => I::RefCast { + destination: id(destination), + value: id(value), + reference, + span, + }, + I::Copy { + destination, + value, + span, + } => I::Copy { + destination: id(destination), + value: id(value), + span, + }, + other => panic!("Run lowering produced an unexpected instruction: {other:?}"), + } +} + +fn block_returning( + id: BlockId, + instructions: Vec, + value: ValueId, + span: psrs_span::TextRange, +) -> BasicBlock { + BasicBlock { + id, + parameters: vec![], + instructions, + terminator: Some(T::Return { value, span }), + } +} + +fn fresh_value(function: &mut psrs_backend::mir::Function, ty: ValueType) -> ValueId { + let id = ValueId( + function + .values + .iter() + .map(|value| value.id.0) + .max() + .unwrap() + + 1, + ); + function.values.push(ValueDecl { id, ty }); + id +} diff --git a/crates/psrs-driver/src/tests/data_function.rs b/crates/psrs-driver/src/tests/data_function.rs index 2f6d0d42..b78ec6e5 100644 --- a/crates/psrs-driver/src/tests/data_function.rs +++ b/crates/psrs-driver/src/tests/data_function.rs @@ -7,10 +7,32 @@ use super::*; +#[test] +fn uncurried_foreign_signatures_retain_polymorphic_arguments_in_core() { + // Like Data.Number.fromStringImpl, this foreign signature supplies a + // checked universal argument to a parameterized uncurried newtype. + let source = r#" +module Main where +newtype Fn2 a b c = Fn2 (a -> b -> c) +run :: forall a b c. Fn2 a b c -> a -> b -> c +run (Fn2 f) a b = f a b +identity :: forall a. a -> a +identity x = x +foreign import choose :: Fn2 (forall a. a -> a) Int Int +main :: Int +main = run choose identity 42 +"#; + let core = lower_program_to_core(&[("Main.purs", source)]) + .expect("an explicit universal argument survives scheme instantiation"); + core.verify() + .expect("the checked foreign call remains valid Core"); +} + #[test] fn the_application_operators_resolve_through_the_library_re_export() { let source = r#" module Main where +import Effect.Unsafe (unsafePerformEffect) import Prelude import Effect (Effect) @@ -44,7 +66,7 @@ checks = do log (if viaConst == 1 then "const" else "const-wrong") pure unit -main = let ignored = runEffect checks in 0 +main = let ignored = unsafePerformEffect checks in 0 "#; let Some(output) = run_with_wasmtime(source) else { eprintln!("skipping: wasmtime is not installed"); diff --git a/crates/psrs-driver/src/tests/data_tuple.rs b/crates/psrs-driver/src/tests/data_tuple.rs index f19e3e88..3534a3ed 100644 --- a/crates/psrs-driver/src/tests/data_tuple.rs +++ b/crates/psrs-driver/src/tests/data_tuple.rs @@ -1,46 +1,108 @@ -//! The library's `Data.Tuple` surface. -//! -//! `Tuple a b` is the closed record `{ _1 :: a, _2 :: b }` that FE-06 already -//! lowers a tuple to, not an algebraic product. These tests check that the -//! synonym, the tuple syntax, and a record literal are one value, and that the -//! projection functions execute under Wasmtime. +//! Library tuples and native tuple syntax keep their distinct representations. use super::*; #[test] -fn a_tuple_is_the_closed_record_and_executes_under_wasmtime() { - let source = r#" +fn library_tuple_constructor_and_helpers_execute() { + let main = r#" module Main where -import Prelude -import Data.Tuple (Tuple, curry, fst, snd, swap, uncurry) +import Data.Tuple (Tuple(..), curry, fst, snd, swap, uncurry) -fromSyntax :: Tuple Int Int +pair :: Tuple Int Int +pair = Tuple 40 2 + +main = if intEq (uncurry (\left right -> intAdd left right) pair) 42 + then if intEq (fst (swap pair)) 2 + then if intEq (snd (swap pair)) 40 + then if intEq (curry (\tuple -> intAdd (fst tuple) (snd tuple)) 20 22) 42 then 0 else 1 + else 1 + else 1 + else 1 +"#; + let Some(output) = run_with_wasmtime(main) else { + eprintln!("skipping execution: wasmtime is not installed"); + return; + }; + assert_eq!(output.status.code(), Some(0), "{output:?}"); +} + +#[test] +fn native_tuple_syntax_still_has_the_closed_record_representation() { + let main = r#" +module Main where + +import Data.Tuple (Tuple) + +type Pair = { _1 :: Int, _2 :: Int } + +fromSyntax :: Pair fromSyntax = (40, 2) -fromRecord :: Tuple Int Int +fromRecord :: Pair fromRecord = { _1: 40, _2: 2 } -passed :: Boolean -> Int -passed flag = if flag then 1 else 0 +main = 0 +"#; + let Some(output) = run_with_wasmtime(main) else { + eprintln!("skipping execution: wasmtime is not installed"); + return; + }; + assert_eq!(output.status.code(), Some(0), "{output:?}"); +} + +#[test] +fn library_tuple_equality_ordering_and_show_typecheck() { + let main = r#" +module Main where + +import Data.Eq (eq) +import Data.Ord (compare, Ordering) +import Data.Show (show) +import Data.Tuple (Tuple(..)) + +equalPair :: Boolean +equalPair = eq (Tuple 1 "雪") (Tuple 1 "雪") + +orderedPair :: Ordering +orderedPair = compare (Tuple 1 "z") (Tuple 2 "a") + +shownPair :: String +shownPair = show (Tuple 1 "雪") + +main :: Int +main = 0 +"#; + crate::check_program_types_lenient_with_prelude(&[("Main.purs", main)]) + .unwrap_or_else(|errors| panic!("tuple instances should type check: {errors:?}")); +} + +#[test] +fn library_tuple_instances_compare_fields_and_render_utf8() { + let main = r#" +module Main where + +import Data.Eq (eq) +import Data.Ord (compare, Ordering(..)) +import Data.Show (show) +import Data.Tuple (Tuple(..)) main :: Int main = - passed (uncurry (\a b -> a + b) fromSyntax == 42) - + passed (uncurry (\a b -> a + b) fromRecord == 42) - + passed (fst fromSyntax == 40) - + passed (snd fromRecord == 2) - + passed (fst (swap fromSyntax) == 2) - + passed (snd (swap fromRecord) == 40) - + passed (curry (\pair -> fst pair + snd pair) 20 22 == 42) + if eq (Tuple 1 "雪") (Tuple 1 "雪") then + if eq (Tuple 1 "雪") (Tuple 1 "a") then 1 + else case compare (Tuple 1 99) (Tuple 2 0) of + LT -> case compare (Tuple 1 3) (Tuple 1 2) of + GT -> case compare (Tuple 1 2) (Tuple 1 2) of + EQ -> if eq (show (Tuple 1 "雪")) "(Tuple 1 \"雪\")" then 0 else 4 + _ -> 3 + _ -> 2 + _ -> 1 + else 1 "#; - let Some(output) = run_with_wasmtime(source) else { - eprintln!("skipping: wasmtime is not installed"); + let Some(output) = run_with_wasmtime(main) else { + eprintln!("skipping execution: wasmtime is not installed"); return; }; - assert_eq!( - output.status.code(), - Some(7), - "tuple syntax and the record form must be the same value: {output:?}" - ); + assert_eq!(output.status.code(), Some(0), "{output:?}"); } diff --git a/crates/psrs-driver/src/tests/declaration_calls.rs b/crates/psrs-driver/src/tests/declaration_calls.rs new file mode 100644 index 00000000..1c3a411d --- /dev/null +++ b/crates/psrs-driver/src/tests/declaration_calls.rs @@ -0,0 +1,200 @@ +use super::run_with_wasmtime; + +#[test] +fn an_explicit_partial_scope_returns_values_and_traps_on_missing_cases() { + for (argument, expected) in [("Just 42", Some(42)), ("Nothing", None)] { + let source = format!( + r#"module Main where +import Partial.Unsafe (unsafePartial) +data Maybe a = Nothing | Just a +extract :: Partial => Maybe Int -> Int +extract (Just x) = x +main :: Int +main = unsafePartial (extract ({argument})) +"# + ); + let Some(output) = run_with_wasmtime(&source) else { + return; + }; + if let Some(code) = expected { + assert_eq!(output.status.code(), Some(code), "{output:?}"); + assert!(output.stderr.is_empty(), "{output:?}"); + } else { + assert!(!output.status.success(), "{output:?}"); + assert!( + String::from_utf8_lossy(&output.stderr).contains("unreachable"), + "{output:?}" + ); + } + } +} + +#[test] +fn partial_application_calls_the_declaration_and_then_its_returned_function() { + let source = r#"module Main where +data Box a = Box a +invoke :: forall a b c. Int -> Box (a -> b -> c) -> a -> b -> c +invoke _ (Box f) x y = f x y +add x y = intAdd x y +main :: Int +main = + let pending = invoke 0 + result = pending (Box add) 40 2 + in result +"#; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +#[test] +fn polymorphic_constructor_fields_keep_application_quantifier_scope() { + let source = r#"module Main where +data Maybe a = Nothing | Just a +newtype Parser a = Parser (Int -> Maybe a) +constant x _ = x +dictionary :: { empty :: forall a. Parser a } +dictionary = { empty: Parser (constant Nothing) } +main :: Int +main = case (dictionary.empty :: Parser Int) of + Parser p -> case p 0 of + Nothing -> 42 + Just _ -> 1 +"#; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +#[test] +fn constructor_patterns_return_functions_at_the_checked_calling_boundary() { + let source = r#"module Main where +newtype Fn2 a b c = Fn2 (a -> b -> c) +runFn2 :: forall a b c. Fn2 a b c -> a -> b -> c +runFn2 (Fn2 f) a b = f a b +alias = runFn2 +add x y = intAdd x y +main :: Int +main = + let wrapped = Fn2 add + partial = runFn2 wrapped 40 + returned = runFn2 wrapped + throughAlias = alias wrapped + in if booleanAnd (intEq (runFn2 wrapped 40 2) 42) + (booleanAnd (intEq (partial 2) 42) + (booleanAnd (intEq (returned 40 2) 42) + (intEq (throughAlias 40 2) 42))) then 42 else 1 +"#; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert!(output.stdout.is_empty(), "{output:?}"); + assert!(output.stderr.is_empty(), "{output:?}"); +} + +#[test] +fn superclass_dictionary_construction_is_delayed_until_selection() { + let source = r#"module Main where +class Base a where + base :: a -> Int +class Base a <= Child a where + child :: a -> Int +helper :: forall a. Child a => a -> Int +helper = child +instance baseInt :: Base Int where + base = helper +instance childInt :: Child Int where + child x = intAdd x 2 +main :: Int +main = base 40 +"#; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert!(output.stderr.is_empty(), "{output:?}"); +} + +#[test] +fn abstract_array_constructor_transport_converts_element_storage() { + let source = r#"module Main where +class Build f where + build :: forall a. Array a -> f a + consume :: forall a. f a -> Array a +instance buildArray :: Build Array where + build xs = xs + consume xs = xs +roundTrip :: forall f. Build f => f Int -> Array Int +roundTrip = consume +main :: Int +main = case (roundTrip (build [42] :: Array Int)) of + [x] -> x + _ -> 1 +"#; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert!(output.stderr.is_empty(), "{output:?}"); +} + +#[test] +fn generic_value_slots_preserve_nested_array_element_protocols() { + let source = r#"module Main where +data Box a = Box a +box :: forall a. a -> Box a +box x = Box x +hold :: forall a. Array a -> Box (Array a) +hold xs = box xs +main :: Int +main = case hold [[42]] of + Box [[x]] -> x + _ -> 1 +"#; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert!(output.stderr.is_empty(), "{output:?}"); +} + +#[test] +fn closed_uses_of_a_local_row_lambda_keep_distinct_fields_and_captures() { + let source = r#"module Main where +main :: Int +main = + let captured = 2 + select :: forall r. { value :: Int | r } -> Int + select { value: x } = intAdd x captured + first = select { value: 20, left: true } + second = select { value: 18, right: [1,2] } + in intAdd first second +"#; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert!(output.stderr.is_empty(), "{output:?}"); +} + +#[test] +fn generic_value_slots_preserve_records_with_array_fields() { + let source = r#"module Main where +data Box a = Box a +box :: forall a. a -> Box a +box x = Box x +hold :: forall a. Array a -> Box { head :: a, tail :: Array a } +hold xs = box { head: arrayIndex xs 0, tail: xs } +main :: Int +main = case hold [42] of + Box record -> if intEq record.head (arrayIndex record.tail 0) then record.head else 1 +"#; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert!(output.stderr.is_empty(), "{output:?}"); +} diff --git a/crates/psrs-driver/src/tests/declarations/import_aliases.rs b/crates/psrs-driver/src/tests/declarations/import_aliases.rs new file mode 100644 index 00000000..8bc1a836 --- /dev/null +++ b/crates/psrs-driver/src/tests/declarations/import_aliases.rs @@ -0,0 +1,80 @@ +use super::*; + +#[test] +fn a_shared_qualifier_combines_disjoint_values_and_types() { + let a = "module A where\ndata Left = Left\na :: Left\na = Left\n"; + let b = "module B where\ndata Right = Right\nb :: Right\nb = Right\n"; + let facade = "module Facade (module X, left, right) where\nimport A as X\nimport B as X\nleft :: X.Left\nleft = X.a\nright :: X.Right\nright = X.Right\n"; + let main = "module Main where\nimport Facade (Left(..), Right(..), a, b)\nleft :: Left\nleft = a\nright :: Right\nright = b\n"; + resolve_program_sources(&[ + ("A.purs", a), + ("B.purs", b), + ("Facade.purs", facade), + ("Main.purs", main), + ]) + .unwrap(); +} + +#[test] +fn shared_qualifiers_preserve_identity_through_facades() { + let a = "module A where\ndata Thing = Thing\nthing :: Thing\nthing = Thing\n"; + let b = "module B (module A) where\nimport A\n"; + let main = "module Main (module X, value) where\nimport A as X\nimport B as X\nvalue :: X.Thing\nvalue = X.thing\n"; + let modules = + resolve_program_sources(&[("A.purs", a), ("B.purs", b), ("Main.purs", main)]).unwrap(); + let exports = modules[2].exports.as_ref().unwrap(); + assert_eq!(exports.types.len(), 1); + assert_eq!( + exports.types[0].reference, + psrs_hir::TypeReference::Named(modules[0].types[0].id) + ); + assert_eq!( + exports + .values + .iter() + .filter(|value| value.name == "thing") + .count(), + 1 + ); +} + +#[test] +fn an_unused_shared_qualifier_does_not_create_a_conflict() { + let a = "module A where\nthing = 1\n"; + let b = "module B where\nthing = 2\n"; + let main = "module Main where\nimport A as X\nimport B as X\nmain = 42\n"; + resolve_program_sources(&[("A.purs", a), ("B.purs", b), ("Main.purs", main)]).unwrap(); +} + +#[test] +fn shared_qualifier_conflicts_are_reported_at_use_or_reexport() { + for main in [ + "module Main where\nimport A as X\nimport B as X\nmain = X.thing\n", + "module Main (module X) where\nimport A as X\nimport B as X\n", + "module Main where\nimport A as X\nimport B as X\nvalue :: X.Thing\nvalue = X.First\n", + ] { + let a = "module A where\ndata Thing = First\nthing = 1\n"; + let b = "module B where\ndata Thing = Second\nthing = 2\n"; + let errors = resolve_program_sources(&[("A.purs", a), ("B.purs", b), ("Main.purs", main)]) + .unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.diagnostic.code == Some("ScopeConflict")), + "{errors:?}" + ); + assert!( + !errors + .iter() + .any(|error| error.diagnostic.code == Some("ExportConflict")), + "{errors:?}" + ); + } +} + +#[test] +fn the_official_list_exports_resolve_with_the_locked_stdlib() { + let main = + "module Main where\nimport Data.List as L\nmain = L.length (L.fromFoldable [1,2,3])\n"; + check_program_lenient_with_prelude(&[("Main.purs", main)]).unwrap(); +} diff --git a/crates/psrs-driver/src/tests/declarations.rs b/crates/psrs-driver/src/tests/declarations/mod.rs similarity index 78% rename from crates/psrs-driver/src/tests/declarations.rs rename to crates/psrs-driver/src/tests/declarations/mod.rs index 4f6743b2..5b3693de 100644 --- a/crates/psrs-driver/src/tests/declarations.rs +++ b/crates/psrs-driver/src/tests/declarations/mod.rs @@ -1,5 +1,7 @@ use super::*; +mod import_aliases; + #[test] fn resolves_data_constructors_and_user_types() { let source = "module Main where\ndata Maybe a = Nothing | Just a\nmain = Just 1\n"; @@ -110,6 +112,53 @@ fn reports_a_transitive_export_of_an_unexported_type() { ); } +#[test] +fn an_exported_value_may_use_an_unexported_synonym_of_an_exported_type() { + let source = "module Main (step, Box) where\ndata Box = Box\ntype Inner = Box\ntype Step = Inner\nstep :: Step\nstep = Box\n"; + resolve_program_sources(&[("Main.purs", source)]) + .expect("a value signature expands local synonyms before the export check"); +} + +#[test] +fn an_exported_value_synonym_still_requires_its_hidden_type() { + let source = "module Main (step) where\ndata Hidden = Hidden\ntype Alias = Hidden\nstep :: Alias\nstep = Hidden\n"; + let errors = resolve_program_sources(&[("Main.purs", source)]).unwrap_err(); + assert!(errors.iter().any(|error| { + error.diagnostic.code == Some("TransitiveExportError") + && error.diagnostic.message.contains("Hidden") + })); + assert!( + errors + .iter() + .all(|error| !error.diagnostic.message.contains("Alias")) + ); +} + +#[test] +fn an_exported_synonym_still_requires_the_synonym_it_mentions() { + let source = "module Main (Y()) where\ntype X = Int\ntype Y = X\n"; + let errors = resolve_program_sources(&[("Main.purs", source)]).unwrap_err(); + assert!(errors.iter().any(|error| { + error.diagnostic.code == Some("TransitiveExportError") + && error.diagnostic.message.contains("`X`") + })); +} + +#[test] +fn a_quantified_synonym_body_keeps_its_hidden_type() { + let source = "module Main (value) where\ndata Pair a b = Pair a b\ntype Step a = forall r. Pair a r -> r\nvalue :: forall a. Step a\nvalue _ = 0\n"; + let errors = resolve_program_sources(&[("Main.purs", source)]).unwrap_err(); + assert!(errors.iter().any(|error| { + error.diagnostic.code == Some("TransitiveExportError") + && error.diagnostic.message.contains("Pair") + })); + assert!( + errors + .iter() + .all(|error| !error.diagnostic.message.contains("Step")) + ); +} + #[test] fn checks_an_inferred_public_result_type_after_typechecking() { let source = "module Main (value) where\ndata Hidden = Hidden\nidentity x = x\nvalue = identity Hidden\n"; @@ -178,6 +227,28 @@ fn checks_hidden_types_reached_through_an_inferred_record_field() { })); } +#[test] +fn a_hidden_constructor_does_not_require_its_field_type() { + let source = "module Main (Wrap) where\ndata Hidden = Hidden\nnewtype Wrap = Wrap Hidden\n"; + resolve_program_sources(&[("Main.purs", source)]) + .expect("a hidden constructor does not expose its field type"); +} + +#[test] +fn an_exported_constructor_requires_its_field_type() { + let source = "module Main (T(A)) where\ndata Shown = Shown\ndata Hidden = Hidden\ndata T = A Shown | B Hidden\n"; + let errors = resolve_program_sources(&[("Main.purs", source)]).unwrap_err(); + assert!(errors.iter().any(|error| { + error.diagnostic.code == Some("TransitiveExportError") + && error.diagnostic.message.contains("Shown") + })); + assert!( + errors + .iter() + .all(|error| !error.diagnostic.message.contains("Hidden")) + ); +} + #[test] fn reports_a_partial_constructor_export() { let source = "module Main (T(A)) where\ndata T = A | B\n"; diff --git a/crates/psrs-driver/src/tests/deriving/diagnostics.rs b/crates/psrs-driver/src/tests/deriving/diagnostics.rs new file mode 100644 index 00000000..c8959516 --- /dev/null +++ b/crates/psrs-driver/src/tests/deriving/diagnostics.rs @@ -0,0 +1,205 @@ +/// A deriving failure carries the official `errorCode` for its condition, not +/// one catch-all kind. This is the diagnostic half of the deriving design; the +/// official differential battery compares acceptance, not codes. +#[test] +fn deriving_failures_report_their_official_error_codes() { + let unknown_class = r#"module Main where + +class Marker a + +data Box = Box + +derive instance markerBox :: Marker Box + +main :: Int +main = 0 +"#; + let newtype_on_data = r#"module Main where + +class ToInt a where + toInt :: a -> Int + +data Box = Box Int + +derive newtype instance toIntBox :: ToInt Box + +main :: Int +main = 0 +"#; + let functor_module = r#"module Data.Functor where + +class Functor f where + map :: forall a b. (a -> b) -> f a -> f b +"#; + let functor_contravariant = r#"module Main where + +import Data.Functor (class Functor) + +data Contra a = Contra (a -> Int) + +derive instance functorContra :: Functor Contra + +main :: Int +main = 0 +"#; + let newtype_module = r#"module Data.Newtype where + +class Newtype t a +"#; + let explicit_newtype_argument = r#"module Main where + +import Data.Newtype (class Newtype) + +newtype Age = Age Int + +derive instance newtypeAge :: Newtype Age Int + +main :: Int +main = 0 +"#; + let newtype_class_on_data = r#"module Main where + +import Data.Newtype (class Newtype) + +data Box = Box Int + +derive instance newtypeBox :: Newtype Box _ + +main :: Int +main = 0 +"#; + let missing_mapping_instance_module = r#"module Data.Functor where + +class Functor f where + map :: forall a b. (a -> b) -> f a -> f b +"#; + let missing_mapping_instance = r#"module Main where + +import Data.Functor (class Functor) + +data Maybe a = Nothing | Just a + +data Box a = Box (Maybe a) + +derive instance functorBox :: Functor Box + +main :: Int +main = 0 +"#; + fn assert_code(name: &str, expected: &str, sources: &[(&str, &str)]) { + let errors = crate::check_program(sources).expect_err("deriving should be rejected"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.code == Some(expected)), + "`{name}`: expected {expected}, got {errors:?}" + ); + } + + assert_code( + "unknown class has no rule", + "CannotDerive", + &[("Main.purs", unknown_class)], + ); + assert_code( + "derive newtype on a data type", + "InvalidNewtypeInstance", + &[("Main.purs", newtype_on_data)], + ); + assert_code( + "Newtype class on a data type", + "CannotDeriveNewtypeForData", + &[ + ("Data.Newtype.purs", newtype_module), + ("Main.purs", newtype_class_on_data), + ], + ); + assert_code( + "Newtype without a wildcard", + "ExpectedWildcard", + &[ + ("Data.Newtype.purs", newtype_module), + ("Main.purs", explicit_newtype_argument), + ], + ); + assert_code( + "field head with no mapping instance", + "CannotDeriveInvalidConstructorArg", + &[ + ("Data.Functor.purs", missing_mapping_instance_module), + ("Main.purs", missing_mapping_instance), + ], + ); + assert_code( + "Functor with a contravariant field", + "CannotDeriveInvalidConstructorArg", + &[ + ("Data.Functor.purs", functor_module), + ("Main.purs", functor_contravariant), + ], + ); +} + +#[test] +fn fold_deriving_reports_missing_core_values_at_the_declaration() { + for (module, class, method, parameters, fields) in [ + ("Data.Foldable", "Foldable", "foldMap", "a", "a a"), + ("Data.Bifoldable", "Bifoldable", "bifoldMap", "a b", "a b"), + ("Data.Foldable", "Foldable", "foldMap", "a", ""), + ("Data.Bifoldable", "Bifoldable", "bifoldMap", "a b", ""), + ] { + let signature = if class == "Foldable" { + "forall a m. (a -> m) -> t a -> m" + } else { + "forall a b m. (a -> m) -> (b -> m) -> t a b -> m" + }; + let library = + format!("module {module} where\nclass {class} t where\n {method} :: {signature}\n"); + let main = format!( + "module Main where\nimport {module}\ndata Box {parameters} = Box {fields}\nderive instance foldBox :: {class} Box\nmain = 0\n" + ); + let errors = crate::check_program(&[("Fold.purs", &library), ("Main.purs", &main)]) + .expect_err("missing fold operations must reject deriving"); + let error = errors + .iter() + .find(|error| error.diagnostic.code == Some("CannotFindDerivingType")) + .unwrap_or_else(|| panic!("{class}, fields `{fields}`: {errors:?}")); + let span = error.diagnostic.span; + let declaration = &main[span.start as usize..span.end as usize]; + assert!( + declaration.contains("derive instance foldBox"), + "{declaration}" + ); + let operation = if fields.is_empty() { + "mempty" + } else { + "append" + }; + assert!(error.diagnostic.message.contains(operation)); + } +} + +#[test] +fn deriving_rejects_non_constructor_heads_and_invalid_class_arity() { + for (library, main, expected) in [ + ( + "module Data.Eq where\nclass Eq a where\n eq :: a -> a -> Boolean\n", + "module Main where\nimport Data.Eq\nderive instance eqInt :: Eq Int\nmain = 0\n", + "ExpectedTypeConstructor", + ), + ( + "module Data.Eq where\nclass Eq a b where\n eq :: a -> b -> Boolean\n", + "module Main where\nimport Data.Eq\ndata Box = Box\nderive instance eqBox :: Eq Box Box\nmain = 0\n", + "InvalidDerivedInstance", + ), + ] { + let errors = crate::check_program(&[("Data.Eq.purs", library), ("Main.purs", main)]) + .expect_err("invalid deriving head should be rejected"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.code == Some(expected)), + "{errors:?}" + ); + } +} diff --git a/crates/psrs-driver/src/tests/deriving.rs b/crates/psrs-driver/src/tests/deriving/mod.rs similarity index 51% rename from crates/psrs-driver/src/tests/deriving.rs rename to crates/psrs-driver/src/tests/deriving/mod.rs index 1262f929..8c89ef94 100644 --- a/crates/psrs-driver/src/tests/deriving.rs +++ b/crates/psrs-driver/src/tests/deriving/mod.rs @@ -1,3 +1,6 @@ +mod diagnostics; +mod traversals; + #[test] fn derives_newtype_methods_from_the_wrapped_instance() { let sources = [( @@ -83,6 +86,37 @@ main = toInt (Age 42) true }); } +#[test] +fn newtype_deriving_reuses_the_dictionary_under_an_unknown_type_constructor() { + // `Traversable`'s `sequence` / `traverse` quantify an unknown `m`. + // Ordinary `Coercible` cannot lift `NonEmptyArray` ~ `Array` under that + // `m`. Deriving still accepts the instance by reusing `Array`'s dictionary + // after the newtype and underlying-instance checks. + let source = r#"module Main where + +class Applicative m where + pure :: forall a. a -> m a + +class Sequence t where + echo :: forall f a. f (t a) -> f (t a) + run :: forall a m. Applicative m => (t a -> m (t a)) -> t a -> m (t a) + +instance sequenceArray :: Sequence Array where + echo value = value + run f xs = f xs + +newtype NonEmptyArray a = NonEmptyArray (Array a) + +derive newtype instance sequenceNonEmpty :: Sequence NonEmptyArray + +main :: Int +main = 0 +"#; + crate::check_program(&[("Main.purs", source)]).unwrap_or_else(|errors| { + panic!("newtype deriving should reuse the wrapped dictionary: {errors:?}") + }); +} + #[test] fn rejects_newtype_deriving_for_a_data_declaration() { let sources = [( @@ -176,6 +210,149 @@ main = case compare (Pair Low High) (Pair Low Low) of .unwrap_or_else(|errors| panic!("derived Ord should type check: {errors:?}")); } +#[test] +fn derives_eq1_and_ord1_by_delegating_to_the_monomorphic_method() { + let eq = r#"module Data.Eq where + +class Eq a where + eq :: a -> a -> Boolean + +class Eq1 f where + eq1 :: forall a. Eq a => f a -> f a -> Boolean +"#; + let ordering = r#"module Data.Ordering where + +data Ordering = LT | EQ | GT +"#; + let ord = r#"module Data.Ord where + +import Data.Ordering + +class Ord a where + compare :: a -> a -> Ordering + +class Ord1 f where + compare1 :: forall a. Ord a => f a -> f a -> Ordering +"#; + let main = r#"module Main where + +import Data.Eq +import Data.Ord +import Data.Ordering + +data Maybe a = Nothing | Just a + +derive instance eqMaybe :: Eq a => Eq (Maybe a) +derive instance eq1Maybe :: Eq1 Maybe + +derive instance ordMaybe :: Ord a => Ord (Maybe a) +derive instance ord1Maybe :: Ord1 Maybe + +main :: Int +main = 0 +"#; + crate::check_program(&[ + ("Data.Eq.purs", eq), + ("Data.Ordering.purs", ordering), + ("Data.Ord.purs", ord), + ("Main.purs", main), + ]) + .unwrap_or_else(|errors| panic!("Eq1/Ord1 deriving should type check: {errors:?}")); +} + +#[test] +fn derives_generic_representation_for_a_data_type() { + let generic_rep = r#"module Data.Generic.Rep where + +data NoConstructors + +data NoArguments = NoArguments + +newtype Argument a = Argument a + +data Product a b = Product a b + +data Sum a b = Inl a | Inr b + +newtype Constructor (name :: Symbol) a = Constructor a + +class Generic t rep | t -> rep where + from :: t -> rep + to :: rep -> t +"#; + let main = r#"module Main where + +import Data.Generic.Rep + +data Maybe a = Nothing | Just a + +derive instance genericMaybe :: Generic (Maybe a) _ + +main :: Int +main = 0 +"#; + crate::check_program(&[("Data.Generic.Rep.purs", generic_rep), ("Main.purs", main)]) + .unwrap_or_else(|errors| panic!("Generic deriving should type check: {errors:?}")); +} + +#[test] +fn derives_newtype_class_for_a_newtype_with_a_wildcard() { + let newtype_module = r#"module Data.Newtype where + +class Newtype t a +"#; + let main = r#"module Main where + +import Data.Newtype (class Newtype) + +newtype Age = Age Int + +derive instance newtypeAge :: Newtype Age _ + +main :: Int +main = 0 +"#; + crate::check_program(&[("Data.Newtype.purs", newtype_module), ("Main.purs", main)]) + .unwrap_or_else(|errors| panic!("Newtype wildcard deriving should type check: {errors:?}")); +} + +#[test] +fn derives_profunctor_through_a_contravariant_field() { + let profunctor = r#"module Data.Profunctor where + +class Profunctor p where + dimap :: forall a b c d. (a -> b) -> (c -> d) -> p b c -> p a d +"#; + let contravariant = r#"module Data.Functor.Contravariant where + +class Contravariant f where + cmap :: forall a b. (b -> a) -> f a -> f b +"#; + let main = r#"module Main where + +import Data.Profunctor (class Profunctor) +import Data.Functor.Contravariant (class Contravariant) + +newtype Predicate a = Predicate (a -> Boolean) + +instance contravariantPredicate :: Contravariant Predicate where + cmap f (Predicate g) = Predicate (\x -> g (f x)) + +data P a b = P (Predicate a) b + +derive instance profunctorP :: Profunctor P + +main :: Int +main = 0 +"#; + crate::check_program(&[ + ("Data.Profunctor.purs", profunctor), + ("Data.Functor.Contravariant.purs", contravariant), + ("Main.purs", main), + ]) + .unwrap_or_else(|errors| panic!("Profunctor deriving should type check: {errors:?}")); +} + #[test] fn derives_newtype_for_a_partially_applied_type_constructor() { let source = r#"module Main where diff --git a/crates/psrs-driver/src/tests/deriving/traversals.rs b/crates/psrs-driver/src/tests/deriving/traversals.rs new file mode 100644 index 00000000..44361537 --- /dev/null +++ b/crates/psrs-driver/src/tests/deriving/traversals.rs @@ -0,0 +1,191 @@ +#[test] +fn derives_bitraversable_for_a_two_parameter_type() { + let function = r#"module Data.Function where + +identity :: forall a. a -> a +identity x = x +"#; + let functor = r#"module Data.Functor where + +class Functor f where + map :: forall a b. (a -> b) -> f a -> f b +"#; + let apply = r#"module Control.Apply where + +import Data.Functor + +class Functor f <= Apply f where + apply :: forall a b. f (a -> b) -> f a -> f b +"#; + let applicative = r#"module Control.Applicative where + +import Control.Apply + +class Apply f <= Applicative f where + pure :: forall a. a -> f a +"#; + let bitraversable = r#"module Data.Bitraversable where + +import Control.Applicative (class Applicative) + +class Bitraversable t where + bitraverse :: forall f a b c d. Applicative f => (a -> f c) -> (b -> f d) -> t a b -> f (t c d) + bisequence :: forall f a b. Applicative f => t (f a) (f b) -> f (t a b) +"#; + let main = r#"module Main where + +import Data.Function (identity) +import Data.Bitraversable + +data P a b = P a b + +derive instance bitraversableP :: Bitraversable P + +main :: Int +main = 0 +"#; + crate::check_program(&[ + ("Data.Function.purs", function), + ("Data.Functor.purs", functor), + ("Control.Apply.purs", apply), + ("Control.Applicative.purs", applicative), + ("Data.Bitraversable.purs", bitraversable), + ("Main.purs", main), + ]) + .unwrap_or_else(|errors| panic!("Bitraversable deriving should type check: {errors:?}")); +} + +#[test] +fn derives_traversable_for_single_field_constructors() { + let function = r#"module Data.Function where + +identity :: forall a. a -> a +identity x = x +"#; + let functor = r#"module Data.Functor where + +class Functor f where + map :: forall a b. (a -> b) -> f a -> f b +"#; + let apply = r#"module Control.Apply where + +import Data.Functor + +class Functor f <= Apply f where + apply :: forall a b. f (a -> b) -> f a -> f b +"#; + let applicative = r#"module Control.Applicative where + +import Control.Apply + +class Apply f <= Applicative f where + pure :: forall a. a -> f a +"#; + let traversable = r#"module Data.Traversable where + +import Control.Applicative (class Applicative) + +class Traversable t where + traverse :: forall a b f. Applicative f => (a -> f b) -> t a -> f (t b) + sequence :: forall a f. Applicative f => t (f a) -> f (t a) +"#; + let main = r#"module Main where + +import Data.Function (identity) +import Data.Traversable + +data Box a = Box a + +derive instance traversableBox :: Traversable Box + +main :: Int +main = 0 +"#; + crate::check_program(&[ + ("Data.Function.purs", function), + ("Data.Functor.purs", functor), + ("Control.Apply.purs", apply), + ("Control.Applicative.purs", applicative), + ("Data.Traversable.purs", traversable), + ("Main.purs", main), + ]) + .unwrap_or_else(|errors| panic!("Traversable deriving should type check: {errors:?}")); +} + +#[test] +fn derives_foldable_for_single_field_constructors() { + let monoid = r#"module Data.Monoid where + +class Monoid a where + mempty :: a +"#; + let foldable = r#"module Data.Foldable where + +import Data.Monoid + +class Foldable t where + foldr :: forall a b. (a -> b -> b) -> b -> t a -> b + foldl :: forall a b. (b -> a -> b) -> b -> t a -> b + foldMap :: forall a m. Monoid m => (a -> m) -> t a -> m +"#; + let main = r#"module Main where + +import Data.Foldable + +data Box a = Box a + +derive instance foldableBox :: Foldable Box + +main :: Int +main = 0 +"#; + crate::check_program(&[ + ("Data.Monoid.purs", monoid), + ("Data.Foldable.purs", foldable), + ("Main.purs", main), + ]) + .unwrap_or_else(|errors| panic!("Foldable deriving should type check: {errors:?}")); +} + +#[test] +fn derives_bifoldable_for_a_two_parameter_type() { + let semigroup = r#"module Data.Semigroup where + +class Semigroup a where + append :: a -> a -> a +"#; + let monoid = r#"module Data.Monoid where + +import Data.Semigroup + +class Semigroup a <= Monoid a where + mempty :: a +"#; + let bifoldable = r#"module Data.Bifoldable where + +import Data.Monoid + +class Bifoldable p where + bifoldr :: forall a b c. (a -> c -> c) -> (b -> c -> c) -> c -> p a b -> c + bifoldl :: forall a b c. (c -> a -> c) -> (c -> b -> c) -> c -> p a b -> c + bifoldMap :: forall m a b. Monoid m => (a -> m) -> (b -> m) -> p a b -> m +"#; + let main = r#"module Main where + +import Data.Bifoldable + +data P a b = P a b + +derive instance bifoldableP :: Bifoldable P + +main :: Int +main = 0 +"#; + crate::check_program(&[ + ("Data.Semigroup.purs", semigroup), + ("Data.Monoid.purs", monoid), + ("Data.Bifoldable.purs", bifoldable), + ("Main.purs", main), + ]) + .unwrap_or_else(|errors| panic!("Bifoldable deriving should type check: {errors:?}")); +} diff --git a/crates/psrs-driver/src/tests/diagnosis_trace.rs b/crates/psrs-driver/src/tests/diagnosis_trace.rs new file mode 100644 index 00000000..1f633a00 --- /dev/null +++ b/crates/psrs-driver/src/tests/diagnosis_trace.rs @@ -0,0 +1,207 @@ +use super::*; + +#[test] +fn traced_and_untraced_compilation_produce_the_same_artifact_without_default_dumps() { + let sources = [("Main.purs", "module Main where\nmain = 42\n")]; + let ordinary = + compile_program_sources_with_prelude(&sources).expect("ordinary compile should succeed"); + let diagnosis = compile_program_sources_with_prelude_diagnosis(&sources, false); + let traced = diagnosis + .artifact + .expect("diagnosis compile should succeed"); + + assert_eq!(ordinary.wasm, traced.wasm); + assert_eq!(ordinary.wat, traced.wat); + assert_eq!(diagnosis.dumps, PartialIrDumps::default()); + assert!(diagnosis.backend_trace.is_some()); + assert_eq!( + diagnosis + .frontend_trace + .as_ref() + .and_then(|frontend| frontend.output_core), + diagnosis + .backend_trace + .as_ref() + .map(|trace| trace.initial_core), + ); +} + +#[test] +fn external_binding_artifact_flows_into_validation_and_cc() { + let source = "module Main where\nimport Prelude\nimport Effect (Effect)\nimport WASI.Console\nmain :: Effect Unit\nmain = log \"trace\"\n"; + let report = compile_program_sources_with_prelude_diagnosis(&[("Main.purs", source)], false); + assert!( + report.artifact.is_some(), + "effect program should compile: {:?}", + report.diagnostics + ); + let trace = report.backend_trace.expect("backend pass trace"); + let effect = trace + .executions + .iter() + .find(|execution| execution.pass_key == "backend.external_bindings.extract") + .expect("external bindings are extracted"); + assert_eq!(effect.status, TracePassStatus::Completed); + let bindings = *effect + .outputs + .first() + .expect("extract pass outputs bindings"); + let conformance = trace + .executions + .iter() + .find(|execution| execution.pass_key == "backend.external_bindings.validate_conformance") + .expect("binding conformance is observed"); + let cc = trace + .executions + .iter() + .find(|execution| execution.pass_key == "backend.cc.lower") + .expect("CC lowering is observed"); + assert!(conformance.inputs.contains(&bindings)); + assert!(cc.inputs.contains(&bindings)); + assert!(trace.edges.iter().any(|edge| { + edge.execution == effect.id + && edge.artifact == bindings + && edge.role == TraceEdgeRole::Output + })); +} + +#[test] +fn trace_dump_references_name_the_exact_produced_artifacts() { + let source = "module Main where\nmain = 42\n"; + let report = compile_program_sources_with_prelude_diagnosis(&[("Main.purs", source)], true); + assert!(report.artifact.is_some()); + assert!( + report + .dumps + .core + .as_ref() + .is_some_and(|dump| !dump.is_empty()) + ); + assert!( + report + .dumps + .cc + .as_ref() + .is_some_and(|dump| !dump.is_empty()) + ); + assert!( + report + .dumps + .mir + .as_ref() + .is_some_and(|dump| !dump.is_empty()) + ); + let trace = report.backend_trace.expect("backend pass trace"); + let artifacts = report.dump_artifacts; + for id in [artifacts.core, artifacts.cc, artifacts.mir] + .into_iter() + .flatten() + { + assert!(trace.artifacts.iter().any(|artifact| artifact.id == id)); + } + assert_eq!( + trace + .executions + .iter() + .find(|execution| execution.pass_key == "backend.core.optimize") + .and_then(|execution| execution.outputs.first().copied()), + artifacts.core, + ); +} + +#[test] +fn frontend_rejection_has_diagnostics_but_no_backend_trace_or_core_output() { + let report = compile_program_sources_with_prelude_diagnosis( + &[("Main.purs", "module Main where\nmain = @\n")], + false, + ); + assert!(report.artifact.is_none()); + assert!(report.backend_trace.is_none()); + assert!(!report.diagnostics.is_empty()); + let frontend = report.frontend_trace.expect("frontend attempt is observed"); + assert_eq!(frontend.status, TracePassStatus::Rejected); + assert!(frontend.output_core.is_none()); + assert_eq!(frontend.diagnostic_indices.len(), report.diagnostics.len()); +} + +#[test] +fn target_plan_records_provider_and_memory_lineage() { + let source = "module Main where\nmain = 42\n"; + let report = compile_program_sources_with_prelude_diagnosis(&[("Main.purs", source)], false); + assert!( + report.artifact.is_some(), + "program should compile: {:?}", + report.diagnostics + ); + let trace = report.backend_trace.expect("backend pass trace"); + let plan = trace + .executions + .iter() + .find(|execution| execution.pass_key == "backend.target.plan") + .expect("the checked plan is an observed execution"); + assert_eq!(plan.status, TracePassStatus::Completed); + let parameter = |key: &str| { + plan.parameters + .iter() + .find(|parameter| parameter.key == key) + .map(|parameter| parameter.value.as_str()) + }; + assert_eq!(parameter("artifacts"), Some("0")); + assert_eq!(parameter("heap_start"), Some("16")); + assert!( + parameter("selected_providers") + .expect("selected providers are recorded") + .contains("wasi:cli/exit"), + "{:?}", + plan.parameters + ); + assert!( + parameter("external_world") + .expect("the external world is recorded") + .contains("wasi:cli/exit"), + ); + assert!(trace.artifacts.iter().any(|artifact| { + artifact.representation == TraceRepresentation::LinkPlan + && artifact.state == TraceArtifactState::Produced + })); +} + +#[test] +fn target_rejection_keeps_prior_artifacts_and_maps_errors_to_its_execution() { + let prepared = crate::prepare_sources(&[("Main.purs", "module Main where\nmain = 42\n")]) + .expect("frontend should produce checked Core"); + let target = psrs_backend::TargetCapabilities { + component_model: false, + ..Default::default() + }; + let traced = psrs_backend::compile_with_context_traced(prepared.core, target, false); + let failure = traced + .result + .expect_err("target profile should be rejected"); + let target_execution = traced + .trace + .executions + .iter() + .find(|execution| execution.pass_key == "backend.target.requirements") + .expect("the target check is an observed execution"); + assert_eq!(target_execution.status, TracePassStatus::Rejected); + assert_eq!(target_execution.outputs, Vec::::new()); + assert!(!target_execution.diagnostic_indices.is_empty()); + assert!( + target_execution + .diagnostic_indices + .iter() + .all(|index| *index < failure.errors.len()) + ); + assert!(traced.trace.artifacts.iter().any(|artifact| { + artifact.representation == TraceRepresentation::Mir + && artifact.state == TraceArtifactState::Produced + })); + assert!( + !traced + .trace + .artifacts + .iter() + .any(|artifact| { artifact.representation == TraceRepresentation::WasmModule }) + ); +} diff --git a/crates/psrs-driver/src/tests/dictionary_audit/execution.rs b/crates/psrs-driver/src/tests/dictionary_audit/execution.rs index d49f763b..bfeab79d 100644 --- a/crates/psrs-driver/src/tests/dictionary_audit/execution.rs +++ b/crates/psrs-driver/src/tests/dictionary_audit/execution.rs @@ -78,7 +78,7 @@ fn recursive_instance_context_executes() { /// are its dictionaries, in declared order, before the ordinary argument. #[test] fn constrained_dictionary_parameters_precede_ordinary_arguments() { - let (fixture, entry) = ordered_dictionaries_module(); + let (fixture, entry) = ordered_dictionaries_module(false); let mut core = psrs_core::lower_module(fixture).expect("ordered dictionary fixture lowers"); core.entry = Some(entry); let declaration = core @@ -98,9 +98,10 @@ fn constrained_dictionary_parameters_precede_ordinary_arguments() { "dictionary parameters must lead the ordinary argument" ); - let (fixture, entry) = ordered_dictionaries_module(); - let stages = compile_fixture("ordered_dictionaries", fixture, entry); - let cc_function = stages + core.verify().expect("ordered dictionary Core verifies"); + let backend_input = + psrs_backend::cc::lower_module(core).expect("ordered dictionary Core lowers to CC"); + let cc_function = backend_input .cc .functions .iter() @@ -130,16 +131,44 @@ fn constrained_dictionary_parameters_precede_ordinary_arguments() { psrs_backend::cc::ValueShape::Integer, "the ordinary argument must follow the dictionaries" ); - let mir_function = stages - .mir + // Optimization may inline and remove this function. Inspect the calling + // convention at the lowering boundary where it is established. + let (mir, _) = psrs_backend::mir::lower_module_with_bindings( + backend_input.cc, + backend_input.externals, + psrs_backend::TargetCapabilities::default(), + ) + .expect("ordered dictionary CC lowers to MIR"); + let mir_function = mir .functions .iter() .find(|function| function.name == "constrained") .expect("MIR constrained function"); - assert_eq!(mir_function.parameters.len(), 3); + let parameter_types = mir_function + .parameters + .iter() + .map(|parameter| { + &mir_function + .values + .iter() + .find(|value| value.id == *parameter) + .expect("declared MIR parameter value") + .ty + }) + .collect::>(); + assert_eq!(parameter_types.len(), 3); + assert!( + parameter_types[..2] + .iter() + .all(|ty| matches!(ty, psrs_backend::types::ValueType::Ref(_))), + "dictionary references must lead the MIR parameters: {parameter_types:?}" + ); + assert_eq!(*parameter_types[2], psrs_backend::types::ValueType::I32); - let (fixture, entry) = ordered_dictionaries_module(); - expect_fixture_exit("ordered_dictionaries", fixture, entry, 42); + for (second_accepts, expected) in [(false, 42), (true, 0)] { + let (fixture, entry) = ordered_dictionaries_module(second_accepts); + expect_fixture_exit("ordered_dictionaries", fixture, entry, expected); + } } /// DICT-06, DICT-10: a `let`-bound dictionary is constructed once in the diff --git a/crates/psrs-driver/src/tests/dictionary_audit/fixtures/methods.rs b/crates/psrs-driver/src/tests/dictionary_audit/fixtures/methods.rs index cc13a951..60acdd13 100644 --- a/crates/psrs-driver/src/tests/dictionary_audit/fixtures/methods.rs +++ b/crates/psrs-driver/src/tests/dictionary_audit/fixtures/methods.rs @@ -322,6 +322,7 @@ pub(crate) fn recursive_instance_module() -> (thir::Module, SymbolId) { ); let intrinsic = |symbol, name: &str, intrinsic| ExternalSymbol { + source_module: None, symbol, name: name.into(), kind: ExternalKind::Intrinsic(intrinsic), @@ -332,8 +333,8 @@ pub(crate) fn recursive_instance_module() -> (thir::Module, SymbolId) { id: module_id, name: "Main".into(), externals: vec![ - intrinsic(int_sub, "intSub", Intrinsic::I32Sub), - intrinsic(int_le, "intLe", Intrinsic::I32LeS), + intrinsic(int_sub, "intSub", Intrinsic::IntSub), + intrinsic(int_le, "intLe", Intrinsic::IntLe), ], external_types: Vec::new(), types, diff --git a/crates/psrs-driver/src/tests/dictionary_audit/fixtures/ordering.rs b/crates/psrs-driver/src/tests/dictionary_audit/fixtures/ordering.rs index bb595aa7..43ce60d2 100644 --- a/crates/psrs-driver/src/tests/dictionary_audit/fixtures/ordering.rs +++ b/crates/psrs-driver/src/tests/dictionary_audit/fixtures/ordering.rs @@ -7,8 +7,9 @@ use psrs_thir as thir; /// A constrained binding with two dictionary parameters ahead of its ordinary /// argument: `f :: A -> B -> Int -> Int`. The dictionary order is observable -/// because each dictionary uses a distinct method label. -pub(crate) fn ordered_dictionaries_module() -> (thir::Module, SymbolId) { +/// because the first method returns true while the second returns the supplied +/// Boolean. Both dictionary arguments contribute to the result. +pub(crate) fn ordered_dictionaries_module(second_accepts: bool) -> (thir::Module, SymbolId) { let module_id = ModuleId(0); let main = SymbolId::new(module_id, 0); let a_dict = SymbolId::new(module_id, 1); @@ -54,12 +55,53 @@ pub(crate) fn ordered_dictionaries_module() -> (thir::Module, SymbolId) { let b_dict_value = typed( thir::ExprKind::Record(vec![( "b".into(), - typed(thir::ExprKind::Global(is_positive), method, span), + typed( + thir::ExprKind::Lambda { + binder: binder(0, "value", integer, span), + body: Box::new(typed( + thir::ExprKind::Boolean(second_accepts), + boolean, + span, + )), + }, + method, + span, + ), )]), dict_b, span, ); - // `\da -> \db -> \x -> if da.a x then x else 0` + let method_call = |local, dictionary_type, field: &str| { + typed( + thir::ExprKind::Application( + Box::new(typed( + thir::ExprKind::FieldAccess { + expression: Box::new(typed( + thir::ExprKind::Local(LocalId(local)), + dictionary_type, + span, + )), + field: field.into(), + }, + method, + span, + )), + Box::new(typed(thir::ExprKind::Local(LocalId(2)), integer, span)), + ), + boolean, + span, + ) + }; + let second_check = typed( + thir::ExprKind::If { + condition: Box::new(method_call(1, dict_b, "b")), + then_branch: Box::new(typed(thir::ExprKind::Integer(0), integer, span)), + else_branch: Box::new(typed(thir::ExprKind::Local(LocalId(2)), integer, span)), + }, + integer, + span, + ); + // `\da -> \db -> \x -> if da.a x then (if db.b x then 0 else x) else 0` let constrained_value = typed( thir::ExprKind::Lambda { binder: binder(0, "da", dict_a, span), @@ -71,34 +113,8 @@ pub(crate) fn ordered_dictionaries_module() -> (thir::Module, SymbolId) { binder: binder(2, "x", integer, span), body: Box::new(typed( thir::ExprKind::If { - condition: Box::new(typed( - thir::ExprKind::Application( - Box::new(typed( - thir::ExprKind::FieldAccess { - expression: Box::new(typed( - thir::ExprKind::Local(LocalId(0)), - dict_a, - span, - )), - field: "a".into(), - }, - method, - span, - )), - Box::new(typed( - thir::ExprKind::Local(LocalId(2)), - integer, - span, - )), - ), - boolean, - span, - )), - then_branch: Box::new(typed( - thir::ExprKind::Local(LocalId(2)), - integer, - span, - )), + condition: Box::new(method_call(0, dict_a, "a")), + then_branch: Box::new(second_check), else_branch: Box::new(typed( thir::ExprKind::Integer(0), integer, diff --git a/crates/psrs-driver/src/tests/dictionary_audit/negative.rs b/crates/psrs-driver/src/tests/dictionary_audit/negative.rs index 27f37ea7..28b3fae8 100644 --- a/crates/psrs-driver/src/tests/dictionary_audit/negative.rs +++ b/crates/psrs-driver/src/tests/dictionary_audit/negative.rs @@ -35,11 +35,14 @@ impl Types { thir::Type::RowEmpty, thir::Type::RowExtend { label: "isPositive".into(), - ty: self.method, + ty: thir::TypeId(11), tail: thir::TypeId(5), }, thir::Type::Constructor(thir::TypeConstructor::Record), thir::Type::Application(thir::TypeId(7), thir::TypeId(6)), + thir::Type::Constructor(thir::TypeConstructor::Unit), + thir::Type::Application(thir::TypeId(2), thir::TypeId(9)), + thir::Type::Application(thir::TypeId(10), self.method), ] } } diff --git a/crates/psrs-driver/src/tests/effect_arity.rs b/crates/psrs-driver/src/tests/effect_arity.rs deleted file mode 100644 index 88480fbf..00000000 --- a/crates/psrs-driver/src/tests/effect_arity.rs +++ /dev/null @@ -1,166 +0,0 @@ -//! Calling-convention evidence for the effect representation. -//! -//! `Effect τ` is a one-parameter closure. A source arrow such as -//! `String -> Effect Unit` stays saturated at that arrow; the token is not -//! another parameter of `log`. - -use super::run_with_wasmtime; - -#[test] -fn an_effect_of_a_function_is_not_arity_two_and_log_is_saturated() { - let source = "module Main where\nimport Prelude\nimport WASI.Clock\nimport WASI.Console\nmk :: Effect (Int -> Int)\nmk = pure (\\x -> x + 1)\nmain = let stored = now in let message = log \"message\" in runEffect (bind mk (\\f -> pure (f 41)))\n"; - let stages = crate::compile_main_stages(source).expect("Core lowers through CC"); - let function = |name: &str| { - stages - .cc - .functions - .iter() - .find(|function| function.name == name) - .unwrap_or_else(|| panic!("missing function {name}")) - }; - assert_eq!( - function("log").parameters.len(), - 1, - "log keeps the source arrow and leaves the token on the returned closure" - ); - assert_eq!( - function("now").parameters.len(), - 1, - "Effect Int is a one-parameter closure" - ); - assert_eq!( - function("pure").parameters.len(), - 1, - "pure takes the value and returns the token closure" - ); - assert_eq!( - function("bind").parameters.len(), - 2, - "bind takes the effect and the continuation; the token belongs to the result" - ); - assert_ne!( - function("mk").parameters.len(), - 2, - "an alias of Effect (Int -> Int) is not a two-parameter function" - ); - let log_symbol = function("log").symbol; - let log_calls = stages - .cc - .functions - .iter() - .flat_map(|function| direct_calls(&function.assignments)) - .filter(|(symbol, _)| *symbol == log_symbol) - .map(|(_, arguments)| arguments) - .collect::>(); - assert!( - !log_calls.is_empty() && log_calls.iter().all(|count| *count == 1), - "log \"message\" is a saturated one-argument call, got {log_calls:?}" - ); - let Some(output) = run_with_wasmtime(source) else { - eprintln!("skipping execution: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(42), "{output:?}"); - assert!( - output.stdout.is_empty(), - "storing log \"message\" must not run it: {output:?}" - ); -} - -/// `pick :: Boolean -> String -> Effect Unit` has two source parameters. Supplying -/// only the Boolean captures that argument once and returns `String -> Effect Unit`. -/// The token stays on the effect that `pick` returns, and the write waits until -/// that effect is run. -#[test] -fn a_partial_source_application_captures_once_and_defers_the_effect() { - let stored = "module Main where\nimport Prelude\nimport WASI.Console\npick :: Boolean -> String -> Effect Unit\npick choice message = if choice then log message else log \"other\"\nmain = let partial = pick true in 0\n"; - let stages = crate::compile_main_stages(stored).expect("Core lowers through CC"); - let pick = stages - .cc - .functions - .iter() - .find(|function| function.name == "pick") - .expect("pick"); - assert_eq!( - pick.parameters.len(), - 2, - "the token is not a parameter of pick" - ); - let partials = stages - .cc - .functions - .iter() - .filter(|function| { - function.name.starts_with("partial_") - && direct_calls(&function.assignments) - .iter() - .any(|(symbol, arguments)| *symbol == pick.symbol && *arguments == 2) - }) - .collect::>(); - assert_eq!(partials.len(), 1, "one partial application of pick"); - assert_eq!( - partials[0].parameters.len(), - 2, - "the partial keeps the remaining String and the closure environment" - ); - let captures = partials[0] - .assignments - .iter() - .filter(|assignment| { - matches!( - assignment.kind, - psrs_backend::cc::AssignmentKind::ClosureGetCapture { index: 0, .. } - ) - }) - .count(); - assert_eq!(captures, 1, "the supplied Boolean is captured once"); - let Some(stored_output) = run_with_wasmtime(stored) else { - eprintln!("skipping execution: wasmtime is not installed"); - return; - }; - assert_eq!(stored_output.status.code(), Some(0), "{stored_output:?}"); - assert!( - stored_output.stdout.is_empty(), - "building the partial application must not run the effect: {stored_output:?}" - ); - - let ran = "module Main where\nimport Prelude\nimport WASI.Console\npick :: Boolean -> String -> Effect Unit\npick choice message = if choice then log message else log \"other\"\nmain = let partial = pick true in let first = runEffect (partial \"again\") in let second = runEffect (partial \"again\") in 0\n"; - let Some(ran_output) = run_with_wasmtime(ran) else { - eprintln!("skipping execution: wasmtime is not installed"); - return; - }; - assert_eq!(ran_output.status.code(), Some(0), "{ran_output:?}"); - assert_eq!(ran_output.stdout, b"again\nagain\n", "{ran_output:?}"); -} - -fn direct_calls(assignments: &[psrs_backend::cc::Assignment]) -> Vec<(psrs_hir::SymbolId, usize)> { - let mut found = Vec::new(); - for assignment in assignments { - match &assignment.kind { - psrs_backend::cc::AssignmentKind::DirectCall { - function, - arguments, - } => found.push((*function, arguments.len())), - psrs_backend::cc::AssignmentKind::If { - then_assignments, - else_assignments, - .. - } => { - found.extend(direct_calls(then_assignments)); - found.extend(direct_calls(else_assignments)); - } - psrs_backend::cc::AssignmentKind::TagSwitch { - cases, - default_assignments, - .. - } => { - for case in cases { - found.extend(direct_calls(&case.assignments)); - } - found.extend(direct_calls(default_assignments)); - } - _ => {} - } - } - found -} diff --git a/crates/psrs-driver/src/tests/effects/contract.rs b/crates/psrs-driver/src/tests/effects/contract.rs deleted file mode 100644 index 2cd8bd8b..00000000 --- a/crates/psrs-driver/src/tests/effects/contract.rs +++ /dev/null @@ -1,187 +0,0 @@ -use psrs_core::effect::{EffectCommandEntry, EffectCompilation, EffectOperation}; - -fn fixture() -> (psrs_core::Module, EffectCompilation) { - let lowered = crate::lower_source_with_prelude_to_core( - "Main.purs", - "module Main where\nimport Prelude\nmain = 0\n", - ) - .unwrap(); - let core = lowered.core; - let context = lowered.effect_context; - ( - core, - context.expect("embedded Prelude supplies the trusted contract"), - ) -} - -fn rejects(core: psrs_core::Module, context: EffectCompilation, message: &str) { - let errors = psrs_backend::compile_with_context( - core, - Some(context), - psrs_backend::TargetCapabilities::default(), - ) - .expect_err("a malformed explicit Effect contract must fail before encoding"); - assert!( - errors.iter().any(|error| error.message.contains(message)), - "{errors:?}" - ); -} - -#[test] -fn the_backend_rejects_partial_and_duplicate_trusted_operation_metadata() { - let (core, mut context) = fixture(); - context - .trusted - .operations - .retain(|binding| binding.operation != EffectOperation::Trap); - rejects(core, context, "missing a required operation binding"); - - let (core, mut context) = fixture(); - context - .trusted - .operations - .push(context.trusted.operations[0]); - rejects(core, context, "duplicated"); -} - -#[test] -fn the_backend_rejects_operation_identity_and_checked_signature_mismatches() { - let (core, mut context) = fixture(); - context.trusted.operations[0].symbol = core.entry.unwrap(); - rejects(core, context, "no external declaration"); - - let (mut core, context) = fixture(); - let pure = context - .trusted - .operations - .iter() - .find(|binding| binding.operation == EffectOperation::Pure) - .unwrap() - .symbol; - let integer = psrs_core::TypeId( - core.types - .iter() - .position(|ty| { - matches!( - ty, - psrs_core::Type::Constructor(psrs_core::TypeConstructor::Int) - ) - }) - .unwrap() as u32, - ); - core.external_types - .iter_mut() - .find(|external| external.symbol == pure) - .unwrap() - .ty = integer; - rejects(core, context, "invalid checked source signature"); -} - -#[test] -fn the_backend_rejects_an_effect_entry_context_for_an_integer_source_entry() { - let (core, mut context) = fixture(); - context.command_entry = Some(EffectCommandEntry { - symbol: core.entry.unwrap(), - }); - rejects(core, context, "must have type Effect Unit"); -} - -fn action_fixture() -> (psrs_core::Module, EffectCompilation) { - let lowered = crate::lower_source_with_prelude_to_core( - "Main.purs", - "module Main where\nimport Prelude\nmain :: Effect Unit\nmain = pure unit\n", - ) - .unwrap(); - (lowered.core, lowered.effect_context.unwrap()) -} - -#[test] -fn effect_command_metadata_must_name_the_selected_source_entry() { - let (mut core, mut context) = action_fixture(); - let selected = core.entry.unwrap(); - let mut decoy = core - .declarations - .iter() - .find(|decl| decl.symbol == selected) - .unwrap() - .clone(); - decoy.symbol = psrs_hir::SymbolId::new(selected.module, 1_000_000); - decoy.name = "decoy".into(); - context.command_entry = Some(EffectCommandEntry { - symbol: decoy.symbol, - }); - core.declarations.push(decoy); - rejects(core, context, "selected source entry"); -} - -#[test] -fn an_effect_source_entry_requires_command_metadata() { - let (core, mut context) = action_fixture(); - context.command_entry = None; - rejects(core, context, "missing Effect command entry"); -} - -#[test] -fn checked_import_verification_keeps_the_foreign_source_module() { - let prepared = crate::prepare_sources(&[ - ( - "Clock.purs", - "module Clock where\nimport Prelude\nforeign import \"wasi:clocks/monotonic-clock#now\" keep :: Int\n", - ), - ( - "Main.purs", - "module Main where\nimport Prelude\nimport Clock\nmain = keep\n", - ), - ]) - .expect("the clock import links before its checked signature is damaged"); - let mut core = prepared.core; - let context = prepared.effect_context.expect("trusted effect"); - let entry = core.entry.expect("selected main"); - let keep = core - .externals - .iter() - .find(|external| external.name == "keep") - .expect("clock import") - .symbol; - let source_module = core - .external_types - .iter() - .find(|external| external.symbol == keep) - .expect("checked clock signature") - .source_module; - assert_ne!(source_module, entry.module); - core.types - .push(psrs_core::Type::Variable(psrs_hir::TypeVariableId(900_001))); - let free = psrs_core::TypeId((core.types.len() - 1) as u32); - core.external_types - .iter_mut() - .find(|external| external.symbol == keep) - .expect("checked clock signature") - .ty = free; - let context = Some(context); - let optimized = psrs_backend::compile_with_context( - core.clone(), - context.clone(), - psrs_backend::TargetCapabilities::default(), - ) - .expect_err("optimization must reject an unbound variable in a checked import"); - let lowered = psrs_backend::lower_cc_with_context(core, context.as_ref()) - .expect_err("effect lowering must reject an unbound variable in a checked import"); - for errors in [optimized, lowered] { - assert!( - errors.iter().any(|error| { - error.module == Some(source_module) - && error - .message - .contains("type variable is outside its quantifier scope") - }), - "{errors:?}" - ); - assert!( - errors - .iter() - .all(|error| error.module != Some(entry.module)), - "the entry module must not own the import's verification failure: {errors:?}" - ); - } -} diff --git a/crates/psrs-driver/src/tests/effects/entry.rs b/crates/psrs-driver/src/tests/effects/entry.rs deleted file mode 100644 index b47ec88e..00000000 --- a/crates/psrs-driver/src/tests/effects/entry.rs +++ /dev/null @@ -1,287 +0,0 @@ -use super::super::super::*; -use std::process::Output; - -#[test] -fn both_single_source_apis_compile_int_main_with_the_trusted_effect_library() { - let source = "module Main where\nimport Prelude\nmain = 0\n"; - compile_source("Main.purs", source).expect("compile_source must lower trusted Effect imports"); - compile_source_with_dumps("Main.purs", source) - .expect("compile_source_with_dumps must lower trusted Effect imports"); -} - -#[test] -fn an_effect_unit_entry_executes_its_action_once_through_both_source_apis() { - let source = "module Main where\nimport Prelude\nimport WASI.Console\nmain = log \"command\"\n"; - let artifact = compile_source("Main.purs", source).expect("inferred Effect Unit main"); - let dumped = compile_source_with_dumps("Main.purs", source) - .expect("the dump API must preserve Effect lowering context"); - for wasm in [&artifact.wasm, &dumped.artifact.wasm] { - let Some(output) = run_wasm(wasm) else { return }; - assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stdout, b"command\n"); - } -} - -#[test] -fn effect_unit_entry_runs_strict_construction_effects_before_its_action() { - let source = "module Main where\nimport Prelude\nimport WASI.Console\nmain = let ignored = runEffect (log \"construct\") in log \"action\"\n"; - let artifact = compile_source("Main.purs", source).expect("the selected main owns runEffect"); - let Some(output) = run_wasm(&artifact.wasm) else { - return; - }; - assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stdout, b"construct\naction\n"); -} - -#[test] -fn effect_unit_entry_propagates_a_trap_from_the_action() { - let source = "module Main where\nimport Prelude\nimport WASI.Console\nmain :: Effect Unit\nmain = bind (log \"before\") (\\_ -> bind trap (\\_ -> log \"after\"))\n"; - let artifact = compile_source("Main.purs", source).expect("the effect entry should compile"); - let Some(output) = run_wasm(&artifact.wasm) else { - return; - }; - assert!( - !output.status.success(), - "the action trap must escape: {output:?}" - ); - assert!( - String::from_utf8_lossy(&output.stderr).contains("wasm trap:") - || String::from_utf8_lossy(&output.stderr).contains("wasm backtrace"), - "the action trap should include a Wasmtime trap marker: {output:?}" - ); - assert_eq!( - output.stdout, b"before\n", - "the trap must suppress later output" - ); -} - -#[test] -fn the_effect_context_survives_backend_stage_recompilation() { - let source = "module Main where\nimport Prelude\nimport WASI.Console\nmain :: Effect Unit\nmain = log \"recompiled\"\n"; - let lowered = crate::lower_source_with_prelude_to_core("Main.purs", source).unwrap(); - let core = lowered.core; - let context = lowered.effect_context; - let stages = psrs_backend::compile_with_context( - core, - context, - psrs_backend::TargetCapabilities::default(), - ) - .unwrap(); - let replay = psrs_backend::compile_with_context( - stages.core.clone(), - stages.effect_context.clone(), - psrs_backend::TargetCapabilities::default(), - ) - .expect("P8 context must be retained with the returned Core"); - let Some(output) = run_wasm(&replay.artifact.wasm) else { - return; - }; - assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stdout, b"recompiled\n"); -} - -#[test] -fn a_quantified_effect_import_lowers_to_a_monotype_wrapper() { - let clock = ( - "Clock.purs", - "module Clock where\nimport Prelude\nforeign import \"wasi:clocks/monotonic-clock#now\" now :: forall a. Effect Int\n", - ); - let main = ( - "Main.purs", - "module Main where\nimport Prelude\nimport WASI.Console\nimport Clock\nmain :: Effect Unit\nmain = bind now (\\_ -> log \"quantified\")\n", - ); - let artifact = compile_program_sources_with_prelude(&[clock, main]) - .expect("a rank-1 effect import must lower without repeating its forall"); - let Some(output) = run_wasm(&artifact.wasm) else { - return; - }; - assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stdout, b"quantified\n"); -} - -#[test] -fn effect_import_alias_is_expanded_before_suspension_planning() { - let types = ( - "Types.purs", - "module Types where\nimport Prelude\ntype ClockTick = Effect Int\n", - ); - let clock = ( - "Clock.purs", - "module Clock where\nimport Prelude\nimport Types\nforeign import \"wasi:clocks/monotonic-clock#now\" clock :: ClockTick\n", - ); - let main = ( - "Main.purs", - "module Main where\nimport Prelude\nimport WASI.Console\nimport Clock\nmain :: Effect Unit\nmain = bind clock (\\_ -> log \"clock completed\")\n", - ); - let artifact = compile_program_sources_with_prelude(&[types, clock, main]) - .expect("an imported alias in a WIT signature must be classified from checked types"); - let Some(output) = run_wasm(&artifact.wasm) else { - return; - }; - assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stdout, b"clock completed\n"); -} - -#[test] -fn effect_suspension_conformance_errors_keep_the_imports_source_origin() { - let clock = ( - "Clock.purs", - "module Clock where\nimport Prelude\nforeign import \"wasi:clocks/monotonic-clock#now\" clock :: Effect String\n", - ); - let main = ( - "Main.purs", - "module Main where\nimport Prelude\nimport WASI.Console\nimport Clock\nmain = bind clock (\\_ -> log \"unreachable\")\n", - ); - let errors = compile_program_sources_with_prelude(&[clock, main]) - .expect_err("a suspended host import must still match its WIT result"); - assert!( - errors.iter().any(|error| { - error.source == DiagnosticOrigin::Source(0) - && error.diagnostic.stage == "P8 WIT linking" - && error - .diagnostic - .message - .contains("source result type incompatible") - }), - "{errors:#?}" - ); -} - -#[test] -fn class_constrained_wit_imports_are_rejected_with_a_source_diagnostic() { - let source = "module Main where\nimport Prelude\nforeign import \"wasi:clocks/monotonic-clock#now\" clock :: forall a. Eq a => a\nmain = 0\n"; - let errors = compile_program_sources_with_prelude(&[("Main.purs", source)]) - .expect_err("the canonical WIT ABI cannot carry class dictionaries"); - assert!( - errors.iter().any(|error| { - error.source == DiagnosticOrigin::Source(0) - && error.diagnostic.stage == "P5 typecheck" - && error - .diagnostic - .message - .contains("class-constrained WIT imports are not supported") - }), - "{errors:#?}" - ); -} - -#[test] -fn instance_member_references_do_not_bypass_the_run_effect_scope() { - let source = "module Main where\nimport Prelude\nclass Runner a where\n run :: a -> Int\ninstance Runner Int where\n run _ = runEffect (pure 42)\nmain = run 0\n"; - let errors = compile_program_sources_with_prelude(&[("Main.purs", source)]) - .expect_err("instance methods are executable declarations too"); - assert!( - errors.iter().any(|error| { - error.source == DiagnosticOrigin::Source(0) - && error.diagnostic.stage == "P7 entry selection" - && error - .diagnostic - .message - .contains("may only be referenced from the selected command entry") - }), - "{errors:#?}" - ); -} - -#[test] -fn the_selected_entry_may_pass_run_effect_to_a_higher_order_helper() { - let source = "module Main where\nimport Prelude\nrunWith :: (Effect Int -> Int) -> Int\nrunWith runner = runner (pure 42)\nmain = runWith runEffect\n"; - let artifact = compile_source("Main.purs", source) - .expect("the selected declaration may pass its lexically scoped runner"); - let Some(output) = run_wasm(&artifact.wasm) else { - return; - }; - assert_eq!(output.status.code(), Some(42)); -} - -#[test] -fn effect_unit_entry_accepts_a_type_synonym_and_cross_module_value() { - let producer = ( - "Producer.purs", - "module Producer where\nimport Prelude\nimport WASI.Console\ntype Action = Effect Unit\naction :: Action\naction = log \"linked\"\n", - ); - let main = ( - "Main.purs", - "module Main where\nimport Prelude\nimport Producer\nmain = action\n", - ); - let artifact = compile_program_sources_with_prelude(&[producer, main]) - .expect("an inferred entry may return an imported aliased Effect Unit"); - let Some(output) = run_wasm(&artifact.wasm) else { - return; - }; - assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stdout, b"linked\n"); -} - -#[test] -fn an_effect_int_entry_remains_invalid() { - let source = "module Main where\nimport Prelude\nmain :: Effect Int\nmain = pure 42\n"; - let errors = compile_source("Main.purs", source) - .expect_err("the command entry must return Int or Effect Unit"); - assert!(errors.iter().any(|error| { - error.stage == "P7 entry selection" && error.message.contains("Effect Unit") - })); -} - -#[test] -fn typechecking_does_not_require_a_unique_command_entry() { - let left = ("Left.purs", "module Left where\nmain = 0\n"); - let right = ("Right.purs", "module Right where\nmain = 0\n"); - crate::typecheck_program_sources(&[left, right]) - .expect("ordinary typechecking does not select a command entry"); - let errors = compile_program_sources_with_prelude(&[left, right]) - .expect_err("compilation requires one command entry"); - assert!(errors.iter().any(|error| { - error.diagnostic.stage == "P7 entry selection" - && error - .diagnostic - .message - .contains("multiple `main` declarations") - })); -} - -#[test] -fn a_main_in_the_main_module_takes_precedence_and_unique_main_is_the_fallback() { - let other = ("Other.purs", "module Other where\nmain = 1\n"); - let main = ("Main.purs", "module Main where\nmain = 42\n"); - let preferred = compile_program_sources(&[other, main]).unwrap(); - let Some(output) = run_wasm(&preferred.wasm) else { - return; - }; - assert_eq!(output.status.code(), Some(42)); - - let unique = ("Runner.purs", "module Runner where\nmain = 7\n"); - let fallback = compile_program_sources(&[unique]).unwrap(); - let Some(output) = run_wasm(&fallback.wasm) else { - return; - }; - assert_eq!(output.status.code(), Some(7)); -} - -fn run_wasm(wasm: &[u8]) -> Option { - if std::process::Command::new("wasmtime") - .arg("--version") - .output() - .is_err() - { - if std::env::var("PSRS_REQUIRE_WASMTIME").as_deref() == Ok("1") { - panic!("PSRS_REQUIRE_WASMTIME=1 but wasmtime is not installed"); - } - eprintln!("skipping: wasmtime is not installed"); - return None; - } - static COUNTER: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0); - let id = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - let path = std::env::temp_dir().join(format!( - "psrs-effect-entry-{}-{id}.wasm", - std::process::id() - )); - std::fs::write(&path, wasm).unwrap(); - let output = std::process::Command::new("wasmtime") - .arg("run") - .arg(&path) - .output() - .unwrap(); - let _ = std::fs::remove_file(path); - Some(output) -} diff --git a/crates/psrs-driver/src/tests/effects/mod.rs b/crates/psrs-driver/src/tests/effects/mod.rs deleted file mode 100644 index 7c2951d6..00000000 --- a/crates/psrs-driver/src/tests/effects/mod.rs +++ /dev/null @@ -1,500 +0,0 @@ -mod contract; -mod entry; - -use super::super::*; -use std::sync::atomic::{AtomicU32, Ordering}; - -#[test] -fn constructing_an_effect_does_not_execute_it() { - let source = "module Main where\nimport Prelude\nimport WASI.Console\nmain = let action = log \"not printed\" in 0\n"; - let Some(output) = run_effect_program(source) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(0)); - assert!(output.stdout.is_empty()); -} - -#[test] -fn running_effects_preserves_source_order() { - let source = "module Main where\nimport Prelude\nimport WASI.Console\nmain = let first = runEffect (log \"first\") in let second = runEffect (log \"second\") in 0\n"; - let Some(output) = run_effect_program(source) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stdout, b"first\nsecond\n"); -} - -#[test] -fn a_stored_effect_runs_each_time_it_is_explicitly_run() { - let source = "module Main where\nimport Prelude\nimport WASI.Console\nmain = let action = log \"again\" in let first = runEffect action in let second = runEffect action in 0\n"; - let Some(output) = run_effect_program(source) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stdout, b"again\nagain\n"); -} - -#[test] -fn a_function_cannot_be_passed_to_run_effect_as_an_effect() { - let source = "module Main where\nimport Prelude\nmain = runEffect (\\token -> 42)\n"; - let errors = compile_source("Main.purs", source).unwrap_err(); - assert!( - errors.iter().any(|error| { - error.stage == "P5 typecheck" && error.message.contains("type mismatch") - }) - ); -} - -#[test] -fn run_effect_is_only_available_from_the_selected_entry() { - let helper = ( - "Helper.purs", - "module Helper where\nimport Prelude\nrun = runEffect (pure 42)\n", - ); - let main = ( - "Main.purs", - "module Main where\nimport Helper\nmain = run\n", - ); - let errors = compile_program_sources_with_prelude(&[helper, main]).unwrap_err(); - assert!(errors.iter().any(|error| { - error.diagnostic.stage == "P7 entry selection" - && error - .diagnostic - .message - .contains("runEffect` binding may only be referenced") - })); -} - -#[test] -fn transitive_effect_types_keep_their_closure_representation() { - let library_source = ( - "Library.purs", - "module Library where\nimport Prelude\nimport WASI.Clock\naction :: Effect Int\naction = now\n", - ); - let main_source = ( - "Main.purs", - "module Main where\nimport Library\nforward = action\nmain = let ignored = forward in 0\n", - ); - let (sources, trusted_prefix) = - prelude::prepend(&[library_source, main_source]).expect("standard library"); - let typed = - crate::program::typecheck_program_sources_with_trusted_prefix(&sources, trusted_prefix) - .unwrap(); - let library = typed - .iter() - .find(|module| module.name == "Library") - .unwrap(); - let main = typed.iter().find(|module| module.name == "Main").unwrap(); - let action = library - .declarations - .iter() - .find(|declaration| declaration.name == "action") - .unwrap(); - let forward = main - .declarations - .iter() - .find(|declaration| declaration.name == "forward") - .unwrap(); - // `Effect Int` keeps the imported opaque type constructor into Core; it is - // not rewritten to an arrow. The backend selects the closure representation - // by identity. - let Some(psrs_thir::Type::Application(constructor, _)) = - library.types.get(action.ty.0 as usize) - else { - panic!("`Effect Int` must remain an applied opaque type"); - }; - assert!(matches!( - library.types.get(constructor.0 as usize), - Some(psrs_thir::Type::Constructor( - psrs_thir::TypeConstructor::User(_) - )) - )); - assert_eq!( - main.types.get(forward.ty.0 as usize), - library.types.get(action.ty.0 as usize) - ); - let artifact = compile_program_sources_with_prelude(&[library_source, main_source]); - assert!(artifact.is_ok(), "{artifact:?}"); -} - -#[test] -fn an_untrusted_prelude_effect_remains_an_ordinary_user_type() { - let prelude_source = ( - "Prelude.purs", - "module Prelude where\nforeign import data Effect :: Type -> Type\nforeign import \"wasi:clocks/monotonic-clock#now\" foreignEffect :: Effect Int\n", - ); - let main_source = ( - "Main.purs", - "module Main where\nimport Prelude\nforward :: Effect Int\nforward = foreignEffect\nmain = 0\n", - ); - let typed = crate::program::typecheck_program_sources(&[prelude_source, main_source]).unwrap(); - let main = typed.iter().find(|module| module.name == "Main").unwrap(); - let forward = main - .declarations - .iter() - .find(|declaration| declaration.name == "forward") - .unwrap(); - let psrs_thir::Type::Application(effect_constructor, _) = &main.types[forward.ty.0 as usize] - else { - panic!("untrusted Prelude.Effect should remain an applied user type"); - }; - assert!(matches!( - main.types[effect_constructor.0 as usize], - psrs_thir::Type::Constructor(psrs_thir::TypeConstructor::User(_)) - )); - let errors = compile_program_sources(&[prelude_source, main_source]) - .expect_err("the untrusted nominal Effect remains an ordinary WIT result type"); - assert!( - errors.iter().any(|error| { - error.diagnostic.stage == "P8 WIT linking" - && error - .diagnostic - .message - .contains("source result type incompatible") - }), - "{errors:#?}" - ); -} - -#[test] -fn running_pure_returns_the_supplied_value_without_an_external_action() { - let source = "module Main where\nimport Prelude\nmain = runEffect (pure 42)\n"; - let Some(output) = run_effect_program(source) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(42)); - assert!( - output.stdout.is_empty(), - "pure must not perform an external action: {output:?}" - ); -} - -#[test] -fn bind_runs_the_first_effect_before_the_continuation() { - let source = "module Main where\nimport Prelude\nimport WASI.Console\nmain = let action = bind (log \"first\") (\\_ -> log \"second\") in let result = runEffect action in 0\n"; - let Some(output) = run_effect_program(source) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stdout, b"first\nsecond\n"); -} - -#[test] -fn bind_passes_the_first_result_to_the_continuation() { - let source = "module Main where\nimport Prelude\nimport WASI.Console\nmain = let action = bind (pure 1) (\\x -> if x < 2 then log \"small\" else log \"big\") in let result = runEffect action in 0\n"; - let Some(output) = run_effect_program(source) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stdout, b"small\n"); -} - -#[test] -fn nested_binds_preserve_left_to_right_order() { - let source = "module Main where\nimport Prelude\nimport WASI.Console\nmain = let action = bind (log \"a\") (\\_ -> bind (log \"b\") (\\_ -> log \"c\")) in let result = runEffect action in 0\n"; - let Some(output) = run_effect_program(source) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stdout, b"a\nb\nc\n"); -} - -#[test] -fn bind_runs_the_returned_effect_exactly_once() { - let source = "module Main where\nimport Prelude\nimport WASI.Console\nmain = let action = bind (log \"once\") (\\_ -> log \"tail\") in let result = runEffect action in 0\n"; - let Some(output) = run_effect_program(source) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stdout, b"once\ntail\n"); -} - -#[test] -fn an_effect_captured_by_a_closure_is_not_run() { - let source = "module Main where\nimport Prelude\nimport WASI.Console\nmain = let action = log \"captured\" in let holder = \\n -> action in let stored = holder in 0\n"; - let Some(output) = run_effect_program(source) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(0)); - assert!( - output.stdout.is_empty(), - "capturing an effect must not run it: {output:?}" - ); -} - -#[test] -fn passing_an_effect_to_a_function_does_not_run_it() { - let source = "module Main where\nimport Prelude\nimport WASI.Console\nconsume :: forall a. Effect a -> Int\nconsume effect = 0\nmain = let action = log \"passed\" in consume action\n"; - let Some(output) = run_effect_program(source) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(0)); - assert!( - output.stdout.is_empty(), - "passing an effect must not run it: {output:?}" - ); -} - -#[test] -fn a_polymorphic_effect_uses_ordinary_adapters() { - let source = "module Main where\nimport Prelude\nimport WASI.Console\nidentityEffect :: forall a. Effect a -> Effect a\nidentityEffect effect = effect\nmain = let action = identityEffect (log \"generic\") in let result = runEffect action in 0\n"; - let Some(output) = run_effect_program(source) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stdout, b"generic\n"); -} - -#[test] -fn a_polymorphic_effect_carries_string_and_number_values() { - let string = "module Main where\nimport Prelude\nimport WASI.Console\nmain = let action = bind (pure \"s\") (\\_ -> log \"str\") in let result = runEffect action in 0\n"; - let Some(output) = run_effect_program(string) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stdout, b"str\n"); - - let number = "module Main where\nimport Prelude\nimport WASI.Console\nmain = let action = bind (pure 1.5) (\\_ -> log \"num\") in let result = runEffect action in 0\n"; - let Some(output) = run_effect_program(number) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stdout, b"num\n"); -} - -#[test] -fn a_polymorphic_effect_carries_a_gc_aggregate() { - let source = "module Main where\nimport Prelude\nimport WASI.Console\ndata Pair a = Pair a a\nidentityEffect :: forall a. Effect a -> Effect a\nidentityEffect effect = effect\nmain = let action = identityEffect (pure (Pair 1 2)) in let result = runEffect (bind action (\\p -> log \"poly\")) in 0\n"; - let Some(output) = run_effect_program(source) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stdout, b"poly\n"); -} - -#[test] -fn bind_preserves_wasi_results_across_stdout_and_stderr() { - let source = "module Main where\nimport Prelude\nimport WASI.Console\nmain = let action = bind (error \"stderr\") (\\_ -> log \"stdout\") in let result = runEffect action in 0\n"; - let Some(output) = run_effect_program(source) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stdout, b"stdout\n"); - assert_eq!(output.stderr, b"stderr\n"); -} - -#[test] -fn a_linked_module_forwards_an_effect_without_running_it() { - let producer = ( - "Producer.purs", - "module Producer where\nimport Prelude\nimport WASI.Console\naction :: Effect Unit\naction = log \"linked\"\n", - ); - let main = ( - "Main.purs", - "module Main where\nimport Prelude\nimport Producer\nforward :: Effect Unit\nforward = action\nmain = let stored = forward in 0\n", - ); - let Some(output) = run_effect_program_sources(&[producer, main]) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(0)); - assert!( - output.stdout.is_empty(), - "forwarding an effect must not run it: {output:?}" - ); -} - -#[test] -fn a_linked_module_forwards_an_effect_that_runs_only_when_selected() { - let producer = ( - "Producer.purs", - "module Producer where\nimport Prelude\nimport WASI.Console\naction :: Effect Unit\naction = log \"linked\"\n", - ); - let main = ( - "Main.purs", - "module Main where\nimport Prelude\nimport Producer\nforward :: Effect Unit\nforward = action\nmain = let result = runEffect forward in 0\n", - ); - let Some(output) = run_effect_program_sources(&[producer, main]) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stdout, b"linked\n"); -} - -#[test] -fn a_boolean_effect_runs_through_bind() { - // `Boolean` and `Int` both lower to Wasm `i32`, so their closure signatures - // must share one concrete function type; otherwise the erased call site's - // `ref.cast` traps even though both are representationally identical. - let source = "module Main where\nimport Prelude\nimport WASI.Console\nmain = let action = bind (pure true) (\\x -> if x then log \"yes\" else log \"no\") in let result = runEffect action in 0\n"; - let Some(output) = run_effect_program(source) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stdout, b"yes\n"); -} - -#[test] -fn discard_defined_from_bind_sequences_effects() { - // The Prelude defines `discard first next = bind first next`; the direct - // call returns an effect closure that must be adapted from the - // declaration's generic body result to the instantiated `Effect Int`. - let source = "module Main where\nimport Prelude\nimport WASI.Console\nmain = let action = discard (log \"a\") (\\_ -> log \"b\") in let result = runEffect action in 0\n"; - let Some(output) = run_effect_program(source) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stdout, b"a\nb\n"); -} - -#[test] -fn a_standard_apply_composes_higher_order_effects() { - // `apply` is defined from `bind`; its first argument is an - // `Effect (a -> b)`, so the inner function must cross the erased boundary - // without losing its runtime call signature. - let source = "module Main where\nimport Prelude\nmain = runEffect (apply (pure (\\x -> x + 1)) (pure 41))\n"; - let Some(output) = run_effect_program(source) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(42)); -} - -#[test] -fn map_composes_a_higher_order_effect() { - let source = - "module Main where\nimport Prelude\nmain = runEffect (map (\\x -> x + 1) (pure 41))\n"; - let Some(output) = run_effect_program(source) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(42)); -} - -#[test] -fn ado_notation_combines_effectful_arguments_in_order() { - // `ado x <- a; y <- b; in f x y` desugars to - // `apply (map (\x -> \y -> f x y) a) b`; the curried continuation crosses - // the erased generic function boundary. - let source = "module Main where\nimport Prelude\nmain = runEffect (ado\n x <- pure 40\n y <- pure 2\n in x + y)\n"; - let Some(output) = run_effect_program(source) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(42)); -} - -#[test] -fn ado_notation_runs_effects_left_to_right() { - // The `ado` block stays outside `let`: its `in` closes the nearest - // layout `let`, so the Int entry is the block's own result. - let source = "module Main where\nimport Prelude\nimport WASI.Console\nmain = runEffect (ado\n first <- log \"first\"\n second <- log \"second\"\n in 0)\n"; - let Some(output) = run_effect_program(source) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stdout, b"first\nsecond\n"); -} - -#[test] -fn an_effect_of_a_function_returns_a_separate_closure() { - // `Effect (Int -> Int)` is a closure whose result is a function value, not - // an effect that has been flattened into two parameters. Binding it and - // applying the returned function must still produce 42. - let source = "module Main where\nimport Prelude\nfoo :: Effect (Int -> Int)\nfoo = pure (\\x -> x + 1)\nmain = runEffect (bind foo (\\f -> pure (f 41)))\n"; - let Some(output) = run_effect_program(source) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(42)); -} - -#[test] -fn an_effect_of_an_effect_runs_the_inner_effect() { - // `Effect (Effect Int)` stores one effect closure as the value of another. - // The inner closure must survive the outer bind and run exactly here. - let source = "module Main where\nimport Prelude\nnested :: Effect (Effect Int)\nnested = pure (pure 42)\nmain = runEffect (bind nested (\\inner -> inner))\n"; - let Some(output) = run_effect_program(source) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(42)); -} - -#[test] -fn a_polymorphic_effect_combinator_runs() { - // A user-defined higher-order combinator over an erased `Effect a` value - // must compose through the closure representation. - let source = "module Main where\nimport Prelude\nrepeatAction :: forall a. Effect a -> Effect a\nrepeatAction action = bind action (\\_ -> action)\nmain = runEffect (repeatAction (pure 42))\n"; - let Some(output) = run_effect_program(source) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(42)); -} - -fn run_effect_program(source: &str) -> Option { - if !wasmtime_available() { - return None; - } - static COUNTER: AtomicU32 = AtomicU32::new(0); - let artifact = compile_source("Main.purs", source).unwrap(); - run_artifact(&artifact.wasm, "effect", &COUNTER) -} - -/// Runs a program linked from several source modules with the embedded -/// standard library. -fn run_effect_program_sources(sources: &[(&str, &str)]) -> Option { - if !wasmtime_available() { - return None; - } - static COUNTER: AtomicU32 = AtomicU32::new(0); - let artifact = compile_program_sources_with_prelude(sources).unwrap(); - run_artifact(&artifact.wasm, "effect-multi", &COUNTER) -} - -fn wasmtime_available() -> bool { - if std::process::Command::new("wasmtime") - .arg("--version") - .output() - .is_ok() - { - return true; - } - if std::env::var("PSRS_REQUIRE_WASMTIME").as_deref() == Ok("1") { - panic!("PSRS_REQUIRE_WASMTIME=1 but wasmtime is not installed"); - } - false -} - -fn run_artifact(wasm: &[u8], tag: &str, counter: &AtomicU32) -> Option { - let id = counter.fetch_add(1, Ordering::Relaxed); - let path = std::env::temp_dir().join(format!("psrs-{tag}-{}-{id}.wasm", std::process::id())); - std::fs::write(&path, wasm).unwrap(); - let output = std::process::Command::new("wasmtime") - .arg("run") - .arg(&path) - .output() - .unwrap(); - let _ = std::fs::remove_file(path); - Some(output) -} diff --git a/crates/psrs-driver/src/tests/foldable.rs b/crates/psrs-driver/src/tests/foldable.rs index c36570af..7fcab594 100644 --- a/crates/psrs-driver/src/tests/foldable.rs +++ b/crates/psrs-driver/src/tests/foldable.rs @@ -11,6 +11,8 @@ use super::*; fn monoid_identities_and_folds_run_under_wasmtime() { let source = r#" module Main where +import Effect (Effect) +import Effect.Unsafe (unsafePerformEffect) import Prelude import Effect.Console (log) @@ -41,7 +43,7 @@ checks = do log (foldMap (\x -> x) (One "one")) pure unit -main = let ignored = runEffect checks in 0 +main = let ignored = unsafePerformEffect checks in 0 "#; let Some(output) = run_with_wasmtime(source) else { eprintln!("skipping: wasmtime is not installed"); diff --git a/crates/psrs-driver/src/tests/functions.rs b/crates/psrs-driver/src/tests/functions.rs index 24d79242..a9e2243a 100644 --- a/crates/psrs-driver/src/tests/functions.rs +++ b/crates/psrs-driver/src/tests/functions.rs @@ -107,20 +107,35 @@ main = identity 42 #[test] fn runs_a_polymorphic_identity_with_a_number() { - let source = r#"module Main where + for (input, expected) in [("1.5", 42), ("2.5", 1)] { + let source = format!( + r#"module Main where +foreign import "psrs:intrinsic#numberEq" equalNumber :: Number -> Number -> Boolean identity :: forall a. a -> a identity value = value use :: Number -> Int -use value = 42 -main = use (identity 1.5) -"#; - let artifact = compile_source("Main.purs", source).expect("lowering a polymorphic Number"); - assert!(artifact.wat.contains("f64")); - let Some(output) = run_with_wasmtime(source) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(42)); +use value = if equalNumber value 1.5 then 42 else 1 +main = use (identity {input}) +"# + ); + // The optimized artifact may fold the identity and comparison. Number + // representation belongs to MIR lowering, before those optimizations. + let mir = lower_source_to_mir(&source); + assert!( + mir.functions + .iter() + .flat_map(|function| &function.blocks) + .flat_map(|block| &block.instructions) + .any(|instruction| matches!( + instruction, + psrs_backend::mir::Instruction::NumberConstant { value, .. } if value == input + )) + ); + let Some(output) = run_with_wasmtime(&source) else { + return; + }; + assert_eq!(output.status.code(), Some(expected), "{input}: {output:?}"); + } } #[test] @@ -197,6 +212,7 @@ fn runs_a_recursive_polymorphic_reference_identity() { // optimizer must prune that dead arm before verifying. let source = r#"module Main where import Data.Eq ((==)) +import Data.Ring ((-)) lastArr :: forall a. Int -> Array a -> Array a lastArr n x = if n == 0 then x else lastArr (n - 1) x main = arrayIndex (lastArr 3 [40, 42]) 1 diff --git a/crates/psrs-driver/src/tests/functor.rs b/crates/psrs-driver/src/tests/functor.rs index 424b5f43..60d3a564 100644 --- a/crates/psrs-driver/src/tests/functor.rs +++ b/crates/psrs-driver/src/tests/functor.rs @@ -1,10 +1,9 @@ //! The library's `Functor` class and its `<$>` operator. //! -//! `Prelude` re-exports the class from `Data.Functor` and adds the `Effect` -//! instance. These tests check that one `map` covers arrays, `Maybe`, -//! `Either`, and `Effect`, and that mapping an effect does not run it. - -use super::*; +//! `Prelude` re-exports the class from `Data.Functor`. The `Effect` instance +//! comes from importing `Effect`. These tests check that one `map` covers +//! arrays, `Maybe`, `Either`, and `Effect`, and that mapping an effect does +//! not run it. #[test] fn functor_map_transforms_array_maybe_and_either() { @@ -12,6 +11,7 @@ fn functor_map_transforms_array_maybe_and_either() { module Main where import Prelude +import Effect (Effect) import Data.Either (Either(..)) import Data.Maybe (Maybe(..)) import Effect.Console (log) @@ -26,7 +26,7 @@ main = do Right n -> show n Left _ -> "left") "#; - let Some(output) = run_with_wasmtime(source) else { + let Some(output) = super::run_program_with_prelude_wasmtime(&[("Main.purs", source)]) else { eprintln!("skipping: wasmtime is not installed"); return; }; @@ -40,6 +40,7 @@ fn mapping_an_effect_does_not_run_it_until_the_action_runs() { module Main where import Prelude +import Effect (Effect) import Effect.Console (log) tick :: Effect Int @@ -54,7 +55,7 @@ main = do value <- pending log (show value) "#; - let Some(output) = run_with_wasmtime(source) else { + let Some(output) = super::run_program_with_prelude_wasmtime(&[("Main.purs", source)]) else { eprintln!("skipping: wasmtime is not installed"); return; }; diff --git a/crates/psrs-driver/src/tests/generic_aggregate_audit.rs b/crates/psrs-driver/src/tests/generic_aggregate_audit.rs index 1c59130b..898b6b50 100644 --- a/crates/psrs-driver/src/tests/generic_aggregate_audit.rs +++ b/crates/psrs-driver/src/tests/generic_aggregate_audit.rs @@ -56,7 +56,7 @@ fn execute_wasm_output(name: &str, wasm: &[u8]) -> Result { @@ -99,9 +99,10 @@ fn linked_modules_share_canonical_layouts() { canonical_arrays, 1, "linked modules must share one canonical Array(Erased) layout" ); - assert!( - array_new_default_count(&stages.mir) >= 1, - "the linked producer boundary must retain a reconstruction path" + assert_eq!( + array_new_default_count(&stages.mir), + 0, + "the linked producer boundary must preserve the original array storage" ); match execute_wasm("linked_modules", &stages.artifact.wasm) { Ok(Some(code)) => assert_eq!(code, 42, "linked round-trip should preserve its element"), @@ -227,7 +228,7 @@ fn audit_battery() { }, Case { name: "record_roundtrip_nested", - source: "module Main where\ncopy :: forall a. { inner :: { value :: a } } -> { inner :: { value :: a } }\ncopy record = record\nmain = arrayIndex (copy { inner: { value: [40, 42] } }.inner.value) 1\n", + source: "module Main where\ncopy :: forall a. { inner :: { value :: a } } -> { inner :: { value :: a } }\ncopy record = record\nmain = arrayIndex ((copy { inner: { value: [40, 42] } }).inner.value) 1\n", exit: 42, }, // GA-06 dependent ADT fields, multiple instantiations @@ -320,7 +321,7 @@ fn audit_battery() { }, Case { name: "scalar_unit_array", - source: "module Main where\nimport Prelude\nimport WASI.Console\ncopy :: forall a. Array a -> Array a\ncopy values = values\nmain = arrayLength (copy [runEffect (log \"\")])\n", + source: "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\nimport Prelude\nimport WASI.Console\ncopy :: forall a. Array a -> Array a\ncopy values = values\nmain = arrayLength (copy [unsafePerformEffect (log \"\")])\n", exit: 1, }, Case { @@ -409,13 +410,13 @@ fn empty_array_reconstruction_executes() { ); let stages = psrs_backend::compile_with_context( prepared.core, - prepared.effect_context, psrs_backend::TargetCapabilities::default(), ) .expect("an empty concrete array must still lower through the conversion path"); - assert!( - array_new_default_count(&stages.mir) >= 1, - "the empty array should still allocate its canonical destination" + assert_eq!( + array_new_default_count(&stages.mir), + 0, + "the empty array must not be reconstructed at a polymorphic boundary" ); assert!( stages @@ -426,9 +427,9 @@ fn empty_array_reconstruction_executes() { .flat_map(|block| &block.instructions) .any(|instruction| matches!( instruction, - psrs_backend::mir::Instruction::ArraySet { .. } + psrs_backend::mir::Instruction::ArrayNew { elements, .. } if elements.is_empty() )), - "the conversion loop should retain its element store even for an empty source" + "the empty source literal retains its single storage allocation" ); match execute_wasm("empty_array_reconstruction", &stages.artifact.wasm) { Ok(Some(code)) => assert_eq!(code, 0, "empty array round-trip should have length zero"), diff --git a/crates/psrs-driver/src/tests/guard_coverage/constrained.rs b/crates/psrs-driver/src/tests/guard_coverage/constrained.rs new file mode 100644 index 00000000..6c531a25 --- /dev/null +++ b/crates/psrs-driver/src/tests/guard_coverage/constrained.rs @@ -0,0 +1,61 @@ +use super::*; + +#[test] +fn guarded_polymorphic_results_use_the_enclosing_dictionary() { + let prefix = "module Main where\nclass Build f where\n build :: forall a. a -> f a\ndata Box a = Box a\ninstance buildBox :: Build Box where\n build = Box\nchoose :: forall f a. Build f => Boolean -> a -> f a\n"; + for (body, flag) in [ + ( + "choose = case _, _ of\n flag, value\n | flag -> build value\n | true -> build value\n", + "true", + ), + ( + "choose = case _, _ of\n flag, value\n | flag -> build value\n | true -> build value\n", + "false", + ), + ( + "choose flag value = case flag of\n false -> build value\n true | flag -> build value\n true -> build value\n", + "true", + ), + ( + "choose flag value = case flag of\n true | false -> build value\n _ -> build value\n", + "true", + ), + ] { + let source = format!( + "{prefix}{body}main :: Int\nmain = case (choose {flag} 42 :: Box Int) of\n Box value -> value\n" + ); + let Some(output) = run_with_wasmtime(&source) else { + eprintln!("skipping: wasmtime is not installed"); + return; + }; + assert_eq!(output.status.code(), Some(42), "{body}: {output:?}"); + } +} + +#[test] +fn official_guarded_library_declarations_typecheck() { + let source = "module Main where\nimport Data.Enum\nimport Data.Enum.Generic\nimport Data.List.Lazy\nmain :: Int\nmain = 42\n"; + check_program_types_lenient_with_prelude(&[("Main.purs", source)]).unwrap(); +} + +#[test] +fn official_enum_ranges_execute_ascending_descending_and_equal_guards() { + let source = "module Main where\nimport Prelude\nimport Data.Enum (enumFromTo)\nmain :: Int\nmain = if enumFromTo 1 3 == [1,2,3] && enumFromTo 3 1 == [3,2,1] && enumFromTo 2 2 == [2] then 42 else 1\n"; + let Some(output) = run_with_wasmtime(source) else { + eprintln!("skipping: wasmtime is not installed"); + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +#[test] +fn guarded_results_do_not_invent_a_missing_dictionary() { + let source = "module Main where\nclass Build f where\n build :: forall a. a -> f a\nchoose :: forall f a. Boolean -> a -> f a\nchoose = case _, _ of\n flag, value\n | flag -> build value\n | true -> build value\nmain :: Int\nmain = 42\n"; + let errors = check_source("Main.purs", source).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.code == Some("NoInstanceFound")), + "{errors:?}" + ); +} diff --git a/crates/psrs-driver/src/tests/guard_coverage.rs b/crates/psrs-driver/src/tests/guard_coverage/mod.rs similarity index 99% rename from crates/psrs-driver/src/tests/guard_coverage.rs rename to crates/psrs-driver/src/tests/guard_coverage/mod.rs index f02deaca..10ce423a 100644 --- a/crates/psrs-driver/src/tests/guard_coverage.rs +++ b/crates/psrs-driver/src/tests/guard_coverage/mod.rs @@ -1,5 +1,7 @@ use super::*; +mod constrained; + #[test] fn shadowed_boolean_case_guard_reports_its_source_row_and_keeps_first_match() { let source = "module Main where\nchoose input guard = case input of\n true -> 11\n true | guard -> 22\n false -> 33\nmain = choose true false\n"; diff --git a/crates/psrs-driver/src/tests/guards.rs b/crates/psrs-driver/src/tests/guards.rs index d9433c38..f61b48f0 100644 --- a/crates/psrs-driver/src/tests/guards.rs +++ b/crates/psrs-driver/src/tests/guards.rs @@ -93,6 +93,16 @@ fn boolean_case_patterns_preserve_source_order() { assert_eq!(output.status.code(), Some(22), "{output:?}"); } +#[test] +fn an_anonymous_if_condition_is_a_function_parameter() { + let source = "module Main where\nchoose left right = (if _ then left else right) true\nmain = choose 7 9\n"; + let Some(output) = run_with_wasmtime(source) else { + eprintln!("skipping: wasmtime is not installed"); + return; + }; + assert_eq!(output.status.code(), Some(7), "{output:?}"); +} + #[test] fn anonymous_case_inputs_become_function_parameters_in_source_order() { let source = "module Main where\nchoose = case _, 2, _ of\n _, 2, _ -> 19\n _, _, _ -> 23\nmain = choose 1 3\n"; @@ -276,7 +286,7 @@ fn repeated_names_inside_a_case_named_pattern_are_rejected() { #[test] fn multiple_case_scrutinees_preserve_order_and_guard_fallthrough() { - let source = "module Main where\nimport Prelude\nimport WASI.Console\nmain = case runEffect (log \"1\"), runEffect (log \"2\"), runEffect (log \"3\"), runEffect (log \"4\"), runEffect (log \"5\"), runEffect (log \"6\"), runEffect (log \"7\"), runEffect (log \"8\"), runEffect (log \"9\"), runEffect (log \"10\") of\n _, _, _, _, _, _, _, _, _, _ | false -> 10\n _, _, _, _, _, _, _, _, _, _ | true -> 22\n"; + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\nimport Prelude\nimport WASI.Console\nmain = case unsafePerformEffect (log \"1\"), unsafePerformEffect (log \"2\"), unsafePerformEffect (log \"3\"), unsafePerformEffect (log \"4\"), unsafePerformEffect (log \"5\"), unsafePerformEffect (log \"6\"), unsafePerformEffect (log \"7\"), unsafePerformEffect (log \"8\"), unsafePerformEffect (log \"9\"), unsafePerformEffect (log \"10\") of\n _, _, _, _, _, _, _, _, _, _ | false -> 10\n _, _, _, _, _, _, _, _, _, _ | true -> 22\n"; let artifact = compile_source("Main.purs", source).expect("multi-scrutinee guarded case compiles"); assert!(!has_non_exhaustive_warning(&artifact)); @@ -372,12 +382,13 @@ fn official_guard_and_case_sources_lower_through_p2() { #[test] fn user_defined_false_otherwise_does_not_prove_guard_coverage() { - let constants = "module Boolean.Constants (otherwise) where\nimport Prelude\notherwise :: Boolean\notherwise = false\n"; - let boolean = "module Data.Boolean (otherwise) where\nimport Boolean.Constants (otherwise)\n"; - let main = "module Main where\nimport Prelude\nimport Data.Boolean (otherwise)\nread n\n | otherwise = n\nmain = read 1\n"; + let constants = + "module Boolean.Constants (otherwise) where\notherwise :: Boolean\notherwise = false\n"; + let alias = "module Boolean.Alias (otherwise) where\nimport Boolean.Constants (otherwise)\n"; + let main = "module Main where\nimport Boolean.Alias (otherwise)\nread n\n | otherwise = n\nmain = read 1\n"; let errors = compile_program_sources_with_prelude(&[ ("Boolean.Constants.purs", constants), - ("Data.Boolean.purs", boolean), + ("Boolean.Alias.purs", alias), ("Main.purs", main), ]) .expect_err("an arbitrary binding named otherwise is not a coverage proof"); @@ -393,13 +404,12 @@ fn user_defined_false_otherwise_does_not_prove_guard_coverage() { #[test] fn cross_module_true_alias_is_a_verified_unconditional_guard() { - let constants = - "module Boolean.Constants (truth) where\nimport Prelude\ntruth :: Boolean\ntruth = true\n"; - let boolean = "module Data.Boolean (otherwise) where\nimport Prelude\nimport Boolean.Constants (truth)\notherwise :: Boolean\notherwise = truth\n"; - let main = "module Main where\nimport Prelude\nimport Data.Boolean (otherwise)\nread n\n | otherwise = n\nmain = read 11\n"; + let constants = "module Boolean.Constants (truth) where\ntruth :: Boolean\ntruth = true\n"; + let alias = "module Boolean.Alias (otherwise) where\nimport Boolean.Constants (truth)\notherwise :: Boolean\notherwise = truth\n"; + let main = "module Main where\nimport Boolean.Alias (otherwise)\nread n\n | otherwise = n\nmain = read 11\n"; let artifact = compile_program_sources_with_prelude(&[ ("Boolean.Constants.purs", constants), - ("Data.Boolean.purs", boolean), + ("Boolean.Alias.purs", alias), ("Main.purs", main), ]) .expect("resolved aliases to true prove guard coverage"); diff --git a/crates/psrs-driver/src/tests/integration.rs b/crates/psrs-driver/src/tests/integration.rs index eecc4aa2..60c5bd2b 100644 --- a/crates/psrs-driver/src/tests/integration.rs +++ b/crates/psrs-driver/src/tests/integration.rs @@ -79,24 +79,44 @@ fn captures_number_values_in_closures() { #[test] fn compiles_if_expression_through_cfg_to_structured_wasm() { - let source = - "module Main where\nchoose condition = if condition then 9 else 2\nmain = choose true\n"; - let artifact = compile_source("Main.purs", source).unwrap(); - // The unified reducible structurer lowers a diamond to nested `block` - // regions with depth-relative branches, not a result-typed `if`. - assert!(artifact.wat.contains("block")); - assert!(artifact.wat.contains("br_if")); - assert!(artifact.wasm.len() > 8); - let Some(output) = run_with_wasmtime(source) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(9)); + for (condition, expected) in [("true", 9), ("false", 2)] { + let source = format!( + "module Main where\n\ + apply :: (Boolean -> Int) -> Boolean -> Int\n\ + apply f condition = f condition\n\ + choose condition = if condition then 9 else 2\n\ + main = apply choose {condition}\n" + ); + // Inspect the diamond at MIR lowering, and pass `choose` as a function + // value so the current optimizer retains its runtime branch body. + // The final encoding is free to choose any equivalent control flow. + let mir = lower_source_to_mir(&source); + let choose = mir + .functions + .iter() + .find(|function| function.name == "choose") + .expect("unoptimized choose function"); + assert!(choose.blocks.iter().any(|block| matches!( + block.terminator, + Some(psrs_backend::mir::Terminator::Branch { then_block, else_block, .. }) + if then_block != else_block + ))); + let Some(output) = run_with_wasmtime(&source) else { + return; + }; + assert_eq!( + output.status.code(), + Some(expected), + "{condition}: {output:?}" + ); + } } #[test] fn folds_top_level_scalar_references_to_constants() { - let source = "module Main where\nimport Prelude\nanswer = 40\nmain = answer + 2\n"; + // `intAdd` is the scalar integer primitive; the library `+` is a class + // method and is not a constant-foldable scalar operation. + let source = "module Main where\nanswer = 40\nmain = intAdd answer 2\n"; let artifact = compile_source("Main.purs", source).unwrap(); assert!(artifact.wat.contains("i32.const 42")); } @@ -208,7 +228,7 @@ fn compiles_multiple_user_sources_with_the_embedded_prelude() { let helper = ("Helper.purs", "module Helper where\nanswer = 40\n"); let main = ( "Main.purs", - "module Main where\nimport Helper\nimport Prelude\nimport WASI.Console\nmain = let ignored = runEffect (log \"linked\") in 0\n", + "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\nimport Helper\nimport Prelude\nimport WASI.Console\nmain = let ignored = unsafePerformEffect (log \"linked\") in 0\n", ); let artifact = compile_program_sources_with_prelude(&[helper, main]).unwrap(); assert!(artifact.wat.contains("wasi:cli/stdout@0.2.12")); diff --git a/crates/psrs-driver/src/tests/intrinsic_contracts.rs b/crates/psrs-driver/src/tests/intrinsic_contracts.rs new file mode 100644 index 00000000..69618e23 --- /dev/null +++ b/crates/psrs-driver/src/tests/intrinsic_contracts.rs @@ -0,0 +1,78 @@ +use super::*; + +#[test] +fn retired_arithmetic_bindings_are_rejected() { + for name in ["intDiv", "intMod"] { + let source = format!( + "module Main where\nforeign import \"psrs:intrinsic#{name}\" old :: Int -> Int -> Int\nmain = old 3 (-2)\n" + ); + let errors = + crate::check_program(&[("Main.purs", source.as_str())]).expect_err("retired binding"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.message.contains(&format!("`{name}`"))), + "{errors:?}" + ); + let source = format!("module Main where\nmain = {name} 3 2\n"); + assert!(crate::check_program(&[("Main.purs", source.as_str())]).is_err()); + } +} + +#[test] +fn official_euclidean_policy_executes_for_both_signs_and_zero() { + let source = r#"module Main where +import Prelude +import Data.EuclideanRing as E + +check a b q r = E.div a b == q && E.mod a b == r +checkHigher f a b expected = f a b == expected +main = if check 5 3 1 2 + && check (-5) 3 (-2) 1 + && check 5 (-3) (-1) 2 + && check (-5) (-3) 2 1 + && check 3 (-2) (-1) 1 + && check (-3) 2 (-2) 1 + && check 6 (-3) (-2) 0 + && check (-6) (-3) 2 0 + && check 1 0 0 0 && check (-1) 0 0 0 && check 0 0 0 0 + && checkHigher E.div 3 (-2) (-1) + && checkHigher E.mod 3 (-2) 1 + then 42 else 1 +"#; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert!(output.stdout.is_empty(), "{output:?}"); + assert!(output.stderr.is_empty(), "{output:?}"); +} + +#[test] +fn balanced_public_array_traversal_uses_the_truncating_storage_leaf() { + let source = r#"module Main where +import Prelude +import Data.Maybe (Maybe(..)) +import Data.Traversable (traverse) + +large = case traverse (\x -> Just (x + 1)) [1,2,3,4,5,6,7,8,9] of + Just xs -> arrayLength xs == 9 && arrayIndex xs 0 == 2 + && arrayIndex xs 4 == 6 && arrayIndex xs 8 == 10 + Nothing -> false +empty = case traverse (\x -> Just (x + 1)) [] of + Just xs -> arrayLength xs == 0 + Nothing -> false +failure = case traverse (\x -> if x == 5 then Nothing else Just x) [1,2,3,4,5,6,7,8,9] of + Nothing -> true + Just _ -> false +main = if large && empty && failure then 42 else 1 +"#; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert!( + output.stdout.is_empty() && output.stderr.is_empty(), + "{output:?}" + ); +} diff --git a/crates/psrs-driver/src/tests/let_constraints.rs b/crates/psrs-driver/src/tests/let_constraints.rs new file mode 100644 index 00000000..9ef9c6ea --- /dev/null +++ b/crates/psrs-driver/src/tests/let_constraints.rs @@ -0,0 +1,416 @@ +//! Monomorphic local bindings share unknowns and constraints with the enclosing +//! declaration. A local call can determine a previously unknown monad as `Maybe`; +//! the enclosing declaration solves the resulting obligation. + +fn assert_checks(source: &str) { + crate::check_program(&[("Main.purs", source)]) + .unwrap_or_else(|errors| panic!("program should type check: {errors:?}")); +} + +#[test] +fn a_signed_function_solves_constraints_of_its_where_binding() { + let source = r#" +module Main where + +class Bind m where + bind :: forall a b. m a -> (a -> m b) -> m b + +class Enum a where + succ :: a -> Maybe a + +data Maybe a = Nothing | Just a + +instance bindMaybe :: Bind Maybe where + bind (Just value) continuation = continuation value + bind Nothing _ = Nothing + +instance enumInt :: Enum Int where + succ n = Just n + +enumFromTo :: forall a. Enum a => a -> Maybe a +enumFromTo from = go succ from + where + go step value = bind (step value) (\next -> Just next) + +main :: Int +main = 0 +"#; + assert_checks(source); +} + +#[test] +fn a_constraint_on_an_outer_unknown_stays_with_the_enclosing_binding() { + let source = r#" +module Main where + +class Show a where + show :: a -> Int + +instance showInt :: Show Int where + show _ = 1 + +shown value = let displayed = show value in displayed + +main :: Int +main = shown 1 +"#; + assert_checks(source); +} + +#[test] +fn a_recursive_local_keeps_a_shared_constraint_for_the_enclosing_use() { + let source = r#" +module Main where + +class Semiring a where + add :: a -> a -> a + +instance semiringInt :: Semiring Int where + add value _ = value + +loop :: Int -> Int +loop n = + let + go value = add value (go value) + in + go n + +main :: Int +main = loop 1 +"#; + assert_checks(source); +} + +#[test] +fn a_guarded_case_use_instantiates_a_where_binding() { + let source = r#" +module Main where + +class Bind m where + bind :: forall a b. m a -> (a -> m b) -> m b + +class Enum a where + succ :: a -> Maybe a + pred :: a -> Maybe a + +data Maybe a = Nothing | Just a + +instance bindMaybe :: Bind Maybe where + bind (Just value) continuation = continuation value + bind Nothing _ = Nothing + +instance enumInt :: Enum Int where + succ n = Just n + pred n = Just n + +enumFromTo :: forall a. Enum a => a -> a -> Maybe a +enumFromTo = case _, _ of + from, to + | true -> go succ from + | true -> go pred from + where + go step value = bind (step value) (\next -> Just next) + +main :: Int +main = 0 +"#; + assert_checks(source); +} + +#[test] +fn a_where_stepper_accepts_an_integer_seed() { + let source = r#" +module Main where + +class Semiring a where + add :: a -> a -> a + sub :: a -> a -> a + +instance semiringInt :: Semiring Int where + add x _ = x + sub x _ = x + +infixl 6 add as + +infixl 6 sub as - + +class Ord a where + le :: a -> a -> Boolean + +instance ordInt :: Ord Int where + le _ _ = true + +infix 4 le as <= + +class Ord a <= BoundedEnum a where + toEnum :: Int -> Maybe a + fromEnum :: a -> Int + +data Maybe a = Nothing | Just a +data Tuple a b = Tuple a b + +class Partial + +fromJust :: forall a. Partial => Maybe a -> a +fromJust (Just value) = value + +class Functor f where + map :: forall a b. (a -> b) -> f a -> f b + +infixl 4 map as <$> + +class Unfoldable t where + unfoldr :: forall a b. (b -> Maybe (Tuple a b)) -> b -> t a + +class Semigroupoid a where + compose :: forall b c d. a c d -> a b c -> a b d + +instance semigroupoidFn :: Semigroupoid (->) where + compose f g value = f (g value) + +composeFlipped :: forall a b c d. Semigroupoid a => a b c -> a c d -> a b d +composeFlipped f g = compose g f + +infixr 9 composeFlipped as >>> + +unsafePartial :: forall a. (Partial => a) -> a +unsafePartial = discharge + +discharge :: forall a b. a -> b +discharge value = discharge value + +otherwise = true + +enumFromThenTo :: forall f a. Unfoldable f => Functor f => BoundedEnum a => a -> a -> a -> f a +enumFromThenTo = unsafePartial \a b c -> + let + a' = fromEnum a + b' = fromEnum b + c' = fromEnum c + in + (toEnum >>> fromJust) <$> unfoldr (go (b' - a') c') a' + where + go step to index + | index <= to = Just (Tuple index (index + step)) + | otherwise = Nothing + +main :: Int +main = 0 +"#; + assert_checks(source); +} + +#[test] +fn a_concrete_missing_instance_inside_a_let_is_still_rejected() { + let source = r#" +module Main where + +class Need a where + need :: a -> a + +bad :: Int -> Int +bad value = let needed = need value in needed + +main :: Int +main = bad 1 +"#; + let errors = + crate::check_program(&[("Main.purs", source)]).expect_err("Need Int has no instance"); + assert!( + errors.iter().any(|error| { + error.diagnostic.code == Some("NoInstanceFound") + && error.diagnostic.message.contains("Need Int") + }), + "expected a missing Need Int instance, got {errors:?}" + ); +} + +#[test] +fn a_local_constraint_does_not_choose_an_unrelated_lexical_given() { + assert_checks( + r#" +module Main where + +class Measure a where + measure :: a -> Int + +instance measureInt :: Measure Int where + measure value = value + +outer :: forall a. Measure a => a -> Int +outer value = measured 1 + where + measured input = measure input + +main :: Int +main = outer 0 +"#, + ); +} + +#[test] +fn a_superclass_functional_dependency_improves_a_local_result() { + assert_checks( + r#" +module Main where + +class Convert a b | a -> b where + convert :: a -> b + +class Convert a b <= Middle a b +class Middle a b <= Child a b + +outer :: forall a b. Child a b => a -> Int +outer value = let converted = convert value in 0 + +main :: Int +main = 0 +"#, + ); +} + +#[test] +fn a_solved_local_dictionary_uses_the_enclosing_methods_quantified_variables() { + assert_checks( + r#" +module Main where + +class Delay l where + delay :: (Int -> l) -> l + +data List a = Nil | Cons a (List a) + +instance delayList :: Delay (List a) where + delay thunk = thunk 0 + +class Build f where + build :: forall a. a -> f a + +instance buildList :: Build List where + build = go + where + go value = delay (\_ -> Cons value Nil) + +main :: Int +main = 0 +"#, + ); +} + +#[test] +fn an_unquantified_variable_of_a_solved_local_constraint_stays_ambiguous() { + let source = r#" +module Main where + +class Delay l where + delay :: (Int -> l) -> l + +data List a = Nil +instance delayList :: Delay (List a) where + delay thunk = thunk 0 + +class Choose a where + choose :: List a -> Int + +instance chooseInt :: Choose Int where + choose _ = 0 + +class Build f where + build :: forall a. a -> f a + +instance buildList :: Build List where + build _ = let discarded = choose (delay (\_ -> Nil)) in Nil + +main :: Int +main = 0 +"#; + let errors = crate::check_program(&[("Main.purs", source)]) + .expect_err("the local result does not quantify the delayed element type"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.code == Some("AmbiguousTypeVariables")), + "expected the unquantified constraint to stay ambiguous: {errors:?}" + ); +} + +#[test] +fn a_signed_body_owns_its_unobservable_instantiation_variables() { + assert_checks( + r#" +module Main where + +data Proxy a = Proxy + +discard :: forall a. Proxy a -> Int +discard _ = 0 + +main :: Int +main = discard Proxy +"#, + ); +} + +#[test] +fn a_local_body_owns_its_unobservable_instantiation_variables() { + assert_checks( + r#" +module Main where + +data Proxy a = Proxy + +discard :: forall a. Proxy a -> Int +discard _ = 0 + +main = let discarded = discard Proxy in 0 +"#, + ); +} + +#[test] +fn an_instance_body_owns_its_unobservable_instantiation_variables() { + assert_checks( + r#" +module Main where + +data Proxy a = Proxy + +discard :: forall a. Proxy a -> Int +discard _ = 0 + +class Measure f where + measure :: forall a. f a -> Int + +instance measureProxy :: Measure Proxy where + measure _ = discard Proxy + +main :: Int +main = 0 +"#, + ); +} + +#[test] +fn body_only_quantifiers_preserve_execution() { + let source = r#" +module Main where + +data Proxy a = Proxy + +discard :: forall a. Proxy a -> Int +discard _ = 21 + +class Measure f where + measure :: forall a. f a -> Int + +instance measureProxy :: Measure Proxy where + measure _ = discard Proxy + +main :: Int +main = let part = discard Proxy in intAdd part (measure Proxy) +"#; + let Some(output) = super::run_with_wasmtime(source) else { + eprintln!("skipping: wasmtime is not installed"); + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} diff --git a/crates/psrs-driver/src/tests/library_foreign.rs b/crates/psrs-driver/src/tests/library_foreign.rs new file mode 100644 index 00000000..f55a6254 --- /dev/null +++ b/crates/psrs-driver/src/tests/library_foreign.rs @@ -0,0 +1,129 @@ +#[test] +fn library_foreign_values_preserve_identity_and_checked_signatures() { + let sources = [ + ( + "Native.purs", + "module Native where\nforeign import step :: Int -> Int\n", + ), + ( + "Main.purs", + "module Main where\nimport Native\nmain = step 42\n", + ), + ]; + let modules = crate::typecheck_program_sources(&sources).expect("declared foreign type checks"); + let native = &modules[0]; + let external = native + .externals + .iter() + .find(|external| external.name == "step") + .unwrap(); + assert_eq!( + external.kind, + psrs_hir::ExternalKind::Library { + module: "Native".into() + } + ); + assert!( + native + .external_types + .iter() + .any(|checked| checked.symbol == external.symbol) + ); + let mut core = crate::prepare_sources(&sources) + .expect("foreign signatures lower to Core") + .core; + core.external_types + .retain(|checked| checked.symbol != external.symbol); + assert!( + core.verify().is_err(), + "a library declaration needs its checked signature" + ); +} + +#[test] +fn a_library_foreign_value_can_own_an_operator_fixity() { + let sources = [( + "Main.purs", + "module Main where\nforeign import operation :: Int -> Int -> Int\ninfixl 4 operation as %%\nmain = 1 %% 2\n", + )]; + crate::check_program(&sources).expect("foreign values are declared before fixity resolution"); +} + +#[test] +fn a_source_foreign_declaration_shadows_a_bootstrap_primitive() { + let sources = [( + "Main.purs", + "module Main where\nforeign import intAdd :: Int -> Int\nmain :: Int\nmain = intAdd 42\n", + )]; + crate::check_program(&sources) + .expect("the source declaration owns its declared arity and type"); +} + +#[test] +fn a_reexported_foreign_value_shadows_a_bootstrap_primitive() { + let sources = [ + ( + "Native.purs", + "module Native where\nforeign import intAdd :: Int -> Int\n", + ), + ( + "Facade.purs", + "module Facade (module Native) where\nimport Native\n", + ), + ( + "Main.purs", + "module Main where\nimport Facade\nmain :: Int\nmain = intAdd 42\n", + ), + ]; + crate::check_program(&sources).expect("imports keep their declaring symbol and signature"); +} + +#[test] +fn a_library_foreign_signature_rejects_source_class_constraints() { + let sources = [( + "Main.purs", + "module Main where\nclass Equal a where\n equal :: a -> a -> Boolean\nforeign import compareValues :: forall a. Equal a => a -> a -> Boolean\nmain = 0\n", + )]; + let errors = crate::check_program(&sources).expect_err("purs rejects foreign constraints"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.message.contains("class constraints")) + ); +} + +#[test] +fn an_unimplemented_library_foreign_value_is_an_explicit_linking_error() { + let sources = [( + "Main.purs", + "module Main where\nforeign import step :: Int -> Int\nmain = step 42\n", + )]; + let errors = + crate::compile_program_sources(&sources).expect_err("no target implementation exists"); + assert!( + errors.iter().any(|error| { + error.diagnostic.stage == "P8 library linking" + && error.diagnostic.message.contains("Main.step") + && error + .diagnostic + .message + .contains("no target implementation") + }), + "{errors:?}" + ); +} + +#[test] +fn an_unused_static_library_import_does_not_require_a_target_implementation() { + let sources = [ + ( + "Native.purs", + "module Native where\nforeign import missing :: Int -> Int\nanswer :: Int\nanswer = 42\n", + ), + ( + "Main.purs", + "module Main where\nimport Native\nmain = answer\n", + ), + ]; + crate::compile_program_sources(&sources).expect("only reached static imports require linking"); +} diff --git a/crates/psrs-driver/src/tests/local_signatures.rs b/crates/psrs-driver/src/tests/local_signatures.rs new file mode 100644 index 00000000..8a0dad86 --- /dev/null +++ b/crates/psrs-driver/src/tests/local_signatures.rs @@ -0,0 +1,27 @@ +//! An explicit polymorphic annotation on a local binding is predeclared at its +//! written type, so a recursive use instantiates the annotation rather than +//! binding a fresh metavariable to an arrow before the quantifier is checked. + +#[test] +fn a_recursive_local_signature_keeps_its_quantifiers() { + let source = r#"module Main where + +import Prelude + +data List a = Nil | Cons a (List a) + +foldlLocal :: forall b c. (c -> b -> c) -> c -> List b -> c +foldlLocal k c xs = go k c xs + where + go :: forall b2 c2. (c2 -> b2 -> c2) -> c2 -> List b2 -> c2 + go _ acc Nil = acc + go k2 acc (Cons b rest) = go k2 (k2 acc b) rest + +main :: Int +main = foldlLocal (\acc x -> acc + x) 0 (Cons 1 (Cons 2 (Cons 3 Nil))) +"#; + let Some(output) = super::run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(6), "{output:?}"); +} diff --git a/crates/psrs-driver/src/tests/long_expression.rs b/crates/psrs-driver/src/tests/long_expression.rs new file mode 100644 index 00000000..4caef151 --- /dev/null +++ b/crates/psrs-driver/src/tests/long_expression.rs @@ -0,0 +1,37 @@ +use super::*; + +/// Official `purs` accepts this right-nested `infixr` chain. The expression +/// walks have to follow the spine on a heap stack; a native stack proportional +/// to the chain length aborts the compiler. +#[test] +fn long_boolean_conjunction_chain_returns_42() { + let chain = std::iter::repeat_n("true", 448) + .collect::>() + .join(" && "); + let source = format!("module Main where\nimport Prelude\nmain = if {chain} then 42 else 1\n"); + let Some(output) = run_with_wasmtime(&source) else { + eprintln!("skipping: wasmtime is not installed"); + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert!(output.stdout.is_empty(), "{output:?}"); + assert!(output.stderr.is_empty(), "{output:?}"); +} + +#[test] +fn long_right_associative_subtraction_preserves_grouping() { + let chain = std::iter::repeat_n("1", 448) + .collect::>() + .join(" <-> "); + let source = format!( + "module Main where\nimport Prelude\ninfixr 6 difference as <->\n\ + difference x y = x - y\nmain = if ({chain}) == 0 then 42 else 1\n" + ); + let Some(output) = run_with_wasmtime(&source) else { + eprintln!("skipping: wasmtime is not installed"); + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert!(output.stdout.is_empty(), "{output:?}"); + assert!(output.stderr.is_empty(), "{output:?}"); +} diff --git a/crates/psrs-driver/src/tests/mod.rs b/crates/psrs-driver/src/tests/mod.rs index 5ad8a90e..af585f6a 100644 --- a/crates/psrs-driver/src/tests/mod.rs +++ b/crates/psrs-driver/src/tests/mod.rs @@ -5,20 +5,42 @@ use std::sync::atomic::{AtomicU32, Ordering}; static WASM_ARTIFACT_COUNTER: AtomicU32 = AtomicU32::new(0); mod assertions; +mod closure_protocol; mod coercion; +mod control_boundary; +mod control_carriers; +mod control_costs; +mod control_regressions; mod data_function; mod data_tuple; +mod declaration_calls; mod deriving; -mod effect_arity; -mod effects; +mod diagnosis_trace; mod foldable; mod functor; mod guard_coverage; +mod intrinsic_contracts; +mod let_constraints; +mod library_foreign; +mod local_signatures; +mod number_abs; +mod number_acos; +mod number_asin; +mod number_atan; +mod number_atan2; +mod number_decimal; +mod number_elementary; +mod number_rounding; +mod number_sqrt; +mod number_trunc; mod operators; mod partial_application; +mod primitive_foreign; +mod runtime_storage; mod scalars; mod semigroup; mod show; +mod uninhabited; fn lower_source_to_mir(source: &str) -> psrs_backend::mir::Module { let backend_input = crate::lower_main_to_cc(source).expect("Core should lower to CC"); @@ -69,6 +91,23 @@ fn run_program_with_wasmtime(sources: &[(&str, &str)]) -> Option Option { + wasmtime_available()?; + let artifact = compile_program_sources_with_prelude(sources).unwrap(); + let id = WASM_ARTIFACT_COUNTER.fetch_add(1, Ordering::Relaxed); + let path = std::env::temp_dir().join(format!("psrs-{}-{id}.wasm", std::process::id())); + std::fs::write(&path, &artifact.wasm).unwrap(); + let output = std::process::Command::new("wasmtime") + .arg("run") + .arg(&path) + .output() + .unwrap(); + let _ = std::fs::remove_file(&path); + Some(output) +} + fn run_with_wasmtime_args(source: &str, args: &[&str]) -> Option { run_wasmtime(source, args, None) } @@ -93,7 +132,14 @@ fn run_wasmtime_with_dirs( ) -> Option { wasmtime_available()?; use std::io::Write; + if let Some(path) = std::env::var_os("PSRS_DUMP_CC") { + let compilation = crate::compile_source_with_dumps("Main.purs", source).unwrap(); + std::fs::write(path, compilation.dumps.cc).unwrap(); + } let artifact = compile_source("Main.purs", source).unwrap(); + if let Some(path) = std::env::var_os("PSRS_DUMP_WAT") { + std::fs::write(path, &artifact.wat).unwrap(); + } let id = WASM_ARTIFACT_COUNTER.fetch_add(1, Ordering::Relaxed); let path = std::env::temp_dir().join(format!("psrs-{}-{id}.wasm", std::process::id())); std::fs::write(&path, &artifact.wasm).unwrap(); @@ -130,6 +176,7 @@ mod backend; mod declarations; mod integration; mod kinds; +mod long_expression; mod resolution; mod typecheck; mod wasi; @@ -150,7 +197,12 @@ mod guard_let; mod guards; mod module_loader; +mod native_representation; mod library_types; mod tail_calls; + +mod symbol_reflection; + +mod recursive_closures; diff --git a/crates/psrs-driver/src/tests/module_loader.rs b/crates/psrs-driver/src/tests/module_loader.rs index 0a814c27..1e22f832 100644 --- a/crates/psrs-driver/src/tests/module_loader.rs +++ b/crates/psrs-driver/src/tests/module_loader.rs @@ -97,66 +97,21 @@ fn loads_the_standard_library_from_disk_in_trusted_order() { .iter() .map(|module| module.module_name.as_str()) .collect::>(); + assert_eq!(names.first().copied(), Some("Prelude")); + assert!(names.contains(&"WASI")); assert_eq!( - names, - [ - "Prelude", - "Data.Function", - "Data.Semigroup", - "Data.Monoid", - "Data.Eq", - "Data.Ord", - "Data.Semiring", - "Data.Show", - "Effect", - "Effect.Console", - "Test.Assert", - "Data.Maybe", - "Data.Either", - "Data.Functor", - "Data.Tuple", - "Data.Foldable", - "WASI.Resource", - "WASI.IO", - "WASI.Clock", - "WASI.Random", - "WASI.Console", - "WASI.Process", - "WASI.FileSystem", - "WASI.Network", - "WASI" - ] + names.len(), + names.iter().collect::>().len(), + "trusted module names are unique: {names:?}" ); for module in modules { let path = std::path::Path::new(&module.path); + let relative = module.module_name.replace('.', "/") + ".purs"; assert!( - path.ends_with("lib/Prelude.purs") - || path.ends_with("lib/Data/Function.purs") - || path.ends_with("lib/Data/Semigroup.purs") - || path.ends_with("lib/Data/Monoid.purs") - || path.ends_with("lib/Data/Eq.purs") - || path.ends_with("lib/Data/Ord.purs") - || path.ends_with("lib/Data/Semiring.purs") - || path.ends_with("lib/Data/Show.purs") - || path.ends_with("lib/Effect.purs") - || path.ends_with("lib/Effect/Console.purs") - || path.ends_with("lib/Test/Assert.purs") - || path.ends_with("lib/Data/Maybe.purs") - || path.ends_with("lib/Data/Either.purs") - || path.ends_with("lib/Data/Functor.purs") - || path.ends_with("lib/Data/Tuple.purs") - || path.ends_with("lib/Data/Foldable.purs") - || path.ends_with("lib/WASI/Resource.purs") - || path.ends_with("lib/WASI/IO.purs") - || path.ends_with("lib/WASI/Clock.purs") - || path.ends_with("lib/WASI/Random.purs") - || path.ends_with("lib/WASI/Console.purs") - || path.ends_with("lib/WASI/Process.purs") - || path.ends_with("lib/WASI/FileSystem.purs") - || path.ends_with("lib/WASI/Network.purs") - || path.ends_with("lib/WASI.purs"), - "{}", - module.path + path.ends_with(format!("lib/{relative}")), + "{} should be the file for {}", + module.path, + module.module_name ); let on_disk = std::fs::read_to_string(path).expect("the standard-library file should exist"); @@ -164,11 +119,39 @@ fn loads_the_standard_library_from_disk_in_trusted_order() { } check_source( "Main.purs", - "module Main where\nimport Prelude\nimport WASI.Console\nimport WASI.Clock\nmain = let stamp = runEffect now in let action = runEffect (log \"ok\") in stamp\n", + "module Main where\nimport Prelude\nimport Effect (Effect)\nimport WASI.Console\nimport WASI.Clock\nmain :: Effect Int\nmain = do\n _ <- now\n log \"ok\"\n pure 0\n", ) .expect("the on-disk standard library should typecheck with a user module"); } +#[test] +fn resolves_the_standard_library_closure_imported_by_a_wasi_program() { + let source = "module Main where\nimport Prelude\nimport Effect (Effect)\nimport WASI.Console\nimport WASI.Clock\nmain :: Effect Int\nmain = do\n _ <- now\n log \"ok\"\n pure 0\n"; + let (sources, trusted_prefix) = crate::prelude::prepend(&[("Main.purs", source)]) + .expect("the standard library should load from disk"); + assert!(trusted_prefix > 0); + crate::resolve_program_sources(&sources) + .expect("the imported standard library closure should resolve"); +} + +#[test] +fn lowers_parenthesized_kinded_class_parameters_as_binders() { + let module = crate::lower_source_to_ast( + "KindedClass.purs", + "module KindedClass where\nclass IsSymbol (sym :: Symbol) where\n reflect :: Proxy sym -> String\n", + ) + .expect("the kinded class should parse and lower"); + let psrs_ast::TypeDeclaration::Class(class) = &module.type_declarations[0] else { + panic!("expected a class declaration"); + }; + assert_eq!(class.parameters.len(), 1); + assert_eq!(class.parameters[0].name.text, "sym"); + assert!(matches!( + class.parameters[0].kind.as_ref().map(|kind| &kind.kind), + Some(psrs_ast::TypeKind::Name(name)) if name.text == "Symbol" + )); +} + #[test] fn does_not_discover_a_user_module_shadowing_the_standard_library() { let directory = temp_directory("stdlib-shadow"); @@ -181,7 +164,7 @@ fn does_not_discover_a_user_module_shadowing_the_standard_library() { .unwrap(); std::fs::write( &main, - "module Main where\nimport Prelude\nmain = runEffect (pure 1)\n", + "module Main where\nimport Prelude\nmain :: Int\nmain = 1\n", ) .unwrap(); diff --git a/crates/psrs-driver/src/tests/native_representation/algebraic.rs b/crates/psrs-driver/src/tests/native_representation/algebraic.rs new file mode 100644 index 00000000..0af43903 --- /dev/null +++ b/crates/psrs-driver/src/tests/native_representation/algebraic.rs @@ -0,0 +1,60 @@ +const CUSTOM_HANDLER: &str = r#"module Main where +import Prelude +import Effect (Effect) +import Effect.Unsafe (unsafePerformEffect) +import Run (Run) +import Run as Run +import Type.Proxy (Proxy(..)) +import WASI.Console (log) + +data Query a = Query (Int -> a) +derive instance functorQuery :: Functor Query + +query :: Proxy "query" +query = Proxy + +program :: Run (query :: Query) Int +program = do + n <- Run.lift query (Query identity) + pure (n + 1) + +handle :: forall a. Query a -> Effect a +handle (Query k) = do + log "handler" + pure (k 41) + +main :: Int +main = let action = Run.interpretRec (Run.on query handle Run.case_) program + _ = unsafePerformEffect (log "stored") + first = unsafePerformEffect action + second = unsafePerformEffect action + in first + second - 42 +"#; + +#[test] +fn a_custom_algebraic_handler_builds_reusable_native_effects_without_running_early() { + let Some(output) = + super::super::run_program_with_prelude_wasmtime(&[("Main.purs", CUSTOM_HANDLER)]) + else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert_eq!(output.stdout, b"stored\nhandler\nhandler\n", "{output:?}"); + assert!(output.stderr.is_empty(), "{output:?}"); +} + +#[test] +fn interpreting_one_operation_does_not_discard_a_residual_row() { + let source = CUSTOM_HANDLER.replace( + "program :: Run (query :: Query) Int", + "program :: Run (query :: Query, residual :: Query) Int", + ); + let errors = crate::check_program_types_lenient_with_prelude(&[("Main.purs", &source)]) + .expect_err("case_ cannot eliminate the residual operation"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.stage == "P5 typecheck"), + "{errors:?}" + ); +} diff --git a/crates/psrs-driver/src/tests/native_representation/arrays.rs b/crates/psrs-driver/src/tests/native_representation/arrays.rs new file mode 100644 index 00000000..3274eb5e --- /dev/null +++ b/crates/psrs-driver/src/tests/native_representation/arrays.rs @@ -0,0 +1,56 @@ +#[test] +fn public_mutable_arrays_copy_safe_conversions_and_share_growth_through_aliases() { + let source = r#"module Main where +import Prelude +import Control.Monad.ST as ST +import Data.Array.ST as A +import Data.Array.ST.Partial as PartialArray +import Data.Maybe (Maybe(..)) +import Effect.Unsafe (unsafePerformEffect) +import WASI.Console (log) +import Partial.Unsafe (unsafePartial) + +update :: forall r a. a -> A.STArray r a -> ST.ST r Boolean +update value xs = A.poke 0 value xs + +main :: Int +main = ST.run do + let original = [1, 2] + xs <- A.thaw original + let alias = xs + _ <- update 7 xs + frozen <- A.freeze xs + copied <- A.clone xs + _ <- update 8 alias + _ <- A.push 9 xs + grown <- A.length alias + current <- A.peek 0 alias + cloneFirst <- A.peek 0 copied + cloneLength <- A.length copied + _ <- A.modify 1 (\value -> + let _ = unsafePerformEffect (log "array-callback") in value + 10) alias + second <- A.peek 1 xs + text <- A.thaw ["first"] + _ <- update "second" text + changed <- A.peek 0 text + partialFirst <- unsafePartial (PartialArray.peek 0 xs) + _ <- unsafePartial (PartialArray.poke 2 10 xs) + partialThird <- A.peek 2 xs + owned <- A.unsafeThaw [4, 5] + _ <- update 6 owned + transferred <- A.unsafeFreeze owned + if arrayIndex original 0 == 1 && arrayIndex frozen 0 == 7 + && grown == 3 && current == Just 8 && cloneFirst == Just 7 + && cloneLength == 2 && second == Just 12 && changed == Just "second" + && partialFirst == 8 && partialThird == Just 10 + && arrayIndex transferred 0 == 6 + then pure 42 else pure 0 +"#; + let Some(output) = super::super::run_program_with_prelude_wasmtime(&[("Main.purs", source)]) + else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert_eq!(output.stdout, b"array-callback\n", "{output:?}"); + assert!(output.stderr.is_empty(), "{output:?}"); +} diff --git a/crates/psrs-driver/src/tests/native_representation/command.rs b/crates/psrs-driver/src/tests/native_representation/command.rs new file mode 100644 index 00000000..8b8ce07c --- /dev/null +++ b/crates/psrs-driver/src/tests/native_representation/command.rs @@ -0,0 +1,50 @@ +#[test] +fn the_configured_native_command_runner_executes_effect_unit_once() { + let source = r#"module Main where +import Prelude +import Effect (Effect) +import WASI.Console (log) +main :: Effect Unit +main = do + log "command" + pure unit +"#; + let Some(output) = super::super::run_program_with_prelude_wasmtime(&[("Main.purs", source)]) + else { + return; + }; + assert_eq!(output.status.code(), Some(0), "{output:?}"); + assert_eq!(output.stdout, b"command\n", "{output:?}"); + assert!(output.stderr.is_empty(), "{output:?}"); +} + +#[test] +fn the_native_command_runner_propagates_traps_and_rejects_a_different_result_type() { + let source = r#"module Main where +import Prelude +import Effect (Effect) +import Test.Assert (assert) +import WASI.Console (log) +main :: Effect Unit +main = do + log "before" + assert false + log "after" +"#; + let Some(output) = super::super::run_program_with_prelude_wasmtime(&[("Main.purs", source)]) + else { + return; + }; + assert!(!output.status.success(), "{output:?}"); + assert_eq!(output.stdout, b"before\n", "{output:?}"); + let invalid = "module Main where\nimport Prelude\nimport Effect (Effect)\nmain :: Effect Int\nmain = pure 42\n"; + let errors = crate::compile_program_sources_with_prelude(&[("Main.purs", invalid)]) + .expect_err("the ordinary runner accepts only its declared input"); + assert!( + errors.iter().any(|error| error + .diagnostic + .message + .contains("command entry type disagrees with the configured runner input")), + "{errors:?}" + ); +} diff --git a/crates/psrs-driver/src/tests/native_representation/fixtures/HostOrder.purs b/crates/psrs-driver/src/tests/native_representation/fixtures/HostOrder.purs new file mode 100644 index 00000000..84b6618a --- /dev/null +++ b/crates/psrs-driver/src/tests/native_representation/fixtures/HostOrder.purs @@ -0,0 +1,25 @@ +module Main where + +import Prelude + +import Effect (Effect) +import Effect.Unsafe (unsafePerformEffect) +import WASI.Clock (now) +import WASI.Console (log) +import WASI.Process (arguments) +import WASI.Random (randomBytes) + +run :: Effect Int +run = do + log "first" + names <- arguments + log "second" + bytes <- randomBytes 2 + tick <- now + if arrayLength names >= 0 && arrayLength bytes == 2 && tick * 0 == 0 then pure 42 else pure 0 + +main :: Int +main = + let stored = log "stored" + _ = stored + in unsafePerformEffect run diff --git a/crates/psrs-driver/src/tests/native_representation/fixtures/Identity.purs b/crates/psrs-driver/src/tests/native_representation/fixtures/Identity.purs new file mode 100644 index 00000000..a7b5f482 --- /dev/null +++ b/crates/psrs-driver/src/tests/native_representation/fixtures/Identity.purs @@ -0,0 +1,61 @@ +module Main where + +import Prelude + +import Control.Monad.ST as ST +import Control.Monad.ST.Ref as STRef +import Effect (Effect) +import Effect.Ref as Ref +import Effect.Unsafe (unsafePerformEffect) +import WASI.Console (log) + +readAny :: forall a. Ref.Ref a -> Effect a +readAny = Ref.read + +writeAny :: forall a. a -> Ref.Ref a -> Effect Unit +writeAny = Ref.write + +rank :: Effect Int +rank = do + int <- Ref.new 41 + text <- Ref.new "boxed" + record <- Ref.new { value: 0 } + a <- readAny int + b <- readAny text + _ <- writeAny { value: a + 1 } record + seen <- readAny record + _ <- writeAny "stored" text + again <- readAny text + if b == "boxed" && seen.value == 42 && again == "stored" then pure 42 else pure 0 + +share :: Effect Boolean +share = do + cell <- Ref.new [1] + let alias = cell + _ <- writeAny [2, 3] cell + read <- readAny alias + pure (arrayLength read == 2) + +order :: Effect Int +order = do + cell <- Ref.new 0 + log "before" + _ <- writeAny 7 cell + log "after" + readAny cell + +use :: forall r. STRef.STRef r Int -> ST.ST r Int +use = STRef.read + +stRank :: Int +stRank = ST.run do + ref <- STRef.new 20 + value <- use ref + pure (value + 1) + +main :: Int +main = unsafePerformEffect do + ranked <- rank + shared <- share + ordered <- order + if ranked == 42 && shared && ordered == 7 && stRank == 21 then pure 42 else pure 0 diff --git a/crates/psrs-driver/src/tests/native_representation/fixtures/Malformed.purs b/crates/psrs-driver/src/tests/native_representation/fixtures/Malformed.purs new file mode 100644 index 00000000..bad6ed67 --- /dev/null +++ b/crates/psrs-driver/src/tests/native_representation/fixtures/Malformed.purs @@ -0,0 +1,18 @@ +module Main where + +import Prelude + +import Effect (Effect) +import Effect.Ref as Ref +import Effect.Unsafe (unsafePerformEffect) +import Unsafe.Coerce (unsafeCoerce) +import WASI.Console (log) + +readBad :: Effect Int +readBad = Ref.read (unsafeCoerce 1 :: Ref.Ref Int) + +main :: Int +main = unsafePerformEffect do + value <- readBad + log "reached" + pure value diff --git a/crates/psrs-driver/src/tests/native_representation/host.rs b/crates/psrs-driver/src/tests/native_representation/host.rs new file mode 100644 index 00000000..20c65771 --- /dev/null +++ b/crates/psrs-driver/src/tests/native_representation/host.rs @@ -0,0 +1,56 @@ +//! Official WASI and Test.Assert wrappers after WIT import schemes became live +//! layout roots. Construction stays inert; `unsafePerformEffect` runs `Host.lift`. + +const OFFICIAL_HOST_LEAVES: &str = r#"module Main where + +import Prelude + +import Effect.Unsafe (unsafePerformEffect) +import Test.Assert (assert) +import WASI.Clock (now, wallNow) +import WASI.Console (log) +import WASI.IO (getStdout) +import WASI.Random (randomBytes, randomU64) + +main :: Int +main = + let tick = unsafePerformEffect now * 0 + wall = unsafePerformEffect wallNow + noise = unsafePerformEffect randomU64 * 0 + bytes = unsafePerformEffect (randomBytes 4) + _ = unsafePerformEffect (log "host-leaf") + _ = unsafePerformEffect getStdout + _ = unsafePerformEffect (assert true) + in if arrayLength bytes == 4 && wall.seconds * 0 == tick + noise + then 42 + else 0 +"#; + +#[test] +fn official_wasi_and_assert_wrappers_run_through_effect() { + let Some(output) = + super::super::run_program_with_prelude_wasmtime(&[("Main.purs", OFFICIAL_HOST_LEAVES)]) + else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert_eq!( + output.stdout, b"host-leaf\n", + "WASI.Console.log must write through Host.lift: {output:?}" + ); +} + +const HOST_ORDER: &str = include_str!("fixtures/HostOrder.purs"); + +#[test] +fn stored_host_work_stays_inert_and_runs_in_source_order() { + super::assert_matches_stdlib_oracle(HOST_ORDER, "conformance/native/HostOrder.purs"); + let Some(output) = + super::super::run_program_with_prelude_wasmtime(&[("Main.purs", HOST_ORDER)]) + else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert_eq!(output.stdout, b"first\nsecond\n", "{output:?}"); + assert!(output.stderr.is_empty(), "{output:?}"); +} diff --git a/crates/psrs-driver/src/tests/native_representation/identity.rs b/crates/psrs-driver/src/tests/native_representation/identity.rs new file mode 100644 index 00000000..ff0bb346 --- /dev/null +++ b/crates/psrs-driver/src/tests/native_representation/identity.rs @@ -0,0 +1,33 @@ +//! Rank-N cell transport, in-place polymorphic writes, and ignored-write order. +//! The programs are the stdlib oracle cases. CI has no sibling checkout, so the +//! bytes live in this crate and are compared with that checkout when it exists. + +const IDENTITY: &str = include_str!("fixtures/Identity.purs"); +const MALFORMED: &str = include_str!("fixtures/Malformed.purs"); + +#[test] +fn polymorphic_refs_keep_cell_identity_and_write_order() { + super::assert_matches_stdlib_oracle(IDENTITY, "conformance/native/Identity.purs"); + let Some(output) = super::super::run_program_with_prelude_wasmtime(&[("Main.purs", IDENTITY)]) + else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert_eq!(output.stdout, b"before\nafter\n", "{output:?}"); + assert!(output.stderr.is_empty(), "{output:?}"); +} + +#[test] +fn a_coerced_non_cell_ref_is_rejected_before_codegen() { + super::assert_matches_stdlib_oracle(MALFORMED, "conformance/native/Malformed.purs"); + let errors = crate::compile_program_sources_with_prelude(&[("Main.purs", MALFORMED)]) + .expect_err("Int and Ref have different runtime shapes"); + assert!( + errors.iter().any(|error| { + error.diagnostic.stage == "P8 closure conversion" + && error.diagnostic.message + == "unsupported aggregate conversion between normalized runtime shapes" + }), + "{errors:?}" + ); +} diff --git a/crates/psrs-driver/src/tests/native_representation/mod.rs b/crates/psrs-driver/src/tests/native_representation/mod.rs new file mode 100644 index 00000000..3dc1f700 --- /dev/null +++ b/crates/psrs-driver/src/tests/native_representation/mod.rs @@ -0,0 +1,116 @@ +//! Official Effect/ST adapters and generic representation regressions. + +mod algebraic; +mod arrays; +mod command; +mod host; +mod identity; +mod region; +mod semantics; +mod sources; +mod uncurried; + +use sources::ENTRY; + +const NAIVE_NOMINAL_EFFECT: &str = r#"module Main where +data Native f a = Return a | Suspend (f (Native f a)) +data NativeF next = NativeF next +newtype Effect a = Effect (Native NativeF a) +type role Effect representational +main :: Int +main = 0 +"#; + +const PHANTOM_FREE_ROLES: &str = r#"module Main where +data Val +data View f = Ret Val | Bound (f Val) (Val -> Native f Val) +data Native f a = Native (View f) +data NativeF next = NativeF next +data Global +newtype Effect a = Effect (Native NativeF a) +type role Effect representational +newtype ST r a = ST (Native NativeF a) +type role ST nominal representational +newtype STRef r a = STRef Int +type role STRef nominal representational +main :: Int +main = 0 +"#; + +fn rejects_role(source: &str) { + let errors = crate::check_program(&[("Main.purs", source)]).expect_err("should be rejected"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.code == Some("RoleMismatch")), + "expected RoleMismatch, got {errors:?}" + ); +} + +#[test] +fn a_naive_free_monad_rejects_a_representational_effect_role() { + rejects_role(NAIVE_NOMINAL_EFFECT); +} + +#[test] +fn a_phantom_free_native_program_accepts_official_effect_and_st_roles() { + crate::check_program(&[("Main.purs", PHANTOM_FREE_ROLES)]).unwrap_or_else(|errors| { + panic!("phantom Free should accept official Effect/ST roles: {errors:?}") + }); +} + +#[test] +fn official_st_and_effect_run_storage_and_fresh_refs() { + let Some(output) = super::run_program_with_prelude_wasmtime(&[("Main.purs", ENTRY)]) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +const HOST_CLOCK: &str = r#"module Main where +import Prelude +import Effect.Unsafe (unsafePerformEffect) +import PSRS.Effect.Host as Host +foreign import "wasi:clocks/monotonic-clock#now" clockNow :: Int +main :: Int +main = unsafePerformEffect (Host.lift \_ -> clockNow) * 0 + 42 +"#; + +#[test] +fn host_lift_delays_a_raw_canonical_clock_import() { + let Some(output) = super::run_program_with_prelude_wasmtime(&[("Main.purs", HOST_CLOCK)]) + else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +const OFFICIAL_CLOCK: &str = r#"module Main where +import Prelude +import Effect.Unsafe (unsafePerformEffect) +import WASI.Clock (now) +main :: Int +main = unsafePerformEffect now * 0 + 42 +"#; + +#[test] +fn official_clock_now_runs_through_effect() { + let Some(output) = super::run_program_with_prelude_wasmtime(&[("Main.purs", OFFICIAL_CLOCK)]) + else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +/// The public oracle stays in the sibling `psrs-stdlib` checkout. GitHub CI +/// only has this repository, so the driver embeds a copy. When the sibling is +/// present, the copy must be the same bytes. +fn assert_matches_stdlib_oracle(embedded: &str, relative: &str) { + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../psrs-stdlib") + .join(relative); + let Ok(oracle) = std::fs::read_to_string(&path) else { + return; + }; + assert_eq!(embedded, oracle, "{}", path.display()); +} diff --git a/crates/psrs-driver/src/tests/native_representation/region.rs b/crates/psrs-driver/src/tests/native_representation/region.rs new file mode 100644 index 00000000..f00264ab --- /dev/null +++ b/crates/psrs-driver/src/tests/native_representation/region.rs @@ -0,0 +1,150 @@ +//! Region isolation and constructor-hiding on the official ST facade. + +fn rejects(main: &str, code: &str) { + let errors = crate::check_program_types_lenient_with_prelude(&[("Main.purs", main)]) + .expect_err("program should be rejected"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.code == Some(code)), + "expected {code}, got {errors:?}" + ); +} + +#[test] +fn run_rejects_an_escaping_st_reference() { + rejects( + r#"module Main where +import Prelude +import Control.Monad.ST as ST +import Control.Monad.ST.Ref as STRef +import Effect (Effect) + +test _ = do + r <- pure (ST.run (STRef.new 0)) + pure r + +main :: forall a. a -> Effect Int +main = test +"#, + "EscapedSkolem", + ); +} + +#[test] +fn run_rejects_an_escaping_st_action() { + rejects( + r#"module Main where +import Prelude +import Control.Monad.ST (ST) +import Control.Monad.ST as ST +import Control.Monad.ST.Ref as STRef + +leak :: forall r. ST r (ST r Int) +leak = do + ref <- STRef.new 0 + pure (STRef.read ref) + +main :: Int +main = ST.run leak +"#, + "EscapedSkolem", + ); +} + +#[test] +fn a_reference_cannot_be_read_in_another_region() { + rejects( + r#"module Main where +import Control.Monad.ST (ST) +import Control.Monad.ST.Ref (STRef, read) + +bad :: forall r s a. STRef r a -> ST s a +bad = read + +main :: Int +main = 0 +"#, + "TypesDoNotUnify", + ); +} + +#[test] +fn nominal_stref_region_rejects_coerce() { + rejects( + r#"module Main where +import Control.Monad.ST.Ref (STRef) +import Safe.Coerce (coerce) + +bad :: forall r s a. STRef r a -> STRef s a +bad = coerce + +main :: Int +main = 0 +"#, + "NoInstanceFound", + ); +} + +#[test] +fn host_clock_is_not_an_st_request() { + rejects( + r#"module Main where +import Control.Monad.ST (ST) +import WASI.Clock (now) + +bad :: forall r. ST r Int +bad = now + +main :: Int +main = 0 +"#, + "TypesDoNotUnify", + ); +} + +#[test] +fn the_st_constructor_is_not_exported() { + rejects( + r#"module Main where +import Control.Monad.ST (ST) + +unwrap :: forall r a. ST r a -> Int +unwrap (ST _) = 0 + +main :: Int +main = 0 +"#, + "UnknownName", + ); +} + +#[test] +fn st_and_stref_regions_keep_the_official_region_kind() { + for name in ["ST", "STRef"] { + let source = format!( + "module Main where\nimport Control.Monad.ST.Internal ({name})\nforeign import bad :: {name} Int Int\nmain :: Int\nmain = 0\n" + ); + let errors = crate::check_program_kinds_lenient_with_prelude(&[("Main.purs", &source)]) + .expect_err("a Type cannot inhabit the Region kind"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.code == Some("KindsDoNotUnify")), + "{errors:?}" + ); + } +} + +#[test] +fn public_array_and_uncurried_adapters_keep_foreign_constructors_hidden() { + for (module, constructor) in [ + ("Data.Array.ST", "STArray"), + ("Control.Monad.ST.Uncurried", "STFn1"), + ] { + let source = format!( + "module Main where\nimport {module} as A\nunwrap (A.{constructor} value) = value\nmain :: Int\nmain = 0\n" + ); + rejects(&source, "UnknownName"); + } +} diff --git a/crates/psrs-driver/src/tests/native_representation/semantics.rs b/crates/psrs-driver/src/tests/native_representation/semantics.rs new file mode 100644 index 00000000..cc354485 --- /dev/null +++ b/crates/psrs-driver/src/tests/native_representation/semantics.rs @@ -0,0 +1,227 @@ +fn execute(source: &str, stdout: &[u8]) { + let Some(output) = super::super::run_program_with_prelude_wasmtime(&[("Main.purs", source)]) + else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert_eq!(output.stdout, stdout, "{output:?}"); + assert!(output.stderr.is_empty(), "{output:?}"); +} + +#[test] +fn pure_bind_delays_its_callback_but_keeps_factory_arguments_strict() { + execute( + r#"module Main where +import Prelude +import Effect (Effect) +import Effect.Unsafe (unsafePerformEffect) +import WASI.Console (log) + +factory :: Int -> Effect Int +factory n = pure n >>= \value -> + let _ = unsafePerformEffect (log "callback") in pure (value + 1) + +main :: Int +main = + let argument = let _ = unsafePerformEffect (log "argument") in 41 + action = factory argument + _ = unsafePerformEffect (log "stored") + first = unsafePerformEffect action + second = unsafePerformEffect action + in first + second - 42 +"#, + b"argument\nstored\ncallback\ncallback\n", + ); +} + +#[test] +fn public_loops_monadrec_and_left_and_right_bind_chains_are_stack_safe() { + execute( + r#"module Main where +import Prelude +import Control.Monad.Rec.Class (Step(..), tailRecM) +import Control.Monad.ST as ST +import Control.Monad.ST.Ref as STRef +import Effect (Effect, forE) +import Effect.Ref as Ref +import Effect.Unsafe (unsafePerformEffect) +import WASI.Console (log) + +left :: Int -> Effect Int -> Effect Int +left n action = if n == 0 then action else left (n - 1) (action >>= \x -> pure (x + 1)) + +right :: Int -> Effect Int +right n = if n == 0 then pure 0 else pure unit >>= \_ -> map (_ + 1) (right (n - 1)) + +mixed :: Effect Int +mixed = do + ref <- Ref.new 0 + forE 0 20000 \index -> do + Ref.modify_ (_ + 1) ref + if index == 0 || index == 19999 then log "edge" else pure unit + tailRecM (\n -> if n == 20000 then pure (Done unit) + else do + Ref.modify_ (_ + 1) ref + pure (Loop (n + 1))) 0 + Ref.read ref + +local :: Int +local = ST.run do + ref <- STRef.new 0 + ST.for 0 20000 \_ -> void (STRef.modify (_ + 1) ref) + STRef.read ref + +main :: Int +main = if unsafePerformEffect (left 20000 (pure 0)) == 20000 + && unsafePerformEffect (right 20000) == 20000 + && unsafePerformEffect mixed == 40000 && local == 20000 + then 42 else 0 +"#, + b"edge\nedge\n", + ); +} + +#[test] +fn repeated_runs_of_one_allocation_action_produce_independent_refs() { + execute( + r#"module Main where +import Prelude +import Effect (Effect) +import Effect.Ref (Ref) +import Effect.Ref as Ref +import Effect.Unsafe (unsafePerformEffect) + +allocate :: Effect (Ref Int) +allocate = Ref.new 0 + +main :: Int +main = unsafePerformEffect do + first <- allocate + second <- allocate + _ <- Ref.write 42 first + a <- Ref.read first + b <- Ref.read second + if a == 42 && b == 0 then pure 42 else pure 0 +"#, + b"", + ); +} + +#[test] +fn global_conversion_and_heterogeneous_cells_preserve_identity_across_runs() { + execute( + r#"module Main where +import Prelude +import Control.Monad.ST.Global (toEffect) +import Control.Monad.ST.Ref as STRef +import Effect (Effect) +import Effect.Ref as Ref +import Effect.Unsafe (unsafePerformEffect) + +change :: forall a. a -> Ref.Ref a -> Effect a +change value ref = Ref.write value ref *> Ref.read ref + +main :: Int +main = unsafePerformEffect do + global <- toEffect (STRef.new 0) + _ <- toEffect (STRef.write 40 global) + before <- toEffect (STRef.read global) + int <- Ref.new 0 + text <- Ref.new "old" + function <- Ref.new (\x -> x) + record <- Ref.new { value: 0, label: "old" } + let alias = int + a <- change (before + 1) int + b <- change "new" text + c <- change (\x -> x + 1) function + d <- change { value: 42, label: "new" } record + seen <- Ref.read alias + _ <- toEffect (STRef.modify (_ + 2) global) + after <- toEffect (STRef.read global) + if a == 41 && seen == 41 && b == "new" && c 41 == 42 + && d.value == 42 && d.label == b && after == 42 + then pure 42 else pure 0 +"#, + b"", + ); +} + +#[test] +fn ref_modify_invokes_its_callback_once_and_preserves_reentrant_order() { + execute( + r#"module Main where +import Prelude +import Effect.Ref as Ref +import Effect.Unsafe (unsafePerformEffect) + +main :: Int +main = unsafePerformEffect do + ref <- Ref.new 10 + calls <- Ref.new 0 + result <- Ref.modify' (\old -> + let _ = unsafePerformEffect (Ref.modify_ (_ + 1) calls) + _ = unsafePerformEffect (Ref.write 99 ref) + in { state: old + 1, value: old + 32 }) ref + now <- Ref.read ref + count <- Ref.read calls + writeResult <- Ref.write 12 ref + last <- Ref.modify (_ + 1) ref + if result == 42 && now == 11 && count == 1 && writeResult == unit && last == 13 + then pure 42 else pure 0 +"#, + b"", + ); +} + +#[test] +fn new_with_self_calls_once_and_publishes_the_same_reference() { + execute( + r#"module Main where +import Prelude +import Effect.Ref (Ref) +import Effect.Ref as Ref +import Effect.Unsafe (unsafePerformEffect) + +data Node = Node Int (Unit -> Ref Node) + +main :: Int +main = unsafePerformEffect do + count <- Ref.new 0 + ref <- Ref.newWithSelf (\self -> + let _ = unsafePerformEffect (Ref.modify_ (_ + 1) count) + in Node 41 (\_ -> self)) + Node before self <- Ref.read ref + _ <- Ref.write (Node (before + 1) self) (self unit) + Node after _ <- Ref.read ref + calls <- Ref.read count + if before == 41 && after == 42 && calls == 1 then pure 42 else pure 0 +"#, + b"", + ); +} + +#[test] +fn an_unpublished_self_read_traps_without_running_the_successor() { + let source = r#"module Main where +import Prelude +import Effect.Ref as Ref +import Effect.Unsafe (unsafePerformEffect) +import WASI.Console (log) + +main :: Int +main = unsafePerformEffect do + _ <- Ref.newWithSelf (\self -> unsafePerformEffect (Ref.read self)) + log "unreachable" + pure 42 +"#; + let Some(output) = super::super::run_program_with_prelude_wasmtime(&[("Main.purs", source)]) + else { + return; + }; + assert!(!output.status.success(), "{output:?}"); + assert!(output.stdout.is_empty(), "{output:?}"); + assert!( + String::from_utf8_lossy(&output.stderr).contains("unreachable"), + "{output:?}" + ); +} diff --git a/crates/psrs-driver/src/tests/native_representation/sources.rs b/crates/psrs-driver/src/tests/native_representation/sources.rs new file mode 100644 index 00000000..ca920897 --- /dev/null +++ b/crates/psrs-driver/src/tests/native_representation/sources.rs @@ -0,0 +1,62 @@ +//! Official Effect/ST programs over the package native representation. + +pub const ENTRY: &str = r#"module Main where + +import Prelude + +import Control.Monad.ST as ST +import Control.Monad.ST.Global (toEffect) +import Control.Monad.ST.Ref as STRef +import Effect (Effect) +import Effect.Ref (Ref) +import Effect.Ref as Ref +import Effect.Unsafe (unsafePerformEffect) + +storage :: forall r. ST.ST r Int +storage = do + ints <- STRef.new 0 + strs <- STRef.new "a" + _ <- STRef.write 41 ints + _ <- STRef.write "b" strs + first <- STRef.read ints + second <- STRef.read strs + if second == "b" then pure (first + 1) else pure 0 + +effectProgram :: Effect Int +effectProgram = do + ints <- Ref.new 0 + strs <- Ref.new "a" + _ <- Ref.write 41 ints + _ <- Ref.write "b" strs + first <- Ref.read ints + second <- Ref.read strs + if second == "b" then pure (first + 1) else pure 0 + +freshRefs :: Effect Int +freshRefs = do + a <- Ref.read r1 + b <- Ref.read r2 + if a == 1 && b == 2 then pure 42 else pure 0 + where + r1 :: Ref Int + r1 = unsafePerformEffect (do + r <- Ref.new 0 + _ <- Ref.write 1 r + pure r) + r2 :: Ref Int + r2 = unsafePerformEffect (do + r <- Ref.new 0 + _ <- Ref.write 2 r + pure r) + +main :: Int +main = + let viaST = ST.run storage + viaSTAgain = ST.run storage + viaEffect = unsafePerformEffect effectProgram + viaGlobal = unsafePerformEffect (toEffect storage) + viaFresh = unsafePerformEffect freshRefs + in if viaST == 42 && viaSTAgain == 42 && viaEffect == 42 && viaGlobal == 42 && viaFresh == 42 + then 42 + else 0 +"#; diff --git a/crates/psrs-driver/src/tests/native_representation/uncurried.rs b/crates/psrs-driver/src/tests/native_representation/uncurried.rs new file mode 100644 index 00000000..4d8c9c8c --- /dev/null +++ b/crates/psrs-driver/src/tests/native_representation/uncurried.rs @@ -0,0 +1,123 @@ +fn factory_source(st: bool, arity: usize) -> String { + let parameters = (1..=arity).map(|n| format!("x{n}")).collect::>(); + let signature = format!( + "{} -> {}", + vec!["Int"; arity].join(" -> "), + if st { "ST.ST r Int" } else { "Effect Int" } + ); + let arguments = std::iter::once("argument".to_owned()) + .chain((2..=arity).map(|n| n.to_string())) + .collect::>() + .join(" "); + let (imports, prefix, quantify, eliminate) = if st { + ( + "import Control.Monad.ST as ST\nimport Control.Monad.ST.Uncurried as U", + "STFn", + "forall r. ", + "ST.run", + ) + } else { + ( + "import Effect (Effect)\nimport Effect.Uncurried as U", + "EffectFn", + "", + "unsafePerformEffect", + ) + }; + let action_type = if st { + "forall r. ST.ST r Int" + } else { + "Effect Int" + }; + format!( + r#"module Main where +import Prelude +import Effect.Unsafe (unsafePerformEffect) +import Effect.Console (log) +{imports} + +callback :: {quantify}{signature} +callback {} = let _ = unsafePerformEffect (log "factory") in pure ({}) + +main :: Int +main = + let argument = let _ = unsafePerformEffect (log "argument") in 1 + action :: {action_type} + action = U.run{prefix}{arity} (U.mk{prefix}{arity} callback) {arguments} + _ = unsafePerformEffect (log "stored") + first = {eliminate} action + second = {eliminate} action + in if first == {} && second == {} then 42 else 0 +"#, + parameters.join(" "), + parameters + .iter() + .enumerate() + .map(|(index, name)| format!("{name} * {}", index + 1)) + .collect::>() + .join(" + "), + arity * (arity + 1) * (2 * arity + 1) / 6, + arity * (arity + 1) * (2 * arity + 1) / 6, + ) +} + +fn factories_are_delayed(st: bool) { + for arity in 1..=10 { + let source = factory_source(st, arity); + let Some(output) = + super::super::run_program_with_prelude_wasmtime(&[("Main.purs", &source)]) + else { + return; + }; + assert_eq!(output.status.code(), Some(42), "arity {arity}: {output:?}"); + assert_eq!( + output.stdout, b"argument\nstored\nfactory\nfactory\n", + "arity {arity}: {output:?}" + ); + assert!(output.stderr.is_empty(), "arity {arity}: {output:?}"); + } +} + +#[test] +fn st_uncurried_factories_wait_for_execution_at_every_arity() { + factories_are_delayed(true); +} + +#[test] +fn effect_uncurried_factories_wait_for_execution_at_every_arity() { + factories_are_delayed(false); +} + +#[test] +fn effect_uncurried_transports_mixed_values_and_preserves_pure_instances() { + let source = r#"module Main where +import Prelude +import Effect.Uncurried as U +import Effect.Unsafe (unsafePerformEffect) +import Effect.Console (log) + +record :: U.EffectFn2 String (Int -> Int) { label :: String, answer :: Int } +record = U.mkEffectFn2 \label f -> pure { label, answer: f 41 } + +left :: U.EffectFn1 Int String +left = U.mkEffectFn1 \_ -> log "left" *> pure "L" + +right :: U.EffectFn1 Int String +right = U.mkEffectFn1 \_ -> log "right" *> pure "R" + +main :: Int +main = unsafePerformEffect do + value <- U.runEffectFn2 record "mixed" (_ + 1) + joined <- U.runEffectFn1 (left <> right) 0 + empty <- U.runEffectFn1 (mempty :: U.EffectFn1 Int String) 0 + if value.label == "mixed" && value.answer == 42 && joined == "LR" && empty == "" + then pure 42 else pure 0 +"#; + let Some(output) = super::super::run_program_with_prelude_wasmtime(&[("Main.purs", source)]) + else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert_eq!(output.stdout, b"left\nright\n", "{output:?}"); + assert!(output.stderr.is_empty(), "{output:?}"); +} diff --git a/crates/psrs-driver/src/tests/number_abs.rs b/crates/psrs-driver/src/tests/number_abs.rs new file mode 100644 index 00000000..8d293728 --- /dev/null +++ b/crates/psrs-driver/src/tests/number_abs.rs @@ -0,0 +1,50 @@ +use super::*; + +#[test] +fn public_number_abs_preserves_magnitude_and_canonicalizes_negative_zero() { + let source = r#" +module Main where +import Prelude +import Data.Number as Number +apply f value = f value +positiveInfinity = 1.0 / 0.0 +negativeInfinity = negate positiveInfinity +nan = 0.0 / 0.0 +checks = Number.abs (-42.5) == 42.5 + && Number.abs 42.5 == 42.5 + && Number.abs (-4294967296.5) == 4294967296.5 + && Number.abs (-5.0e-324) == 5.0e-324 + && Number.abs (-1.7976931348623157e308) == 1.7976931348623157e308 + && 1.0 / Number.abs (negate 0.0) == positiveInfinity + && 1.0 / Number.abs 0.0 == positiveInfinity + && Number.abs negativeInfinity == positiveInfinity + && Number.abs positiveInfinity == positiveInfinity + && Number.abs nan /= Number.abs nan + && apply Number.abs (-42.0) == 42.0 +main :: Int +main = if checks then 42 else 1 +"#; + let mir = lower_source_to_mir(source); + assert!(format!("{mir:#?}").contains("F64Abs")); + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert!(output.stdout.is_empty() && output.stderr.is_empty()); +} + +#[test] +fn number_abs_requires_number_operand_and_result() { + for ty in ["Int -> Number", "Number -> Int", "forall a. a -> a"] { + let source = format!( + "module Main where\nforeign import \"psrs:intrinsic#numberAbs\" magnitude :: {ty}\nmain = 0\n" + ); + let errors = compile_program_sources(&[("Main.purs", &source)]) + .expect_err("absolute value requires a checked Number contract"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.stage == "P8 primitive linking") + ); + } +} diff --git a/crates/psrs-driver/src/tests/number_acos.rs b/crates/psrs-driver/src/tests/number_acos.rs new file mode 100644 index 00000000..1226d5e7 --- /dev/null +++ b/crates/psrs-driver/src/tests/number_acos.rs @@ -0,0 +1,60 @@ +use super::*; + +#[test] +fn public_number_acos_matches_domain_boundaries_and_rejects_exterior_values() { + let source = r#" +module Main where +import Prelude +import Data.Number as Number +foreign import "psrs:intrinsic#numberNeg" negative :: Number -> Number +apply f value = f value +positiveInfinity = 1.0 / 0.0 +negativeInfinity = negative positiveInfinity +nan = 0.0 / 0.0 +checks = Number.acos 1.0 == 0.0 + && 1.0 / Number.acos 1.0 == positiveInfinity + && Number.acos (-1.0) == 3.141592653589793 + && Number.acos 0.0 == 1.5707963267948966 + && Number.acos (negative 0.0) == 1.5707963267948966 + && Number.acos 0.5 == 1.0471975511965979 + && Number.acos (-0.5) == 2.0943951023931957 + && Number.acos 2.0 /= Number.acos 2.0 + && Number.acos (-2.0) /= Number.acos (-2.0) + && Number.acos positiveInfinity /= Number.acos positiveInfinity + && Number.acos negativeInfinity /= Number.acos negativeInfinity + && Number.acos nan /= Number.acos nan + && apply Number.acos 1.0 == 0.0 +main :: Int +main = if checks then 42 else 1 +"#; + let artifact = compile_program_sources_with_prelude(&[("Main.purs", source)]) + .expect("inverse cosine should compile"); + assert!( + artifact + .wasm + .windows(b"number_acos".len()) + .any(|window| window == b"number_acos"), + "the numeric runtime export should be linked" + ); + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert!(output.stdout.is_empty() && output.stderr.is_empty()); +} + +#[test] +fn number_acos_requires_number_operand_and_result() { + for ty in ["Int -> Number", "Number -> Int", "forall a. a -> a"] { + let source = format!( + "module Main where\nforeign import \"psrs:intrinsic#numberAcos\" angle :: {ty}\nmain = 0\n" + ); + let errors = compile_program_sources(&[("Main.purs", &source)]) + .expect_err("inverse cosine requires a checked Number contract"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.stage == "P8 primitive linking") + ); + } +} diff --git a/crates/psrs-driver/src/tests/number_asin.rs b/crates/psrs-driver/src/tests/number_asin.rs new file mode 100644 index 00000000..ec9c8583 --- /dev/null +++ b/crates/psrs-driver/src/tests/number_asin.rs @@ -0,0 +1,60 @@ +use super::*; + +#[test] +fn public_number_asin_matches_domain_boundaries_zero_signs_and_exterior_values() { + let source = r#" +module Main where +import Prelude +import Data.Number as Number +foreign import "psrs:intrinsic#numberNeg" negative :: Number -> Number +apply f value = f value +positiveInfinity = 1.0 / 0.0 +negativeInfinity = negative positiveInfinity +nan = 0.0 / 0.0 +checks = Number.asin 0.0 == 0.0 + && 1.0 / Number.asin 0.0 == positiveInfinity + && 1.0 / Number.asin (negative 0.0) == negativeInfinity + && Number.asin 1.0 == 1.5707963267948966 + && Number.asin (-1.0) == negative 1.5707963267948966 + && Number.asin 0.5 == 0.5235987755982989 + && Number.asin (-0.5) == negative 0.5235987755982989 + && Number.asin 2.0 /= Number.asin 2.0 + && Number.asin (-2.0) /= Number.asin (-2.0) + && Number.asin positiveInfinity /= Number.asin positiveInfinity + && Number.asin negativeInfinity /= Number.asin negativeInfinity + && Number.asin nan /= Number.asin nan + && apply Number.asin 0.0 == 0.0 +main :: Int +main = if checks then 42 else 1 +"#; + let artifact = compile_program_sources_with_prelude(&[("Main.purs", source)]) + .expect("inverse sine should compile"); + assert!( + artifact + .wasm + .windows(b"number_asin".len()) + .any(|window| window == b"number_asin"), + "the numeric runtime export should be linked" + ); + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert!(output.stdout.is_empty() && output.stderr.is_empty()); +} + +#[test] +fn number_asin_requires_number_operand_and_result() { + for ty in ["Int -> Number", "Number -> Int", "forall a. a -> a"] { + let source = format!( + "module Main where\nforeign import \"psrs:intrinsic#numberAsin\" angle :: {ty}\nmain = 0\n" + ); + let errors = compile_program_sources(&[("Main.purs", &source)]) + .expect_err("inverse sine requires a checked Number contract"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.stage == "P8 primitive linking") + ); + } +} diff --git a/crates/psrs-driver/src/tests/number_atan.rs b/crates/psrs-driver/src/tests/number_atan.rs new file mode 100644 index 00000000..c5608766 --- /dev/null +++ b/crates/psrs-driver/src/tests/number_atan.rs @@ -0,0 +1,60 @@ +use super::*; + +#[test] +fn public_number_atan_matches_zero_signs_unit_slopes_and_infinities() { + let source = r#" +module Main where +import Prelude +import Data.Number as Number +foreign import "psrs:intrinsic#numberNeg" negative :: Number -> Number +apply f value = f value +positiveInfinity = 1.0 / 0.0 +negativeInfinity = negative positiveInfinity +nan = 0.0 / 0.0 +tiny = 5.0e-324 +checks = tiny > 0.0 + && Number.atan tiny == tiny + && Number.atan (negative tiny) == negative tiny + && Number.atan 0.0 == 0.0 + && 1.0 / Number.atan 0.0 == positiveInfinity + && 1.0 / Number.atan (negative 0.0) == negativeInfinity + && Number.atan 1.0 == 0.7853981633974483 + && Number.atan (-1.0) == negative 0.7853981633974483 + && Number.atan positiveInfinity == 1.5707963267948966 + && Number.atan negativeInfinity == negative 1.5707963267948966 + && Number.atan nan /= Number.atan nan + && apply Number.atan 0.0 == 0.0 +main :: Int +main = if checks then 42 else 1 +"#; + let artifact = compile_program_sources_with_prelude(&[("Main.purs", source)]) + .expect("inverse tangent should compile"); + assert!( + artifact + .wasm + .windows(b"number_atan".len()) + .any(|window| window == b"number_atan"), + "the numeric runtime export should be linked" + ); + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert!(output.stdout.is_empty() && output.stderr.is_empty()); +} + +#[test] +fn number_atan_requires_number_operand_and_result() { + for ty in ["Int -> Number", "Number -> Int", "forall a. a -> a"] { + let source = format!( + "module Main where\nforeign import \"psrs:intrinsic#numberAtan\" angle :: {ty}\nmain = 0\n" + ); + let errors = compile_program_sources(&[("Main.purs", &source)]) + .expect_err("inverse tangent requires a checked Number contract"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.stage == "P8 primitive linking") + ); + } +} diff --git a/crates/psrs-driver/src/tests/number_atan2.rs b/crates/psrs-driver/src/tests/number_atan2.rs new file mode 100644 index 00000000..a086ad11 --- /dev/null +++ b/crates/psrs-driver/src/tests/number_atan2.rs @@ -0,0 +1,69 @@ +use super::*; + +#[test] +fn public_number_atan2_matches_quadrants_zero_signs_and_infinities() { + let source = r#" +module Main where +import Prelude +import Data.Number as Number +foreign import "psrs:intrinsic#numberNeg" negative :: Number -> Number +apply f y x = f y x +positiveInfinity = 1.0 / 0.0 +negativeInfinity = negative positiveInfinity +nan = 0.0 / 0.0 +checks = Number.atan2 0.0 1.0 == 0.0 + && 1.0 / Number.atan2 0.0 1.0 == positiveInfinity + && 1.0 / Number.atan2 (negative 0.0) 1.0 == negativeInfinity + && Number.atan2 0.0 (negative 1.0) == 3.141592653589793 + && Number.atan2 (negative 0.0) (negative 1.0) == negative 3.141592653589793 + && Number.atan2 1.0 0.0 == 1.5707963267948966 + && Number.atan2 (negative 1.0) 0.0 == negative 1.5707963267948966 + && Number.atan2 1.0 (negative 0.0) == 1.5707963267948966 + && Number.atan2 1.0 positiveInfinity == 0.0 + && 1.0 / Number.atan2 (negative 1.0) positiveInfinity == negativeInfinity + && Number.atan2 1.0 negativeInfinity == 3.141592653589793 + && Number.atan2 positiveInfinity positiveInfinity == 0.7853981633974483 + && Number.atan2 positiveInfinity negativeInfinity == 2.356194490192345 + && Number.atan2 negativeInfinity negativeInfinity == negative 2.356194490192345 + && Number.atan2 0.1 (negative 1.0e-20) == 1.5707963267948966 + && Number.atan2 nan 1.0 /= Number.atan2 nan 1.0 + && Number.atan2 1.0 nan /= Number.atan2 1.0 nan + && apply Number.atan2 0.0 1.0 == 0.0 +main :: Int +main = if checks then 42 else 1 +"#; + let artifact = compile_program_sources_with_prelude(&[("Main.purs", source)]) + .expect("four-quadrant inverse tangent should compile"); + assert!( + artifact + .wasm + .windows(b"number_atan2".len()) + .any(|window| window == b"number_atan2"), + "the numeric runtime export should be linked" + ); + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert!(output.stdout.is_empty() && output.stderr.is_empty()); +} + +#[test] +fn number_atan2_requires_number_operands_and_result() { + for ty in [ + "Int -> Number -> Number", + "Number -> Number -> Int", + "forall a. a -> a -> a", + ] { + let source = format!( + "module Main where\nforeign import \"psrs:intrinsic#numberAtan2\" angle :: {ty}\nmain = 0\n" + ); + let errors = compile_program_sources(&[("Main.purs", &source)]) + .expect_err("four-quadrant inverse tangent requires a checked Number contract"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.stage == "P8 primitive linking") + ); + } +} diff --git a/crates/psrs-driver/src/tests/number_decimal.rs b/crates/psrs-driver/src/tests/number_decimal.rs new file mode 100644 index 00000000..e0a4ccc5 --- /dev/null +++ b/crates/psrs-driver/src/tests/number_decimal.rs @@ -0,0 +1,102 @@ +use super::*; + +#[test] +fn public_number_from_string_executes_the_official_uncurried_wrapper() { + let source = r#" +module Main where +import Prelude +import Data.Number as Number +import Data.Maybe (Maybe(..)) +main :: Int +main = case Number.fromString "42.5" of + Just value -> if value == 42.5 then 42 else 1 + Nothing -> 1 +"#; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert!(output.stdout.is_empty() && output.stderr.is_empty()); +} + +#[test] +fn complete_decimal_conversion_preserves_rounding_range_and_zero_signs() { + let source = r#" +module Main where +foreign import "psrs:intrinsic#numberFromDecimal" parse :: String -> Number +foreign import "psrs:intrinsic#numberEq" equal :: Number -> Number -> Boolean +foreign import "psrs:intrinsic#numberDiv" divide :: Number -> Number -> Number +foreign import "psrs:intrinsic#numberNeg" negative :: Number -> Number +foreign import "psrs:intrinsic#booleanAnd" both :: Boolean -> Boolean -> Boolean +foreign import "psrs:intrinsic#numberNe" unequal :: Number -> Number -> Boolean +invalid value = let number = parse value in unequal number number +precision = both (equal (parse "9007199254740993") 9007199254740992.0) + (equal (parse "1.00000000000000011102230246251565404236316680908203125") 1.0) +range = both (equal (parse "5e-324") 5.0e-324) (equal (parse "1e309") (divide 1.0 0.0)) +zeros = equal (divide 1.0 (parse "-1e-9999")) (divide (negative 1.0) 0.0) +grammar = both (invalid "42.5tail") (both (invalid "Infinity") (both (invalid " 42.5") (invalid "1e+"))) +main = if both precision (both range (both zeros grammar)) then 42 else 1 +"#; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert!(output.stdout.is_empty() && output.stderr.is_empty()); +} + +#[test] +fn decimal_foreign_bindings_require_string_operand_and_number_result() { + for ty in [ + "Int -> Number", + "String -> Int", + "Number -> Number", + "forall a. a -> a", + ] { + let source = format!( + "module Main where\nforeign import \"psrs:intrinsic#numberFromDecimal\" parse :: {ty}\nmain = 0\n" + ); + let errors = compile_program_sources(&[("Main.purs", &source)]) + .expect_err("invalid decimal primitive contract"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.stage == "P8 primitive linking") + ); + } +} + +#[test] +fn explicit_type_applications_retain_universal_newtype_arguments() { + let source = r#" +module Main where +newtype Wrapper a b = Wrapper (a -> b) +make :: forall @a @b. (a -> b) -> Wrapper a b +make f = Wrapper f +makePoly :: forall @b. ((forall a. a -> a) -> b) -> Wrapper (forall a. a -> a) b +makePoly = make @(forall a. a -> a) +apply :: (forall a. a -> a) -> Int +apply f = if f true then f 42 else 1 +wrapped :: Wrapper (forall a. a -> a) Int +wrapped = makePoly @Int apply +run :: forall a b. Wrapper a b -> a -> b +run (Wrapper f) value = f value +main :: Int +main = run wrapped (\x -> x) +"#; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert!(output.stdout.is_empty() && output.stderr.is_empty()); + let invalid = r#" +module Main where +data Box a = Box a +make :: forall @a. a -> Box a +make value = Box value +wrapped :: Box (forall a. a -> a) +wrapped = make @(forall a. a -> a) (\x -> 42) +main :: Int +main = 42 +"#; + assert!(compile_source("Main.purs", invalid).is_err()); +} diff --git a/crates/psrs-driver/src/tests/number_elementary.rs b/crates/psrs-driver/src/tests/number_elementary.rs new file mode 100644 index 00000000..4c2e3591 --- /dev/null +++ b/crates/psrs-driver/src/tests/number_elementary.rs @@ -0,0 +1,107 @@ +use super::*; + +#[test] +fn public_number_elementary_functions_match_javascript_special_cases() { + let source = r#" +module Main where +import Prelude +import Data.Number as Number +foreign import "psrs:intrinsic#numberNeg" negative :: Number -> Number +positiveInfinity = 1.0 / 0.0 +negativeInfinity = negative positiveInfinity +zero = Number.sin 0.0 == 0.0 + && 1.0 / Number.sin 0.0 == positiveInfinity + && 1.0 / Number.sin (negative 0.0) == negativeInfinity + && Number.cos 0.0 == 1.0 + && Number.tan 0.0 == 0.0 + && 1.0 / Number.tan (negative 0.0) == negativeInfinity + && Number.exp 0.0 == 1.0 + && Number.log 1.0 == 0.0 + && Number.log positiveInfinity == positiveInfinity + && Number.log 0.0 == negativeInfinity + && Number.isNaN (Number.log (negative 1.0)) + && Number.isNaN Number.nan + && Number.isNaN (Number.cos positiveInfinity) + && not (Number.isNaN 0.0) + && not (Number.isNaN Number.infinity) + && Number.infinity == positiveInfinity + && negative Number.infinity == negativeInfinity + && Number.min 1.0 2.0 == 1.0 + && Number.max 1.0 2.0 == 2.0 + && 1.0 / Number.min (negative 0.0) 0.0 == negativeInfinity + && 1.0 / Number.max (negative 0.0) 0.0 == positiveInfinity + && 1.0 / Number.max (negative 0.0) (negative 0.0) == negativeInfinity + && Number.isNaN (Number.min Number.nan 1.0) + && Number.isNaN (Number.max 1.0 Number.nan) + && Number.pow 3.0 2.0 == 9.0 + && Number.pow Number.nan 0.0 == 1.0 + && Number.isNaN (Number.pow 1.0 Number.nan) + && Number.isNaN (Number.pow 1.0 Number.infinity) + && Number.isNaN (Number.pow (negative 1.0) negativeInfinity) + && Number.remainder 5.3 2.0 == 1.2999999999999998 + && 1.0 / Number.remainder (negative 1.0) 1.0 == negativeInfinity + && Number.isNaN (Number.remainder 1.0 0.0) + && Number.isNaN (Number.remainder positiveInfinity 1.0) + && Number.sign 4.0 == 1.0 + && Number.sign (negative 4.0) == negative 1.0 + && Number.sign Number.infinity == 1.0 + && Number.sign negativeInfinity == negative 1.0 + && 1.0 / Number.sign (negative 0.0) == negativeInfinity + && Number.isNaN (Number.sign Number.nan) +main :: Int +main = if zero then 42 else 1 +"#; + let artifact = compile_program_sources_with_prelude(&[("Main.purs", source)]) + .expect("remaining Number foreigns should compile"); + for export in [ + "number_sin", + "number_cos", + "number_tan", + "number_exp", + "number_log", + "number_pow", + "number_min", + "number_max", + "number_sign", + "number_remainder", + "number_is_nan", + "number_nan", + "number_infinity", + ] { + assert!( + artifact + .wasm + .windows(export.len()) + .any(|window| window == export.as_bytes()), + "{export} should be linked" + ); + } + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert!(output.stdout.is_empty() && output.stderr.is_empty()); +} + +#[test] +fn number_constants_and_predicates_require_their_checked_types() { + for (binding, ty) in [ + ("numberNan", "Int"), + ("numberInfinity", "Int"), + ("numberIsNaN", "Number -> Number"), + ("numberRemainder", "Int -> Number -> Number"), + ("numberPow", "forall a. a -> a -> a"), + ] { + let source = format!( + "module Main where\nforeign import \"psrs:intrinsic#{binding}\" value :: {ty}\nmain = 0\n" + ); + let errors = compile_program_sources(&[("Main.purs", &source)]) + .expect_err("the binding requires its checked Number contract"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.stage == "P8 primitive linking"), + "{binding} {ty}: {errors:?}" + ); + } +} diff --git a/crates/psrs-driver/src/tests/number_rounding.rs b/crates/psrs-driver/src/tests/number_rounding.rs new file mode 100644 index 00000000..909fbbac --- /dev/null +++ b/crates/psrs-driver/src/tests/number_rounding.rs @@ -0,0 +1,70 @@ +use super::*; + +#[test] +fn number_floor_and_ceil_preserve_range_nonfinite_values_and_signed_zero() { + for (binding, instruction, positive, negative, large, zero_input) in [ + ( + "numberFloor", + "F64Floor", + "42.0", + "43.0", + "4294967296.0", + "0.5", + ), + ( + "numberCeil", + "F64Ceil", + "43.0", + "42.0", + "4294967297.0", + "(negative 0.5)", + ), + ] { + let zero_sign = if binding == "numberFloor" { + "1.0" + } else { + "(negative 1.0)" + }; + let source = format!( + r#" +module Main where +foreign import "psrs:intrinsic#{binding}" rounding :: Number -> Number +foreign import "psrs:intrinsic#numberDiv" divide :: Number -> Number -> Number +foreign import "psrs:intrinsic#numberNeg" negative :: Number -> Number +foreign import "psrs:intrinsic#numberEq" equal :: Number -> Number -> Boolean +foreign import "psrs:intrinsic#numberNe" unequal :: Number -> Number -> Boolean +foreign import "psrs:intrinsic#booleanAnd" and :: Boolean -> Boolean -> Boolean +fraction = and (equal (rounding 42.9) {positive}) (equal (rounding (negative 42.9)) (negative {negative})) +large = equal (rounding 4294967296.5) {large} +zeros = and (equal (divide 1.0 (rounding {zero_input})) (divide {zero_sign} 0.0)) (equal (divide 1.0 (rounding (negative 0.0))) (divide (negative 1.0) 0.0)) +nonfinite = and (equal (rounding (divide 1.0 0.0)) (divide 1.0 0.0)) (and (equal (rounding (divide (negative 1.0) 0.0)) (divide (negative 1.0) 0.0)) (unequal (rounding (divide 0.0 0.0)) (rounding (divide 0.0 0.0)))) +main = if and fraction (and large (and zeros nonfinite)) then 42 else 1 +"# + ); + let mir = lower_source_to_mir(&source); + assert!(format!("{mir:#?}").contains(instruction)); + let Some(output) = run_with_wasmtime(&source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{binding}: {output:?}"); + assert!(output.stdout.is_empty() && output.stderr.is_empty()); + } +} + +#[test] +fn number_floor_and_ceil_reject_invalid_foreign_contracts() { + for binding in ["numberFloor", "numberCeil"] { + for ty in ["Int -> Number", "Number -> Int", "forall a. a -> a"] { + let source = format!( + "module Main where\nforeign import \"psrs:intrinsic#{binding}\" rounding :: {ty}\nmain = 0\n" + ); + let errors = compile_program_sources(&[("Main.purs", &source)]) + .expect_err("rounding requires Number operand and result"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.stage == "P8 primitive linking") + ); + } + } +} diff --git a/crates/psrs-driver/src/tests/number_sqrt.rs b/crates/psrs-driver/src/tests/number_sqrt.rs new file mode 100644 index 00000000..bd22d695 --- /dev/null +++ b/crates/psrs-driver/src/tests/number_sqrt.rs @@ -0,0 +1,51 @@ +use super::*; + +#[test] +fn public_number_sqrt_matches_ieee_squares_zero_signs_and_nonfinite_values() { + let source = r#" +module Main where +import Prelude +import Data.Number as Number +foreign import "psrs:intrinsic#numberNeg" negative :: Number -> Number +apply f value = f value +positiveInfinity = 1.0 / 0.0 +negativeInfinity = negative positiveInfinity +nan = 0.0 / 0.0 +checks = Number.sqrt 4.0 == 2.0 + && Number.sqrt 9.0 == 3.0 + && Number.sqrt 4294967296.0 == 65536.0 + && Number.sqrt 0.0 == 0.0 + && 1.0 / Number.sqrt 0.0 == positiveInfinity + && 1.0 / Number.sqrt (negative 0.0) == negativeInfinity + && Number.sqrt positiveInfinity == positiveInfinity + && Number.sqrt (-1.0) /= Number.sqrt (-1.0) + && Number.sqrt negativeInfinity /= Number.sqrt negativeInfinity + && Number.sqrt nan /= Number.sqrt nan + && apply Number.sqrt 4.0 == 2.0 +main :: Int +main = if checks then 42 else 1 +"#; + let mir = lower_source_to_mir(source); + assert!(format!("{mir:#?}").contains("F64Sqrt")); + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert!(output.stdout.is_empty() && output.stderr.is_empty()); +} + +#[test] +fn number_sqrt_requires_number_operand_and_result() { + for ty in ["Int -> Number", "Number -> Int", "forall a. a -> a"] { + let source = format!( + "module Main where\nforeign import \"psrs:intrinsic#numberSqrt\" root :: {ty}\nmain = 0\n" + ); + let errors = compile_program_sources(&[("Main.purs", &source)]) + .expect_err("square root requires a checked Number contract"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.stage == "P8 primitive linking") + ); + } +} diff --git a/crates/psrs-driver/src/tests/number_trunc.rs b/crates/psrs-driver/src/tests/number_trunc.rs new file mode 100644 index 00000000..6cc23e09 --- /dev/null +++ b/crates/psrs-driver/src/tests/number_trunc.rs @@ -0,0 +1,42 @@ +use super::*; + +#[test] +fn number_trunc_preserves_number_range_nonfinite_values_and_signed_zero() { + let source = r#" +module Main where +foreign import "psrs:intrinsic#numberTrunc" truncate :: Number -> Number +foreign import "psrs:intrinsic#numberDiv" divide :: Number -> Number -> Number +foreign import "psrs:intrinsic#numberNeg" negative :: Number -> Number +foreign import "psrs:intrinsic#numberEq" equal :: Number -> Number -> Boolean +foreign import "psrs:intrinsic#numberNe" unequal :: Number -> Number -> Boolean +foreign import "psrs:intrinsic#booleanAnd" and :: Boolean -> Boolean -> Boolean +fraction = and (equal (truncate 42.9) 42.0) (equal (truncate (negative 42.9)) (negative 42.0)) +large = and (equal (truncate 4294967296.5) 4294967296.0) (equal (truncate (negative 4294967296.5)) (negative 4294967296.0)) +zeros = and (equal (divide 1.0 (truncate 0.5)) (divide 1.0 0.0)) (equal (divide 1.0 (truncate (negative 0.5))) (divide (negative 1.0) 0.0)) +nonfinite = and (equal (truncate (divide 1.0 0.0)) (divide 1.0 0.0)) (and (equal (truncate (divide (negative 1.0) 0.0)) (divide (negative 1.0) 0.0)) (unequal (truncate (divide 0.0 0.0)) (truncate (divide 0.0 0.0)))) +main = if and fraction (and large (and zeros nonfinite)) then 42 else 1 +"#; + let mir = lower_source_to_mir(source); + assert!(format!("{mir:#?}").contains("F64Trunc")); + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert!(output.stdout.is_empty() && output.stderr.is_empty()); +} + +#[test] +fn number_trunc_checks_foreign_operand_and_result_contracts() { + for ty in ["Int -> Number", "Number -> Int", "forall a. a -> a"] { + let source = format!( + "module Main where\nforeign import \"psrs:intrinsic#numberTrunc\" truncate :: {ty}\nmain = 0\n" + ); + let errors = compile_program_sources(&[("Main.purs", &source)]) + .expect_err("truncation requires Number operand and result"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.stage == "P8 primitive linking") + ); + } +} diff --git a/crates/psrs-driver/src/tests/operators/local_types.rs b/crates/psrs-driver/src/tests/operators/local_types.rs new file mode 100644 index 00000000..a9402787 --- /dev/null +++ b/crates/psrs-driver/src/tests/operators/local_types.rs @@ -0,0 +1,26 @@ +use super::*; + +#[test] +fn a_local_signature_with_a_type_operator_chain_is_desugared() { + // A `where`/`let` binding signature is elaborated as a `Typed` expression. + // Its written type must go through P4 like any other type, or the operator + // chain survives into kind checking as `UnloweredTypeOperator`. + let source = r#"module Main where + +import Prelude + +type Pair a b = { _1 :: a, _2 :: b } + +infixr 5 type Pair as :+: + +main :: Int +main = helper { _1: 42, _2: true } + where + helper :: Int :+: Boolean -> Int + helper p = p._1 +"#; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} diff --git a/crates/psrs-driver/src/tests/operators/mod.rs b/crates/psrs-driver/src/tests/operators/mod.rs index cab83afd..c9e164dc 100644 --- a/crates/psrs-driver/src/tests/operators/mod.rs +++ b/crates/psrs-driver/src/tests/operators/mod.rs @@ -1,11 +1,16 @@ +mod local_types; +mod natural_transformation; mod opaque_type; mod prim_type; +mod qualified_patterns; +mod sections; +mod synonyms; use super::*; const FIXITY_SOURCE: &str = "module Main where\n\ infixr 4 subtract as <+>\n\ - subtract x y = x - y\n\ + subtract x y = intSub x y\n\ main = 10 <+> 3 <+> 2\n"; #[test] @@ -456,7 +461,7 @@ fn ambiguous_imported_fixity_targets_report_scope_conflicts() { fn rejects_ambiguous_fixity_groups_for_values_patterns_and_types() { let cases = [ ( - "module Main where\ninfixl 5 subtract as <+>\ninfixr 5 subtract as <*>\nsubtract x y = x - y\nmain = 1 <+> 2 <*> 3\n", + "module Main where\ninfixl 5 subtract as <+>\ninfixr 5 subtract as <*>\nsubtract x y = intSub x y\nmain = 1 <+> 2 <*> 3\n", "operators of the same precedence have mixed associativity", ), ( diff --git a/crates/psrs-driver/src/tests/operators/natural_transformation.rs b/crates/psrs-driver/src/tests/operators/natural_transformation.rs new file mode 100644 index 00000000..7374fc0c --- /dev/null +++ b/crates/psrs-driver/src/tests/operators/natural_transformation.rs @@ -0,0 +1,56 @@ +use super::*; + +#[test] +fn unimported_natural_transformation_alias_is_rejected_in_bare_and_infix_forms() { + for ty in ["Holder ((~>) Int)", "Int ~> Int"] { + let source = format!( + "module Main where\ndata Holder f = Holder (f Int)\nfoo :: {ty}\nfoo = foo\nmain = 42\n" + ); + let errors = resolve_program_sources(&[("Main.purs", &source)]) + .expect_err("a type operator requires a declaration or import"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.code == Some("UnknownName")), + "{errors:?}" + ); + } +} + +#[test] +fn hiding_the_natural_transformation_alias_does_not_restore_a_builtin_arrow() { + let library = ( + "Alias.purs", + "module Alias where\ninfixr 0 type Function as ~>\n", + ); + let source = "module Main where\nimport Alias hiding (type (~>))\ndata Holder f = Holder (f Int)\nfoo :: Holder ((~>) Int)\nfoo = Holder (\\x -> x)\nmain = 42\n"; + let errors = resolve_program_sources(&[library, ("Main.purs", source)]) + .expect_err("a hidden type alias is not in scope"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.code == Some("UnknownName")), + "{errors:?}" + ); +} + +#[test] +fn a_declared_bare_natural_transformation_alias_retains_its_target() { + let source = "module Main where\ninfixr 0 type Function as ~>\ndata Holder f = Holder (f Int)\nfoo :: Holder ((~>) Int)\nfoo = Holder (\\x -> x)\nmain = 42\n"; + typecheck_program_sources(&[("Main.purs", source)]) + .expect("the explicitly declared Function alias is a valid partial constructor"); +} + +#[test] +fn other_bare_type_operator_aliases_use_the_same_lookup() { + let source = "module Main where\ninfixr 0 type Function as :+\ndata Holder f = Holder (f Int)\nfoo :: Holder ((:+) Int)\nfoo = Holder (\\x -> x)\nmain = 42\n"; + typecheck_program_sources(&[("Main.purs", source)]).expect("ordinary operator alias"); + let source = source.replace("infixr 0 type Function as :+\n", ""); + let errors = resolve_program_sources(&[("Main.purs", &source)]).expect_err("missing alias"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.code == Some("UnknownName")), + "{errors:?}" + ); +} diff --git a/crates/psrs-driver/src/tests/operators/qualified_patterns.rs b/crates/psrs-driver/src/tests/operators/qualified_patterns.rs new file mode 100644 index 00000000..e3760ae8 --- /dev/null +++ b/crates/psrs-driver/src/tests/operators/qualified_patterns.rs @@ -0,0 +1,26 @@ +use super::*; + +#[test] +fn qualified_constructor_operator_patterns_reassociate_and_execute() { + let library = ( + "Chain.purs", + r"module Chain where +data Chain a = Link a (Chain a) | Empty +infixr 6 Link as : +data Wrapped = Wrap Int +", + ); + let main = ( + "Main.purs", + r"module Main where +import Chain as Foo.Bar +values = Foo.Bar.Wrap 20 Foo.Bar.: Foo.Bar.Wrap 22 Foo.Bar.: Foo.Bar.Empty +main :: Int +main = case values of + Foo.Bar.Wrap first Foo.Bar.: Foo.Bar.Wrap second Foo.Bar.: _ -> intAdd first second + _ -> 0 +", + ); + let output = run_program_with_wasmtime(&[library, main]).expect("required Wasmtime runtime"); + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} diff --git a/crates/psrs-driver/src/tests/operators/sections.rs b/crates/psrs-driver/src/tests/operators/sections.rs new file mode 100644 index 00000000..e06fe09f --- /dev/null +++ b/crates/psrs-driver/src/tests/operators/sections.rs @@ -0,0 +1,21 @@ +use super::*; + +#[test] +fn backticked_sections_keep_operand_order_and_resolve_local_values() { + let source = r#" +module Main where +subtract :: Int -> Int -> Int +subtract x y = intSub x y +main :: Int +main = let left = (_ `subtract` 8) + right = (50 `subtract` _) + local x y = intSub x y + in if intEq (left 50) 42 + then if intEq (right 8) 42 then (_ `local` 8) 50 else 0 + else 0 +"#; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} diff --git a/crates/psrs-driver/src/tests/operators/synonyms.rs b/crates/psrs-driver/src/tests/operators/synonyms.rs new file mode 100644 index 00000000..0c3d9c99 --- /dev/null +++ b/crates/psrs-driver/src/tests/operators/synonyms.rs @@ -0,0 +1,40 @@ +use super::*; + +#[test] +fn higher_order_alias_arguments_are_substituted_before_elaboration() { + let source = r"module Main where +type Apply f x = f x +type Ignore f = Int +type Wrap a b = { value :: a, other :: b } +value :: Apply (Apply (Wrap Int)) Boolean +value = { value: 42, other: true } +main :: Ignore (Wrap Int) +main = value.value +"; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +#[test] +fn open_row_alias_operator_preserves_the_row_tail() { + let source = r"module Main where +type RowApply f a = f a +infixr 0 type RowApply as + +type VALUE r = (value :: Int | r) +read :: forall r. Record (VALUE + r) -> Int +read row = row.value +main = read { value: 42, other: true } +"; + typecheck_program_sources(&[("Main.purs", source)]) + .expect("the open tail survives kind checking and elaboration"); + let closed = source.replace( + "forall r. Record (VALUE + r)", + "Record (VALUE + (other :: Boolean))", + ); + let Some(output) = run_with_wasmtime(&closed) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} diff --git a/crates/psrs-driver/src/tests/parameterized_shapes.rs b/crates/psrs-driver/src/tests/parameterized_shapes.rs index e0e24fcd..94c028eb 100644 --- a/crates/psrs-driver/src/tests/parameterized_shapes.rs +++ b/crates/psrs-driver/src/tests/parameterized_shapes.rs @@ -1,7 +1,7 @@ use super::*; #[test] -fn maps_generic_arrays_across_polymorphic_adt_boundaries() { +fn preserves_generic_arrays_across_polymorphic_adt_boundaries() { let source = "\ module Main where data Wrap a = Wrap (Array a) @@ -13,23 +13,19 @@ unwrap value = case value of main = arrayIndex (unwrap (wrap [40, 42])) 1 "; let prepared = crate::prepare_main(source).expect("source should lower to Core"); - let generic_cc = psrs_backend::lower_cc_with_context( - prepared.core.clone(), - prepared.effect_context.as_ref(), - ) - .expect("generic source Core should lower to CC before P7") - .cc; + let generic_cc = psrs_backend::cc::lower_module(prepared.core.clone()) + .expect("generic source Core should lower to CC before P7") + .cc; let generic_array_maps = aggregate_conversions(&generic_cc) .iter() .map(|conversion| array_map_count(&conversion.plan)) .sum::(); assert!( - generic_array_maps >= 2, - "expected pre-P7 concrete/generic boundary maps; found {generic_array_maps}" + generic_array_maps == 0, + "expected no array copying at pre-P7 concrete/generic boundaries; found {generic_array_maps}" ); let stages = psrs_backend::compile_with_context( prepared.core, - prepared.effect_context, psrs_backend::TargetCapabilities::default(), ) .expect("generic Array a must map at concrete call boundaries"); @@ -82,12 +78,12 @@ main = arrayIndex (unwrap (wrap [40, 42])) 1 }) .count(); assert!( - array_maps >= 2, - "expected specialization and canonicalization maps; found {array_maps}" + array_maps == 0, + "expected no array reconstruction; found {array_maps}" ); - assert!(stages.artifact.wat.contains("array.new_default")); + assert!(!stages.artifact.wat.contains("array.new_default")); assert!(stages.artifact.wat.contains("array.get")); - assert!(stages.artifact.wat.contains("array.set")); + assert!(stages.artifact.wat.contains("array.new_fixed")); let Some(output) = run_wasmtime(source) else { eprintln!("skipping execution: wasmtime is not installed"); return; @@ -108,22 +104,22 @@ copy record = record { value = record.value } main = arrayIndex ((copy { items: [40, 42], value: 7 }).items) 1 "; let prepared = crate::prepare_main(source).expect("source should lower to Core"); - let generic_cc = psrs_backend::lower_cc_with_context( - prepared.core.clone(), - prepared.effect_context.as_ref(), - ) - .expect("generic source Core should lower to CC before P7") - .cc; + let generic_cc = psrs_backend::cc::lower_module(prepared.core.clone()) + .expect("generic source Core should lower to CC before P7") + .cc; let generic_conversions = aggregate_conversions(&generic_cc); assert!( generic_conversions .iter() - .any(|conversion| { contains_canonical_record_array_map(&conversion.plan) }), - "expected a pre-P7 canonical closed-record map containing a nested array map" + .all(|conversion| array_map_count(&conversion.plan) == 0), + "record conversion must retain its nested array storage" ); + assert!(generic_cc.functions.iter().flat_map(|function| &function.assignments).any(|assignment| + matches!(assignment.kind, psrs_backend::cc::AssignmentKind::ProductNew { representation, .. } + if generic_cc.representations.product_labels(representation) == Some(&["items".to_owned(), "value".to_owned()]))), + "record conversion retains explicit product construction"); let stages = psrs_backend::compile_with_context( prepared.core, - prepared.effect_context, psrs_backend::TargetCapabilities::default(), ) .expect("closed generic records should map across concrete instantiations"); @@ -143,7 +139,7 @@ main = arrayIndex ((copy { items: [40, 42], value: 7 }).items) 1 } #[test] -fn maps_nested_generic_arrays_recursively_across_instantiations() { +fn preserves_nested_generic_arrays_across_instantiations() { let source = "\ module Main where duplicate :: forall a. Array (Array a) -> Array (Array a) @@ -151,12 +147,9 @@ duplicate values = values main = arrayIndex (arrayIndex (duplicate [[40, 42]]) 0) 1 "; let prepared = crate::prepare_main(source).expect("source should lower to Core"); - let generic_cc = psrs_backend::lower_cc_with_context( - prepared.core.clone(), - prepared.effect_context.as_ref(), - ) - .expect("generic source Core should lower to CC before P7") - .cc; + let generic_cc = psrs_backend::cc::lower_module(prepared.core.clone()) + .expect("generic source Core should lower to CC before P7") + .cc; let conversions = aggregate_conversions(&generic_cc); let maximum_nested_array_maps = conversions .iter() @@ -164,12 +157,11 @@ main = arrayIndex (arrayIndex (duplicate [[40, 42]]) 0) 1 .max() .unwrap_or_default(); assert!( - maximum_nested_array_maps >= 2, - "expected pre-P7 recursively nested ArrayMap plans; found depth {maximum_nested_array_maps}" + maximum_nested_array_maps == 0, + "expected no recursive array reconstruction; found depth {maximum_nested_array_maps}" ); let stages = psrs_backend::compile_with_context( prepared.core, - prepared.effect_context, psrs_backend::TargetCapabilities::default(), ) .expect("nested generic arrays should map recursively"); @@ -196,23 +188,19 @@ concrete values = values main = arrayIndex (applyArray concrete [40, 42]) 1 "; let prepared = crate::prepare_main(source).expect("source should lower to Core"); - let generic_cc = psrs_backend::lower_cc_with_context( - prepared.core.clone(), - prepared.effect_context.as_ref(), - ) - .expect("generic source Core should lower to CC before P7") - .cc; + let generic_cc = psrs_backend::cc::lower_module(prepared.core.clone()) + .expect("generic source Core should lower to CC before P7") + .cc; let array_maps = aggregate_conversions(&generic_cc) .iter() .map(|conversion| array_map_count(&conversion.plan)) .sum::(); assert!( - array_maps >= 2, - "expected pre-P7 higher-order argument and result ArrayMap plans; found {array_maps}" + array_maps == 0, + "expected no pre-P7 higher-order array reconstruction; found {array_maps}" ); let stages = psrs_backend::compile_with_context( prepared.core, - prepared.effect_context, psrs_backend::TargetCapabilities::default(), ) .expect("higher-order adapters should convert generic aggregate arguments and results"); @@ -295,20 +283,6 @@ fn array_map_depth(conversion: &psrs_backend::cc::ValueConversion) -> usize { } } -fn contains_canonical_record_array_map(conversion: &psrs_backend::cc::ValueConversion) -> bool { - use psrs_backend::cc::ValueConversion; - - match conversion { - ValueConversion::ProductMap { labels, fields, .. } => { - labels == &["items".to_owned(), "value".to_owned()] - && fields.iter().any(|field| array_map_count(field) > 0) - } - ValueConversion::ArrayMap { element, .. } => contains_canonical_record_array_map(element), - ValueConversion::Sequence(steps) => steps.iter().any(contains_canonical_record_array_map), - _ => false, - } -} - fn run_wasmtime(source: &str) -> Option { let required = std::env::var("PSRS_REQUIRE_WASMTIME").as_deref() == Ok("1"); if std::process::Command::new("wasmtime") diff --git a/crates/psrs-driver/src/tests/partial_application.rs b/crates/psrs-driver/src/tests/partial_application.rs index 8785dc49..35cb8ac5 100644 --- a/crates/psrs-driver/src/tests/partial_application.rs +++ b/crates/psrs-driver/src/tests/partial_application.rs @@ -48,3 +48,55 @@ main = flip (\a b -> a + b) 20 22 }; assert_eq!(output.status.code(), Some(42), "{output:?}"); } + +#[test] +fn a_partial_application_of_a_local_closure_defers_the_remaining_argument() { + // The callee is a local value, not a top-level declaration, so the + // remaining parameter is exposed by a generated closure that calls the + // captured callee indirectly. + let source = r#" +module Main where + +main :: Int +main = + let combine = \a b -> intAdd a b + in let step = combine 20 + in step 22 +"#; + let Some(output) = run_with_wasmtime(source) else { + eprintln!("skipping: wasmtime is not installed"); + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +#[test] +fn a_partial_application_of_a_dictionary_method_defers_the_remaining_argument() { + // A class method reached through a field access is also an indirect + // callee; its result still names the abstract constructor. + let source = r#" +module Main where + +class Chain f where + chain :: forall a b. f a -> (a -> f b) -> f b + +instance chainReader :: Chain ((->) Int) where + chain m k x = k (m x) x + +action :: Int -> Int +action x = x + +run :: forall f. Chain f => f Int -> f Int +run a = + let step = chain a + in step (\_ -> a) + +main :: Int +main = run action 42 +"#; + let Some(output) = run_with_wasmtime(source) else { + eprintln!("skipping: wasmtime is not installed"); + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} diff --git a/crates/psrs-driver/src/tests/pattern_matching_audit.rs b/crates/psrs-driver/src/tests/pattern_matching_audit.rs index b00802b4..09e8074d 100644 --- a/crates/psrs-driver/src/tests/pattern_matching_audit.rs +++ b/crates/psrs-driver/src/tests/pattern_matching_audit.rs @@ -47,7 +47,7 @@ fn flatten_cc(assignments: &[psrs_backend::cc::Assignment]) -> Vec<&psrs_backend /// re-evaluated it per alternative would print the side effect twice. #[test] fn case_evaluates_its_scrutinee_exactly_once() { - let source = "module Main where\nimport Prelude\nimport WASI.Console\ndata I = A | B\nmain = case { tag: A, payload: runEffect (log \"once\") } of\n { tag: A, payload: _ } -> 0\n { tag: B, payload: _ } -> 1\n"; + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\nimport Prelude\nimport WASI.Console\ndata I = A | B\nmain = case { tag: A, payload: unsafePerformEffect (log \"once\") } of\n { tag: A, payload: _ } -> 0\n { tag: B, payload: _ } -> 1\n"; let Some(output) = run_with_wasmtime(source) else { eprintln!("skipping: wasmtime is not installed"); return; @@ -211,6 +211,22 @@ fn recursive_pattern_compilation_terminates_and_stays_first_match() { assert_eq!(output.status.code(), Some(42)); } +/// PM-10: a recursive product grows the specialization query unless missing +/// constructor signatures use the default matrix before witness construction. +#[test] +fn recursive_binary_products_execute_in_either_constructor_order() { + for declaration in ["Node Int Tree Tree | Leaf", "Leaf | Node Int Tree Tree"] { + let source = format!( + "module Main where\ndata Tree = {declaration}\nread tree = case tree of\n Node n Leaf Leaf -> n\n Node _ _ _ -> 1\n Leaf -> 0\nmain = read (Node 42 Leaf Leaf)\n" + ); + let Some(output) = run_with_wasmtime(&source) else { + eprintln!("skipping: wasmtime is not installed"); + return; + }; + assert_eq!(output.status.code(), Some(42), "{source}"); + } +} + /// PM-11: the optimizer and Wasm lowering preserve the selected branch value /// and keep the source-associated redundancy warning. #[test] diff --git a/crates/psrs-driver/src/tests/polymorphism_erasure_audit.rs b/crates/psrs-driver/src/tests/polymorphism_erasure_audit.rs index bd743c1a..ac199a37 100644 --- a/crates/psrs-driver/src/tests/polymorphism_erasure_audit.rs +++ b/crates/psrs-driver/src/tests/polymorphism_erasure_audit.rs @@ -121,7 +121,7 @@ fn erased_boolean_box_preserves_both_values() { #[test] fn erased_string_box_preserves_nonempty_contents() { - let source = "module Main where\nimport Prelude\nimport WASI.Console\ncopy :: forall a. Array a -> Array a\ncopy values = values\nmain = let ignored = runEffect (log (arrayIndex (copy [\"high\", \"value\"]) 1)) in 0\n"; + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\nimport Prelude\nimport WASI.Console\ncopy :: forall a. Array a -> Array a\ncopy values = values\nmain = let ignored = unsafePerformEffect (log (arrayIndex (copy [\"high\", \"value\"]) 1)) in 0\n"; let artifact = compile_source("Main.purs", source).expect("string round trip should compile"); match execute_component("erased_string_contents", &artifact.wasm) { Ok(Some(output)) => { @@ -151,7 +151,7 @@ fn erased_number_box_preserves_nan_and_signed_zero() { fn erased_identity_boxes_char_and_unit() { let char_source = "module Main where\nidentity :: forall a. a -> a\nidentity value = value\nmain = charToInt (identity 'B')\n"; expect_exit("erased_char", char_source, 66); - let unit_source = "module Main where\nimport Prelude\nimport WASI.Console\nidentity :: forall a. a -> a\nidentity value = value\nmain = let ignored = identity (runEffect (log \"\")) in 42\n"; + let unit_source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\nimport Prelude\nimport WASI.Console\nidentity :: forall a. a -> a\nidentity value = value\nmain = let ignored = identity (unsafePerformEffect (log \"\")) in 42\n"; expect_exit("erased_unit", unit_source, 42); } @@ -215,7 +215,7 @@ fn escaping_closures_capture_int_and_reference_values() { #[test] fn escaping_closures_capture_a_string_value() { - let source = "module Main where\nimport Prelude\nimport WASI.Console\nmakeReader :: String -> (Int -> String)\nmakeReader message = \\index -> if index == 0 then message else message\nmain = let reader = makeReader \"captured\" in let ignored = runEffect (log (reader 0)) in 42\n"; + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\nimport Prelude\nimport WASI.Console\nmakeReader :: String -> (Int -> String)\nmakeReader message = \\index -> if index == 0 then message else message\nmain = let reader = makeReader \"captured\" in let ignored = unsafePerformEffect (log (reader 0)) in 42\n"; let artifact = compile_source("Main.purs", source).expect("string capture should compile"); match execute_component("escaping_string_capture", &artifact.wasm) { Ok(Some(output)) => { @@ -243,7 +243,7 @@ fn a_curried_function_argument_generates_a_partial_adapter() { // `\x -> \y -> f x y` where `map` expects a generic `a -> b`. The erased // adapter must partially apply it: the outer closure takes the target's // arguments and returns an inner closure for the source's remaining ones. - let source = "module Main where\nimport Prelude\nmain = runEffect (ado\n x <- pure 40\n y <- pure 2\n in x + y)\n"; + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\nimport Prelude\nmain = unsafePerformEffect (ado\n x <- pure 40\n y <- pure 2\n in x + y)\n"; let mir = pre_optimization_mir(source); let names = mir .functions @@ -269,7 +269,7 @@ fn linked_modules_round_trip_an_erased_high_bit_int() { ); let consumer = ( "Main.purs", - "module Main where\nimport Producer\nmain = identity 2000000000 - 1999999958\n", + "module Main where\nimport Producer\nmain = intSub (identity 2000000000) 1999999958\n", ); let core = lower_program_to_core(&[producer, consumer]) .expect("the linked erased program should lower to Core"); @@ -286,18 +286,18 @@ fn linked_modules_round_trip_an_erased_high_bit_int() { } #[test] -fn a_locally_generalized_binding_instantiates_at_int() { - // `id` is generalized at the local `let`, so its runtime value is erased. +fn an_explicitly_polymorphic_local_binding_instantiates_at_int() { + // `id` declares a local polymorphic annotation, so its runtime value is erased. // The `id 42` use must box the erased argument and recover the integer // result exactly as a top-level polymorphic declaration would. - let source = "module Main where\nmain = let id = \\x -> x in id 42\n"; + let source = "module Main where\nmain = let id = (\\x -> x) :: forall a. a -> a in id 42\n"; expect_exit("local_polymorphic_int", source, 42); } #[test] -fn a_locally_generalized_binding_instantiates_at_two_types() { +fn an_explicitly_polymorphic_local_binding_instantiates_at_two_types() { // The same erased local value is recovered at `String` and at `Int`. - let source = "module Main where\nimport Prelude\nimport WASI.Console\nmain = let id = \\x -> x in runEffect (do\n log (id \"hello\")\n pure (id 42))\n"; + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\nimport Prelude\nimport WASI.Console\nmain = let id = (\\x -> x) :: forall a. a -> a in unsafePerformEffect (do\n log (id \"hello\")\n pure (id 42))\n"; let artifact = compile_source("Main.purs", source).expect("the two-type local use should compile"); match execute_component("local_polymorphic_two_types", &artifact.wasm) { @@ -311,29 +311,30 @@ fn a_locally_generalized_binding_instantiates_at_two_types() { } #[test] -fn a_locally_generalized_binding_passes_to_a_polymorphic_function() { +fn an_explicitly_polymorphic_local_binding_passes_to_a_polymorphic_function() { // `apply :: forall a. (a -> a) -> a -> a` receives the erased local value, // so the argument and result cross the erased boundary at the concrete type. - let source = "module Main where\napply :: forall a. (a -> a) -> a -> a\napply f x = f x\nmain = let id = \\x -> x in apply id 42\n"; + let source = "module Main where\napply :: forall a. (a -> a) -> a -> a\napply f x = f x\nmain = let id = (\\x -> x) :: forall a. a -> a in apply id 42\n"; expect_exit("local_polymorphic_argument", source, 42); } #[test] -fn a_curried_locally_generalized_binding_instantiates() { - // A locally generalized curried function with an argument: the flattened +fn a_curried_explicitly_polymorphic_local_binding_instantiates() { + // An explicitly polymorphic local curried function with an argument: the flattened // erased source has two parameters, matching the concrete use. - let source = "module Main where\nmain = let const = \\x -> \\y -> x in const 42 \"ignored\"\n"; + let source = "module Main where\nmain = let const = (\\x -> \\y -> x) :: forall a b. a -> b -> a in const 42 \"ignored\"\n"; expect_exit("local_polymorphic_curried", source, 42); } #[test] -fn a_locally_generalized_identity_instantiates_at_a_function_type() { +fn an_explicitly_polymorphic_local_identity_instantiates_at_a_function_type() { // `(id id) 42`: the outer `id` is instantiated at `Int -> Int`, so its // flattened use type `(Int -> Int) -> (Int -> Int)` is wider than the // erased source `a -> a`. The recursive adapter must eta-expand, call the // erased source with the inner `id`, recover the result at `Int -> Int`, // and apply `42`. - let source = "module Main where\nmain = let id = \\x -> x in (id id) 42\n"; + let source = + "module Main where\nmain = let id = (\\x -> x) :: forall a. a -> a in (id id) 42\n"; expect_exit("local_polymorphic_function_type_identity", source, 42); } @@ -342,8 +343,7 @@ fn a_local_polymorphic_value_is_applied_after_a_function_type_instantiation() { // The same eta-expansion reached with a concrete function argument: // `id` is instantiated at `Int -> Int`, applied to `\y -> y + 1`, and the // recovered function is applied to `41`. - let source = - "module Main where\nimport Prelude\nmain = let id = \\x -> x in (id (\\y -> y + 1)) 41\n"; + let source = "module Main where\nimport Prelude\nmain = let id = (\\x -> x) :: forall a. a -> a in (id (\\y -> y + 1)) 41\n"; expect_exit("local_polymorphic_function_type_argument", source, 42); } diff --git a/crates/psrs-driver/src/tests/primitive_foreign.rs b/crates/psrs-driver/src/tests/primitive_foreign.rs new file mode 100644 index 00000000..fbda1c31 --- /dev/null +++ b/crates/psrs-driver/src/tests/primitive_foreign.rs @@ -0,0 +1,498 @@ +use super::*; + +fn run_library_program_with_wasmtime(sources: &[(&str, &str)]) -> Option { + let (sources, _) = crate::prelude::prepend(sources).unwrap(); + run_program_with_wasmtime(&sources) +} + +#[test] +fn primitive_foreign_bindings_validate_unused_operand_and_result_types() { + for ty in [ + "Int -> Int", + "Number -> Number", + "Int -> Number -> Number", + "forall a. a -> a", + ] { + let source = format!( + "module Main where\nforeign import \"psrs:intrinsic#intToNumber\" convert :: {ty}\nmain = 0\n" + ); + let errors = compile_program_sources(&[("Main.purs", &source)]) + .expect_err("invalid primitive contract"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.stage == "P8 primitive linking"), + "{errors:?}" + ); + } +} + +#[test] +fn primitive_foreign_binding_names_are_registry_operations() { + let source = "module Main where\nforeign import \"psrs:intrinsic#missing\" convert :: Int -> Number\nmain = 0\n"; + let errors = check_program(&[("Main.purs", source)]).expect_err("unknown operation"); + assert!( + errors.iter().any(|error| error + .diagnostic + .message + .contains("unknown primitive binding")), + "{errors:?}" + ); +} + +#[test] +fn primitive_foreign_bindings_keep_unimplemented_categories_explicit() { + let source = + "module Main where\nforeign import \"psrs:intrinsic#unit\" singleton :: Unit\nmain = 0\n"; + let errors = compile_program_sources(&[("Main.purs", source)]) + .expect_err("nullary foreign lowering is not implemented yet"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.stage == "P8 primitive linking" + && error + .diagnostic + .message + .contains("no foreign-function implementation yet")), + "{errors:?}" + ); +} + +#[test] +fn primitive_foreign_bindings_execute_as_first_class_functions() { + let source = "module Main where\nforeign import \"psrs:intrinsic#intToNumber\" convert :: Int -> Number\nforeign import \"psrs:intrinsic#numberAdd\" add :: Number -> Number -> Number\nforeign import \"psrs:intrinsic#numberEq\" equal :: Number -> Number -> Boolean\napply f x = f x\nmain = if equal (apply (add (convert 40)) 2.0) 42.0 then 42 else 1\n"; + let Some(output) = run_library_program_with_wasmtime(&[("Main.purs", source)]) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +#[test] +fn primitive_foreign_bindings_preserve_cross_module_operator_identity() { + let native = "module Native where\nforeign import \"psrs:intrinsic#intAdd\" sum :: Int -> Int -> Int\ninfixl 6 sum as %%\n"; + let main = "module Main where\nimport Native\nmain = 40 %% 2\n"; + let Some(output) = + run_library_program_with_wasmtime(&[("Native.purs", native), ("Main.purs", main)]) + else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +#[test] +fn vendored_bit_operations_execute_the_declared_target_contract() { + let source = "module Main where\nimport Data.Int.Bits as Bits\nmain = if booleanAnd (intEq (Bits.and 63 42) 42) (booleanAnd (intEq (Bits.or 32 10) 42) (booleanAnd (intEq (Bits.xor 63 21) 42) (booleanAnd (intEq (Bits.shl 21 33) 42) (booleanAnd (intEq (Bits.shr (intNeg 84) 33) (intNeg 42)) (booleanAnd (intEq (Bits.zshr (intNeg 1) 1) 2147483647) (booleanAnd (intEq (Bits.zshr (intNeg 1) 0) (intNeg 1)) (intEq (Bits.complement (intNeg 43)) 42))))))) then 42 else 1\n"; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +#[test] +fn primitive_foreign_bindings_match_pinned_official_scalar_observations() { + let sources = [ + ( + "Golden.purs", + include_str!("../../tests/fixtures/stdlib-scalar/Golden.purs"), + ), + ( + "Main.purs", + include_str!("../../tests/fixtures/stdlib-scalar/Main.purs"), + ), + ]; + let modules = crate::prelude::sources().unwrap(); + for declaration in sources[0].1.lines().skip(1) { + assert!( + modules + .iter() + .any(|module| module.text.lines().any(|line| line == declaration)), + "oracle fixture must retain the actual vendored binding: {declaration}" + ); + } + let Some(output) = run_library_program_with_wasmtime(&sources) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +#[test] +fn primitive_foreign_array_schemes_execute_multiple_element_instantiations() { + let native = r#"module Native where +foreign import "psrs:intrinsic#arrayLength" size :: forall a. Array a -> Int +foreign import "psrs:intrinsic#arrayIndex" at :: forall a. Array a -> Int -> a +foreign import "psrs:intrinsic#arrayUpdate" replace :: forall a. Array a -> Int -> a -> Array a +foreign import "psrs:intrinsic#arrayAppend" append :: forall a. Array a -> Array a -> Array a +"#; + let main = r#"module Main where +import Native +apply f x = f x +main = + let + original = [1, 2] + joined = append original [40] + changed = replace joined 0 9 + texts = append ["λ"] ["😀"] + records = replace [{ value: 1 }] 0 { value: 42 } + in if booleanAnd (intEq (size joined) 3) + (booleanAnd (intEq (at original 0) 1) + (booleanAnd (intEq (at joined 0) 1) + (booleanAnd (intEq (at changed 0) 9) + (booleanAnd (intEq (apply (at joined) 2) 40) + (booleanAnd (numberEq (at [1.0, 2.0] 1) 2.0) + (booleanAnd (intEq (size texts) 2) + (booleanAnd (intEq (arrayLength (stringToBytes (at texts 1))) 4) + (intEq (at records 0).value 42)))))))) then 42 else 1 +"#; + let Some(output) = + run_library_program_with_wasmtime(&[("Native.purs", native), ("Main.purs", main)]) + else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +#[test] +fn primitive_foreign_array_contracts_reject_inconsistent_quantified_elements() { + for (operation, ty) in [ + ("arrayFill", "forall a b. Int -> a -> Array b"), + ("arrayFill", "forall a. a -> a -> Array a"), + ("arrayWrite", "forall a b. Array a -> Int -> b -> Array a"), + ("arrayWrite", "forall a b. Array a -> Int -> a -> Array b"), + ("arrayAppend", "forall a b. Array a -> Array b -> Array a"), + ("arrayIndex", "forall a b. Array a -> Int -> b"), + ("arrayUpdate", "forall a b. Array a -> Int -> b -> Array a"), + ("arrayLength", "forall a. Array a -> a"), + ] { + let source = format!( + "module Main where\nforeign import \"psrs:intrinsic#{operation}\" f :: {ty}\nmain = 0\n" + ); + let errors = compile_program_sources(&[("Main.purs", &source)]) + .expect_err("wrong array binding contract"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.stage == "P8 primitive linking"), + "{errors:?}" + ); + } +} + +#[test] +fn library_array_apply_preserves_callback_order_and_captures() { + let source = r#"module Main where +import PSRS.Array (arrayApply) +cartesian = arrayApply +add offset x = intAdd offset x +main = + let + input = [10, 20] + result = cartesian [add 1, \x -> intMul x 2] input + numbers = cartesian [\x -> numberAdd (intToNumber x) 0.5] [4, 8] + records = cartesian [\x -> { value: intAdd x 2 }] [40] + nested = cartesian [\x -> [x, intAdd x 1]] [5, 9] + emptyValues = cartesian [\x -> arrayIndex ([] :: Array Int) x] [] + emptyFunctions = cartesian ([] :: Array (Int -> Int)) [1, 2] + in if booleanAnd (intEq (arrayLength result) 4) + (booleanAnd (intEq (arrayIndex result 0) 11) + (booleanAnd (intEq (arrayIndex result 1) 21) + (booleanAnd (intEq (arrayIndex result 2) 20) + (booleanAnd (intEq (arrayIndex result 3) 40) + (booleanAnd (numberEq (arrayIndex numbers 1) 8.5) + (booleanAnd (intEq (arrayIndex records 0).value 42) + (booleanAnd (intEq (arrayIndex (arrayIndex nested 1) 1) 10) + (booleanAnd (intEq (arrayLength emptyValues) 0) + (booleanAnd (intEq (arrayLength emptyFunctions) 0) + (intEq (arrayIndex input 0) 10)))))))))) then 42 else 1 +"#; + let Some(output) = run_library_program_with_wasmtime(&[("Main.purs", source)]) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +#[test] +fn library_array_apply_returns_curried_functions_through_aliases() { + for operation in ["foreignApply", "arrayApply"] { + let source = format!( + r#"module Main where +import PSRS.Array (arrayApply) +foreignApply = arrayApply +add x y = intAdd x y +main = (arrayIndex ({operation} [add] [40]) 0) 2 +"# + ); + let Some(output) = run_library_program_with_wasmtime(&[("Main.purs", &source)]) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + } +} + +#[test] +fn array_apply_traps_before_wrapping_an_unrepresentable_result_length() { + let source = r#"module Main where +import PSRS.Array (arrayApply) +cartesian = arrayApply +double n xs = if intEq n 0 then xs else double (intSub n 1) (arrayAppend xs xs) +main = arrayLength (cartesian (double 16 [\x -> x]) (double 16 [0])) +"#; + let Some(output) = run_library_program_with_wasmtime(&[("Main.purs", source)]) else { + return; + }; + assert!( + !output.status.success(), + "the 2^32 product must trap, not return an empty array" + ); + assert!( + String::from_utf8_lossy(&output.stderr).contains("out of bounds array access"), + "{output:?}" + ); +} + +#[test] +fn library_array_apply_matches_pinned_official_observations() { + let golden = include_str!("../../tests/fixtures/stdlib-array/Golden.purs"); + let declaration = golden + .lines() + .find(|line| line.starts_with("arrayApply ::")) + .unwrap(); + let module = crate::prelude::sources() + .unwrap() + .iter() + .find(|module| module.module_name == "Control.Apply") + .unwrap(); + assert!( + module.text.lines().any(|line| line == declaration), + "fixture must retain the actual library binding" + ); + let sources = [ + ("Golden.purs", golden), + ( + "Main.purs", + include_str!("../../tests/fixtures/stdlib-array/Main.purs"), + ), + ]; + let Some(output) = run_library_program_with_wasmtime(&sources) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +#[test] +fn primitive_array_fill_and_write_preserve_initialization_and_aliases() { + let source = r#"module Main where +foreign import "psrs:intrinsic#arrayFill" filled :: forall a. Int -> a -> Array a +foreign import "psrs:intrinsic#arrayWrite" write :: forall a. Array a -> Int -> a -> Array a +main = + let + xs = filled 3 1 + put = write xs 1 + ys = put 42 + ignored = arrayWrite xs 2 9 + texts = filled 2 "😀" + records = filled 2 { value: 42 } + functions = filled 2 (\x -> intAdd x 2) + empty = filled 0 "λ" + in if booleanAnd (intEq (arrayLength xs) 3) + (booleanAnd (intEq (arrayIndex xs 1) 42) + (booleanAnd (intEq (arrayIndex ys 1) 42) + (booleanAnd (intEq (arrayIndex xs 2) 9) + (booleanAnd (intEq (arrayLength (stringToBytes (arrayIndex texts 1))) 4) + (booleanAnd (intEq (arrayIndex records 1).value 42) + (booleanAnd (intEq ((arrayIndex functions 1) 40) 42) + (intEq (arrayLength empty) 0))))))) then 42 else 1 +"#; + let Some(output) = run_library_program_with_wasmtime(&[("Main.purs", source)]) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +#[test] +fn primitive_array_fill_and_write_trap_on_invalid_lengths_or_indices() { + for body in [ + "arrayLength (filled (intNeg 1) 0)", + "arrayLength (write [1] 1 2)", + "arrayLength (write [1] (intNeg 1) 2)", + ] { + let source = format!( + "module Main where\nforeign import \"psrs:intrinsic#arrayFill\" filled :: forall a. Int -> a -> Array a\nforeign import \"psrs:intrinsic#arrayWrite\" write :: forall a. Array a -> Int -> a -> Array a\nmain = {body}\n" + ); + let Some(output) = run_library_program_with_wasmtime(&[("Main.purs", &source)]) else { + return; + }; + assert!(!output.status.success(), "{body}: {output:?}"); + } +} + +#[test] +fn library_array_bind_matches_pinned_official_observations() { + let golden = include_str!("../../tests/fixtures/stdlib-array-bind/Golden.purs"); + let signature = golden + .lines() + .find(|line| line.starts_with("arrayBind ::")) + .unwrap(); + let modules = crate::prelude::sources().unwrap(); + let native = modules + .iter() + .find(|module| module.module_name == "Control.Bind") + .unwrap(); + assert!(native.text.lines().any(|line| line == signature)); + let sources = [ + ("Golden.purs", golden), + ( + "Main.purs", + include_str!("../../tests/fixtures/stdlib-array-bind/Main.purs"), + ), + ]; + let Some(output) = run_library_program_with_wasmtime(&sources) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +#[test] +fn library_st_matches_pinned_official_observations() { + let modules = crate::prelude::sources().unwrap(); + let internal = modules + .iter() + .find(|module| module.module_name == "Control.Monad.ST.Internal") + .unwrap(); + for signature in [ + "run :: forall a. (forall r. ST r a) -> a", + "while :: forall r a. ST r Boolean -> ST r a -> ST r Unit", + "for :: forall r a. Int -> Int -> (Int -> ST r a) -> ST r Unit", + "foreach :: forall r a. Array a -> (a -> ST r Unit) -> ST r Unit", + "new :: forall a r. a -> ST r (STRef r a)", + "read :: forall a r. STRef r a -> ST r a", + "modifyImpl :: forall r a b. (a -> { state :: a, value :: b }) -> STRef r a -> ST r b", + "write :: forall a r. a -> STRef r a -> ST r a", + ] { + assert!( + internal.text.lines().any(|line| line == signature), + "missing official ST signature: {signature}" + ); + } + let uncurried = modules + .iter() + .find(|module| module.module_name == "Control.Monad.ST.Uncurried") + .unwrap(); + for signature in [ + "mkSTFn1 :: forall a t r.", + "runSTFn1 :: forall a t r.", + "mkSTFn10 :: forall a b c d e f g h i j t r.", + "runSTFn10 :: forall a b c d e f g h i j t r.", + ] { + assert!( + uncurried.text.lines().any(|line| line == signature), + "missing official STFn signature: {signature}" + ); + } + let source = include_str!("../../tests/fixtures/stdlib-st/Main.purs"); + let Some(output) = run_library_program_with_wasmtime(&[("Main.purs", source)]) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +#[test] +fn library_uncurried_matches_pinned_official_observations() { + let modules = crate::prelude::sources().unwrap(); + let uncurried = modules + .iter() + .find(|module| module.module_name == "Data.Function.Uncurried") + .unwrap(); + for signature in [ + "mkFn0 :: forall a. (Unit -> a) -> Fn0 a", + "mkFn2 :: forall a b c. (a -> b -> c) -> Fn2 a b c", + "runFn0 :: forall a. Fn0 a -> a", + "runFn10 :: forall a b c d e f g h i j k. Fn10 a b c d e f g h i j k -> a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> k", + ] { + assert!( + uncurried.text.lines().any(|line| line == signature), + "missing official signature: {signature}" + ); + } + let source = include_str!("../../tests/fixtures/stdlib-uncurried/Main.purs"); + let Some(output) = run_library_program_with_wasmtime(&[("Main.purs", source)]) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +#[test] +fn library_array_operations_match_pinned_official_observations() { + let modules = crate::prelude::sources().unwrap(); + let array = modules + .iter() + .find(|module| module.module_name == "Data.Array") + .unwrap(); + for signature in [ + "length :: forall a. Array a -> Int", + "reverse :: forall a. Array a -> Array a", + "concat :: forall a. Array (Array a) -> Array a", + "sliceImpl :: forall a. Fn3 Int Int (Array a) (Array a)", + ] { + assert!( + array.text.lines().any(|line| line == signature), + "missing official signature: {signature}" + ); + } + let non_empty = modules + .iter() + .find(|module| module.module_name == "Data.Array.NonEmpty.Internal") + .unwrap(); + for signature in [ + "foldr1Impl :: forall a. Fn2 (a -> a -> a) (NonEmptyArray a) a", + "foldl1Impl :: forall a. Fn2 (a -> a -> a) (NonEmptyArray a) a", + ] { + assert!( + non_empty.text.lines().any(|line| line == signature), + "missing official signature: {signature}" + ); + } + let source = include_str!("../../tests/fixtures/stdlib-array-ops/Main.purs"); + let Some(output) = run_library_program_with_wasmtime(&[("Main.purs", source)]) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +#[test] +fn library_array_callbacks_match_pinned_official_observations() { + let source = include_str!("../../tests/fixtures/stdlib-array-callbacks/Main.purs"); + let Some(output) = run_library_program_with_wasmtime(&[("Main.purs", source)]) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert!( + output.stdout.is_empty() && output.stderr.is_empty(), + "{output:?}" + ); +} + +#[test] +fn library_array_extend_matches_pinned_official_observations() { + let golden = include_str!("../../tests/fixtures/stdlib-array-extend/Golden.purs"); + let signature = golden + .lines() + .find(|line| line.starts_with("arrayExtend ::")) + .unwrap(); + let modules = crate::prelude::sources().unwrap(); + let native = modules + .iter() + .find(|module| module.module_name == "Control.Extend") + .unwrap(); + assert!(native.text.lines().any(|line| line == signature)); + let sources = [ + ("Golden.purs", golden), + ( + "Main.purs", + include_str!("../../tests/fixtures/stdlib-array-extend/Main.purs"), + ), + ]; + let Some(output) = run_library_program_with_wasmtime(&sources) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} diff --git a/crates/psrs-driver/src/tests/records.rs b/crates/psrs-driver/src/tests/records.rs index d2624334..4fd72b42 100644 --- a/crates/psrs-driver/src/tests/records.rs +++ b/crates/psrs-driver/src/tests/records.rs @@ -1,5 +1,65 @@ use super::*; +#[test] +fn explicit_update_expression_uses_the_local_record_even_when_labels_match() { + let source = r#" +module Main where +main :: Int +main = let field = { value: 0, kept: 42 } + original = { field: { value: 0, kept: 1 } } + explicit = original { field = field { value = 7 } } + nested = original { field { value = 7 } } + in if intEq nested.field.kept 1 then explicit.field.kept else 0 +"#; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +#[test] +fn record_wildcards_bind_in_source_order_and_keep_nested_scopes() { + let source = r#" +module Main where +make :: Int -> Int -> { z :: Int, a :: Int, nested :: Int -> { value :: Int }, fixed :: Int } +make = { z: _, a: _, nested: { value: _ }, fixed: 7 } +main :: Int +main = let record = make 40 2 + in if intEq record.z 40 + then if intEq record.a 2 + then if intEq record.fixed 7 then (record.nested 42).value else 0 + else 0 + else 0 +"#; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +#[test] +fn record_update_wildcards_share_nested_paths_and_keep_explicit_scopes() { + let source = r#" +module Main where +update :: { z :: Int, nested :: { a :: Int, b :: Int }, callback :: Int -> { value :: Int } } + -> Int -> Int -> Int + -> { z :: Int, nested :: { a :: Int, b :: Int }, callback :: Int -> { value :: Int } } +update = _ { z = _, nested { a = _, b = _ }, callback = { value: _ } } +main :: Int +main = let original = { z: 0, nested: { a: 0, b: 0 }, callback: \x -> { value: x } } + record = update original 10 20 30 + in if intEq record.z 10 + then if intEq record.nested.a 20 + then if intEq record.nested.b 30 then (record.callback 42).value else 0 + else 0 + else 0 +"#; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + #[test] fn runs_a_tuple_as_a_closed_record() { let source = "\ @@ -52,9 +112,19 @@ fn runs_a_record_field_access_through_a_gc_struct() { assert_eq!(output.status.code(), Some(42)); } +#[test] +fn anonymous_record_accessor_projects_nested_fields() { + let source = "module Main where\nproject :: { inner :: { value :: Int } } -> Int\nproject = _.inner.value\nmain = if intEq (project { inner: { value: 42 } }) 42 then 0 else 1\n"; + let Some(output) = run_program_with_wasmtime(&[("Main.purs", source)]) else { + eprintln!("skipping execution: wasmtime is not installed"); + return; + }; + assert_eq!(output.status.code(), Some(0), "{output:?}"); +} + #[test] fn evaluates_record_fields_in_source_order_before_canonical_layout() { - let source = "module Main where\nimport Prelude\nimport WASI.Console\nmain = let record = { z: runEffect (log \"z\"), a: runEffect (log \"a\") } in 0\n"; + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\nimport Prelude\nimport WASI.Console\nmain = let record = { z: unsafePerformEffect (log \"z\"), a: unsafePerformEffect (log \"a\") } in 0\n"; let core = lower_source_to_core("Main.purs", source).expect("source should lower to Core"); let main = core .declarations diff --git a/crates/psrs-driver/src/tests/recursive_closures.rs b/crates/psrs-driver/src/tests/recursive_closures.rs new file mode 100644 index 00000000..57b81fe6 --- /dev/null +++ b/crates/psrs-driver/src/tests/recursive_closures.rs @@ -0,0 +1,32 @@ +use super::*; + +#[test] +fn recursive_local_function_can_return_a_branch_selected_closure() { + let source = r"module Main where +import Prelude +main = go 42 0 + where + go n = if n == 0 then (\a -> a) else (\a -> go (n - 1) (a + 1)) +"; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +#[test] +fn recursive_local_aliases_share_the_same_capture_environment() { + let source = r"module Main where +import Prelude +main = outer 1 + where + outer step = go 42 0 + where + alias = go + go n a = if n == 0 then a else alias (n - 1) (a + step) +"; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} diff --git a/crates/psrs-driver/src/tests/resolution.rs b/crates/psrs-driver/src/tests/resolution.rs index 2ac7b21d..8b86330e 100644 --- a/crates/psrs-driver/src/tests/resolution.rs +++ b/crates/psrs-driver/src/tests/resolution.rs @@ -175,7 +175,7 @@ markerInt = 1\n"; #[test] fn resolves_a_program_against_the_on_disk_standard_library() { - let source = "module Main where\nimport Prelude\nmain = runEffect (pure 1)\n"; + let source = "module Main where\nimport Prelude\nimport Effect (Effect)\nmain :: Effect Int\nmain = pure 1\n"; let errors = check_program_lenient(&[("Main.purs", source)]).unwrap_err(); assert!( errors @@ -190,7 +190,7 @@ fn resolves_a_program_against_the_on_disk_standard_library() { #[test] fn attributes_a_library_backed_diagnostic_to_the_user_source() { - let source = "module Main where\nimport Prelude\nmain = runEffect (pure missingName)\n"; + let source = "module Main where\nimport Prelude\nimport Effect (Effect)\nmain :: Effect Int\nmain = pure missingName\n"; let errors = check_program_lenient_with_prelude(&[("Main.purs", source)]).unwrap_err(); assert!( errors.iter().any(|error| { diff --git a/crates/psrs-driver/src/tests/runtime_storage.rs b/crates/psrs-driver/src/tests/runtime_storage.rs new file mode 100644 index 00000000..08c03b69 --- /dev/null +++ b/crates/psrs-driver/src/tests/runtime_storage.rs @@ -0,0 +1,142 @@ +use super::*; + +const IMPORTS: &str = r#" +foreign import "psrs:runtime-storage#array_fill" filled :: forall a. Int -> a -> Array a +foreign import "psrs:runtime-storage#array_read" read :: forall a. Array a -> Int -> a +foreign import "psrs:runtime-storage#array_write" write :: forall a. Array a -> Int -> a -> Unit +"#; + +#[test] +fn runtime_storage_preserves_aliases_and_recovers_payloads() { + let source = format!( + r#"module Main where +{IMPORTS} +main = + let + xs = filled 2 1 + alias = xs + put = write xs 1 + ignored = put 42 + texts = filled 1 "😀" + records = filled 1 {{ value: 42 }} + functions = filled 1 (\x -> intAdd x 2) + nested = filled 1 (filled 1 42) + in if booleanAnd (intEq (read alias 1) 42) + (booleanAnd (intEq (arrayLength (stringToBytes (read texts 0))) 4) + (booleanAnd (intEq (read records 0).value 42) + (booleanAnd (intEq ((read functions 0) 40) 42) + (intEq (read (read nested 0) 0) 42)))) then 42 else 1 +"# + ); + let Some(output) = run_program_with_wasmtime(&[("Main.purs", &source)]) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +#[test] +fn runtime_storage_validates_unused_source_contracts() { + for (export, ty) in [ + ("array_fill", "Int -> Int -> Array Number"), + ("array_read", "forall a b. Array a -> Int -> b"), + ("array_write", "forall a. Array a -> Int -> a -> Int"), + ("array_write", "Array Int -> Int -> Number -> Unit"), + ("array_read", "forall a. Array a -> Number -> a"), + ("array_fill", "forall a. a -> Array a"), + ("array_read", "Array Boolean -> Int -> Int"), + ] { + let source = format!( + "module Main where\nforeign import \"psrs:runtime-storage#{export}\" op :: {ty}\nmain = 0\n" + ); + let errors = compile_program_sources(&[("Main.purs", &source)]) + .expect_err("invalid storage contract"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.stage == "P8 runtime linking"), + "{export}: {errors:?}" + ); + } +} + +#[test] +fn runtime_storage_valid_unused_imports_do_not_link_a_provider() { + let source = format!("module Main where\n{IMPORTS}\nmain = 42\n"); + let unused = compile_program_sources(&[("Main.purs", &source)]).unwrap(); + assert!(!unused.wat.contains("array_fill"), "{}", unused.wat); + let source = format!("module Main where\n{IMPORTS}\nmain = read (filled 1 42) 0\n"); + let used = compile_program_sources(&[("Main.purs", &source)]).unwrap(); + assert!(used.wat.contains("array_fill"), "{}", used.wat); +} + +#[test] +fn runtime_storage_traps_on_invalid_ranges() { + for (body, trap) in [ + ("arrayLength (filled (intNeg 1) 0)", "unreachable"), + ("read (filled 1 0) 1", "out of bounds array access"), + ( + "let ignored = write (filled 1 0) 1 42 in 0", + "out of bounds array access", + ), + ] { + let source = format!("module Main where\n{IMPORTS}\nmain = {body}\n"); + let Some(output) = run_program_with_wasmtime(&[("Main.purs", &source)]) else { + return; + }; + assert!(!output.status.success(), "{body}: {output:?}"); + assert!( + String::from_utf8_lossy(&output.stderr).contains(trap), + "{output:?}" + ); + } +} + +#[test] +fn runtime_storage_trap_has_no_normal_return() { + for ty in ["Int", "forall a. a"] { + let source = format!( + "module Main where\nforeign import \"psrs:runtime-storage#trap\" abort :: {ty}\nmain :: Int\nmain = abort\n" + ); + let Some(output) = run_program_with_wasmtime(&[("Main.purs", &source)]) else { + return; + }; + assert!(!output.status.success(), "{output:?}"); + assert!( + String::from_utf8_lossy(&output.stderr).contains("unreachable"), + "{output:?}" + ); + } +} + +#[test] +fn partial_crash_with_reaches_the_storage_trap() { + // `Partial._crashWith` is an official JavaScript `foreign import` that + // throws. The target implements it with the checked non-returning storage + // trap, so a real library caller must never return a value for the absent + // result type. + let source = "module Main where\nimport Prelude\nimport Partial.Unsafe (unsafeCrashWith)\nmain :: Int\nmain = unsafeCrashWith \"boom\"\n"; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert!(!output.status.success(), "{output:?}"); + assert!( + String::from_utf8_lossy(&output.stderr).contains("unreachable"), + "{output:?}" + ); +} + +#[test] +fn runtime_storage_monomorphic_operands_use_the_same_array() { + let source = r#"module Main where +foreign import "psrs:runtime-storage#array_fill" filled :: Int -> Int -> Array Int +foreign import "psrs:runtime-storage#array_read" read :: Array Int -> Int -> Int +foreign import "psrs:runtime-storage#array_write" write :: Array Int -> Int -> Int -> Unit +main = let xs = filled 1 0 + ignored = write xs 0 42 + in read xs 0 +"#; + let Some(output) = run_program_with_wasmtime(&[("Main.purs", source)]) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} diff --git a/crates/psrs-driver/src/tests/scalars.rs b/crates/psrs-driver/src/tests/scalars.rs index 151d5f87..6d2b9a5a 100644 --- a/crates/psrs-driver/src/tests/scalars.rs +++ b/crates/psrs-driver/src/tests/scalars.rs @@ -9,10 +9,10 @@ subtractInt left right = left - right checkIntArithmetic = booleanAnd (equalInt (40 + 2) 42) (booleanAnd (equalInt (7 - 2) 5) (booleanAnd (equalInt (6 * 7) 42) (booleanAnd (equalInt (10 / 3) 3) (equalInt (10 % 3) 1)))) checkIntWrapping = equalInt (2147483647 + 1) (subtractInt (intNeg 2147483647) 1) -checkIntFloor = booleanAnd (equalInt (intDiv (intNeg 5) 3) (intNeg 2)) (equalInt (intMod (intNeg 5) 3) 1) +checkIntEuclidean = booleanAnd (equalInt (div (intNeg 5) 3) (intNeg 2)) (equalInt (mod (intNeg 5) 3) 1) checkIntBits = booleanAnd (equalInt (intAnd 6 3) 2) (booleanAnd (equalInt (intOr 4 1) 5) (booleanAnd (equalInt (intXor 6 3) 5) (booleanAnd (equalInt (intShl 3 2) 12) (booleanAnd (equalInt (intShr (intNeg 8) 1) (intNeg 4)) (equalInt (intZshr (intNeg 1) 1) 2147483647))))) checkIntComparisons = booleanAnd (5 == 5) (booleanAnd (5 /= 6) (booleanAnd (4 < 5) (booleanAnd (5 <= 5) (booleanAnd (6 > 5) (5 >= 5))))) -checkInt = booleanAnd checkIntArithmetic (booleanAnd checkIntWrapping (booleanAnd checkIntFloor (booleanAnd checkIntBits checkIntComparisons))) +checkInt = booleanAnd checkIntArithmetic (booleanAnd checkIntWrapping (booleanAnd checkIntEuclidean (booleanAnd checkIntBits checkIntComparisons))) checkIntUnary = booleanAnd (equalInt (intNeg 5) (subtractInt 0 5)) (equalInt (intComplement 0) (intNeg 1)) checkNumberUnary = booleanAnd (numberEq (numberNeg 1.5) (numberSub 0.0 1.5)) (numberEq (intToNumber 5) 5.0) @@ -35,6 +35,25 @@ checkChar = booleanAnd (charEq 'A' 'A') (booleanAnd (charNe 'A' 'B') (booleanAnd main = if booleanAnd checkInt (booleanAnd checkUnary (booleanAnd checkNumber (booleanAnd checkBoolean checkChar))) then 0 else 1 "#; +#[test] +fn hexadecimal_integer_literals_use_the_signed_32_bit_range() { + crate::check_program(&[( + "Main.purs", + "module Main where\nvalue :: Int\nvalue = 0xFFFF\n", + )]) + .expect("0xFFFF fits in a signed 32-bit Int"); + let errors = crate::check_program(&[( + "Main.purs", + "module Main where\nvalue :: Int\nvalue = 0x80000000\n", + )]) + .expect_err("0x80000000 is outside signed 32-bit Int"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.code == Some("IntOutOfRange")) + ); +} + #[test] fn scalar_intrinsics_are_reachable_from_source_and_execute_with_documented_semantics() { let core = lower_source_to_core("Main.purs", SCALAR_SOURCE) @@ -51,25 +70,23 @@ fn scalar_intrinsics_are_reachable_from_source_and_execute_with_documented_seman "IntToBoolean", "CharToInt", "IntToChar", - "IntDiv", - "IntMod", - "I32Add", - "I32Sub", - "I32Mul", - "I32DivS", - "I32RemS", + "IntAdd", + "IntSub", + "IntMul", + "IntQuot", + "IntRem", "IntAnd", "IntOr", "IntXor", "IntShl", "IntShr", "IntZshr", - "I32Eq", - "I32Ne", - "I32LtS", - "I32LeS", - "I32GtS", - "I32GeS", + "IntEq", + "IntNe", + "IntLt", + "IntLe", + "IntGt", + "IntGe", "NumberAdd", "NumberSub", "NumberMul", @@ -108,22 +125,39 @@ fn scalar_intrinsics_are_reachable_from_source_and_execute_with_documented_seman ); } +#[test] +fn data_int_bits_uses_the_integer_bitwise_intrinsics() { + let main = r#" +module Main where +import Data.Int.Bits ((.&.)) +main = if intEq (6 .&. 3) 2 then 0 else 1 +"#; + let sources = [("Main.purs", main)]; + let (sources, _) = crate::prelude::prepend(&sources).expect("the standard library should load"); + let Some(output) = super::run_program_with_wasmtime(&sources) else { + eprintln!("skipping execution: wasmtime is not installed"); + return; + }; + assert_eq!(output.status.code(), Some(0), "{output:?}"); +} + const CASE_HELPER_SOURCE: &str = "\ module Main where +import Prelude data Choice = First | Second pick choice = case choice of - First -> intDiv 7 2 - Second -> intMod 7 2 + First -> div 7 2 + Second -> mod 7 2 main = pick First "; #[test] -fn generates_floor_helpers_for_division_nested_in_case_branches() { +fn executes_library_euclidean_division_nested_in_case_branches() { let stages = crate::compile_main_stages(CASE_HELPER_SOURCE) - .expect("case-nested division must generate its helper before MIR lowering"); + .expect("case-nested library division must lower through MIR"); assert!( stages.cc.functions.iter().any(|function| { function.assignments.iter().any(|assignment| { @@ -135,18 +169,6 @@ fn generates_floor_helpers_for_division_nested_in_case_branches() { }), "the case must lower to a tag switch" ); - for helper in ["__psrs_euclidean_int_div", "__psrs_euclidean_int_mod"] { - assert_eq!( - stages - .mir - .functions - .iter() - .filter(|function| function.name == helper) - .count(), - 1, - "expected exactly one {helper}" - ); - } let Some(output) = super::run_with_wasmtime(CASE_HELPER_SOURCE) else { eprintln!("skipping execution: wasmtime is not installed"); return; @@ -154,7 +176,7 @@ fn generates_floor_helpers_for_division_nested_in_case_branches() { assert_eq!( output.status.code(), Some(3), - "floor div 7 2 through a case branch must be 3: {output:?}" + "library div 7 2 through a case branch must be 3: {output:?}" ); } @@ -288,50 +310,49 @@ fn assert_runtime_trap(source: &str, needle: &str) { } #[test] -fn truncated_and_floor_division_trap_on_zero_divisor_and_signed_overflow() { - assert_runtime_trap( - "module Main where\nmain = 1 / 0\n", - "integer divide by zero", - ); - assert_runtime_trap( - "module Main where\nmain = 1 % 0\n", - "integer divide by zero", - ); +fn truncating_and_library_euclidean_division_zero_and_signed_overflow() { + // Library division/modulo handle zero before evaluating raw quotient/remainder. + // Truncating primitives preserve the Wasm divide-by-zero and overflow traps. + let Some(output) = + super::run_with_wasmtime("module Main where\nimport Prelude\nmain = 1 / 0\n") + else { + eprintln!("skipping execution: wasmtime is not installed"); + return; + }; + assert_eq!(output.status.code(), Some(0), "{output:?}"); + assert_runtime_trap( - "module Main where\nmain = intDiv 1 0\n", + "module Main where\nimport Prelude\nmain = 1 % 0\n", "integer divide by zero", ); assert_runtime_trap( - "module Main where\nmain = intMod 1 0\n", + "module Main where\nmain = intQuot 1 0\n", "integer divide by zero", ); assert_runtime_trap( - "module Main where\nmain = ((intNeg 2147483647) - 1) / (intNeg 1)\n", + "module Main where\nimport Prelude\nmain = ((intNeg 2147483647) - 1) / (intNeg 1)\n", "integer overflow", ); assert_runtime_trap( - "module Main where\nmain = intDiv ((intNeg 2147483647) - 1) (intNeg 1)\n", + "module Main where\nimport Prelude\nmain = intQuot ((intNeg 2147483647) - 1) (intNeg 1)\n", "integer overflow", ); } #[test] fn runs_division_and_modulo_inside_a_case_arm() { - // Floor division and modulo are lowered through generated helpers. The - // primitive operations only appear inside the match arms, so helper - // detection must walk the tag switch. + // Public arithmetic composes library branches with truncating primitives. + // Both case arms must retain the library policy through normal lowering. let source = r#"module Main where import Prelude data Tag = A | B compute t = case t of - A -> intDiv 7 3 - B -> intMod 7 3 + A -> div 7 3 + B -> mod 7 3 main = compute A + compute B + 39 "#; - let compilation = compile_source_with_dumps("Main.purs", source) - .expect("lowering division and modulo inside a case arm"); - assert!(compilation.dumps.mir.contains("__psrs_euclidean_int_div")); - assert!(compilation.dumps.mir.contains("__psrs_euclidean_int_mod")); + compile_source("Main.purs", source) + .expect("lowering library division and modulo inside a case arm"); let Some(output) = super::run_with_wasmtime(source) else { eprintln!("skipping execution: wasmtime is not installed"); return; diff --git a/crates/psrs-driver/src/tests/semigroup.rs b/crates/psrs-driver/src/tests/semigroup.rs index 57ca5b8c..b1e83acd 100644 --- a/crates/psrs-driver/src/tests/semigroup.rs +++ b/crates/psrs-driver/src/tests/semigroup.rs @@ -38,6 +38,8 @@ fn the_semigroup_operator_concatenates_strings_and_arrays() { // instance keeps the one UTF-8 representation. let source = r#" module Main where +import Effect (Effect) +import Effect.Unsafe (unsafePerformEffect) import Prelude import Effect.Console (log) @@ -59,7 +61,7 @@ checks = do log (if arrayIndex numbers 4 == 5 then "element-ok" else "element-wrong") pure unit -main = let ignored = runEffect checks in 0 +main = let ignored = unsafePerformEffect checks in 0 "#; let Some(output) = run_with_wasmtime(source) else { eprintln!("skipping: wasmtime is not installed"); diff --git a/crates/psrs-driver/src/tests/show.rs b/crates/psrs-driver/src/tests/show.rs index ad3af3ad..7865b171 100644 --- a/crates/psrs-driver/src/tests/show.rs +++ b/crates/psrs-driver/src/tests/show.rs @@ -11,6 +11,8 @@ use super::*; fn show_renders_the_instances_the_corpus_prints() { let source = r#" module Main where +import Effect (Effect) +import Effect.Unsafe (unsafePerformEffect) import Prelude import Effect.Console (log) @@ -45,7 +47,7 @@ checks = do log (show "cafés") pure unit -main = let ignored = runEffect checks in 0 +main = let ignored = unsafePerformEffect checks in 0 "#; let Some(output) = run_with_wasmtime(source) else { eprintln!("skipping: wasmtime is not installed"); @@ -89,6 +91,131 @@ main = let ignored = runEffect checks in 0 ); } +#[test] +fn formatter_plan_records_the_pinned_runtime_artifact_digests() { + let source = r#" +module Main where + +import Prelude +import Effect.Console (log) + +main = let ignored = log (show 1.0e21) in 0 +"#; + let report = compile_program_sources_with_prelude_diagnosis(&[("Main.purs", source)], false); + assert!( + report.artifact.is_some(), + "show should compile: {:?}", + report.diagnostics + ); + let trace = report.backend_trace.expect("backend pass trace"); + let plan = trace + .executions + .iter() + .find(|execution| execution.pass_key == "backend.target.plan") + .expect("the checked plan is an observed execution"); + let parameter = |key: &str| { + plan.parameters + .iter() + .find(|parameter| parameter.key == key) + .map(|parameter| parameter.value.as_str()) + }; + assert_eq!(parameter("artifacts"), Some("2")); + let digests = parameter("artifact_digests").expect("artifact digests are recorded"); + assert!(digests.contains("psrs:runtime-number"), "{digests}"); + assert!(digests.contains("psrs:runtime-allocator"), "{digests}"); + assert!( + digests.contains("4eca097da7112522a23cd790d7562d643090ac7d5f63aa493e853b23925dfb23"), + "{digests}" + ); + assert!( + digests.contains("458a7df7eb27038c4d6b22aa52fc8ea283c6dca41120b0c0bf31f6a851aa0260"), + "{digests}" + ); + assert!( + parameter("selected_providers") + .expect("selected providers are recorded") + .contains("NumberToString"), + ); +} + +#[test] +fn show_covers_number_and_aggregate_boundaries() { + let source = r#" +module Main where +import Effect (Effect) +import Effect.Unsafe (unsafePerformEffect) + +import Prelude +import Effect.Console (log) + +checks :: Effect Unit +checks = do + log (show (0.0 / 0.0)) + log (show (1.0 / 0.0)) + log (show ((0.0 - 1.0) / 0.0)) + log (show (numberNeg 0.0)) + log (show 1.0e-6) + log (show 1.0e-7) + log (show 1.0e20) + log (show 5.0e-324) + log (show ([] :: Array Int)) + log (show [[1, 2], [3]]) + pure unit + +main = let ignored = unsafePerformEffect checks in 0 +"#; + let Some(output) = run_with_wasmtime(source) else { + eprintln!("skipping: wasmtime is not installed"); + return; + }; + assert_eq!(output.status.code(), Some(0), "{output:?}"); + let stdout = String::from_utf8_lossy(&output.stdout); + let expected = concat!( + "NaN\n", + "Infinity\n", + "-Infinity\n", + "0.0\n", + "0.000001\n", + "1e-7\n", + "100000000000000000000.0\n", + "5e-324\n", + "[]\n", + "[[1,2],[3]]\n", + ); + assert_eq!( + stdout.as_ref(), + expected, + "stdout:\n{stdout}\nstderr:\n{}", + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn formats_many_numbers_without_exhausting_the_runtime_stack() { + // The formatter is nonrecursive; a large array of numbers calls its raw + // export once per element through the same private stack region. + let mut elements = String::new(); + for index in 0..64 { + if index > 0 { + elements.push(','); + } + elements.push_str(&format!("{index}.5")); + } + let source = format!( + "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\n\nimport Prelude\nimport Effect.Console (log)\n\nmain = let ignored = unsafePerformEffect (log (show [{elements}])) in 0\n" + ); + let Some(output) = run_with_wasmtime(&source) else { + eprintln!("skipping: wasmtime is not installed"); + return; + }; + assert_eq!(output.status.code(), Some(0), "{output:?}"); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.starts_with("[0.5,1.5,") && stdout.ends_with("63.5]\n"), + "stdout should hold every formatted element: {stdout}" + ); +} + #[test] fn a_type_without_show_is_rejected() { let source = r#" @@ -110,3 +237,26 @@ main = show Box "expected NoInstanceFound, got {errors:?}" ); } + +#[test] +fn retained_show_strings_survive_wasi_allocation_and_memory_growth() { + // The formatter's initial heap has one page. A 70,000-byte canonical + // random result must grow shared memory while the GC String stays live. + let source = r#"module Main where +import Effect (Effect) +import Effect.Unsafe (unsafePerformEffect) +import Prelude +import Effect.Console (log) +import WASI.Random (randomBytes) +main = let retained = show 1.0e21 + first = unsafePerformEffect (log retained) + large = unsafePerformEffect (randomBytes 70000) + next = unsafePerformEffect (log (show 5.0e-324)) + last = unsafePerformEffect (log retained) + in if arrayLength large == 70000 && retained == "1e+21" then 42 else 1 +"#; + let output = run_with_wasmtime(source).expect("Wasmtime required for growth evidence"); + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert_eq!(output.stdout, b"1e+21\n5e-324\n1e+21\n"); + assert!(output.stderr.is_empty(), "{output:?}"); +} diff --git a/crates/psrs-driver/src/tests/source_evidence.rs b/crates/psrs-driver/src/tests/source_evidence.rs index 2e94710f..90b4f22b 100644 --- a/crates/psrs-driver/src/tests/source_evidence.rs +++ b/crates/psrs-driver/src/tests/source_evidence.rs @@ -64,6 +64,10 @@ fn walk_expr(expression: &psrs_thir::Expr, seen: &mut Seen) { walk_evidence(evidence, seen); seen.coercible = true; } + ExprKind::UnsafeCoerce { value, .. } => { + walk_expr(value, seen); + seen.coercible = true; + } ExprKind::Array(elements) => { for element in elements { walk_expr(element, seen); @@ -123,6 +127,7 @@ fn walk_expr(expression: &psrs_thir::Expr, seen: &mut Seen) { fn walk_evidence(evidence: &psrs_thir::Evidence, seen: &mut Seen) { use psrs_thir::EvidenceKind; match &evidence.kind { + EvidenceKind::DictionaryValue(value) => walk_expr(value, seen), EvidenceKind::Given(_) => seen.given = true, EvidenceKind::Global(_) => seen.global = true, EvidenceKind::Superclass { parent, .. } => { diff --git a/crates/psrs-driver/src/tests/symbol_reflection.rs b/crates/psrs-driver/src/tests/symbol_reflection.rs new file mode 100644 index 00000000..1f3237c0 --- /dev/null +++ b/crates/psrs-driver/src/tests/symbol_reflection.rs @@ -0,0 +1,221 @@ +use psrs_thir::{Evidence, EvidenceKind, Expr, ExprKind}; + +const PROXY: &str = "module Type.Proxy where\ndata Proxy (s :: Symbol) = Proxy\n"; +const SYMBOL: &str = "module Data.Symbol where\nimport Type.Proxy (Proxy)\n\ +class IsSymbol (s :: Symbol) where\n reflectSymbol :: Proxy s -> String\n"; + +fn program(main: &str) -> Vec { + crate::typecheck_program_sources(&[ + ("Proxy.purs", PROXY), + ("Symbol.purs", SYMBOL), + ("Main.purs", main), + ]) + .unwrap_or_else(|errors| panic!("symbol program should check: {errors:?}")) +} + +#[test] +fn symbol_reflection_survives_aliases_and_reexports() { + let bridge = "module Bridge (module S) where\nimport Data.Symbol as S\n"; + let main = r#"module Main where +import Bridge as B +import Type.Proxy (Proxy(..)) +main :: String +main = B.reflectSymbol (Proxy :: Proxy "λ😀") +"#; + crate::check_program(&[ + ("Proxy.purs", PROXY), + ("Symbol.purs", SYMBOL), + ("Bridge.purs", bridge), + ("Main.purs", main), + ]) + .expect("canonical class identity survives alias and re-export"); +} + +#[test] +fn a_same_named_user_class_has_no_compiler_dictionary_rule() { + let ordinary = SYMBOL.replace("module Data.Symbol", "module Ordinary"); + let main = r#"module Main where +import Ordinary +import Type.Proxy (Proxy(..)) +main :: String +main = reflectSymbol (Proxy :: Proxy "unicode") +"#; + let errors = crate::check_program(&[ + ("Proxy.purs", PROXY), + ("Ordinary.purs", &ordinary), + ("Main.purs", main), + ]) + .expect_err("user class needs its own instance"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.code == Some("NoInstanceFound")), + "{errors:?}" + ); +} + +#[test] +fn a_malformed_compiler_class_contract_is_rejected() { + let malformed = SYMBOL.replace("-> String", "-> Int"); + let errors = crate::check_program(&[("Proxy.purs", PROXY), ("Symbol.purs", &malformed)]) + .expect_err("compiler interface must declare the supported method type"); + assert!( + errors + .iter() + .any(|error| error.diagnostic.message.contains("dictionary contract")), + "{errors:?}" + ); +} + +#[test] +fn an_unknown_symbol_is_not_defaulted() { + let main = r#"module Main where +import Data.Symbol +import Type.Proxy (Proxy(..)) +main :: String +main = reflectSymbol Proxy +"#; + crate::check_program(&[ + ("Proxy.purs", PROXY), + ("Symbol.purs", SYMBOL), + ("Main.purs", main), + ]) + .expect_err("reflection requires a known symbol or lexical dictionary"); +} + +#[test] +fn a_symbol_given_is_used_under_a_quantifier() { + program( + r#"module Main where +import Data.Symbol +import Type.Proxy (Proxy(..)) +reflect :: forall s. IsSymbol s => Proxy s -> String +reflect proxy = reflectSymbol proxy +main :: String +main = reflect (Proxy :: Proxy "") +"#, + ); +} + +fn constructed_evidence(expression: &mut Expr) -> Option<&mut Evidence> { + match &mut expression.kind { + ExprKind::Evidence(evidence) => Some(evidence), + ExprKind::Application(function, _) => constructed_evidence(function), + ExprKind::FieldAccess { expression, .. } => constructed_evidence(expression), + _ => None, + } +} + +#[test] +fn constructed_dictionaries_are_verified_as_ordinary_terms() { + let mut modules = program( + r#"module Main where +import Data.Symbol +import Type.Proxy (Proxy(..)) +main :: String +main = reflectSymbol (Proxy :: Proxy "λ😀") +"#, + ); + let main = modules.last_mut().unwrap(); + let evidence = + constructed_evidence(&mut main.declarations[0].value).expect("constructed evidence"); + let EvidenceKind::DictionaryValue(value) = &mut evidence.kind else { + panic!("expected runtime dictionary") + }; + let ExprKind::Record(fields) = &mut value.kind else { + panic!("expected dictionary fields") + }; + let ExprKind::Lambda { binder, body } = &mut fields[0].1.kind else { + panic!("expected reflection method") + }; + assert_eq!(body.kind, ExprKind::String("λ😀".into())); + // A bound proxy cannot serve as the method's String result. + body.kind = ExprKind::Local(binder.id); + assert!( + main.verify().is_err(), + "method body result disagrees with its binder" + ); +} + +#[test] +fn symbol_reflection_returns_canonical_utf8_at_runtime() { + let source = r#"module Main where +import Data.Symbol (reflectSymbol) +import Type.Proxy (Proxy(..)) +main :: Int +main = + let bytes = stringToBytes (reflectSymbol (Proxy :: Proxy "λ😀")) + in if intEq (arrayLength bytes) 6 then + if intEq (arrayIndex bytes 0) 206 then + if intEq (arrayIndex bytes 1) 187 then + if intEq (arrayIndex bytes 2) 240 then + if intEq (arrayIndex bytes 3) 159 then + if intEq (arrayIndex bytes 4) 152 then + if intEq (arrayIndex bytes 5) 128 then 42 else 1 + else 2 + else 3 + else 4 + else 5 + else 6 + else 7 +"#; + let Some(output) = super::run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +#[test] +fn compiler_interface_contracts_accept_equivalent_type_synonyms() { + let symbol = SYMBOL + .replace( + "class IsSymbol", + "type Reflected s = Proxy s -> String\nclass IsSymbol", + ) + .replace( + "reflectSymbol :: Proxy s -> String", + "reflectSymbol :: Reflected s", + ); + crate::check_program(&[ + ("Proxy.purs", PROXY), + ("Symbol.purs", &symbol), + ( + "Main.purs", + r#"module Main where +import Data.Symbol +import Type.Proxy (Proxy(..)) +main :: String +main = reflectSymbol (Proxy :: Proxy "known") +"#, + ), + ]) + .expect("semantic interface validation expands synonyms"); +} + +#[test] +fn constructed_dictionary_methods_cannot_reference_missing_locals() { + let mut modules = program( + r#"module Main where +import Data.Symbol +import Type.Proxy (Proxy(..)) +main :: String +main = reflectSymbol (Proxy :: Proxy "known") +"#, + ); + let main = modules.last_mut().unwrap(); + let evidence = constructed_evidence(&mut main.declarations[0].value).unwrap(); + let EvidenceKind::DictionaryValue(value) = &mut evidence.kind else { + panic!("runtime dictionary") + }; + let ExprKind::Record(fields) = &mut value.kind else { + panic!("dictionary record") + }; + let ExprKind::Lambda { body, .. } = &mut fields[0].1.kind else { + panic!("method lambda") + }; + body.kind = ExprKind::Local(psrs_hir::LocalId(u32::MAX)); + assert!( + main.verify().is_err(), + "dictionary bodies have ordinary lexical scope" + ); +} diff --git a/crates/psrs-driver/src/tests/tail_calls.rs b/crates/psrs-driver/src/tests/tail_calls.rs index 9434ce5a..504f9a2d 100644 --- a/crates/psrs-driver/src/tests/tail_calls.rs +++ b/crates/psrs-driver/src/tests/tail_calls.rs @@ -3,7 +3,7 @@ //! Self tail recursion is loopified on every profile, so a deep self-recursive //! program runs in constant stack. Non-self tail calls become `return_call` / //! `return_call_ref` only when the target enables the tail-call proposal; the -//! stable profile keeps them as ordinary calls plus return. +//! stable profile enables both instructions. use psrs_backend::TargetCapabilities; use psrs_backend::mir::Terminator; @@ -67,9 +67,15 @@ fn has_return_call_ref(mir: &psrs_backend::mir::Module) -> bool { #[test] fn self_tail_recursion_runs_in_constant_stack() { // 100000 frames would exhaust the default Wasmtime stack; loopification - // must make this a `loop` on the tail-call-disabled stable profile. + // must make this a `loop` even on a profile with tail calls disabled. let source = "module Main where\nimport Prelude\ncount :: Int -> Int\ncount n = if n == 0 then 42 else count (n - 1)\nmain = count 100000\n"; - let stages = compile_with(source, TargetCapabilities::default()); + let stages = compile_with( + source, + TargetCapabilities { + tail_call: false, + ..TargetCapabilities::default() + }, + ); assert!( !has_tail_call(&stages.mir), "self-recursion must become a loop, not a return_call" @@ -84,10 +90,7 @@ fn self_tail_recursion_runs_in_constant_stack() { #[test] fn non_self_tail_calls_use_return_call_when_enabled() { let source = "module Main where\nimport Prelude\neven :: Int -> Int\neven n = if n == 0 then 42 else odd (n - 1)\nodd :: Int -> Int\nodd n = if n == 0 then 7 else even (n - 1)\nmain = even 100000\n"; - let target = TargetCapabilities { - tail_call: true, - ..TargetCapabilities::default() - }; + let target = TargetCapabilities::default(); let stages = compile_with(source, target); assert!( has_tail_call(&stages.mir), @@ -110,10 +113,7 @@ fn indirect_tail_recursion_uses_return_call_ref() { // With 100000 iterations this only succeeds if the reference tail call // reuses the frame. let source = "module Main where\nimport Prelude\nrun :: (Int -> Int) -> Int -> Int\nrun step n = if n == 0 then 42 else step (n - 1)\ntick :: Int -> Int\ntick n = run tick n\nmain = tick 100000\n"; - let target = TargetCapabilities { - tail_call: true, - ..TargetCapabilities::default() - }; + let target = TargetCapabilities::default(); let stages = compile_with(source, target); assert!( has_return_call_ref(&stages.mir), @@ -133,7 +133,13 @@ fn indirect_tail_recursion_uses_return_call_ref() { #[test] fn disabled_profile_keeps_an_ordinary_call() { let source = "module Main where\nimport Prelude\nf :: Int -> Int\nf x = g x\ng :: Int -> Int\ng x = x + 1\nmain = f 41\n"; - let stages = compile_with(source, TargetCapabilities::default()); + let stages = compile_with( + source, + TargetCapabilities { + tail_call: false, + ..TargetCapabilities::default() + }, + ); assert!( !has_tail_call(&stages.mir), "a disabled profile must not emit a tail-call terminator" @@ -148,3 +154,42 @@ fn disabled_profile_keeps_an_ordinary_call() { Err(error) => panic!("{error}"), } } + +#[test] +fn ordinary_pure_newtype_bind_loops_run_in_constant_stack() { + let source = r#"module Main where +import Prelude +newtype Task a = Task (Unit -> a) +pureTask :: forall a. a -> Task a +pureTask value = Task \_ -> value +bindTask :: forall a b. Task a -> (a -> Task b) -> Task b +bindTask (Task action) next = Task \token -> + case next (action token) of + Task continuation -> continuation token +loopTask :: Int -> Task Unit +loopTask n = if n == 0 then pureTask unit + else bindTask (pureTask unit) (\_ -> loopTask (n - 1)) +execute :: forall a. Task a -> a +execute (Task action) = action unit +main :: Int +main = execute (bindTask (loopTask 100000) (\_ -> pureTask 42)) +"#; + let stages = compile_with(source, TargetCapabilities::default()); + assert!(has_return_call_ref(&stages.mir)); + match execute_component("pure_newtype_bind_loop", &stages.artifact.wasm) { + Ok(Some(output)) => assert_eq!(output.status.code(), Some(42), "{output:?}"), + Ok(None) => {} + Err(error) => panic!("{error}"), + } +} + +#[test] +fn self_tail_recursion_remaps_forwarded_arguments() { + let source = "module Main where\nimport Prelude\nrotate :: Int -> Int -> Int -> Int\nrotate n a b = if n == 0 then a else rotate (n - 1) b a\nmain = rotate 100000 42 7\n"; + let stages = compile_with(source, TargetCapabilities::default()); + match execute_component("tail_loop_forwarded_arguments", &stages.artifact.wasm) { + Ok(Some(output)) => assert_eq!(output.status.code(), Some(42), "{output:?}"), + Ok(None) => {} + Err(error) => panic!("{error}"), + } +} diff --git a/crates/psrs-driver/src/tests/typecheck/mod.rs b/crates/psrs-driver/src/tests/typecheck/mod.rs index 0adcc725..b7c2f363 100644 --- a/crates/psrs-driver/src/tests/typecheck/mod.rs +++ b/crates/psrs-driver/src/tests/typecheck/mod.rs @@ -110,6 +110,7 @@ fn typechecks_do_notation_over_effect() { let source = "\ module Main where import Prelude +import Effect (Effect) import WASI.Console action :: Effect Int action = do @@ -117,7 +118,8 @@ action = do let doubled = handle + handle _ <- log \"value\" pure doubled -main = runEffect action +main :: Effect Int +main = action "; assert!(check_source("Main.purs", source).is_ok()); } @@ -129,12 +131,14 @@ fn typechecks_a_non_variable_do_binder() { let source = "\ module Main where import Prelude +import Effect (Effect) data Pair = Pair Int Int action :: Effect Int action = do Pair x y <- pure (Pair 1 2) pure (x + y) -main = runEffect action +main :: Effect Int +main = action "; assert!(check_source("Main.purs", source).is_ok()); } @@ -144,12 +148,14 @@ fn rejects_a_do_binder_with_the_wrong_case_type() { let source = "\ module Main where import Prelude +import Effect (Effect) data Pair = Pair Int Int action :: Effect Int action = do Pair x y <- pure 1 pure (x + y) -main = runEffect action +main :: Effect Int +main = action "; assert!(check_source("Main.purs", source).is_err()); } @@ -161,12 +167,14 @@ fn typechecks_ado_notation_over_effect() { let source = "\ module Main where import Prelude +import Effect (Effect) action :: Effect Int action = ado x <- pure 20 y <- pure 22 in x + y -main = runEffect action +main :: Effect Int +main = action "; assert!(check_source("Main.purs", source).is_ok()); } @@ -176,12 +184,14 @@ fn typechecks_an_ado_value_and_let() { let source = "\ module Main where import Prelude +import Effect (Effect) action :: Effect Int action = ado _ <- pure 1 let base = 40 in base + 2 -main = runEffect action +main :: Effect Int +main = action "; assert!(check_source("Main.purs", source).is_ok()); } @@ -289,12 +299,14 @@ fn a_lenient_type_check_type_checks_a_module_whose_sibling_is_missing() { #[test] fn a_lenient_type_check_attributes_a_diagnostic_to_the_user_source_not_the_library() { - let source = "module Main where\nimport Prelude\nmain = runEffect (pure missingName)\n"; + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\nimport Prelude\nmain = unsafePerformEffect (pure missingName)\n"; let errors = check_program_types_lenient_with_prelude(&[("Main.purs", source)]).unwrap_err(); assert!( errors.iter().any(|error| { error.source == DiagnosticOrigin::Source(0) && error.diagnostic.code == Some("UnknownName") + && &source[error.diagnostic.span.start as usize..error.diagnostic.span.end as usize] + == "missingName" }), "the trusted prefix must not shift a user's diagnostic: {errors:?}" ); diff --git a/crates/psrs-driver/src/tests/uninhabited.rs b/crates/psrs-driver/src/tests/uninhabited.rs new file mode 100644 index 00000000..a4ec74cd --- /dev/null +++ b/crates/psrs-driver/src/tests/uninhabited.rs @@ -0,0 +1,33 @@ +//! A constructorless algebraic data type is uninhabited: no expression builds +//! one, so its only values come from unchecked coercions, and the backend lays +//! it out as an erased reference. This exercises a type (like the reference +//! `VariantF`) that has no constructors of its own yet carries a coerced +//! concrete representation. + +#[test] +fn a_constructorless_data_type_round_trips_through_unsafe_coercion() { + let source = r#"module Main where + +import Prelude +import Unsafe.Coerce (unsafeCoerce) + +data Empty :: Type -> Type +data Empty a + +newtype Rep a = Rep { value :: a } + +toEmpty :: forall a. Rep a -> Empty a +toEmpty = unsafeCoerce + +fromEmpty :: forall a. Empty a -> Rep a +fromEmpty = unsafeCoerce + +main :: Int +main = case fromEmpty (toEmpty (Rep { value: 42 })) of + Rep r -> r.value +"#; + let Some(output) = super::run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} diff --git a/crates/psrs-driver/src/tests/wasi/classes/deriving/adapters.rs b/crates/psrs-driver/src/tests/wasi/classes/deriving/adapters.rs new file mode 100644 index 00000000..ffe040a4 --- /dev/null +++ b/crates/psrs-driver/src/tests/wasi/classes/deriving/adapters.rs @@ -0,0 +1,186 @@ +use super::*; + +#[test] +fn newtype_deriving_reuses_a_dictionary_under_an_unknown_constructor() { + let source = r#"module Main where + +data Id a = Id a + +class Tick m where + project :: m Int -> Int + +instance tickId :: Tick Id where + project (Id n) = n + +class Echo t where + echo :: forall f. f t -> f t + run :: forall m. Tick m => (t -> m t) -> t -> Int + +instance echoInt :: Echo Int where + echo value = value + run f n = project (f n) + +newtype Age = Age Int + +derive newtype instance echoAge :: Echo Age + +main :: Int +main = case echo (Id (Age 42)) of + Id (Age n) -> run (\x -> Id x) (Age n) + _ -> 0 +"#; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!( + output.status.code(), + Some(42), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn polymorphic_newtype_deriving_executes_its_adapter() { + let source = r#"module Main where + +class ToInt a where + toInt :: forall b. a -> b -> Int + +instance toIntInt :: ToInt Int where + toInt value _ = value + +newtype Age = Age Int + +derive newtype instance toIntAge :: ToInt Age + +main :: Int +main = if intEq (toInt (Age 42) true) 42 then toInt (Age 42) [1, 2] else 0 +"#; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42)); +} + +#[test] +fn function_contravariant_deriving_executes_its_adapter() { + let profunctor = r#"module Data.Profunctor where + +class Profunctor p where + dimap :: forall a b c d. (b -> a) -> (c -> d) -> p a c -> p b d + lcmap :: forall a b c. (b -> a) -> p a c -> p b c + rmap :: forall a b c. (b -> c) -> p a b -> p a c + +instance profunctorFunction :: Profunctor (->) where + dimap f g h = \x -> g (h (f x)) + lcmap f h = \x -> h (f x) + rmap f h = \x -> f (h x) +"#; + let contravariant = r#"module Data.Functor.Contravariant where + +class Contravariant f where + cmap :: forall a b. (b -> a) -> f a -> f b +"#; + let main = r#"module Main where + +import Data.Functor.Contravariant +import Data.Profunctor (class Profunctor) + +data Predicate a = Predicate (a -> Boolean) + +derive instance contravariantPredicate :: Contravariant Predicate + +main :: Int +main = case cmap (\x -> intAdd x 1) (Predicate (\x -> intEq x 42)) of + Predicate f -> if f 41 then if f 40 then 0 else 42 else 1 +"#; + let Some(output) = run_program_with_wasmtime(&[ + ("Data.Profunctor.purs", profunctor), + ("Data.Functor.Contravariant.purs", contravariant), + ("Main.purs", main), + ]) else { + return; + }; + assert_eq!(output.status.code(), Some(42)); +} + +#[test] +fn profunctor_deriving_maps_record_function_input_and_result() { + let profunctor = r#"module Data.Profunctor where + +class Profunctor p where + dimap :: forall a b c d. (b -> a) -> (c -> d) -> p a c -> p b d + +instance profunctorFunction :: Profunctor (->) where + dimap f g h = \x -> g (h (f x)) +"#; + let contravariant = r#"module Data.Functor.Contravariant where + +class Contravariant f where + cmap :: forall a b. (b -> a) -> f a -> f b +"#; + let main = r#"module Main where + +import Data.Functor.Contravariant +import Data.Profunctor + +data Predicate a b = Predicate { run :: a -> b, inert :: Int } + +derive instance profunctorPredicate :: Profunctor Predicate + +main :: Int +main = case dimap (\x -> intAdd x 1) (\y -> intAdd y 2) (Predicate { run: \x -> x, inert: 7 }) of + Predicate r -> if intEq r.inert 7 then r.run 39 else 0 +"#; + let Some(output) = run_program_with_wasmtime(&[ + ("Data.Profunctor.purs", profunctor), + ("Data.Functor.Contravariant.purs", contravariant), + ("Main.purs", main), + ]) else { + return; + }; + assert_eq!(output.status.code(), Some(42)); +} + +#[test] +fn contravariant_deriving_maps_record_predicates_and_retains_inert_fields() { + let profunctor = r#"module Data.Profunctor where + +class Profunctor p where + dimap :: forall a b c d. (b -> a) -> (c -> d) -> p a c -> p b d + lcmap :: forall a b c. (b -> a) -> p a c -> p b c + rmap :: forall a b c. (b -> c) -> p a b -> p a c + +instance profunctorFunction :: Profunctor (->) where + dimap f g h = \x -> g (h (f x)) + lcmap f h = \x -> h (f x) + rmap f h = \x -> f (h x) +"#; + let contravariant = r#"module Data.Functor.Contravariant where + +class Contravariant f where + cmap :: forall a b. (b -> a) -> f a -> f b +"#; + let main = r#"module Main where + +import Data.Functor.Contravariant +import Data.Profunctor (class Profunctor) + +data Predicate a = Predicate { run :: a -> Boolean, inert :: Int } + +derive instance contravariantPredicate :: Contravariant Predicate + +main :: Int +main = case cmap (\x -> intAdd x 1) (Predicate { run: \x -> intEq x 42, inert: 7 }) of + Predicate r -> if intEq r.inert 7 then if r.run 41 then if r.run 40 then 0 else 42 else 1 else 2 +"#; + let Some(output) = run_program_with_wasmtime(&[ + ("Data.Profunctor.purs", profunctor), + ("Data.Functor.Contravariant.purs", contravariant), + ("Main.purs", main), + ]) else { + return; + }; + assert_eq!(output.status.code(), Some(42)); +} diff --git a/crates/psrs-driver/src/tests/wasi/classes/deriving/folds.rs b/crates/psrs-driver/src/tests/wasi/classes/deriving/folds.rs new file mode 100644 index 00000000..6f1dce45 --- /dev/null +++ b/crates/psrs-driver/src/tests/wasi/classes/deriving/folds.rs @@ -0,0 +1,61 @@ +use super::*; + +#[test] +fn derived_bifoldable_preserves_left_right_mapping_and_fold_order() { + let semigroup = r#"module Data.Semigroup where + +class Semigroup a where + append :: a -> a -> a +"#; + let monoid = r#"module Data.Monoid where + +import Data.Semigroup + +class Semigroup a <= Monoid a where + mempty :: a +"#; + let foldable = r#"module Data.Bifoldable where + +import Data.Monoid + +class Bifoldable t where + bifoldr :: forall a b c. (a -> c -> c) -> (b -> c -> c) -> c -> t a b -> c + bifoldl :: forall a b c. (c -> a -> c) -> (c -> b -> c) -> c -> t a b -> c + bifoldMap :: forall a b m. Monoid m => (a -> m) -> (b -> m) -> t a b -> m +"#; + let main = r#"module Main where + +import Data.Bifoldable +import Data.Monoid +import Data.Semigroup + +instance semigroupInt :: Semigroup Int where + append left right = intAdd left right + +instance monoidInt :: Monoid Int where + mempty = 0 + +data Box a b = Empty | Box { left :: a, right :: b, inert :: Int } + +derive instance bifoldableBox :: Bifoldable Box + +main :: Int +main = if intEq (bifoldMap (\x -> x) (\y -> intAdd y 1) (Box { left: 20, right: 21, inert: 7 })) 42 + then if intEq (bifoldr (\x acc -> intAdd x (intMul 10 acc)) (\x acc -> intAdd x (intMul 10 acc)) 0 (Box { left: 1, right: 2, inert: 7 })) 21 + then if intEq (bifoldl (\acc x -> intAdd (intMul 10 acc) x) (\acc x -> intAdd (intMul 10 acc) x) 0 (Box { left: 1, right: 2, inert: 7 })) 12 + then if intEq (bifoldMap (\x -> x) (\y -> y) (Empty :: Box Int Int)) 0 then 42 else 1 + else 2 + else 3 + else 4 +"#; + let Some(output) = run_program_with_wasmtime(&[ + ("Data.Semigroup.purs", semigroup), + ("Data.Monoid.purs", monoid), + ("Data.Bifoldable.purs", foldable), + ("Main.purs", main), + ]) else { + eprintln!("skipping: wasmtime is not installed"); + return; + }; + assert_eq!(output.status.code(), Some(42)); +} diff --git a/crates/psrs-driver/src/tests/wasi/classes/deriving/generic.rs b/crates/psrs-driver/src/tests/wasi/classes/deriving/generic.rs new file mode 100644 index 00000000..d6700073 --- /dev/null +++ b/crates/psrs-driver/src/tests/wasi/classes/deriving/generic.rs @@ -0,0 +1,64 @@ +use super::*; + +#[test] +fn generic_deriving_round_trips_constructor_tags_and_fields() { + let generic_rep = r#"module Data.Generic.Rep where + +data NoConstructors + +data NoArguments = NoArguments + +newtype Argument a = Argument a + +data Product a b = Product a b + +data Sum a b = Inl a | Inr b + +newtype Constructor (name :: Symbol) a = Constructor a + +class Generic t rep | t -> rep where + from :: t -> rep + to :: rep -> t +"#; + let main = r#"module Main where + +import Data.Generic.Rep + +data Choice a = Empty | Single a | Pair a Int + +derive instance genericChoice :: Generic (Choice a) _ + +main :: Int +main = case to (from (Pair 40 2)) of + Pair x y -> case to (from (Single 7)) of + Single z -> if intEq z 7 then case to (from (Empty :: Choice Int)) of + Empty -> intAdd x y + _ -> 0 + else 1 + _ -> 2 + _ -> 3 +"#; + let Some(output) = + run_program_with_wasmtime(&[("Data.Generic.Rep.purs", generic_rep), ("Main.purs", main)]) + else { + return; + }; + assert_eq!(output.status.code(), Some(42)); +} + +#[test] +fn generic_round_trip_uses_the_trusted_representation_declarations() { + let source = r#"module Main where +import Data.Generic.Rep (class Generic, to, from) +data Choice a = Empty | Single a | Pair a Int +derive instance genericChoice :: Generic (Choice a) _ +main :: Int +main = case to (from (Pair 40 2)) of + Pair x y -> intAdd x y + _ -> 0 +"#; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} diff --git a/crates/psrs-driver/src/tests/wasi/classes/deriving/higher_kinded.rs b/crates/psrs-driver/src/tests/wasi/classes/deriving/higher_kinded.rs new file mode 100644 index 00000000..97ec7125 --- /dev/null +++ b/crates/psrs-driver/src/tests/wasi/classes/deriving/higher_kinded.rs @@ -0,0 +1,58 @@ +use super::*; + +#[test] +fn eq_and_ord_expand_applied_variable_aliases_and_use_higher_kinded_dictionaries() { + let eq = r#"module Data.Eq where +class Eq a where + eq :: a -> a -> Boolean +class Eq1 f where + eq1 :: forall a. Eq a => f a -> f a -> Boolean +instance eqInt :: Eq Int where + eq x y = intEq x y +"#; + let ordering = "module Data.Ordering where\ndata Ordering = LT | EQ | GT\n"; + let ord = r#"module Data.Ord where +import Data.Ordering +class Ord a where + compare :: a -> a -> Ordering +class Ord1 f where + compare1 :: forall a. Ord a => f a -> f a -> Ordering +instance ordInt :: Ord Int where + compare x y = if intEq x y then EQ else if intLt x y then LT else GT +"#; + let main = r#"module Main where +import Data.Eq +import Data.Ord +import Data.Ordering + +data Maybe a = Nothing | Just a +derive instance eqMaybe :: Eq a => Eq (Maybe a) +derive instance eq1Maybe :: Eq1 Maybe +derive instance ordMaybe :: Ord a => Ord (Maybe a) +derive instance ord1Maybe :: Ord1 Maybe + +type Applied f a = f a +data Box f a = Box (Applied f a) +derive instance eqBox :: (Eq1 f, Eq a) => Eq (Box f a) +derive instance ordBox :: (Ord1 f, Ord a) => Ord (Box f a) + +main :: Int +main = if eq (Box (Just 4)) (Box (Just 4)) then + if eq (Box (Just 4)) (Box (Just 5)) then 1 else + case compare (Box (Just 4)) (Box (Just 5)) of + LT -> 42 + _ -> 2 + else 3 +"#; + let sources = [ + ("Data.Eq.purs", eq), + ("Data.Ordering.purs", ordering), + ("Data.Ord.purs", ord), + ("Main.purs", main), + ]; + crate::check_program(&sources).expect("applied-variable aliases should type check"); + let Some(output) = run_program_with_wasmtime(&sources) else { + return; + }; + assert_eq!(output.status.code(), Some(42)); +} diff --git a/crates/psrs-driver/src/tests/wasi/classes/deriving.rs b/crates/psrs-driver/src/tests/wasi/classes/deriving/mod.rs similarity index 79% rename from crates/psrs-driver/src/tests/wasi/classes/deriving.rs rename to crates/psrs-driver/src/tests/wasi/classes/deriving/mod.rs index d2ae860f..0cee778f 100644 --- a/crates/psrs-driver/src/tests/wasi/classes/deriving.rs +++ b/crates/psrs-driver/src/tests/wasi/classes/deriving/mod.rs @@ -2,6 +2,8 @@ use super::super::super::*; +mod traversals; + #[test] fn imported_newtype_derived_dictionary_executes_its_coercion_adapter() { let library = r#"module Lib (Age(..), class ToInt, toInt) where @@ -198,6 +200,60 @@ main = case map (\value -> intAdd value 1) (Box (Some 41)) of assert_eq!(output.status.code(), Some(42)); } +#[test] +fn derived_foldable_executes_through_its_instances() { + let semigroup = r#"module Data.Semigroup where + +class Semigroup a where + append :: a -> a -> a +"#; + let monoid = r#"module Data.Monoid where + +import Data.Semigroup + +class Semigroup a <= Monoid a where + mempty :: a +"#; + let foldable = r#"module Data.Foldable where + +import Data.Monoid + +class Foldable t where + foldr :: forall a b. (a -> b -> b) -> b -> t a -> b + foldl :: forall a b. (b -> a -> b) -> b -> t a -> b + foldMap :: forall a m. Monoid m => (a -> m) -> t a -> m +"#; + let main = r#"module Main where + +import Data.Foldable +import Data.Monoid +import Data.Semigroup + +instance semigroupInt :: Semigroup Int where + append left right = intAdd left right + +instance monoidInt :: Monoid Int where + mempty = 0 + +data Box a = Box a + +derive instance foldableBox :: Foldable Box + +main :: Int +main = intAdd (foldMap (\x -> x) (Box 40)) (foldr (\x acc -> intAdd x acc) 0 (Box 2)) +"#; + let Some(output) = run_program_with_wasmtime(&[ + ("Data.Semigroup.purs", semigroup), + ("Data.Monoid.purs", monoid), + ("Data.Foldable.purs", foldable), + ("Main.purs", main), + ]) else { + eprintln!("skipping: wasmtime is not installed"); + return; + }; + assert_eq!(output.status.code(), Some(42)); +} + #[test] fn derives_bifunctor_mapping_for_both_type_parameters() { let bifunctor = r#"module Data.Bifunctor where @@ -226,3 +282,11 @@ main = case bimap (\value -> intAdd value 1) (\value -> intAdd value 1) (Pair 40 }; assert_eq!(output.status.code(), Some(42)); } + +mod adapters; +mod generic; + +mod higher_kinded; +mod records; + +mod folds; diff --git a/crates/psrs-driver/src/tests/wasi/classes/deriving/records.rs b/crates/psrs-driver/src/tests/wasi/classes/deriving/records.rs new file mode 100644 index 00000000..b4ea283d --- /dev/null +++ b/crates/psrs-driver/src/tests/wasi/classes/deriving/records.rs @@ -0,0 +1,216 @@ +use super::*; + +#[test] +fn derived_functor_maps_record_fields_and_retains_inert_fields() { + let library = r#"module Data.Functor where + +class Functor f where + map :: forall a b. (a -> b) -> f a -> f b + +data Option a = None | Some a + +instance functorOption :: Functor Option where + map f value = case value of + None -> None + Some item -> Some (f item) +"#; + let main = r#"module Main where + +import Data.Functor + +type Wrapped a = Option a + +data Box a = Box { nested :: Wrapped a, scalar :: a, flag :: Boolean } + +derive instance functorBox :: Functor Box + +main :: Int +main = case map (\value -> intAdd value 1) (Box { nested: Some 20, scalar: 20, flag: true }) of + Box r -> case r.nested of + Some x -> if r.flag then intAdd x r.scalar else 0 + _ -> 0 +"#; + let Some(output) = + run_program_with_wasmtime(&[("Data.Functor.purs", library), ("Main.purs", main)]) + else { + eprintln!("skipping: wasmtime is not installed"); + return; + }; + assert_eq!( + output.status.code(), + Some(42), + "{}", + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn derived_foldable_preserves_record_label_order_in_both_fold_directions() { + let semigroup = r#"module Data.Semigroup where + +class Semigroup a where + append :: a -> a -> a +"#; + let monoid = r#"module Data.Monoid where + +import Data.Semigroup + +class Semigroup a <= Monoid a where + mempty :: a +"#; + let foldable = r#"module Data.Foldable where + +import Data.Monoid + +class Foldable t where + foldr :: forall a b. (a -> b -> b) -> b -> t a -> b + foldl :: forall a b. (b -> a -> b) -> b -> t a -> b + foldMap :: forall a m. Monoid m => (a -> m) -> t a -> m +"#; + let main = r#"module Main where + +import Data.Foldable +import Data.Monoid +import Data.Semigroup + +instance semigroupInt :: Semigroup Int where + append left right = intAdd left right + +instance monoidInt :: Monoid Int where + mempty = 0 + +data Box a = Box { first :: a, last :: a, flag :: Boolean } + +derive instance foldableBox :: Foldable Box + +value = Box { first: 1, last: 2, flag: true } +main :: Int +main = if intEq (foldMap (\x -> x) value) 3 then + if intEq (foldr (\x acc -> intAdd x (intMul 10 acc)) 0 value) 21 then + if intEq (foldl (\acc x -> intAdd (intMul 10 acc) x) 0 value) 12 then 42 else 1 + else 2 + else 3 +"#; + let Some(output) = run_program_with_wasmtime(&[ + ("Data.Semigroup.purs", semigroup), + ("Data.Monoid.purs", monoid), + ("Data.Foldable.purs", foldable), + ("Main.purs", main), + ]) else { + eprintln!("skipping: wasmtime is not installed"); + return; + }; + assert_eq!( + output.status.code(), + Some(42), + "{}", + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn derived_traversable_maps_record_effects_and_preserves_inert_fields() { + let function = r#"module Data.Function where + +identity :: forall a. a -> a +identity x = x +"#; + let functor = r#"module Data.Functor where + +class Functor f where + map :: forall a b. (a -> b) -> f a -> f b +"#; + let apply = r#"module Control.Apply where + +import Data.Functor + +class Functor f <= Apply f where + apply :: forall a b. f (a -> b) -> f a -> f b +"#; + let applicative = r#"module Control.Applicative where + +import Control.Apply + +class Apply f <= Applicative f where + pure :: forall a. a -> f a +"#; + let traversable = r#"module Data.Traversable where + +import Control.Applicative (class Applicative) + +class Traversable t where + traverse :: forall a b f. Applicative f => (a -> f b) -> t a -> f (t b) + sequence :: forall a f. Applicative f => t (f a) -> f (t a) +"#; + let main = r#"module Main where + +import Data.Function (identity) +import Data.Traversable +import Data.Functor +import Control.Apply +import Control.Applicative + +data Maybe a = Nothing | Just a +instance functorMaybe :: Functor Maybe where + map f value = case value of + Nothing -> Nothing + Just x -> Just (f x) +instance applyMaybe :: Apply Maybe where + apply f value = case f of + Nothing -> Nothing + Just g -> map g value +instance applicativeMaybe :: Applicative Maybe where + pure = Just + +data Box a = Empty Int | Box { first :: a, last :: a, inert :: Int } + +derive instance traversableBox :: Traversable Box + +main :: Int +main = case traverse (\x -> Just (intAdd x 1)) (Box { first: 20, last: 20, inert: 7 }) of + Just (Box r) -> if intEq r.inert 7 then case sequence (Empty 9 :: Box (Maybe Int)) of + Just (Empty n) -> if intEq n 9 then intAdd r.first r.last else 1 + _ -> 2 + else 3 + _ -> 0 +"#; + let sources = [ + ("Data.Function.purs", function), + ("Data.Functor.purs", functor), + ("Control.Apply.purs", apply), + ("Control.Applicative.purs", applicative), + ("Data.Traversable.purs", traversable), + ("Main.purs", main), + ]; + let Some(output) = run_program_with_wasmtime(&sources) else { + return; + }; + assert_eq!( + output.status.code(), + Some(42), + "{}", + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn bifunctor_deriving_maps_both_record_parameters_and_retains_inert_fields() { + let bifunctor = r#"module Data.Bifunctor where +class Bifunctor f where + bimap :: forall a b c d. (a -> b) -> (c -> d) -> f a c -> f b d +"#; + let main = r#"module Main where +import Data.Bifunctor +data Box a b = Box { left :: a, right :: b, inert :: Int } +derive instance bifunctorBox :: Bifunctor Box +main :: Int +main = case bimap (\x -> intAdd x 1) (\y -> intAdd y 2) (Box { left: 19, right: 20, inert: 7 }) of + Box r -> if intEq r.inert 7 then intAdd r.left r.right else 0 +"#; + let Some(output) = + run_program_with_wasmtime(&[("Data.Bifunctor.purs", bifunctor), ("Main.purs", main)]) + else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} diff --git a/crates/psrs-driver/src/tests/wasi/classes/deriving/traversals.rs b/crates/psrs-driver/src/tests/wasi/classes/deriving/traversals.rs new file mode 100644 index 00000000..bc79ac7e --- /dev/null +++ b/crates/psrs-driver/src/tests/wasi/classes/deriving/traversals.rs @@ -0,0 +1,286 @@ +use super::*; + +#[test] +fn derived_traversable_executes_through_its_instances() { + let function = r#"module Data.Function where + +identity :: forall a. a -> a +identity x = x +"#; + let functor = r#"module Data.Functor where + +class Functor f where + map :: forall a b. (a -> b) -> f a -> f b +"#; + let apply = r#"module Control.Apply where + +import Data.Functor + +class Functor f <= Apply f where + apply :: forall a b. f (a -> b) -> f a -> f b +"#; + let applicative = r#"module Control.Applicative where + +import Control.Apply + +class Apply f <= Applicative f where + pure :: forall a. a -> f a +"#; + let traversable = r#"module Data.Traversable where + +import Control.Applicative (class Applicative) + +class Traversable t where + traverse :: forall a b f. Applicative f => (a -> f b) -> t a -> f (t b) + sequence :: forall a f. Applicative f => t (f a) -> f (t a) +"#; + let main = r#"module Main where + +import Data.Function (identity) +import Data.Traversable +import Data.Functor +import Control.Apply +import Control.Applicative + +data Maybe a = Nothing | Just a +instance functorMaybe :: Functor Maybe where + map f value = case value of + Nothing -> Nothing + Just x -> Just (f x) +instance applyMaybe :: Apply Maybe where + apply f value = case f of + Nothing -> Nothing + Just g -> map g value +instance applicativeMaybe :: Applicative Maybe where + pure = Just + +data Box a = Box a + +derive instance traversableBox :: Traversable Box + +main :: Int +main = case traverse (\x -> Just (intAdd x 1)) (Box 41) of + Just (Box x) -> x + _ -> 0 +"#; + let Some(output) = run_program_with_wasmtime(&[ + ("Data.Function.purs", function), + ("Data.Functor.purs", functor), + ("Control.Apply.purs", apply), + ("Control.Applicative.purs", applicative), + ("Data.Traversable.purs", traversable), + ("Main.purs", main), + ]) else { + return; + }; + assert_eq!(output.status.code(), Some(42)); +} + +#[test] +fn derived_bitraversable_preserves_both_effects_and_sequence() { + let function = r#"module Data.Function where + +identity :: forall a. a -> a +identity x = x +"#; + let functor = r#"module Data.Functor where + +class Functor f where + map :: forall a b. (a -> b) -> f a -> f b +"#; + let apply = r#"module Control.Apply where + +import Data.Functor + +class Functor f <= Apply f where + apply :: forall a b. f (a -> b) -> f a -> f b +"#; + let applicative = r#"module Control.Applicative where + +import Control.Apply + +class Apply f <= Applicative f where + pure :: forall a. a -> f a +"#; + let traversable = r#"module Data.Bitraversable where + +import Control.Applicative (class Applicative) + +class Bitraversable t where + bitraverse :: forall a b c d f. Applicative f => (a -> f c) -> (b -> f d) -> t a b -> f (t c d) + bisequence :: forall a b f. Applicative f => t (f a) (f b) -> f (t a b) +"#; + let main = r#"module Main where + +import Data.Function (identity) +import Data.Bitraversable +import Data.Functor +import Control.Apply +import Control.Applicative + +data Maybe a = Nothing | Just a +instance functorMaybe :: Functor Maybe where + map f value = case value of + Nothing -> Nothing + Just x -> Just (f x) +instance applyMaybe :: Apply Maybe where + apply f value = case f of + Nothing -> Nothing + Just g -> map g value +instance applicativeMaybe :: Applicative Maybe where + pure = Just + +data Box a b = Box a b + +derive instance bitraversableBox :: Bitraversable Box + +main :: Int +main = case bitraverse (\x -> Just (intAdd x 1)) (\y -> Just (intAdd y 2)) (Box 19 20) of + Just (Box x y) -> if intEq x 20 then if intEq y 22 then case bisequence (Box (Just 20) (Just 22)) of + Just (Box a b) -> intAdd a b + _ -> 1 + else 2 + else 3 + _ -> 0 +"#; + let Some(output) = run_program_with_wasmtime(&[ + ("Data.Function.purs", function), + ("Data.Functor.purs", functor), + ("Control.Apply.purs", apply), + ("Control.Applicative.purs", applicative), + ("Data.Bitraversable.purs", traversable), + ("Main.purs", main), + ]) else { + return; + }; + assert_eq!(output.status.code(), Some(42)); +} + +#[test] +fn derived_traversable_uses_its_higher_kinded_context_dictionary() { + let function = r#"module Data.Function where + +identity :: forall a. a -> a +identity x = x +"#; + let functor = r#"module Data.Functor where + +class Functor f where + map :: forall a b. (a -> b) -> f a -> f b +"#; + let apply = r#"module Control.Apply where + +import Data.Functor + +class Functor f <= Apply f where + apply :: forall a b. f (a -> b) -> f a -> f b +"#; + let applicative = r#"module Control.Applicative where + +import Control.Apply + +class Apply f <= Applicative f where + pure :: forall a. a -> f a +"#; + let traversable = r#"module Data.Traversable where + +import Control.Applicative (class Applicative) + +class Traversable t where + traverse :: forall a b f. Applicative f => (a -> f b) -> t a -> f (t b) + sequence :: forall a f. Applicative f => t (f a) -> f (t a) +"#; + let main = r#"module Main where + +import Data.Function (identity) +import Data.Traversable +import Data.Functor +import Control.Apply +import Control.Applicative + +data Maybe a = Nothing | Just a +instance functorMaybe :: Functor Maybe where + map f value = case value of + Nothing -> Nothing + Just x -> Just (f x) +instance applyMaybe :: Apply Maybe where + apply f value = case f of + Nothing -> Nothing + Just g -> map g value +instance applicativeMaybe :: Applicative Maybe where + pure = Just + +data Box f a = Box (f a) + +derive instance traversableMaybe :: Traversable Maybe + +derive instance traversableBox :: Traversable f => Traversable (Box f) + +main :: Int +main = case traverse (\x -> Just (intAdd x 1)) (Box (Just 41)) of + Just (Box (Just x)) -> x + _ -> 0 +"#; + let Some(output) = run_program_with_wasmtime(&[ + ("Data.Function.purs", function), + ("Data.Functor.purs", functor), + ("Control.Apply.purs", apply), + ("Control.Applicative.purs", applicative), + ("Data.Traversable.purs", traversable), + ("Main.purs", main), + ]) else { + return; + }; + assert_eq!(output.status.code(), Some(42)); +} + +#[test] +fn derived_traversable_sequences_through_the_trusted_core_libraries() { + let source = r#"module Main where +import Prelude +import Data.Maybe (Maybe(..)) +import Data.Traversable (class Traversable, traverse, sequence) +import Data.Foldable (class Foldable) +data Box a = Box a +derive instance functorBox :: Functor Box +derive instance foldableBox :: Foldable Box +derive instance traversableBox :: Traversable Box +main :: Int +main = case traverse (\x -> Just (intAdd x 1)) (Box 41) of + Just (Box x) -> if intEq x 42 then case sequence (Box (Just 42)) of + Just (Box y) -> y + _ -> 1 + else 2 + _ -> 0 +"#; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +#[test] +fn derived_bitraversable_sequences_through_the_trusted_core_libraries() { + let source = r#"module Main where +import Prelude +import Data.Maybe (Maybe(..)) +import Data.Bifunctor (class Bifunctor) +import Data.Bifoldable (class Bifoldable) +import Data.Bitraversable (class Bitraversable, bitraverse, bisequence) +data Box a b = Box { left :: a, right :: b, inert :: Int } +derive instance bifunctorBox :: Bifunctor Box +derive instance bifoldableBox :: Bifoldable Box +derive instance bitraversableBox :: Bitraversable Box +main :: Int +main = case bitraverse (\x -> Just (intAdd x 1)) (\y -> Just (intAdd y 2)) (Box { left: 19, right: 20, inert: 7 }) of + Just (Box r) -> if intEq r.inert 7 then case bisequence (Box { left: Just r.left, right: Just r.right, inert: 7 }) of + Just (Box s) -> intAdd s.left s.right + _ -> 1 + else 2 + _ -> 0 +"#; + let Some(output) = run_with_wasmtime(source) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} diff --git a/crates/psrs-driver/src/tests/wasi/classes/fundeps.rs b/crates/psrs-driver/src/tests/wasi/classes/fundeps/mod.rs similarity index 99% rename from crates/psrs-driver/src/tests/wasi/classes/fundeps.rs rename to crates/psrs-driver/src/tests/wasi/classes/fundeps/mod.rs index 57c552c0..2459be56 100644 --- a/crates/psrs-driver/src/tests/wasi/classes/fundeps.rs +++ b/crates/psrs-driver/src/tests/wasi/classes/fundeps/mod.rs @@ -6,6 +6,9 @@ use super::super::super::*; +mod nested_context; +mod rows; + const FUNCTIONAL_DEPENDENCY_SOURCE: &str = r#" module Main where diff --git a/crates/psrs-driver/src/tests/wasi/classes/fundeps/nested_context.rs b/crates/psrs-driver/src/tests/wasi/classes/fundeps/nested_context.rs new file mode 100644 index 00000000..41f11742 --- /dev/null +++ b/crates/psrs-driver/src/tests/wasi/classes/fundeps/nested_context.rs @@ -0,0 +1,56 @@ +use super::*; + +const ROW_CONTEXT: &str = r"module Main where +import Prim.Row as R +import Prim.RowList as RL +class FoldableVFRL :: RL.RowList (Type -> Type) -> Row (Type -> Type) -> Constraint +class FoldableVFRL rl row | rl -> row +instance foldableCons :: + ( FoldableVFRL rl r, R.Cons k f r r' ) => FoldableVFRL (RL.Cons k f rl) r' +class TraversableVFRL :: RL.RowList (Type -> Type) -> Row (Type -> Type) -> Constraint +class FoldableVFRL rl row <= TraversableVFRL rl row | rl -> row +instance traversableCons :: + ( TraversableVFRL rl r, R.Cons k f r r', R.Union r rx r' ) => TraversableVFRL (RL.Cons k f rl) r' +main :: Int +main = 42 +"; + +#[test] +fn nested_instance_context_improves_from_a_given_superclass_row_dependency() { + check_source("Main.purs", ROW_CONTEXT).expect("the superclass determines the residual row"); +} + +#[test] +fn nested_instance_context_without_a_residual_row_dictionary_is_rejected() { + let source = ROW_CONTEXT.replace("TraversableVFRL rl r, R.Cons", "R.Cons"); + let errors = check_source("Main.purs", &source).expect_err("no residual dictionary exists"); + assert!( + errors + .iter() + .any(|error| error.code == Some("NoInstanceFound")), + "{errors:?}" + ); +} + +#[test] +fn nested_instance_context_improvement_executes_user_class_dictionaries() { + let source = r"module Main where +data Wrapper a = Wrapper a +class Describe a b | a -> b where + describe :: a -> b +class Value a where + value :: a -> Int +instance describeInt :: Describe Int Int where + describe x = x +instance valueInt :: Value Int where + value x = x +instance valueWrapper :: (Describe a b, Value b) => Value (Wrapper a) where + value (Wrapper x) = value (describe x) +useGiven :: forall a b. Describe a b => Value b => Wrapper a -> Int +useGiven wrapped = value wrapped +main :: Int +main = useGiven (Wrapper 42) +"; + let output = run_with_wasmtime(source).expect("required Wasmtime runtime"); + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} diff --git a/crates/psrs-driver/src/tests/wasi/classes/fundeps/rows.rs b/crates/psrs-driver/src/tests/wasi/classes/fundeps/rows.rs new file mode 100644 index 00000000..cec510eb --- /dev/null +++ b/crates/psrs-driver/src/tests/wasi/classes/fundeps/rows.rs @@ -0,0 +1,42 @@ +use super::*; + +const SOURCE: &str = r"module Main where +import Type.Equality (class TypeEquals) +import Type.Proxy (Proxy) +import Unsafe.Coerce (unsafeCoerce) +data R r a = R a +fromRows :: forall f r1 r2 a. TypeEquals (Proxy r1) (Proxy r2) => f r2 a -> f r1 a +fromRows = unsafeCoerce +inject :: forall r a. a -> R (value :: Array | r) a +inject = R +class Lift m where + lift :: forall a. a -> m a +instance liftR :: TypeEquals (Proxy r1) (Proxy (value :: Array | r2)) => Lift (R r1) where + lift value = fromRows (inject value) +class Lift m <= Child m where + stamp :: m Int -> Int +instance childR :: TypeEquals (Proxy r1) (Proxy (other :: Array, value :: Array | r2)) => Child (R r1) where + stamp (R n) = n +main = stamp (R 42 :: R (other :: Array, value :: Array) Int) +"; + +#[test] +fn superclass_context_refines_a_row_tail_from_a_given_dependency() { + let Some(output) = run_with_wasmtime(SOURCE) else { + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); +} + +#[test] +fn superclass_context_does_not_invent_a_missing_row_field() { + let source = SOURCE.replace("other :: Array, value :: Array | r2", "other :: Array | r2"); + let errors = compile_source("Main.purs", &source) + .expect_err("an abstract tail cannot guarantee a missing field"); + assert!( + errors + .iter() + .any(|error| error.code == Some("TypesDoNotUnify")), + "{errors:?}" + ); +} diff --git a/crates/psrs-driver/src/tests/wasi/classes/mod.rs b/crates/psrs-driver/src/tests/wasi/classes/mod.rs index 59a214b8..927301f8 100644 --- a/crates/psrs-driver/src/tests/wasi/classes/mod.rs +++ b/crates/psrs-driver/src/tests/wasi/classes/mod.rs @@ -396,6 +396,20 @@ fn an_ambiguous_instance_context_variable_is_reported() { ); } +#[test] +fn instance_context_variables_determined_by_functional_dependencies_are_accepted() { + let source = r#" +module Main where + +class KeyValue key value | key -> value +class Container key + +instance containerKeyValue :: KeyValue key value => Container key +"#; + crate::typecheck_program_sources(&[("Main.purs", source)]) + .expect("the instance head determines `value` through KeyValue's functional dependency"); +} + const CLASS_DEFAULT_SOURCE: &str = r#" module Main where @@ -434,14 +448,12 @@ fn a_class_method_body_is_rejected_as_invalid_purescript() { } #[test] -fn a_deriving_declaration_is_reported_as_unsupported() { +fn a_deriving_declaration_for_a_class_with_no_rule_is_rejected() { let errors = compile_source("Main.purs", DERIVE_SOURCE).expect_err("deriving must be rejected"); assert!( - errors.iter().any(|error| { - error - .message - .contains("known-class deriving rule is unavailable") - }), + errors + .iter() + .any(|error| error.code == Some("CannotDerive")), "unexpected diagnostics: {errors:?}" ); } diff --git a/crates/psrs-driver/src/tests/wasi/do_notation.rs b/crates/psrs-driver/src/tests/wasi/do_notation.rs index 4cfae754..9347889f 100644 --- a/crates/psrs-driver/src/tests/wasi/do_notation.rs +++ b/crates/psrs-driver/src/tests/wasi/do_notation.rs @@ -3,13 +3,14 @@ use super::*; #[test] fn runs_a_do_notation_effect_sequence_when_wasmtime_is_available() { // `do` desugars to `bind`/`discard`/`let`. The block runs the two writes - // left to right, binds a value for the final `pure`, and `runEffect` yields + // left to right, binds a value for the final `pure`, and `unsafePerformEffect` yields // the entry's exit code. let source = r#"module Main where +import Effect.Unsafe (unsafePerformEffect) import Prelude import WASI.Console -main = runEffect +main = unsafePerformEffect do log "first" let label = "second" diff --git a/crates/psrs-driver/src/tests/wasi/filesystem.rs b/crates/psrs-driver/src/tests/wasi/filesystem.rs index d0affd76..19da008f 100644 --- a/crates/psrs-driver/src/tests/wasi/filesystem.rs +++ b/crates/psrs-driver/src/tests/wasi/filesystem.rs @@ -3,27 +3,28 @@ use super::*; #[test] fn writes_and_reads_a_file_through_preopens_when_wasmtime_is_available() { let source = r#"module Main where +import Effect.Unsafe (unsafePerformEffect) import Prelude import Data.Either (Either(..)) import WASI.Console (log) import WASI.FileSystem main = - let dirs = runEffect preopens in + let dirs = unsafePerformEffect preopens in let dir = arrayIndex dirs 0 in - let created = runEffect (openWrite (dir._1) "roundtrip.txt") in + let created = unsafePerformEffect (openWrite (dir._1) "roundtrip.txt") in case created of Left _ -> 1 Right file -> - let wrote = runEffect (writeString file "roundtrip") in - let closed = runEffect (dropDescriptor file) in - let reopened = runEffect (openRead (dir._1) "roundtrip.txt") in + let wrote = unsafePerformEffect (writeString file "roundtrip") in + let closed = unsafePerformEffect (dropDescriptor file) in + let reopened = unsafePerformEffect (openRead (dir._1) "roundtrip.txt") in case reopened of Left _ -> 2 Right reader -> - let contents = runEffect (withDescriptor reader (\openReader -> readString openReader 64)) in + let contents = unsafePerformEffect (withDescriptor reader (\openReader -> readString openReader 64)) in case contents of Left _ -> 3 - Right text -> let ignored = runEffect (log text) in 0 + Right text -> let ignored = unsafePerformEffect (log text) in 0 "#; let directory = std::env::temp_dir().join(format!( "psrs-fs-{}-{}", @@ -53,14 +54,15 @@ fn reports_a_unit_success_result_as_left_when_wasmtime_is_available() { // error on `Left`, so a failing unit-success operation reports `Left` // instead of trapping. let source = r#"module Main where +import Effect.Unsafe (unsafePerformEffect) import Prelude import Data.Either (Either(..)) import WASI.FileSystem main :: Int main = - let dirs = runEffect preopens in + let dirs = unsafePerformEffect preopens in let dir = arrayIndex dirs 0 in - case runEffect (removeDirectoryAt (dir._1) "missing-directory") of + case unsafePerformEffect (removeDirectoryAt (dir._1) "missing-directory") of Right _ -> 1 Left _ -> 0 "#; @@ -94,6 +96,7 @@ fn stats_and_reads_a_directory_through_preopens_when_wasmtime_is_available() { // `option` fields, and `read-directory-entry` returns // `option`, both stored erased inside their `Either`. let source = r#"module Main where +import Effect.Unsafe (unsafePerformEffect) import Prelude import Data.Either (Either(..)) import Data.Maybe (Maybe(..)) @@ -101,35 +104,35 @@ import WASI.Console (log) import WASI.FileSystem main :: Int main = - let dirs = runEffect preopens in + let dirs = unsafePerformEffect preopens in let dir = arrayIndex dirs 0 in let d = dir._1 in - let created = runEffect (openWrite d "probe.txt") in + let created = unsafePerformEffect (openWrite d "probe.txt") in case created of Left _ -> 1 Right file -> - let wrote = runEffect (writeString file "probe") in - let closed = runEffect (dropDescriptor file) in - let stats = runEffect (stat d) in + let wrote = unsafePerformEffect (writeString file "probe") in + let closed = unsafePerformEffect (dropDescriptor file) in + let stats = unsafePerformEffect (stat d) in case stats of Left _ -> 2 Right s -> case s.type of Directory -> - let fileStats = runEffect (statAt d { symlinkFollow: true } "probe.txt") in + let fileStats = unsafePerformEffect (statAt d { symlinkFollow: true } "probe.txt") in case fileStats of Left _ -> 3 Right f -> case f.type of RegularFile -> - let entries = runEffect (readDirectory d) in + let entries = unsafePerformEffect (readDirectory d) in case entries of Left _ -> 4 Right stream -> - let entry = runEffect (readDirectoryEntry stream) in - let dropped = runEffect (dropDirectoryEntryStream stream) in + let entry = unsafePerformEffect (readDirectoryEntry stream) in + let dropped = unsafePerformEffect (dropDirectoryEntryStream stream) in case entry of Left _ -> 5 Right Nothing -> 6 - Right (Just _) -> let ignored = runEffect (log "stat-ok") in 0 + Right (Just _) -> let ignored = unsafePerformEffect (log "stat-ok") in 0 _ -> 7 _ -> 8 "#; diff --git a/crates/psrs-driver/src/tests/wasi/mod.rs b/crates/psrs-driver/src/tests/wasi/mod.rs index 8ad8306c..e1d4a21e 100644 --- a/crates/psrs-driver/src/tests/wasi/mod.rs +++ b/crates/psrs-driver/src/tests/wasi/mod.rs @@ -2,9 +2,8 @@ use super::*; mod do_notation; mod filesystem; -mod wat; +mod payloads; mod where_clause; -use wat::*; mod classes; mod coercion; @@ -14,7 +13,7 @@ mod wrappers; #[test] fn lowers_string_log_to_wasi_stdout() { - let source = "module Main where\nimport Prelude\nimport WASI.Console\nmain = let ignored = runEffect (log \"hello world\") in 0\n"; + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\nimport Prelude\nimport WASI.Console\nmain = let ignored = unsafePerformEffect (log \"hello world\") in 0\n"; let artifact = compile_source("Main.purs", source).unwrap(); assert!(artifact.wat.contains("wasi:cli/stdout@0.2.12")); assert!(artifact.wat.contains("wasi:io/streams@0.2.12")); @@ -65,7 +64,7 @@ fn runs_main_as_a_wasi_component_when_wasmtime_is_available() { #[test] fn prints_hello_world_when_wasmtime_is_available() { let Some(output) = run_with_wasmtime( - "module Main where\nimport Prelude\nimport WASI.Console\nmain = let ignored = runEffect (log \"hello world\") in 0\n", + "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\nimport Prelude\nimport WASI.Console\nmain = let ignored = unsafePerformEffect (log \"hello world\") in 0\n", ) else { eprintln!("skipping: wasmtime is not installed"); return; @@ -78,7 +77,7 @@ fn prints_hello_world_when_wasmtime_is_available() { fn prints_a_non_ascii_literal_when_wasmtime_is_available() { // `h`, `é` (U+00E9), `λ` (U+03BB): the GC UTF-16 string must be encoded as // UTF-8 at the canonical ABI boundary, not truncated to bytes. - let source = "module Main where\nimport Prelude\nimport WASI.Console\nmain = let ignored = runEffect (log \"h\u{e9}\u{3bb}\") in 0\n"; + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\nimport Prelude\nimport WASI.Console\nmain = let ignored = unsafePerformEffect (log \"h\u{e9}\u{3bb}\") in 0\n"; let Some(output) = run_with_wasmtime(source) else { eprintln!("skipping: wasmtime is not installed"); return; @@ -89,7 +88,7 @@ fn prints_a_non_ascii_literal_when_wasmtime_is_available() { #[test] fn prints_an_interned_literal_once_per_use_when_wasmtime_is_available() { - let source = "module Main where\nimport Prelude\nimport WASI.Console\nmain = let first = runEffect (log \"twice\") in let second = runEffect (log \"twice\") in 0\n"; + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\nimport Prelude\nimport WASI.Console\nmain = let first = unsafePerformEffect (log \"twice\") in let second = unsafePerformEffect (log \"twice\") in 0\n"; let Some(output) = run_with_wasmtime(source) else { eprintln!("skipping: wasmtime is not installed"); return; @@ -100,7 +99,7 @@ fn prints_an_interned_literal_once_per_use_when_wasmtime_is_available() { #[test] fn prints_an_empty_literal_when_wasmtime_is_available() { - let source = "module Main where\nimport Prelude\nimport WASI.Console\nmain = let ignored = runEffect (log \"\") in 0\n"; + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\nimport Prelude\nimport WASI.Console\nmain = let ignored = unsafePerformEffect (log \"\") in 0\n"; let Some(output) = run_with_wasmtime(source) else { eprintln!("skipping: wasmtime is not installed"); return; @@ -112,7 +111,7 @@ fn prints_an_empty_literal_when_wasmtime_is_available() { #[test] fn reads_the_monotonic_clock_when_wasmtime_is_available() { let Some(output) = run_with_wasmtime( - "module Main where\nimport Prelude\nimport WASI.Clock\nmain = (runEffect now) * 0\n", + "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\nimport Prelude\nimport WASI.Clock\nmain = (unsafePerformEffect now) * 0\n", ) else { eprintln!("skipping: wasmtime is not installed"); return; @@ -123,7 +122,7 @@ fn reads_the_monotonic_clock_when_wasmtime_is_available() { #[test] fn writes_to_stderr_when_wasmtime_is_available() { let Some(output) = run_with_wasmtime( - "module Main where\nimport Prelude\nimport WASI.Console\nmain = let x = runEffect (error \"oops\") in 7\n", + "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\nimport Prelude\nimport WASI.Console\nmain = let x = unsafePerformEffect (error \"oops\") in 7\n", ) else { eprintln!("skipping: wasmtime is not installed"); return; @@ -186,10 +185,10 @@ fn lowers_a_list_of_strings_to_an_array() { #[test] fn lowers_the_environment_arguments_wrapper_to_an_array() { - let source = "module Main where\n\ + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\n\ import Prelude\n\ import WASI.Process\n\ - main = arrayLength (runEffect arguments)\n"; + main = arrayLength (unsafePerformEffect arguments)\n"; let artifact = compile_source("Main.purs", source) .expect("WASI.Process.arguments should lower to an array"); assert!(artifact.wat.contains("wasi:cli/environment@0.2.12")); @@ -217,10 +216,10 @@ fn reads_environment_arguments_when_wasmtime_is_available() { // `get-arguments` returns the canonical list; the wrapper recovers // it into a GC `Array String`. The host supplies argv, whose first element // is the module path under Wasmtime. - let source = "module Main where\n\ + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\n\ import Prelude\n\ import WASI.Process\n\ - main = arrayLength (runEffect arguments)\n"; + main = arrayLength (unsafePerformEffect arguments)\n"; let Some(output) = run_with_wasmtime_args(source, &["alpha", "beta"]) else { eprintln!("skipping: wasmtime is not installed"); return; @@ -289,11 +288,11 @@ fn rejects_a_wasi_interface_outside_the_component_capability_profile() { #[test] fn reads_random_bytes_when_wasmtime_is_available() { - let source = "module Main where\n\ + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\n\ import Prelude\n\ import WASI.Console\n\ import WASI.Random\n\ - main = let noise = runEffect randomU64 in let bytes = runEffect (randomBytes 8) in let length = arrayLength bytes in noise * length * 0\n"; + main = let noise = unsafePerformEffect randomU64 in let bytes = unsafePerformEffect (randomBytes 8) in let length = arrayLength bytes in noise * length * 0\n"; let artifact = compile_source("Main.purs", source).expect("library randomBytes should lower"); assert!(artifact.wat.contains("wasi:random/random@0.2.12")); assert!(artifact.wat.contains("get-random-bytes")); @@ -308,63 +307,41 @@ fn reads_random_bytes_when_wasmtime_is_available() { assert!(output.stdout.is_empty(), "{output:?}"); } -/// `get-random-bytes` is not a direct call from the command entry in either -/// program: the import sits in the effect closure, and `runEffect` reaches it -/// with `call_ref`. A stored action must not `call_ref` from the entry. +/// Native construction is inert; interpreting a reusable action executes its +/// host request and continuation on each run, preserving the returned list. #[test] -fn stored_random_bytes_leaves_get_random_bytes_inside_the_effect_closure() { - let stored = "module Main where\n\ - import WASI.Random\n\ - main = let action = randomBytes 8 in 0\n"; - let forced = "module Main where\n\ - import Prelude\n\ - import WASI.Random\n\ - main = let value = runEffect (randomBytes 8) in 0\n"; - let stored_wat = compile_source("Main.purs", stored) - .expect("a stored randomBytes action should compile") - .wat; - let forced_wat = compile_source("Main.purs", forced) - .expect("runEffect (randomBytes 8) should compile") - .wat; - - let stored_core = core_main(&stored_wat); - let forced_core = core_main(&forced_wat); - let stored_import = imported_func(stored_core, "wasi:random/random@0.2.12", "get-random-bytes"); - let forced_import = imported_func(forced_core, "wasi:random/random@0.2.12", "get-random-bytes"); - let stored_entry = exported_func(stored_core, "wasi:cli/run@0.2.12#run"); - let forced_entry = exported_func(forced_core, "wasi:cli/run@0.2.12#run"); - let stored_funcs = core_functions(stored_core); - let forced_funcs = core_functions(forced_core); - - let stored_from_entry = reachable_by_call(&stored_funcs, stored_entry); - assert!( - !calls_import(&stored_funcs, &stored_from_entry, stored_import), - "the command entry must not call get-random-bytes when the action is not run" - ); - assert!( - !has_call_ref(&stored_funcs, &stored_from_entry), - "storing the action must not run the effect" - ); - assert!( - import_is_reached_only_from_a_closure(&stored_funcs, stored_entry, stored_import), - "get-random-bytes must be inside the effect closure" - ); - - let forced_from_entry = reachable_by_call(&forced_funcs, forced_entry); - assert!( - has_call_ref(&forced_funcs, &forced_from_entry), - "runEffect must invoke the closure from the command entry" - ); - assert!( - import_is_reached_only_from_a_closure(&forced_funcs, forced_entry, forced_import), - "runEffect still calls get-random-bytes from the closure, not the entry" - ); - - let Some(output) = run_with_wasmtime(stored) else { - eprintln!("skipping: wasmtime is not installed"); - return; - }; - assert_eq!(output.status.code(), Some(0), "{output:?}"); +fn stored_random_bytes_is_inert_and_repeated_runs_execute_the_continuation() { + let program = r#"module Main where +import Prelude +import Effect.Unsafe (unsafePerformEffect) +import WASI.Console (log) +import WASI.Random (randomBytes) + +action = do + bytes <- randomBytes 8 + log "bytes" + pure (arrayLength bytes) + +"#; + for (entry, expected_exit, expected_stdout) in [ + ("main = let stored = action in 0", 0, ""), + ( + "main = let first = unsafePerformEffect action in let second = unsafePerformEffect action in first + second", + 16, + "bytes\nbytes\n", + ), + ] { + let source = format!("{program}{entry}\n"); + let artifact = compile_source("Main.purs", &source).expect("random action should compile"); + assert!(artifact.wat.contains("get-random-bytes")); + let Some(output) = run_with_wasmtime(&source) else { + eprintln!("skipping: wasmtime is not installed"); + return; + }; + assert_eq!(output.status.code(), Some(expected_exit), "{output:?}"); + assert_eq!(output.stdout, expected_stdout.as_bytes(), "{output:?}"); + assert!(output.stderr.is_empty(), "{output:?}"); + } } #[test] @@ -386,10 +363,10 @@ fn rejects_an_import_of_unexported_write_stdout() { fn passes_a_byte_array_to_another_import() { // A WIT `list` is `Array Int`, so a byte array crosses the boundary as // uninterpreted integers rather than as text (DEC-16). - let source = "module Main where\n\ + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\n\ import Prelude\n\ import WASI.IO (getStdout, blockingWriteAndFlush)\n\ - main = let ignored = runEffect (bind getStdout (\\handle -> bind (blockingWriteAndFlush handle [104, 105]) (\\result -> pure 0))) in 0\n"; + main = let ignored = unsafePerformEffect (bind getStdout (\\handle -> bind (blockingWriteAndFlush handle [104, 105]) (\\result -> pure 0))) in 0\n"; let Some(output) = run_with_wasmtime(source) else { eprintln!("skipping: wasmtime is not installed"); return; @@ -427,59 +404,19 @@ fn rejects_an_import_of_unexported_exit_with_code_raw() { ); } -/// The library `exit-with-code` import sits in the effect closure, and -/// `runEffect` reaches it with `call_ref`. A stored action must not -/// `call_ref` from the entry. The synthesized command entry calls a separate -/// `exit-with-code` import with `main`'s code; `imported_func` names the -/// library import, which is emitted first. +/// Storing a native exit request permits normal completion; interpreting it +/// exits with the requested code before the following log can execute. #[test] -fn stored_exit_with_code_leaves_exit_with_code_inside_the_effect_closure() { - let stored = "module Main where\n\ - import WASI.Process\n\ - main = let action = exitWithCode 0 in 0\n"; - let forced = "module Main where\n\ - import Prelude\n\ - import WASI.Process\n\ - main = let value = runEffect (exitWithCode 0) in 0\n"; - let stored_wat = compile_source("Main.purs", stored) - .expect("a stored exitWithCode action should compile") - .wat; - let forced_wat = compile_source("Main.purs", forced) - .expect("runEffect (exitWithCode 0) should compile") - .wat; - - let stored_core = core_main(&stored_wat); - let forced_core = core_main(&forced_wat); - let stored_import = imported_func(stored_core, "wasi:cli/exit@0.2.12", "exit-with-code"); - let forced_import = imported_func(forced_core, "wasi:cli/exit@0.2.12", "exit-with-code"); - let stored_entry = exported_func(stored_core, "wasi:cli/run@0.2.12#run"); - let forced_entry = exported_func(forced_core, "wasi:cli/run@0.2.12#run"); - let stored_funcs = core_functions(stored_core); - let forced_funcs = core_functions(forced_core); - - let stored_from_entry = reachable_by_call(&stored_funcs, stored_entry); - assert!( - !calls_import(&stored_funcs, &stored_from_entry, stored_import), - "the command entry must not call the library exit-with-code when the action is not run" - ); - assert!( - !has_call_ref(&stored_funcs, &stored_from_entry), - "storing the action must not run the effect" - ); - assert!( - import_is_reached_only_from_a_closure(&stored_funcs, stored_entry, stored_import), - "exit-with-code must be inside the effect closure" - ); - - let forced_from_entry = reachable_by_call(&forced_funcs, forced_entry); - assert!( - has_call_ref(&forced_funcs, &forced_from_entry), - "runEffect must invoke the closure from the command entry" - ); - assert!( - import_is_reached_only_from_a_closure(&forced_funcs, forced_entry, forced_import), - "runEffect still calls exit-with-code from the closure, not the entry" - ); +fn stored_exit_is_inert_and_interpreted_exit_stops_the_program() { + let stored = + "module Main where\nimport WASI.Process\nmain = let action = exitWithCode 99 in 0\n"; + let forced = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\nimport Prelude\nimport WASI.Process\nimport WASI.Console (log)\nmain = let value = unsafePerformEffect (exitWithCode 99) in let after = unsafePerformEffect (log \"after exit\") in 0\n"; + for (source, expected_exit) in [(stored, 0), (forced, 99)] { + let output = run_with_wasmtime(source).expect("Wasmtime required for exit sequencing"); + assert_eq!(output.status.code(), Some(expected_exit), "{output:?}"); + assert!(output.stdout.is_empty()); + assert!(output.stderr.is_empty(), "{output:?}"); + } } #[test] diff --git a/crates/psrs-driver/src/tests/wasi/payloads.rs b/crates/psrs-driver/src/tests/wasi/payloads.rs new file mode 100644 index 00000000..125c803b --- /dev/null +++ b/crates/psrs-driver/src/tests/wasi/payloads.rs @@ -0,0 +1,33 @@ +use super::*; + +#[test] +fn wit_result_bytes_recover_values_and_empty_arrays_when_wasmtime_is_available() { + let source = r#"module Main where +import Effect.Unsafe (unsafePerformEffect) +import Prelude +import Data.Either (Either(..)) +import WASI.IO (getStdin, blockingRead) +main = + let stream = unsafePerformEffect getStdin in + let empty = unsafePerformEffect (blockingRead stream 0) in + case empty of + Left _ -> 1 + Right zero -> + let result = unsafePerformEffect (blockingRead stream 4) in + case result of + Left _ -> 2 + Right bytes -> + if arrayLength zero == 0 && arrayLength bytes == 4 + && arrayIndex bytes 0 == 0 && arrayIndex bytes 1 == 255 + && arrayIndex bytes 2 == 128 && arrayIndex bytes 3 == 42 + then 42 + else 3 +"#; + let Some(output) = run_with_wasmtime_stdin(source, &[0, 255, 128, 42, 99]) else { + eprintln!("skipping: wasmtime is not installed"); + return; + }; + assert_eq!(output.status.code(), Some(42), "{output:?}"); + assert!(output.stdout.is_empty(), "{output:?}"); + assert!(output.stderr.is_empty(), "{output:?}"); +} diff --git a/crates/psrs-driver/src/tests/wasi/umbrella.rs b/crates/psrs-driver/src/tests/wasi/umbrella.rs index c1901981..841b0a2f 100644 --- a/crates/psrs-driver/src/tests/wasi/umbrella.rs +++ b/crates/psrs-driver/src/tests/wasi/umbrella.rs @@ -4,10 +4,10 @@ use super::*; fn imports_the_wasi_umbrella_for_the_common_case() { // A program can `import WASI` and use the curated console, clock, and // random API without naming a focused module. - let source = "module Main where\n\ + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\n\ import Prelude\n\ import WASI\n\ - main = let ignored = runEffect (log \"umbrella\") in (runEffect now) * 0\n"; + main = let ignored = unsafePerformEffect (log \"umbrella\") in (unsafePerformEffect now) * 0\n"; let artifact = compile_source("Main.purs", source).expect("the umbrella should compile"); assert!(artifact.wat.contains("wasi:cli/stdout@0.2.12")); let Some(output) = run_with_wasmtime(source) else { @@ -22,10 +22,10 @@ fn imports_the_wasi_umbrella_for_the_common_case() { fn polls_a_resource_list_through_the_umbrella_when_wasmtime_is_available() { // `poll :: Array (Resource Pollable) -> Effect (Array Int)`: a resource // newtype as a non-byte list element must intern and lower. - let source = "module Main where\n\ + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\n\ import Prelude\n\ import WASI\n\ - main = let waiter = runEffect (subscribeDuration 0) in arrayLength (runEffect (poll [waiter])) * 0\n"; + main = let waiter = unsafePerformEffect (subscribeDuration 0) in arrayLength (unsafePerformEffect (poll [waiter])) * 0\n"; let artifact = compile_source("Main.purs", source).expect("a Resource list element should lower"); assert!(artifact.wat.contains("wasi:io/poll@0.2.12")); diff --git a/crates/psrs-driver/src/tests/wasi/wat.rs b/crates/psrs-driver/src/tests/wasi/wat.rs deleted file mode 100644 index 766df179..00000000 --- a/crates/psrs-driver/src/tests/wasi/wat.rs +++ /dev/null @@ -1,190 +0,0 @@ -pub(super) fn core_main(wat: &str) -> &str { - let start = wat - .find("(core module $main") - .expect("the component should contain the core module"); - let rest = &wat[start..]; - let mut depth = 0; - for (index, character) in rest.char_indices() { - match character { - '(' => depth += 1, - ')' => { - depth -= 1; - if depth == 0 { - return &rest[..=index]; - } - } - _ => {} - } - } - panic!("the core module is unclosed"); -} - -pub(super) struct CoreFunc { - index: u32, - body: String, -} - -pub(super) fn core_functions(core: &str) -> Vec { - let bytes = core.as_bytes(); - let mut functions = Vec::new(); - let mut depth = 0; - let mut index = 0; - while index < bytes.len() { - if depth == 1 && core[index..].starts_with("(func (;") { - let number_start = index + "(func (;".len(); - let number_end = core[number_start..] - .find(";)") - .expect("a core func should carry an index") - + number_start; - let func_index = core[number_start..number_end] - .parse() - .expect("a core func index should be a decimal"); - let mut inner = 0; - let mut end = index; - let mut cursor = index; - while cursor < bytes.len() { - match bytes[cursor] { - b'(' => inner += 1, - b')' => { - inner -= 1; - if inner == 0 { - end = cursor + 1; - break; - } - } - _ => {} - } - cursor += 1; - } - functions.push(CoreFunc { - index: func_index, - body: core[index..end].to_string(), - }); - index = end; - continue; - } - match bytes[index] { - b'(' => depth += 1, - b')' => depth -= 1, - _ => {} - } - index += 1; - } - functions -} - -pub(super) fn imported_func(core: &str, module: &str, name: &str) -> u32 { - let needle = format!(r#"(import "{module}" "{name}" (func (;"#); - let at = core - .find(&needle) - .unwrap_or_else(|| panic!("missing import {module}#{name}")); - let after = &core[at + needle.len()..]; - let end = after.find(";)").expect("an import func index"); - after[..end] - .parse() - .expect("an import func index should be a decimal") -} - -pub(super) fn exported_func(core: &str, name: &str) -> u32 { - let needle = format!(r#"(export "{name}" (func "#); - let at = core - .find(&needle) - .unwrap_or_else(|| panic!("missing export {name}")); - let after = &core[at + needle.len()..]; - let end = after.find(')').expect("an export func index"); - after[..end] - .trim() - .parse() - .expect("an export func index should be a decimal") -} - -fn direct_calls(body: &str) -> Vec { - let mut calls = Vec::new(); - let mut rest = body; - while let Some(at) = rest.find("call ") { - let boundary = at == 0 || !rest.as_bytes()[at - 1].is_ascii_alphanumeric(); - let after = &rest[at + "call ".len()..]; - let digits = after - .chars() - .take_while(|character| character.is_ascii_digit()) - .collect::(); - if boundary && !digits.is_empty() { - calls.push(digits.parse().expect("call operand")); - } - rest = &rest[at + "call ".len()..]; - } - calls -} - -fn ref_func_targets(body: &str) -> Vec { - let mut targets = Vec::new(); - let mut rest = body; - while let Some(at) = rest.find("ref.func ") { - let after = &rest[at + "ref.func ".len()..]; - let digits = after - .chars() - .take_while(|character| character.is_ascii_digit()) - .collect::(); - if !digits.is_empty() { - targets.push(digits.parse().expect("ref.func operand")); - } - rest = &rest[at + "ref.func ".len()..]; - } - targets -} - -pub(super) fn reachable_by_call( - functions: &[CoreFunc], - entry: u32, -) -> std::collections::HashSet { - let mut seen = std::collections::HashSet::new(); - let mut stack = vec![entry]; - while let Some(current) = stack.pop() { - if !seen.insert(current) { - continue; - } - let Some(function) = functions.iter().find(|function| function.index == current) else { - continue; - }; - stack.extend(direct_calls(&function.body)); - } - seen -} - -pub(super) fn calls_import( - functions: &[CoreFunc], - reached: &std::collections::HashSet, - import: u32, -) -> bool { - functions.iter().any(|function| { - reached.contains(&function.index) && direct_calls(&function.body).contains(&import) - }) -} - -pub(super) fn has_call_ref( - functions: &[CoreFunc], - reached: &std::collections::HashSet, -) -> bool { - functions - .iter() - .any(|function| reached.contains(&function.index) && function.body.contains("call_ref ")) -} - -/// The import call is not in the command entry's direct-call tree. It is in a -/// function reached from `ref.func`, which is the effect closure. -pub(super) fn import_is_reached_only_from_a_closure( - functions: &[CoreFunc], - entry: u32, - import: u32, -) -> bool { - if calls_import(functions, &reachable_by_call(functions, entry), import) { - return false; - } - let mut from_closures = std::collections::HashSet::new(); - for function in functions { - for target in ref_func_targets(&function.body) { - from_closures.extend(reachable_by_call(functions, target)); - } - } - calls_import(functions, &from_closures, import) -} diff --git a/crates/psrs-driver/src/tests/wasi/where_clause.rs b/crates/psrs-driver/src/tests/wasi/where_clause.rs index 6fec155d..7de2ff69 100644 --- a/crates/psrs-driver/src/tests/wasi/where_clause.rs +++ b/crates/psrs-driver/src/tests/wasi/where_clause.rs @@ -7,6 +7,7 @@ fn runs_a_program_with_where_clauses_when_wasmtime_is_available() { // The effect block logs and then returns `describe 39 + fromJust (Just 0) 0 // = 42` as the entry's exit code. let source = r#"module Main where +import Effect.Unsafe (unsafePerformEffect) import Prelude import WASI.Console @@ -23,7 +24,7 @@ fromJust m fallback = case m of result = value + 1 Nothing -> fallback -main = runEffect +main = unsafePerformEffect do log "where clauses" pure (describe 39 + fromJust (Just 0) 0) diff --git a/crates/psrs-driver/src/tests/wasi/wrappers.rs b/crates/psrs-driver/src/tests/wasi/wrappers.rs index 4915c115..460143c7 100644 --- a/crates/psrs-driver/src/tests/wasi/wrappers.rs +++ b/crates/psrs-driver/src/tests/wasi/wrappers.rs @@ -2,10 +2,10 @@ use super::*; #[test] fn reads_the_wall_clock_when_wasmtime_is_available() { - let source = "module Main where\n\ + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\n\ import Prelude\n\ import WASI.Clock\n\ - main = let time = runEffect wallNow in time.seconds * 0\n"; + main = let time = unsafePerformEffect wallNow in time.seconds * 0\n"; let artifact = compile_source("Main.purs", source).expect("the wall-clock wrapper should lower"); assert!(artifact.wat.contains("wasi:clocks/wall-clock@0.2.12")); @@ -18,10 +18,10 @@ fn reads_the_wall_clock_when_wasmtime_is_available() { #[test] fn reads_the_wall_clock_resolution_when_wasmtime_is_available() { - let source = "module Main where\n\ + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\n\ import Prelude\n\ import WASI.Clock\n\ - main = let tick = runEffect wallResolution in tick.nanoseconds * 0\n"; + main = let tick = unsafePerformEffect wallResolution in tick.nanoseconds * 0\n"; let artifact = compile_source("Main.purs", source) .expect("the wall-clock resolution wrapper should lower"); assert!(artifact.wat.contains("resolution")); @@ -34,10 +34,10 @@ fn reads_the_wall_clock_resolution_when_wasmtime_is_available() { #[test] fn reads_the_insecure_seed_when_wasmtime_is_available() { - let source = "module Main where\n\ + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\n\ import Prelude\n\ import WASI.Random\n\ - main = let seed = runEffect insecureSeed in seed._1 * 0\n"; + main = let seed = unsafePerformEffect insecureSeed in seed._1 * 0\n"; let artifact = compile_source("Main.purs", source).expect("the insecure seed wrapper should lower"); assert!(artifact.wat.contains("wasi:random/insecure-seed@0.2.12")); @@ -50,10 +50,10 @@ fn reads_the_insecure_seed_when_wasmtime_is_available() { #[test] fn reads_insecure_random_values_when_wasmtime_is_available() { - let source = "module Main where\n\ + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\n\ import Prelude\n\ import WASI.Random\n\ - main = let noise = runEffect insecureU64 in noise * 0\n"; + main = let noise = unsafePerformEffect insecureU64 in noise * 0\n"; let artifact = compile_source("Main.purs", source).expect("the insecure random wrappers should lower"); assert!(artifact.wat.contains("wasi:random/insecure@0.2.12")); @@ -66,11 +66,11 @@ fn reads_insecure_random_values_when_wasmtime_is_available() { #[test] fn polls_a_clock_subscription_when_wasmtime_is_available() { - let source = "module Main where\n\ + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\n\ import Prelude\n\ import WASI.Clock (subscribeDuration)\n\ import WASI.IO\n\ - main = let waiter = runEffect (subscribeDuration 0) in arrayLength (runEffect (poll [waiter])) * 0\n"; + main = let waiter = unsafePerformEffect (subscribeDuration 0) in arrayLength (unsafePerformEffect (poll [waiter])) * 0\n"; let artifact = compile_source("Main.purs", source).expect("the poll wrapper should lower"); assert!(artifact.wat.contains("wasi:io/poll@0.2.12")); let Some(output) = run_with_wasmtime(source) else { @@ -82,11 +82,11 @@ fn polls_a_clock_subscription_when_wasmtime_is_available() { #[test] fn reads_a_pollable_readiness_when_wasmtime_is_available() { - let source = "module Main where\n\ + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\n\ import Prelude\n\ import WASI.Clock (subscribeDuration)\n\ import WASI.IO (ready, block)\n\ - main = let waiter = runEffect (subscribeDuration 0) in let ignored = runEffect (block waiter) in if runEffect (ready waiter) then 0 else 1\n"; + main = let waiter = unsafePerformEffect (subscribeDuration 0) in let ignored = unsafePerformEffect (block waiter) in if unsafePerformEffect (ready waiter) then 0 else 1\n"; let Some(output) = run_with_wasmtime(source) else { eprintln!("skipping: wasmtime is not installed"); return; @@ -96,10 +96,10 @@ fn reads_a_pollable_readiness_when_wasmtime_is_available() { #[test] fn writes_to_a_stream_when_wasmtime_is_available() { - let source = "module Main where\n\ + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\n\ import Prelude\n\ import WASI.IO\n\ - main = let out = runEffect getStdout in let ignored = runEffect (blockingWriteAndFlush out (stringToBytes \"streamed\")) in 0\n"; + main = let out = unsafePerformEffect getStdout in let ignored = unsafePerformEffect (blockingWriteAndFlush out (stringToBytes \"streamed\")) in 0\n"; let artifact = compile_source("Main.purs", source).expect("the stream write wrapper should lower"); assert!(artifact.wat.contains("wasi:cli/stdout@0.2.12")); @@ -113,10 +113,10 @@ fn writes_to_a_stream_when_wasmtime_is_available() { #[test] fn gets_stdin_when_wasmtime_is_available() { - let source = "module Main where\n\ + let source = "module Main where\nimport Effect.Unsafe (unsafePerformEffect)\n\ import Prelude\n\ import WASI.IO\n\ - main = let input = runEffect getStdin in 0\n"; + main = let input = unsafePerformEffect getStdin in 0\n"; let artifact = compile_source("Main.purs", source).expect("the stdin wrapper should lower"); assert!(artifact.wat.contains("wasi:cli/stdin@0.2.12")); let Some(output) = run_with_wasmtime(source) else { @@ -136,9 +136,8 @@ fn lowers_the_error_debug_string_wrapper() { } /// A module that declares a host `foreign import` must have an explicit export -/// list. Representation lowering suspends the host call inside the effect -/// closure, so a name on that list is the public operation. A name left off -/// the list stays private. +/// list. Raw canonical imports stay private; exported wrappers use Host.lift +/// to delay the call until native interpretation. #[test] fn stdlib_export_lists_do_not_expose_raw_foreign_imports() { let sources = crate::prelude::sources().expect("the standard library should load"); @@ -240,10 +239,11 @@ fn raw_foreign_value_names(text: &str) -> Vec { let Some(close) = rest.find('"') else { continue; }; - // `psrs:effect` names the abstract effect operations (`pure`, `bind`, - // `runEffect`). Those are the public library interface, not a private - // host import hidden behind a wrapper. - if rest[..close].starts_with("psrs:effect#") { + // `psrs:` names compiler bindings (`psrs:intrinsic` checked primitives, + // `psrs:runtime-*` runtime providers). Those are the public library + // interface, not a private host import hidden behind a wrapper; only + // `wasi:` bindings name host imports. + if rest[..close].starts_with("psrs:") { continue; } let after = rest[close + 1..].trim_start(); @@ -261,10 +261,11 @@ fn lowers_a_mapped_result_with_a_variant_payload() { // whose case field is a resource handle, so the decode has to build the // nested source value before erasing it into the `Either` case field. let source = r#"module Main where +import Effect.Unsafe (unsafePerformEffect) import Prelude import Data.Either (Either(..)) import WASI.IO (getStdin, blockingRead, StreamError(..)) -main = case runEffect (blockingRead (runEffect getStdin) 1) of +main = case unsafePerformEffect (blockingRead (unsafePerformEffect getStdin) 1) of Right bytes -> 0 Left (LastOperationFailed _) -> 1 Left Closed -> 2 @@ -295,10 +296,11 @@ fn lowers_an_opaque_handle_resource_drop() { // The synthesized `[resource-drop]` intrinsic takes the opaque handle type, // so a source declaration may name it rather than a bare `Int`. let source = r#"module Main where +import Effect.Unsafe (unsafePerformEffect) import Prelude import WASI.IO (getStdin) import WASI.IO (dropInputStream) -main = let ignored = runEffect (dropInputStream (runEffect getStdin)) in 0 +main = let ignored = unsafePerformEffect (dropInputStream (unsafePerformEffect getStdin)) in 0 "#; let artifact = compile_source("Main.purs", source) .expect("an explicit drop of the opaque input-stream handle should lower"); @@ -308,15 +310,16 @@ main = let ignored = runEffect (dropInputStream (runEffect getStdin)) in 0 #[test] fn reads_stdin_with_blocking_read_when_wasmtime_is_available() { let source = r#"module Main where +import Effect.Unsafe (unsafePerformEffect) import Prelude import Data.Either (Either(..)) import WASI.Console (log) import WASI.IO (getStdin) import WASI.IO (blockingRead, StreamError(..)) main = - let result = runEffect (blockingRead (runEffect getStdin) 5) in + let result = unsafePerformEffect (blockingRead (unsafePerformEffect getStdin) 5) in case result of - Right text -> let ignored = runEffect (log (bytesToString text)) in 0 + Right text -> let ignored = unsafePerformEffect (log (bytesToString text)) in 0 Left (LastOperationFailed _) -> 1 Left Closed -> 2 "#; @@ -336,35 +339,36 @@ fn lowers_the_filesystem_wrapper_surface() { // parameters, a `result` result, an owned handle result, // and stream handles. let source = r#"module Main where +import Effect.Unsafe (unsafePerformEffect) import Prelude import Data.Either (Either(..)) import WASI.FileSystem main = - let dirs = runEffect preopens in + let dirs = unsafePerformEffect preopens in let dir = arrayIndex dirs 0 in let d = dir._1 in - let opened = runEffect (openRead d "a.txt") in - let wrote = runEffect (writeFile d (stringToBytes "x") 0) in - let bytes = runEffect (readFile d 1 0) in - let hash = runEffect (metadataHash d) in - let kind = runEffect (getType d) in - let flags = runEffect (getFlags d) in - let target = runEffect (readlinkAt d "a") in - let same = runEffect (isSameObject d d) in - let stats = runEffect (stat d) in - let statsAt = runEffect (statAt d { symlinkFollow: true } "a") in - let entries = runEffect (readDirectory d) in + let opened = unsafePerformEffect (openRead d "a.txt") in + let wrote = unsafePerformEffect (writeFile d (stringToBytes "x") 0) in + let bytes = unsafePerformEffect (readFile d 1 0) in + let hash = unsafePerformEffect (metadataHash d) in + let kind = unsafePerformEffect (getType d) in + let flags = unsafePerformEffect (getFlags d) in + let target = unsafePerformEffect (readlinkAt d "a") in + let same = unsafePerformEffect (isSameObject d d) in + let stats = unsafePerformEffect (stat d) in + let statsAt = unsafePerformEffect (statAt d { symlinkFollow: true } "a") in + let entries = unsafePerformEffect (readDirectory d) in let readEntry = case entries of Left _ -> 1 Right stream -> - let entry = runEffect (readDirectoryEntry stream) in - let dropped = runEffect (dropDirectoryEntryStream stream) in + let entry = unsafePerformEffect (readDirectoryEntry stream) in + let dropped = unsafePerformEffect (dropDirectoryEntryStream stream) in 0 in - let ignoredTimes = runEffect (setTimes d NoChange NoChange) in - let ignoredTimesAt = runEffect (setTimesAt d { symlinkFollow: true } "a" NoChange NoChange) in - let bracketed = runEffect (withDescriptor d (\handle -> getType handle)) in - let ignored = runEffect (setSize d 0) in + let ignoredTimes = unsafePerformEffect (setTimes d NoChange NoChange) in + let ignoredTimesAt = unsafePerformEffect (setTimesAt d { symlinkFollow: true } "a" NoChange NoChange) in + let bracketed = unsafePerformEffect (withDescriptor d (\handle -> getType handle)) in + let ignored = unsafePerformEffect (setSize d 0) in 0 "#; let artifact = @@ -376,6 +380,7 @@ main = #[test] fn lowers_the_sockets_wrapper_surface() { let source = r#"module Main where +import Effect.Unsafe (unsafePerformEffect) import Prelude import Data.Either (Either(..)) import Data.Maybe (Maybe(..)) @@ -383,26 +388,26 @@ import WASI.Network addr :: IpSocketAddress addr = IpV4SocketAddress { port: 0, address: { _1: 0, _2: 0, _3: 0, _4: 0 } } main = - let network = runEffect instanceNetwork in - let created = runEffect (createTcpSocket Ipv4) in + let network = unsafePerformEffect instanceNetwork in + let created = unsafePerformEffect (createTcpSocket Ipv4) in let tcp = case created of Right socket -> - let ignored = runEffect (tcpStartBind socket network addr) in - let ignoredFinish = runEffect (tcpFinishBind socket) in - let family = runEffect (tcpAddressFamily socket) in - let local = runEffect (tcpLocalAddress socket) in - let remote = runEffect (tcpRemoteAddress socket) in - let dropped = runEffect (dropTcpSocket socket) in + let ignored = unsafePerformEffect (tcpStartBind socket network addr) in + let ignoredFinish = unsafePerformEffect (tcpFinishBind socket) in + let family = unsafePerformEffect (tcpAddressFamily socket) in + let local = unsafePerformEffect (tcpLocalAddress socket) in + let remote = unsafePerformEffect (tcpRemoteAddress socket) in + let dropped = unsafePerformEffect (dropTcpSocket socket) in 0 Left _ -> 1 in - let createdUdp = runEffect (createUdpSocket Ipv4) in + let createdUdp = unsafePerformEffect (createUdpSocket Ipv4) in let udp = case createdUdp of Right socket -> - let local = runEffect (udpLocalAddress socket) in - let remote = runEffect (udpRemoteAddress socket) in - let streams = runEffect (udpSocketStream socket Nothing) in - let dropped = runEffect (dropUdpSocket socket) in + let local = unsafePerformEffect (udpLocalAddress socket) in + let remote = unsafePerformEffect (udpRemoteAddress socket) in + let streams = unsafePerformEffect (udpSocketStream socket Nothing) in + let dropped = unsafePerformEffect (dropUdpSocket socket) in 0 Left _ -> 1 in @@ -455,10 +460,11 @@ fn rejects_a_raw_sockets_import_from_the_library() { #[test] fn drops_a_stream_handle_when_wasmtime_is_available() { let source = r#"module Main where +import Effect.Unsafe (unsafePerformEffect) import Prelude import WASI.IO (getStdin) import WASI.IO (dropInputStream) -main = let ignored = runEffect (dropInputStream (runEffect getStdin)) in 7 +main = let ignored = unsafePerformEffect (dropInputStream (unsafePerformEffect getStdin)) in 7 "#; let artifact = compile_source("Main.purs", source).expect("the drop wrapper should lower"); assert!(artifact.wat.contains("[resource-drop]input-stream")); diff --git a/crates/psrs-driver/tests/control_carrier_runtime.rs b/crates/psrs-driver/tests/control_carrier_runtime.rs new file mode 100644 index 00000000..8530573d --- /dev/null +++ b/crates/psrs-driver/tests/control_carrier_runtime.rs @@ -0,0 +1,11 @@ +//! Nominal carriers compose with ordinary polymorphic and aggregate storage. +mod control_support; + +#[test] +fn control_and_owner_cross_generic_storage_without_changing_identity() { + control_support::execute( + "nominal-carriers", + include_str!("control_support/nominal_carriers.purs"), + 42, + ); +} diff --git a/crates/psrs-driver/tests/control_lifetime.rs b/crates/psrs-driver/tests/control_lifetime.rs new file mode 100644 index 00000000..cc820699 --- /dev/null +++ b/crates/psrs-driver/tests/control_lifetime.rs @@ -0,0 +1,109 @@ +//! Delimiter lifetime checks through escaped, ordinarily typed resumptions. +mod control_support; +use control_support::execute; + +const ESCAPED: &str = r#"module Main where +import Prim.Control as C +data Op next = Op (Int -> next) +handler :: forall x. { saved :: Int -> C.Control Op Int } -> Op x -> (x -> C.Control Op Int) -> { _1 :: { saved :: Int -> C.Control Op Int }, _2 :: C.Control Op Int } +handler s (Op k) cont = { _1: { saved: \n -> cont (k n) }, _2: C.return 42 } +result = C.run { saved: \n -> C.return n } handler (C.perform (Op \x -> x)) +main :: Int +main = (C.run result._1 handler (result._1.saved 42))._2 +"#; + +#[test] +fn rejects_a_resumption_after_its_closed_delimiter_returns() { + execute("expired-closed", ESCAPED, 134); +} + +#[test] +fn rejects_a_resume_node_built_before_the_delimiter_expired() { + execute( + "expired-prebuilt", + r#"module Main where +import Prim.Control as C +data Op next = Op (Int -> next) +handler :: forall x. { saved :: C.Control Op Int } -> Op x -> (x -> C.Control Op Int) -> { _1 :: { saved :: C.Control Op Int }, _2 :: C.Control Op Int } +handler s (Op k) cont = { _1: { saved: cont (k 42) }, _2: C.return 0 } +result = C.run { saved: C.return 0 } handler (C.perform (Op \x -> x)) +main :: Int +main = (C.run result._1 handler result._1.saved)._2 +"#, + 134, + ); +} + +#[test] +fn a_live_multi_shot_resumption_can_be_interpreted_reentrantly() { + execute( + "live-reentrant", + r#"module Main where +import Prim.Control as C +data Op next = Op (Int -> next) +handler :: forall x. Int -> Op x -> (x -> C.Control Op Int) -> { _1 :: Int, _2 :: C.Control Op Int } +handler s (Op k) cont = let nested = C.run s handler (cont (k 42)) in { _1: nested._1, _2: C.return nested._2 } +main :: Int +main = (C.run 0 handler (C.perform (Op \x -> x)))._2 +"#, + 42, + ); +} + +const OPEN: &str = r#"module Main where +import Prim.Control as C +data F next = F (Int -> next) +data G next = G (Int -> next) +data U next = UF (F next) | UG (G next) +type Saved = { saved :: Int -> C.Control U Int } +project :: forall x r. U x -> (F x -> r) -> (G x -> r) -> r +project u l r = case u of + UF f -> l f + UG g -> r g +inner :: forall x. Saved -> F x -> (x -> C.Control U Int) -> { _1 :: Saved, _2 :: C.Control U Int } +inner s (F k) cont = { _1: { saved: \n -> cont (k n) }, _2: C.return 42 } +outer :: forall x. Int -> G x -> (x -> C.Control G { _1 :: Saved, _2 :: Int }) -> { _1 :: Int, _2 :: C.Control G { _1 :: Saved, _2 :: Int } } +outer s (G k) cont = { _1: s, _2: cont (k 0) } +handled = foldOpen { saved: \n -> C.return n } inner project UG (C.perform (UF (F \x -> x))) +result = C.run 0 outer handled +main :: Int +main = result._2._2 +"#; + +#[test] +fn rejects_a_completed_open_delimiter_while_its_outer_delimiter_is_live() { + let text = OPEN.replace("result = C.run 0 outer handled", r"result = C.run 0 outer (C.bind handled (\pair -> foldOpen pair._1 inner project UG (pair._1.saved 42)))"); + execute("expired-open-live-outer", &text, 134); +} + +#[test] +fn rejects_an_open_resumption_after_the_outer_run_returns() { + let text = OPEN.replace("main = result._2._2", "main = (C.run 0 outer (foldOpen result._2._1 inner project UG (result._2._1.saved 42)))._2._2"); + execute("expired-open", &text, 134); +} + +#[test] +fn outer_abandonment_expires_a_suspended_inner_delimiter() { + execute( + "expired-abandoned-open", + r#"module Main where +import Prim.Control as C +data F x = F (Int -> x) +data G x = G (Int -> C.Control U Int) (Int -> x) +data U x = UF (F x) | UG (G x) +type Saved = { saved :: Int -> C.Control U Int } +project :: forall x r. U x -> (F x -> r) -> (G x -> r) -> r +project u l r = case u of + UF f -> l f + UG g -> r g +inner :: forall x. Int -> F x -> (x -> C.Control U Int) -> { _1 :: Int, _2 :: C.Control U Int } +inner s (F k) cont = { _1: s, _2: C.perform (UG (G (\n -> cont (k n)) (\x -> x))) } +outer :: forall x. Saved -> G x -> (x -> C.Control G { _1 :: Int, _2 :: Int }) -> { _1 :: Saved, _2 :: C.Control G { _1 :: Int, _2 :: Int } } +outer s (G saved k) cont = { _1: { saved: saved }, _2: C.return { _1: 0, _2: 42 } } +result = C.run { saved: \n -> C.return n } outer (foldOpen 0 inner project UG (C.perform (UF (F \x -> x)))) +main :: Int +main = (C.run result._1 outer (foldOpen 0 inner project UG (result._1.saved 42)))._2._2 +"#, + 134, + ); +} diff --git a/crates/psrs-driver/tests/control_open_runtime.rs b/crates/psrs-driver/tests/control_open_runtime.rs new file mode 100644 index 00000000..db83f1a7 --- /dev/null +++ b/crates/psrs-driver/tests/control_open_runtime.rs @@ -0,0 +1,306 @@ +//! Value-sensitive execution of open elimination and persistent forwarding. + +mod control_support; +use control_support::execute; + +fn source(program: &str, inner: &str, outer: &str, result: &str) -> String { + format!( + r#"module Main where +import Prim.Control as C +data F next = F (Int -> next) +data G next = G (Int -> next) +data U next = UF (F next) | UG (G next) +project :: forall x r. U x -> (F x -> r) -> (G x -> r) -> r +project u onF onG = case u of + UF f -> onF f + UG g -> onG g +inject :: forall x. G x -> U x +inject = UG +inner :: forall x. Int -> F x -> (x -> C.Control U Int) -> {{ _1 :: Int, _2 :: C.Control U Int }} +inner s (F k) cont = {inner} +outer :: forall x. Int -> G x -> (x -> C.Control G {{ _1 :: Int, _2 :: Int }}) -> {{ _1 :: Int, _2 :: C.Control G {{ _1 :: Int, _2 :: Int }} }} +outer s (G k) cont = {outer} +program :: C.Control U Int +program = {program} +handled :: C.Control G {{ _1 :: Int, _2 :: Int }} +handled = foldOpen 0 inner project inject program +result :: {{ _1 :: Int, _2 :: {{ _1 :: Int, _2 :: Int }} }} +result = C.run 0 outer handled +main :: Int +main = {result} +"# + ) +} + +const INNER: &str = "{ _1: intAdd s 1, _2: cont (k s) }"; +const OUTER: &str = "{ _1: intAdd s 1, _2: cont (k 40) }"; +const MIXED: &str = r"C.bind (C.perform (UF (F \x -> x))) (\a -> C.bind (C.perform (UG (G \x -> x))) (\b -> C.bind (C.perform (UF (F \x -> x))) (\c -> C.return (intAdd a (intAdd b c)))))"; + +#[test] +fn handles_and_forwards_then_reinstalls_the_inner_delimiter() { + execute( + "open-mixed", + &source( + MIXED, + INNER, + OUTER, + "if intEq result._1 1 then if intEq result._2._1 2 then intAdd result._2._2 1 else 1 else 1", + ), + 42, + ); +} + +#[test] +fn constructing_an_open_handler_is_inert() { + let text = source(MIXED, "{ _1: intQuot 1 0, _2: cont (k s) }", OUTER, "42") + .replace("main = 42", "main = let unused = handled in 42"); + execute("open-inert", &text, 42); +} + +#[test] +fn open_handler_can_abandon_its_remainder() { + execute( + "open-abandon", + &source( + r"C.bind (C.perform (UF (F \x -> x))) (\_ -> C.return (intQuot 1 0))", + "{ _1: 7, _2: C.return 42 }", + OUTER, + "if intEq result._1 0 then if intEq result._2._1 7 then result._2._2 else 1 else 1", + ), + 42, + ); +} + +#[test] +fn residual_handler_can_abandon_the_inner_delimiter() { + execute( + "residual-abandon", + &source( + r"C.bind (C.perform (UG (G \x -> x))) (\_ -> C.return (intQuot 1 0))", + INNER, + "{ _1: 7, _2: C.return { _1: 0, _2: 42 } }", + "if intEq result._1 7 then result._2._2 else 1", + ), + 42, + ); +} + +#[test] +fn residual_replay_preserves_each_branchs_inner_state() { + execute( + "residual-replay", + &source( + MIXED, + INNER, + r"{ _1: intAdd s 1, _2: C.bind (cont (k 19)) (\a -> C.bind (cont (k 20)) (\b -> C.return { _1: intAdd a._1 b._1, _2: intAdd a._2 b._2 })) }", + "if intEq result._1 1 then if intEq result._2._1 4 then intAdd result._2._2 1 else 1 else 1", + ), + 42, + ); +} + +#[test] +fn open_handler_can_replay_its_saved_sequence() { + execute( + "open-replay", + &source( + r"C.bind (C.perform (UF (F \x -> x))) (\a -> C.return (intAdd a 1))", + r"{ _1: intAdd s 1, _2: C.bind (cont (k 19)) (\a -> C.bind (cont (k 21)) (\b -> C.return (intAdd a b))) }", + OUTER, + "if intEq result._2._1 1 then result._2._2 else 1", + ), + 42, + ); +} + +#[test] +fn forwarded_remainders_finish_before_the_outer_bind() { + let text = source(MIXED, INNER, OUTER, "result._2._2").replace( + "C.run 0 outer handled", + "C.run 0 outer (C.bind handled (\\a -> C.return { _1: a._1, _2: intAdd a._2 1 }))", + ); + execute("forward-order", &text, 42); +} + +#[test] +fn clause_requests_are_interpreted_with_updated_inner_state() { + execute( + "open-clause-request", + &source( + r"C.perform (UF (F \x -> x))", + r"if intEq s 0 then { _1: 1, _2: C.bind (C.perform (UG (G \x -> x))) (\a -> cont (k (intAdd a 2))) } else { _1: s, _2: cont (k s) }", + OUTER, + "if intEq result._2._1 1 then result._2._2 else 1", + ), + 42, + ); +} + +#[test] +fn long_forwarded_request_sequences_are_stack_safe() { + let mut text = source( + "build 100000", + INNER, + OUTER, + "if intEq result._1 100000 then if intEq result._2._1 100000 then result._2._2 else 1 else 1", + ); + text.push_str(r" +build :: Int -> C.Control U Int +build n = if intEq n 0 then C.return 42 else C.bind (C.perform (UF (F \x -> x))) (\_ -> C.bind (C.perform (UG (G \x -> x))) (\_ -> build (intSub n 1)))"); + execute("long-forward", &text, 42); +} + +#[test] +fn repeated_interpretation_preserves_open_nodes() { + execute( + "open-repeat", + &source( + MIXED, + INNER, + OUTER, + "let other = C.run 0 outer handled in intAdd result._2._2 (intSub other._2._2 40)", + ), + 42, + ); +} + +#[test] +fn nested_open_handlers_remove_distinct_effects_in_order() { + execute( + "open-nested", + r#"module Main where +import Prim.Control as C +data F x = F (Int -> x) +data G x = G (Int -> x) +data H x = H (Int -> x) +data U x = UF (F x) | UG (G x) | UH (H x) +data V x = VG (G x) | VH (H x) +projectF :: forall x r. U x -> (F x -> r) -> (V x -> r) -> r +projectF u l r = case u of + UF f -> l f + UG g -> r (VG g) + UH h -> r (VH h) +injectF :: forall x. V x -> U x +injectF v = case v of + VG g -> UG g + VH h -> UH h +projectG :: forall x r. V x -> (G x -> r) -> (H x -> r) -> r +projectG v l r = case v of + VG g -> l g + VH h -> r h +handlerF :: forall x. Int -> F x -> (x -> C.Control U Int) -> { _1 :: Int, _2 :: C.Control U Int } +handlerF s (F k) cont = { _1: intAdd s 1, _2: cont (k s) } +handlerG :: forall x. Int -> G x -> (x -> C.Control V { _1 :: Int, _2 :: Int }) -> { _1 :: Int, _2 :: C.Control V { _1 :: Int, _2 :: Int } } +handlerG s (G k) cont = { _1: intAdd s 1, _2: cont (k s) } +handlerH :: forall x. Int -> H x -> (x -> C.Control H { _1 :: Int, _2 :: { _1 :: Int, _2 :: Int } }) -> { _1 :: Int, _2 :: C.Control H { _1 :: Int, _2 :: { _1 :: Int, _2 :: Int } } } +handlerH s (H k) cont = { _1: intAdd s 1, _2: cont (k s) } +program :: C.Control U Int +program = C.bind (C.perform (UF (F \x -> x))) (\a -> C.bind (C.perform (UG (G \x -> x))) (\b -> C.bind (C.perform (UH (H \x -> x))) (\c -> C.bind (C.perform (UF (F \x -> x))) (\d -> C.return (intAdd a (intAdd b (intAdd c d))))))) +result = C.run 30 handlerH (foldOpen 10 handlerG projectG VH (foldOpen 0 handlerF projectF injectF program)) +main :: Int +main = if intEq result._1 31 then if intEq result._2._1 11 then if intEq result._2._2._1 2 then intAdd result._2._2._2 1 else 1 else 1 else 1 +"#, + 42, + ); +} + +#[test] +fn open_handler_recovers_aggregate_answers_state_and_captures() { + execute( + "open-aggregate", + r#"module Main where +import Prim.Control as C +data F x = F ({ value :: Int } -> x) +data G x = G ({ value :: Int } -> x) +data U x = UF (F x) | UG (G x) +project :: forall x r. U x -> (F x -> r) -> (G x -> r) -> r +project u l r = case u of + UF f -> l f + UG g -> r g +inner :: forall x. { count :: Int } -> F x -> (x -> C.Control U { answer :: Int }) -> { _1 :: { count :: Int }, _2 :: C.Control U { answer :: Int } } +inner s (F k) cont = { _1: { count: intAdd s.count 1 }, _2: cont (k { value: 1 }) } +outer :: forall x. Int -> G x -> (x -> C.Control G { _1 :: { count :: Int }, _2 :: { answer :: Int } }) -> { _1 :: Int, _2 :: C.Control G { _1 :: { count :: Int }, _2 :: { answer :: Int } } } +outer s (G k) cont = { _1: intAdd s 1, _2: cont (k { value: 39 }) } +program :: C.Control U { answer :: Int } +program = let captured = { offset: 2 } in C.bind (C.perform (UG (G \x -> x))) (\a -> C.bind (C.perform (UF (F \x -> x))) (\b -> C.return { answer: intAdd captured.offset (intAdd a.value b.value) })) +result = C.run 0 outer (foldOpen { count: 0 } inner project UG program) +main :: Int +main = if intEq result._1 1 then if intEq result._2._1.count 1 then result._2._2.answer else 1 else 1 +"#, + 42, + ); +} + +#[test] +fn ignored_open_interpretation_retains_handler_traps() { + execute( + "open-ignored-trap", + &source( + MIXED, + "{ _1: intQuot 1 0, _2: cont (k s) }", + OUTER, + "let ignored = result in 42", + ), + 134, + ); +} + +#[test] +fn open_handler_arguments_are_strict() { + let text = source(MIXED, INNER, OUTER, "let unused = handled in 42") + .replace("foldOpen 0", "foldOpen (intQuot 1 0)"); + execute("open-strict", &text, 134); +} + +#[test] +fn operation_names_do_not_affect_open_lowering() { + let text = source(MIXED, INNER, OUTER, "intAdd result._2._2 1") + .replace("UF", "First") + .replace("UG", "Second") + .replace(" F ", " Query ") + .replace("(F ", "(Query ") + .replace(" G ", " Reply ") + .replace("(G ", "(Reply "); + execute("open-renamed", &text, 42); +} + +#[test] +fn collection_preserves_suspended_open_frames_and_captures() { + let mut text = source( + r"let saved = { offset: 2 } in C.bind (C.perform (UF (F \x -> x))) (\a -> C.return (intAdd a saved.offset))", + r"let churn = C.run 0 noiseHandler (noise 5000) in { _1: intAdd s churn._1, _2: cont (k 40) }", + OUTER, + "if intEq result._2._1 5000 then result._2._2 else 1", + ); + text.push_str(r" +noiseHandler :: forall x. Int -> G x -> (x -> C.Control G Int) -> { _1 :: Int, _2 :: C.Control G Int } +noiseHandler s (G k) cont = { _1: intAdd s 1, _2: cont (k s) } +noise :: Int -> C.Control G Int +noise n = if intEq n 0 then C.return 0 else C.bind (C.perform (G \x -> x)) (\_ -> noise (intSub n 1))"); + control_support::execute_with_gc("open-gc", &text, 42); +} + +#[test] +fn library_state_fold_can_choose_when_to_forward_selected_requests() { + let program = r"C.bind (C.perform (UF (F \x -> x))) (\a -> C.bind (C.perform (UF (F \x -> x))) (\b -> C.bind (C.perform (UF (F \x -> x))) (\c -> C.return (intAdd a (intAdd b c)))))"; + execute( + "open-deep", + &source( + program, + INNER, + OUTER, + "if intEq result._1 0 then if intEq result._2._1 3 then intAdd result._2._2 39 else 1 else 1", + ), + 42, + ); + execute( + "open-shallow-library", + &source( + program, + r"if intEq s 0 then { _1: 1, _2: cont (k 1) } else { _1: s, _2: C.bind (C.perform (UG (G \x -> x))) (\v -> cont (k v)) }", + "{ _1: intAdd s 1, _2: cont (k 20) }", + "if intEq result._1 2 then if intEq result._2._1 1 then intAdd result._2._2 1 else 1 else 1", + ), + 42, + ); +} diff --git a/crates/psrs-driver/tests/control_owned_runtime.rs b/crates/psrs-driver/tests/control_owned_runtime.rs new file mode 100644 index 00000000..11e55015 --- /dev/null +++ b/crates/psrs-driver/tests/control_owned_runtime.rs @@ -0,0 +1,171 @@ +//! Runtime consumption of one-shot owners; construction remains inert. +mod control_support; +use control_support::execute; + +fn source(body: &str) -> String { + let body = body.replace("; ", " in let "); + format!( + r#"module Main where +import Prim.Control as C +data Op next = Op (Int -> next) +handler :: forall x. Int -> Op x -> (x -> C.Control Op Int) -> {{ _1 :: Int, _2 :: C.Control Op Int }} +handler s (Op k) cont = {{ _1: s, _2: cont (k s) }} +main :: Int +main = {body} +"# + ) +} + +#[test] +fn a_one_shot_owner_runs_once() { + execute( + "owned-once", + &source( + r"let owner = C.once (\n -> C.return (intAdd n 2)) in (C.run 0 handler (C.resume owner 40))._2", + ), + 42, + ); +} + +#[test] +fn building_an_unused_resume_does_not_consume_ownership() { + execute( + "owned-inert", + &source( + r"let owner = C.once (\n -> C.return n); unused = C.resume owner 7 in (C.run 0 handler (C.resume owner 42))._2", + ), + 42, + ); +} + +#[test] +fn executing_two_resume_nodes_rejects_the_second() { + execute( + "owned-double", + &source( + r"let owner = C.once (\n -> C.return n) in (C.run 0 handler (C.bind (C.resume owner 19) (\a -> C.bind (C.resume owner 23) (\b -> C.return (intAdd a b)))))._2", + ), + 134, + ); +} + +#[test] +fn repeating_the_same_resume_node_rejects_reinterpretation() { + execute( + "owned-repeat", + &source( + r"let owner = C.once (\n -> C.return n); program = C.resume owner 21; first = C.run 0 handler program; second = C.run 0 handler program in intAdd first._2 second._2", + ), + 134, + ); +} + +#[test] +fn cancelling_a_pending_owner_rejects_resume() { + execute( + "owned-cancel", + &source( + r"let owner = C.once (\n -> C.return n); cancelled = C.cancel owner in (C.run 0 handler (C.resume owner 42))._2", + ), + 134, + ); +} + +#[test] +fn cancellation_is_single_use() { + execute( + "owned-cancel-twice", + &source( + r"let owner = C.once (\n -> C.return n); first = C.cancel owner; second = C.cancel owner in 42", + ), + 134, + ); +} + +#[test] +fn a_consumed_owner_cannot_be_cancelled() { + execute( + "owned-cancel-consumed", + &source( + r"let owner = C.once (\n -> C.return n); first = C.run 0 handler (C.resume owner 42); cancelled = C.cancel owner in first._2", + ), + 134, + ); +} + +#[test] +fn resume_factory_arguments_are_strict() { + execute( + "owned-strict", + &source( + r"let owner = C.once (\n -> C.return n); unused = C.resume owner (intQuot 1 0) in 42", + ), + 134, + ); +} + +#[test] +fn reentrant_interpretation_rejects_a_running_owner() { + execute( + "owned-reentrant", + &source( + r"let holder = [C.once (\n -> C.return n)]; owner = C.once (\n -> let nested = C.run 0 handler (C.resume (arrayIndex holder 0) n) in C.return nested._2); written = arrayWrite holder 0 owner in (C.run 0 handler (C.resume owner 42))._2", + ), + 134, + ); +} + +#[test] +fn reentrant_cancellation_rejects_a_running_owner() { + execute( + "owned-reentrant-cancel", + &source( + r"let holder = [C.once (\n -> C.return n)]; owner = C.once (\n -> let cancelled = C.cancel (arrayIndex holder 0) in C.return n); written = arrayWrite holder 0 owner in (C.run 0 handler (C.resume owner 42))._2", + ), + 134, + ); +} + +#[test] +fn separate_owners_do_not_share_consumption_state() { + execute( + "owned-independent", + &source( + r"let first = C.once (\n -> C.return n); second = C.once (\n -> C.return n) in (C.run 0 handler (C.bind (C.resume first 19) (\a -> C.bind (C.resume second 23) (\b -> C.return (intAdd a b)))))._2", + ), + 42, + ); +} + +#[test] +fn an_ignored_one_shot_interpretation_keeps_its_callback_trap() { + execute( + "owned-ignored", + &source( + r"let owner = C.once (\_ -> C.return (intQuot 1 0)); unused = C.run 0 handler (C.resume owner 42) in 42", + ), + 134, + ); +} + +#[test] +fn an_abandoned_resume_node_does_not_consume_its_owner() { + execute( + "owned-abandoned", + &source( + r"let owner = C.once (\n -> C.return n); program = C.bind (C.perform (Op \x -> x)) (\_ -> C.resume owner 7); skipped = C.run 0 (\s op cont -> { _1: s, _2: C.return 0 }) program in intAdd skipped._2 (C.run 0 handler (C.resume owner 42))._2", + ), + 42, + ); +} + +#[test] +fn collection_preserves_a_pending_one_shot_owners_captures() { + let mut text = source( + r"let saved = { offset: 2 }; owner = C.once (\n -> C.return (intAdd n saved.offset)); churn = C.run 0 handler (noise 5000) in if intEq churn._2 0 then (C.run 0 handler (C.resume owner 40))._2 else 1", + ); + text.push_str(r" +noise :: Int -> C.Control Op Int +noise n = if intEq n 0 then C.return 0 else C.bind (C.perform (Op \x -> x)) (\_ -> noise (intSub n 1))"); + control_support::execute_with_gc("owned-gc", &text, 42); +} diff --git a/crates/psrs-driver/tests/control_owned_surface.rs b/crates/psrs-driver/tests/control_owned_surface.rs new file mode 100644 index 00000000..da4ab256 --- /dev/null +++ b/crates/psrs-driver/tests/control_owned_surface.rs @@ -0,0 +1,67 @@ +//! Nominal one-shot ownership keeps input, output and functor identities checked. +fn source(declarations: &str) -> String { + format!( + r#"module Main where +import Prim.Control as C +data Op next = Op (Int -> next) +data Other next = Other (Int -> next) +owner :: C.Resumption Op Int Int +owner = C.once (\n -> C.return n) +{declarations} +"# + ) +} + +#[test] +fn accepts_the_owned_surface() { + psrs_driver::check_source("Main.purs", &source("program :: C.Control Op Int\nprogram = C.resume owner 42\ncancelled :: Unit\ncancelled = C.cancel owner")).unwrap(); +} + +#[test] +fn rejects_a_wrong_resume_argument() { + assert!(psrs_driver::check_source("Main.purs", &source("bad = C.resume owner true")).is_err()); +} + +#[test] +fn rejects_a_wrong_resume_result() { + assert!( + psrs_driver::check_source( + "Main.purs", + &source("bad :: C.Control Op Boolean\nbad = C.resume owner 42") + ) + .is_err() + ); +} + +#[test] +fn rejects_a_wrong_resume_functor() { + assert!( + psrs_driver::check_source( + "Main.purs", + &source("bad :: C.Control Other Int\nbad = C.resume owner 42") + ) + .is_err() + ); +} + +#[test] +fn rejects_cancellation_of_an_unrestricted_function() { + assert!( + psrs_driver::check_source("Main.purs", &source("bad = C.cancel (\\n -> C.return n)")) + .is_err() + ); +} + +#[test] +fn nominal_ownership_rejects_answer_coercion() { + let text = source( + "newtype Answer = Answer Int\nbad :: C.Resumption Op Answer Int\nbad = coerce owner", + ) + .replace( + "import Prim.Control as C", + "import Prim.Control as C\nimport Safe.Coerce (coerce)", + ); + let identity = text.replace("C.Resumption Op Answer Int", "C.Resumption Op Int Int"); + psrs_driver::check_source("Main.purs", &identity).unwrap(); + assert!(psrs_driver::check_source("Main.purs", &text).is_err()); +} diff --git a/crates/psrs-driver/tests/control_primitive_values.rs b/crates/psrs-driver/tests/control_primitive_values.rs new file mode 100644 index 00000000..147c64c0 --- /dev/null +++ b/crates/psrs-driver/tests/control_primitive_values.rs @@ -0,0 +1,58 @@ +//! Checked compiler primitives used as ordinary values and partial applications. + +mod control_support; +use control_support::execute; + +#[test] +fn aliases_preserve_control_and_scalar_primitive_semantics() { + execute( + "primitive-aliases", + r#"module Main where +import Prim.Control as C +data Op next = Op (Int -> next) +pure :: forall f a. a -> C.Control f a +pure = C.return +bind :: forall f a b. C.Control f a -> (a -> C.Control f b) -> C.Control f b +bind = C.bind +perform :: forall f a. f a -> C.Control f a +perform = C.perform +run :: forall f a s. s -> (forall x. s -> f x -> (x -> C.Control f a) -> { _1 :: s, _2 :: C.Control f a }) -> C.Control f a -> { _1 :: s, _2 :: a } +run = C.run +once :: forall f a b. (a -> C.Control f b) -> C.Resumption f a b +once = C.once +resume :: forall f a b. C.Resumption f a b -> a -> C.Control f b +resume = C.resume +add :: Int -> Int -> Int +add = intAdd +handler :: forall x. Int -> Op x -> (x -> C.Control Op Int) -> { _1 :: Int, _2 :: C.Control Op Int } +handler s (Op k) cont = { _1: s, _2: resume (once cont) (k 41) } +program :: C.Control Op Int +program = bind (perform (Op \x -> x)) (\value -> pure (add value 1)) +main :: Int +main = (run 0 handler program)._2 +"#, + 42, + ); +} + +#[test] +fn partial_applications_keep_checked_argument_types() { + execute( + "primitive-partial", + r#"module Main where +import Prim.Control as C +data Op next = Op (Int -> next) +handler :: forall x. Int -> Op x -> (x -> C.Control Op Int) -> { _1 :: Int, _2 :: C.Control Op Int } +handler s (Op k) cont = { _1: s, _2: cont (k 0) } +finish :: (Int -> C.Control Op Int) -> C.Control Op Int +finish = C.bind (C.return 41) +interpret :: C.Control Op Int -> { _1 :: Int, _2 :: Int } +interpret = C.run 0 handler +increment :: Int -> Int +increment = intAdd 1 +main :: Int +main = (interpret (finish (\value -> C.return (increment value))))._2 +"#, + 42, + ); +} diff --git a/crates/psrs-driver/tests/control_reference.rs b/crates/psrs-driver/tests/control_reference.rs new file mode 100644 index 00000000..279c97a7 --- /dev/null +++ b/crates/psrs-driver/tests/control_reference.rs @@ -0,0 +1,149 @@ +//! Pinned independent-package Control/Run fixtures from +//! `psrs-stdlib/conformance/control/`. They execute ordinary library wrappers. +mod control_support; + +#[test] +fn locked_run_state_threads_put_modify_and_get() { + control_support::execute( + "run-state", + r"module Main where +import Prelude +import Run (Run, extract) +import Run.State (State, put, modify, get, evalState) +program :: Run (state :: State Int) Int +program = do + put 20 + modify (\value -> value + 22) + get +main :: Int +main = extract (evalState 0 program) +", + 42, + ); +} + +#[test] +fn locked_run_choose_executes_both_alternatives() { + control_support::execute( + "run-choose", + r"module Main where +import Prelude +import Run (Run, extract) +import Run.Choose (Choose, calt, runChoose) +import Data.Array (length) +program :: Run (choose :: Choose) Int +program = calt (pure 20) (pure 22) +answers :: Array Int +answers = extract (runChoose program) +main :: Int +main = if answers == [20, 22] && length answers == 2 then 42 else 0 +", + 42, + ); +} + +#[test] +fn locked_run_handlers_compose_reader_state_writer_and_except() { + control_support::execute( + "run-handlers", + r#"module Main where +import Prelude +import Data.Either (Either(..)) +import Data.Tuple (Tuple(..)) +import Run (Run, extract) +import Run.Reader (Reader, ask, local, runReader) +import Run.State (State, put, modify, get, evalState) +import Run.Writer (Writer, tell, censor, foldWriter) +import Run.Except (Except, throw, runExcept) +program :: Run (reader :: Reader Int, state :: State Int, writer :: Writer String, except :: Except String) Int +program = do + seed <- ask + put seed + local (\n -> n + 1) do + adjusted <- ask + modify (\n -> n + adjusted) + tell "a" + censor (\s -> s <> "!") (tell "b") + value <- get + if value == 41 then pure (value + 1) else throw "incorrect state" +main :: Int +main = case extract (runExcept (foldWriter (\a b -> a <> b) "" (evalState 0 (runReader 20 program)))) of + Right (Tuple log answer) -> if log == "ab!" then answer else 0 + Left _ -> 0 +"#, + 42, + ); +} + +#[test] +fn locked_labelled_rows_deep_forwards_and_reinstalls() { + control_support::execute( + "rows-deep", + r#"module Main where +import Prelude +import PSRS.Control as C +import PSRS.Control.Row as R +import Type.Proxy (Proxy(..)) +data Read x = Read (Int -> x) +derive instance functorRead :: Functor Read +program :: R.Computation (left :: Read, right :: Read) Int +program = C.bind (R.performAt (Proxy :: Proxy "left") (Read identity)) \left -> + C.bind (R.performAt (Proxy :: Proxy "right") (Read identity)) \right -> + C.bind (R.performAt (Proxy :: Proxy "left") (Read identity)) (\_ -> C.pure (left + right)) +handleLeft :: forall x. Int -> Read x -> (Int -> x -> R.Computation (right :: Read) { _1 :: Int, _2 :: Int }) -> R.Computation (right :: Read) { _1 :: Int, _2 :: Int } +handleLeft state (Read answer) resume = resume (state + 1) (answer 20) +handleRight :: forall x. Int -> Read x -> (Int -> x -> R.Computation () { _1 :: Int, _2 :: { _1 :: Int, _2 :: Int } }) -> R.Computation () { _1 :: Int, _2 :: { _1 :: Int, _2 :: Int } } +handleRight state (Read answer) resume = resume (state + 1) (answer 22) +main :: Int +main = let result = R.runPure (R.handleDeepAt (Proxy :: Proxy "right") handleRight 0 (R.handleDeepAt (Proxy :: Proxy "left") handleLeft 0 program)) + in if result._1 == 1 && result._2._1 == 2 then result._2._2 else 0 +"#, + 42, + ); +} + +#[test] +fn locked_labelled_rows_shallow_escape_keeps_the_residual() { + control_support::execute( + "rows-shallow", + r#"module Main where +import Prelude +import PSRS.Control as C +import PSRS.Control.Row as R +import Type.Proxy (Proxy(..)) +data Read x = Read (Int -> x) +derive instance functorRead :: Functor Read +program :: R.Computation (ask :: Read) Int +program = C.bind (R.performAt (Proxy :: Proxy "ask") (Read identity)) \first -> + C.bind (R.performAt (Proxy :: Proxy "ask") (Read identity)) \second -> C.pure (first + second) +inner :: forall x. Int -> Read x -> (Int -> x -> R.Computation (ask :: Read) { _1 :: Int, _2 :: Int }) -> R.Computation (ask :: Read) { _1 :: Int, _2 :: Int } +inner state (Read answer) resume = resume (state + 1) (answer 20) +outer :: forall x. Int -> Read x -> (Int -> x -> R.Computation () { _1 :: Int, _2 :: { _1 :: Int, _2 :: Int } }) -> R.Computation () { _1 :: Int, _2 :: { _1 :: Int, _2 :: Int } } +outer state (Read answer) resume = resume (state + 1) (answer 22) +main :: Int +main = let result = R.runPure (R.handleDeepAt (Proxy :: Proxy "ask") outer 0 (R.handleShallowAt (Proxy :: Proxy "ask") inner 0 program)) + in if result._1 == 1 && result._2._1 == 1 then result._2._2 else 0 +"#, + 42, + ); +} + +#[test] +fn locked_labelled_rows_reject_a_nonempty_residual_at_run_pure() { + let source = r"module Main where +import PSRS.Control as C +import PSRS.Control.Row as R +data Read x = Read x +program :: R.Computation (ask :: Read) Int +program = C.pure 42 +main :: Int +main = R.runPure program +"; + let errors = psrs_driver::check_source("Main.purs", source).expect_err("residual row"); + assert!( + errors + .iter() + .any(|error| error.code == Some("TypesDoNotUnify")), + "{errors:?}" + ); +} diff --git a/crates/psrs-driver/tests/control_runtime.rs b/crates/psrs-driver/tests/control_runtime.rs new file mode 100644 index 00000000..850e98a0 --- /dev/null +++ b/crates/psrs-driver/tests/control_runtime.rs @@ -0,0 +1,243 @@ +//! Value-sensitive execution of persistent continuations through closed `run`. + +mod control_support; +use control_support::execute; + +fn source(program: &str, handler: &str, result: &str) -> String { + format!( + r#"module Main where +import Prim.Control as C +data Op next = Op (Int -> next) +handler :: forall x. Int -> Op x -> (x -> C.Control Op Int) -> {{ _1 :: Int, _2 :: C.Control Op Int }} +handler s (Op k) cont = {handler} +program :: C.Control Op Int +program = {program} +result :: {{ _1 :: Int, _2 :: Int }} +result = C.run 0 handler program +main :: Int +main = {result} +"# + ) +} + +const NEXT: &str = "{ _1: intAdd s 1, _2: cont (k s) }"; + +#[test] +fn bind_associations_preserve_order_and_state() { + for (name, program) in [ + ( + "left", + r"C.bind (C.bind (C.perform (Op \x -> x)) (\a -> C.return (intAdd a 40))) (\b -> C.bind (C.perform (Op \x -> x)) (\c -> C.return (intAdd b c)))", + ), + ( + "right", + r"C.bind (C.perform (Op \x -> x)) (\a -> C.bind (C.return (intAdd a 40)) (\b -> C.bind (C.perform (Op \x -> x)) (\c -> C.return (intAdd b c))))", + ), + ] { + execute( + name, + &source( + program, + NEXT, + "if intEq result._1 2 then intAdd result._2 1 else 1", + ), + 42, + ); + } +} + +#[test] +fn a_handler_can_abandon_the_pending_sequence() { + execute( + "abandon", + &source( + r"C.bind (C.perform (Op \x -> x)) (\_ -> C.return (intQuot 1 0))", + "{ _1: 7, _2: C.return 42 }", + "if intEq result._1 7 then result._2 else 1", + ), + 42, + ); +} + +#[test] +fn a_handler_can_replay_then_bind_a_saved_continuation() { + execute( + "replay", + &source( + r"C.bind (C.perform (Op \x -> x)) (\a -> C.bind (C.return (intAdd a 1)) (\b -> C.return b))", + r"{ _1: intAdd s 1, _2: C.bind (cont (k 19)) (\a -> C.bind (cont (k 21)) (\b -> C.return (intAdd a b))) }", + "if intEq result._1 1 then result._2 else 1", + ), + 42, + ); +} + +#[test] +fn replayed_requests_share_current_handler_state_without_consuming_saved_frames() { + execute( + "replayed-requests", + &source( + r"C.bind (C.perform (Op \x -> x)) (\a -> C.bind (C.perform (Op \x -> x)) (\b -> C.return (intAdd a b)))", + r"if intEq s 0 then { _1: 1, _2: C.bind (cont (k 19)) (\a -> C.bind (cont (k 20)) (\b -> C.return (intAdd a b))) } else { _1: intAdd s 1, _2: cont (k s) }", + "if intEq result._1 3 then result._2 else 1", + ), + 42, + ); +} + +#[test] +fn nested_runs_have_independent_pending_sequences() { + execute( + "nested", + &source( + r"C.bind (C.perform (Op \x -> x)) (\a -> let inner = C.run 40 handler (C.bind (C.perform (Op \x -> x)) (\b -> C.return (intAdd b 1))) in C.return (intAdd a inner._2))", + NEXT, + "if intEq result._1 1 then intAdd result._2 1 else 1", + ), + 42, + ); +} + +#[test] +fn constructing_a_computation_does_not_interpret_requests() { + execute( + "inert", + &source( + r"let unused = C.bind (C.perform (Op \x -> x)) (\_ -> C.return (intQuot 1 0)) in C.return 42", + "{ _1: intQuot 1 0, _2: cont (k s) }", + "if intEq result._1 0 then result._2 else 1", + ), + 42, + ); +} + +#[test] +fn ignored_run_results_preserve_handler_traps() { + let text = source( + r"C.perform (Op \x -> x)", + "{ _1: intQuot 1 0, _2: cont (k s) }", + "let ignored = result in 42", + ); + // Wasmtime maps a guest trap to exit 134 on this runner. + execute("ignored-trap", &text, 134); +} + +#[test] +fn computation_factory_arguments_are_strict() { + execute( + "strict-factory", + &source( + "let unused = C.return (intQuot 1 0) in C.return 42", + NEXT, + "result._2", + ), + 134, + ); +} + +#[test] +fn repeating_interpretation_keeps_computation_nodes_immutable() { + execute( + "repeat-run", + &source( + r"C.bind (C.perform (Op \x -> x)) (\a -> C.return (intAdd a 1))", + NEXT, + "let other = C.run 40 handler program in if intEq result._1 1 then if intEq other._1 41 then intAdd result._2 other._2 else 1 else 1", + ), + 42, + ); +} + +#[test] +fn long_bind_chains_and_request_sequences_are_stack_safe() { + for (name, definition) in [ + ( + "long-left", + r"build n c = if intEq n 0 then c else build (intSub n 1) (C.bind c (\a -> C.return (intAdd a 1)))", + ), + ( + "long-right", + r"build n c = if intEq n 0 then c else C.bind c (\a -> build (intSub n 1) (C.return (intAdd a 1)))", + ), + ( + "long-requests", + r"build n c = if intEq n 0 then c else C.bind (C.perform (Op \x -> x)) (\_ -> build (intSub n 1) (C.bind c (\a -> C.return (intAdd a 1))))", + ), + ] { + let mut text = source( + "build 100000 (C.return 0)", + NEXT, + "if intEq result._2 100000 then 42 else 1", + ); + text.push_str("\nbuild :: Int -> C.Control Op Int -> C.Control Op Int\n"); + text.push_str(definition); + text.push('\n'); + execute(name, &text, 42); + } +} + +#[test] +fn erased_aggregate_answers_state_and_captures_are_recovered() { + execute( + "aggregate", + r#"module Main where +import Prim.Control as C +data Op next = Op ({ value :: Int } -> next) +handler :: forall x. { count :: Int } -> Op x -> (x -> C.Control Op { answer :: Int }) -> { _1 :: { count :: Int }, _2 :: C.Control Op { answer :: Int } } +handler s (Op k) cont = { _1: { count: intAdd s.count 1 }, _2: cont (k { value: 40 }) } +program :: C.Control Op { answer :: Int } +program = let saved = { offset: 2 } in C.bind (C.perform (Op \x -> x)) (\a -> C.return { answer: intAdd a.value saved.offset }) +result :: { _1 :: { count :: Int }, _2 :: { answer :: Int } } +result = C.run { count: 0 } handler program +main :: Int +main = if intEq result._1.count 1 then result._2.answer else 1 +"#, + 42, + ); +} + +#[test] +fn collection_preserves_array_and_string_captures_across_suspension() { + execute_gc( + "buffer-gc", + r#"module Main where +import Prim.Control as C +data Op next = Op (Int -> next) +data Noise next = Noise (Int -> next) +handler :: forall x. Int -> Op x -> (x -> C.Control Op Int) -> { _1 :: Int, _2 :: C.Control Op Int } +handler s (Op k) cont = let churn = C.run 0 noiseHandler (noise 4000) in { _1: intAdd s churn._1, _2: cont (k 0) } +noiseHandler :: forall x. Int -> Noise x -> (x -> C.Control Noise Int) -> { _1 :: Int, _2 :: C.Control Noise Int } +noiseHandler s (Noise k) cont = { _1: intAdd s 1, _2: cont (k s) } +noise :: Int -> C.Control Noise Int +noise n = if intEq n 0 then C.return 0 else C.bind (C.perform (Noise \x -> x)) (\_ -> noise (intSub n 1)) +program :: C.Control Op Int +program = let numbers = [40, 2] + text = stringToBytes (numberToString 10.0) + in C.bind (C.perform (Op \x -> x)) (\_ -> C.return (if intEq (arrayLength text) 0 then 0 else intAdd (arrayIndex numbers 0) (arrayLength numbers))) +main :: Int +main = (C.run 0 handler program)._2 +"#, + ); +} + +fn execute_gc(name: &str, source: &str) { + control_support::execute_with_gc(name, source, 42); +} + +#[test] +fn function_answers_use_the_shared_callable_protocol() { + execute( + "function", + r#"module Main where +import Prim.Control as C +data Op next = Op ((Int -> Int) -> next) +handler :: forall x. Int -> Op x -> (x -> C.Control Op Int) -> { _1 :: Int, _2 :: C.Control Op Int } +handler s (Op k) cont = { _1: s, _2: cont (k (\n -> intAdd n 2)) } +program :: C.Control Op Int +program = C.bind (C.perform (Op \x -> x)) (\f -> C.return (f 40)) +main :: Int +main = (C.run 0 handler program)._2 +"#, + 42, + ); +} diff --git a/crates/psrs-driver/tests/control_scoped_replay.rs b/crates/psrs-driver/tests/control_scoped_replay.rs new file mode 100644 index 00000000..7c8884d6 --- /dev/null +++ b/crates/psrs-driver/tests/control_scoped_replay.rs @@ -0,0 +1,24 @@ +//! Replay nested target finish frames without consuming suspended checkpoints. +mod control_support; +use control_support::execute; + +const SOURCE: &str = include_str!("control_support/scoped_replay.purs"); + +#[test] +fn two_replays_preserve_nested_suspended_source_checkpoints() { + execute("scoped-nested-replay", &SOURCE.replace("AFTER", r"C.bind (raw (k 0)) (\b -> C.return { saved: a.saved, built: a.built, value: intAdd a.value b.value })"), 42); +} + +#[test] +fn captures_created_in_a_replay_expire_at_that_branchs_completion() { + for (name, value) in [("call", "a.saved 42"), ("node", "a.built")] { + let after = format!( + "let expired = C.run 0 closed ({value}) in C.return {{ saved: a.saved, built: a.built, value: expired._2 }}" + ); + execute( + &format!("scoped-replay-expired-{name}"), + &SOURCE.replace("AFTER", &after), + 134, + ); + } +} diff --git a/crates/psrs-driver/tests/control_scoped_runtime.rs b/crates/psrs-driver/tests/control_scoped_runtime.rs new file mode 100644 index 00000000..035cc0ab --- /dev/null +++ b/crates/psrs-driver/tests/control_scoped_runtime.rs @@ -0,0 +1,209 @@ +//! Contract v2: source/target scope, persistent replay and extent completion. +mod control_support; +use control_support::execute; + +const HEADER: &str = r"module Main where +import Prim.Control as C +data Op next = Op Int (Int -> next) +handleDeep :: forall u f g s a. (forall x r. u x -> (f x -> r) -> (g x -> r) -> r) -> (forall x. s -> f x -> (s -> x -> C.Control g { _1 :: s, _2 :: a }) -> C.Control g { _1 :: s, _2 :: a }) -> s -> C.Control u a -> C.Control g { _1 :: s, _2 :: a } +handleDeep project clause state computation = C.handle + (\value -> C.return { _1: state, _2: value }) + (\request raw -> project request + (\op -> clause state op (\nextState answer -> handleDeep project clause nextState (raw answer))) + (\op -> C.bind (C.perform op) (\answer -> handleDeep project clause state (raw answer)))) + computation +handleShallow :: forall u f g s a. (forall x r. u x -> (f x -> r) -> (g x -> r) -> r) -> (forall x. s -> f x -> (s -> x -> C.Control u { _1 :: s, _2 :: a }) -> C.Control u { _1 :: s, _2 :: a }) -> s -> C.Control u a -> C.Control u { _1 :: s, _2 :: a } +handleShallow project clause state computation = C.handle + (\value -> C.return { _1: state, _2: value }) + (\request raw -> project request + (\op -> clause state op (\nextState answer -> C.bind (raw answer) (\value -> C.return { _1: nextState, _2: value }))) + (\_ -> C.bind (C.perform request) (\answer -> handleShallow project clause state (raw answer)))) + computation +project :: forall x r. Op x -> (Op x -> r) -> (Op x -> r) -> r +project op selected residual = selected op +outer :: forall x. Int -> Op x -> (x -> C.Control Op { _1 :: Int, _2 :: Int }) -> { _1 :: Int, _2 :: C.Control Op { _1 :: Int, _2 :: Int } } +outer s (Op label k) cont = { _1: intAdd s 1, _2: cont (k 21) } +"; + +fn check(name: &str, program: &str, expected: i32) { + execute(name, &format!("{HEADER}\n{program}"), expected); +} + +#[test] +fn fresh_clause_requests_reach_the_outer_handler() { + check( + "scoped-clause", + r" +inner :: forall x. Int -> Op x -> (Int -> x -> C.Control Op { _1 :: Int, _2 :: Int }) -> C.Control Op { _1 :: Int, _2 :: Int } +inner s (Op label k) cont = C.bind (C.perform (Op 2 (\x -> x))) (\n -> cont (intAdd s 1) (k (intAdd n 21))) +result = C.run 0 outer (handleDeep project inner 0 (C.perform (Op 1 (\x -> x)))) +main = if intEq result._1 1 then if intEq result._2._1 1 then result._2._2 else 0 else 0 +", + 42, + ); +} + +#[test] +fn deep_and_shallow_have_distinct_source_resumption_scopes() { + let body = r" +inner :: forall x. Int -> Op x -> (Int -> x -> C.Control Op { _1 :: Int, _2 :: Int }) -> C.Control Op { _1 :: Int, _2 :: Int } +inner s (Op label k) cont = cont (intAdd s 1) (k 21) +program = C.bind (C.perform (Op 1 (\x -> x))) (\a -> C.bind (C.perform (Op 1 (\x -> x))) (\b -> C.return (intAdd a b))) +result = C.run 0 outer (HANDLE project inner 0 program) +main = if intEq result._1 OUTER then if intEq result._2._1 INNER then result._2._2 else 0 else 0 +"; + check( + "scoped-deep", + &body + .replace("HANDLE", "handleDeep") + .replace("OUTER", "0") + .replace("INNER", "2"), + 42, + ); + check( + "scoped-shallow", + &body + .replace("HANDLE", "handleShallow") + .replace("OUTER", "1") + .replace("INNER", "1"), + 42, + ); +} + +#[test] +fn return_clause_effects_execute_outside_source_dispatch() { + check( + "scoped-return", + r" +ret :: Int -> C.Control Op { _1 :: Int, _2 :: Int } +ret a = C.bind (C.perform (Op 2 (\x -> x))) (\b -> C.return { _1: 7, _2: intAdd a b }) +clause :: forall x. Op x -> (x -> C.Control Op Int) -> C.Control Op { _1 :: Int, _2 :: Int } +clause request raw = C.return { _1: 0, _2: intQuot 1 0 } +result = C.run 0 outer (C.handle ret clause (C.return 21)) +main = if intEq result._1 1 then result._2._2 else 0 +", + 42, + ); +} + +#[test] +fn deep_replay_keeps_independent_state_and_caller_work_outside_capture() { + check( + "scoped-replay", + r" +inner :: forall x. Int -> Op x -> (Int -> x -> C.Control Op { _1 :: Int, _2 :: Int }) -> C.Control Op { _1 :: Int, _2 :: Int } +inner s (Op label k) cont = C.bind (cont 3 (k 19)) (\a -> C.bind (cont 7 (k 21)) (\b -> C.return { _1: intAdd a._1 b._1, _2: intAdd a._2 b._2 })) +program = C.bind (C.perform (Op 1 (\x -> x))) (\a -> C.return (intAdd a 1)) +result = C.run 0 outer (C.bind (handleDeep project inner 0 program) (\pair -> C.bind (C.perform (Op 2 (\x -> x))) (\_ -> C.return pair))) +main = if intEq result._1 1 then if intEq result._2._1 10 then result._2._2 else 0 else 0 +", + 42, + ); +} + +#[test] +fn abandonment_never_executes_the_captured_source_queue() { + check( + "scoped-abandon", + r" +inner :: forall x. Int -> Op x -> (Int -> x -> C.Control Op { _1 :: Int, _2 :: Int }) -> C.Control Op { _1 :: Int, _2 :: Int } +inner s op cont = C.return { _1: 7, _2: 42 } +program = C.bind (C.perform (Op 1 (\x -> x))) (\_ -> C.return (intQuot 1 0)) +result = C.run 0 outer (handleDeep project inner 0 program) +main = if intEq result._1 0 then if intEq result._2._1 7 then result._2._2 else 0 else 0 +", + 42, + ); +} + +const ESCAPE: &str = r" +type Saved = { raw :: Int -> C.Control Op Int, built :: C.Control Op Int } +ret :: Int -> C.Control Op Saved +ret a = C.return { raw: \x -> C.return x, built: C.return a } +clause :: forall x. Op x -> (x -> C.Control Op Int) -> C.Control Op Saved +clause (Op label k) raw = C.return { raw: \n -> raw (k n), built: raw (k 42) } +root :: forall x. Int -> Op x -> (x -> C.Control Op Int) -> { _1 :: Int, _2 :: C.Control Op Int } +root s (Op label k) raw = { _1: s, _2: raw (k 42) } +handled = C.handle ret clause (C.perform (Op 1 (\x -> x))) +main = (C.run 0 root (C.bind handled (\saved -> USE)))._2 +"; + +#[test] +fn target_completion_expires_raw_continuations_while_parent_is_live() { + check( + "scoped-expired-call", + &ESCAPE.replace("USE", "saved.raw 42"), + 134, + ); + check( + "scoped-expired-node", + &ESCAPE.replace("USE", "saved.built"), + 134, + ); +} + +#[test] +fn handle_alias_and_partial_application_keep_distinct_answer_types() { + check( + "scoped-alias", + r" +eliminate :: forall u g a b. (a -> C.Control g b) -> (forall x. u x -> (x -> C.Control u a) -> C.Control g b) -> C.Control u a -> C.Control g b +eliminate = C.handle +ret :: Int -> C.Control Op Boolean +ret n = C.return (intEq n 42) +clause :: forall x. Op x -> (x -> C.Control Op Int) -> C.Control Op Boolean +clause op raw = C.return false +finish :: C.Control Op Int -> C.Control Op Boolean +finish = eliminate ret clause +root :: forall x. Int -> Op x -> (x -> C.Control Op Boolean) -> { _1 :: Int, _2 :: C.Control Op Boolean } +root s (Op label k) raw = { _1: s, _2: raw (k 0) } +main = if (C.run 0 root (finish (C.return 42)))._2 then 42 else 0 +", + 42, + ); +} + +#[test] +fn long_deep_sequences_do_not_grow_the_native_stack() { + check( + "scoped-long", + r" +inner :: forall x. Int -> Op x -> (Int -> x -> C.Control Op { _1 :: Int, _2 :: Int }) -> C.Control Op { _1 :: Int, _2 :: Int } +inner s (Op label k) cont = cont (intAdd s 1) (k 0) +build :: Int -> C.Control Op Int +build n = if intEq n 0 then C.return 42 else C.bind (C.perform (Op 1 (\x -> x))) (\_ -> build (intSub n 1)) +result = C.run 0 outer (handleDeep project inner 0 (build 100000)) +main = if intEq result._2._1 100000 then result._2._2 else 0 +", + 42, + ); +} + +#[test] +fn callback_wrapper_and_adapter_run_through_ordinary_paths() { + // `bump` and `apply` are top-level functions referenced as values, so the + // compiler eta-expands wrappers for them; `partial` is a partial + // application kept as a callable value; `clause` is a source callback the + // handle protocol adapts to the uniform erased ABI. + check( + "scoped-callback-adapter", + r" +bump :: Int -> Int +bump n = intAdd n 1 +apply :: forall a b. (a -> b) -> a -> b +apply f x = f x +partial :: Int -> Int +partial = apply bump +ret :: Int -> C.Control Op Boolean +ret n = C.return (intEq (partial n) 43) +clause :: forall x. Op x -> (x -> C.Control Op Int) -> C.Control Op Boolean +clause op raw = C.return false +finish :: C.Control Op Int -> C.Control Op Boolean +finish = C.handle ret clause +root :: forall x. Int -> Op x -> (x -> C.Control Op Boolean) -> { _1 :: Int, _2 :: C.Control Op Boolean } +root s (Op label k) raw = { _1: s, _2: raw (k 0) } +main = if (C.run 0 root (finish (C.return 42)))._2 then 42 else 0 +", + 42, + ); +} diff --git a/crates/psrs-driver/tests/control_scoped_surface.rs b/crates/psrs-driver/tests/control_scoped_surface.rs new file mode 100644 index 00000000..c6d02698 --- /dev/null +++ b/crates/psrs-driver/tests/control_scoped_surface.rs @@ -0,0 +1,79 @@ +//! Ordinary language rejection of invalid source/target elimination contracts. +const HEADER: &str = r"module Main where +import Prim.Control as C +data F x = F (Int -> x) +data G x = G (Int -> x) +data U x = UF (F x) | UG (G x) +ret :: Int -> C.Control G Boolean +ret n = C.return true +source :: C.Control U Int +source = C.return 42 +"; + +fn rejects(body: &str) { + let errors = psrs_driver::check_source("Main.purs", &format!("{HEADER}\n{body}")) + .expect_err("invalid control contract accepted"); + assert!( + errors + .iter() + .any(|error| error.code == Some("TypesDoNotUnify")), + "{errors:?}" + ); +} + +#[test] +fn rejects_source_requests_in_a_target_clause() { + rejects( + r" +clause :: forall x. U x -> (x -> C.Control U Int) -> C.Control G Boolean +clause request raw = C.bind (C.perform request) (\_ -> C.return true) +bad = C.handle ret clause source +main = 0 +", + ); +} + +#[test] +fn rejects_unrelated_raw_and_target_answers() { + rejects( + r" +clause :: forall x. U x -> (x -> C.Control U Int) -> C.Control G Boolean +clause request raw = C.bind (raw true) (\_ -> C.return true) +bad = C.handle ret clause source +main = 0 +", + ); + rejects( + r" +clause :: forall x. U x -> (x -> C.Control U Int) -> C.Control G Boolean +clause request raw = C.return 42 +bad = C.handle ret clause source +main = 0 +", + ); +} + +#[test] +fn rejects_the_previous_five_argument_handle_signature() { + rejects( + r" +old :: Int -> (forall x. Int -> F x -> (x -> C.Control U Int) -> { _1 :: Int, _2 :: C.Control U Int }) -> (forall x r. U x -> (F x -> r) -> (G x -> r) -> r) -> (forall x. G x -> U x) -> C.Control U Int -> C.Control G { _1 :: Int, _2 :: Int } +old = C.handle +main = 0 +", + ); +} + +#[test] +fn rejects_shallow_residual_effect_removal() { + rejects( + r" +shallow :: C.Control U Int -> C.Control U { value :: Int } +shallow computation = C.handle (\a -> C.return { value: a }) + (\request raw -> C.bind (C.perform request) (\answer -> C.bind (raw answer) (\a -> C.return { value: a }))) computation +bad :: C.Control G { value :: Int } +bad = shallow source +main = 0 +", + ); +} diff --git a/crates/psrs-driver/tests/control_support/mod.rs b/crates/psrs-driver/tests/control_support/mod.rs new file mode 100644 index 00000000..84f706a0 --- /dev/null +++ b/crates/psrs-driver/tests/control_support/mod.rs @@ -0,0 +1,85 @@ +use std::process::Command; + +pub fn execute(name: &str, source: &str, expected: i32) { + run(name, source, expected, false); +} + +#[allow(dead_code)] +pub fn execute_with_gc(name: &str, source: &str, expected: i32) { + run(name, source, expected, true); +} + +fn run(name: &str, source: &str, expected: i32, force_gc: bool) { + let available = Command::new("wasmtime") + .arg("--version") + .output() + .is_ok_and(|output| output.status.success()); + if !available { + assert_ne!( + std::env::var("PSRS_REQUIRE_WASMTIME").as_deref(), + Ok("1"), + "control execution requires wasmtime" + ); + eprintln!("skipping control execution: wasmtime is unavailable"); + return; + } + let artifact = psrs_driver::compile_source("Main.purs", &fold_source(source)) + .unwrap_or_else(|errors| panic!("compile {name}: {errors:?}")); + let path = + std::env::temp_dir().join(format!("psrs-control-{name}-{}.wasm", std::process::id())); + std::fs::write(&path, &artifact.wasm).expect("write component"); + let mut command = Command::new("wasmtime"); + command.arg("run"); + if force_gc { + command + .args(["-O", "gc-heap-initial-size=65536"]) + .env("WASMTIME_LOG", "wasmtime::runtime::vm::gc=trace"); + } + let output = command.arg(&path).output().expect("run component"); + let _ = std::fs::remove_file(path); + if force_gc { + let log = String::from_utf8_lossy(&output.stderr); + std::fs::write( + std::env::temp_dir().join(format!("psrs-control-{name}-gc.log")), + &output.stderr, + ) + .expect("retain collection log"); + assert!( + log.contains("After collection, GC heap's allocated bytes"), + "no observed collection: {log}" + ); + } + assert_eq!( + output.status.code(), + Some(expected), + "expected {name} to exit {expected}, got {output:?}" + ); + if expected == 134 { + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("wasm trap:") && !stderr.contains("cast failure"), + "expected a guest trap without an invalid machine representation in {name}, got: {stderr}" + ); + } +} + +/// Preserve coverage of user-defined open state folds on the revised primitive. +/// This is an ordinary library program, not a legacy compiler overload. Scoped +/// deep/shallow semantics are tested separately in control_scoped_runtime.rs. +pub fn fold_source(source: &str) -> String { + if !source.contains("foldOpen") { + return source.to_owned(); + } + format!( + "{source}\n{}", + r" +foldOpen :: forall f g u a s. s -> (forall x. s -> f x -> (x -> C.Control u a) -> { _1 :: s, _2 :: C.Control u a }) -> (forall x r. u x -> (f x -> r) -> (g x -> r) -> r) -> (forall x. g x -> u x) -> C.Control u a -> C.Control g { _1 :: s, _2 :: a } +foldOpen state clause project inject computation = C.handle + (\value -> C.return { _1: state, _2: value }) + (\request raw -> project request + (\op -> let next = clause state op raw in foldOpen next._1 clause project inject next._2) + (\op -> C.bind (C.perform op) (\answer -> foldOpen state clause project inject (raw answer)))) + computation +" + ) +} diff --git a/crates/psrs-driver/tests/control_support/nominal_carriers.purs b/crates/psrs-driver/tests/control_support/nominal_carriers.purs new file mode 100644 index 00000000..d19ad6f5 --- /dev/null +++ b/crates/psrs-driver/tests/control_support/nominal_carriers.purs @@ -0,0 +1,25 @@ +module Main where +import Prim.Control as C +data Op next = Op (Int -> next) +data Box a = Box a +unbox :: forall a. Box a -> a +unbox (Box value) = value +keep :: forall f a. C.Control f a -> C.Control f a +keep value = value +retain :: forall f x a. C.Resumption f x a -> C.Resumption f x a +retain value = value +apply :: forall a b. (a -> b) -> a -> b +apply function value = function value +choose :: forall f a. Boolean -> C.Control f a -> C.Control f a -> C.Control f a +choose flag left right = if flag then left else right +clause :: forall x. Op x -> (x -> C.Control Op Int) -> C.Control Op Int +clause (Op k) raw = raw (k 40) +handler :: forall x. Int -> Op x -> (x -> C.Control Op Int) -> { _1 :: Int, _2 :: C.Control Op Int } +handler state (Op k) raw = { _1: state, _2: raw (k 0) } +main :: Int +main = let owner = retain (apply unbox (Box (C.once (\n -> C.return (intAdd n 2))))) + spare = C.once (\n -> C.return n) + cancelled = C.cancel spare + saved = { owner: owner, computation: C.perform (Op \x -> x) } + program = C.bind (choose true (apply keep saved.computation) (C.return 0)) (\n -> C.resume saved.owner n) + in (C.run 0 handler (C.handle (\n -> C.return n) clause program))._2 diff --git a/crates/psrs-driver/tests/control_support/scoped_replay.purs b/crates/psrs-driver/tests/control_support/scoped_replay.purs new file mode 100644 index 00000000..a9d60887 --- /dev/null +++ b/crates/psrs-driver/tests/control_support/scoped_replay.purs @@ -0,0 +1,23 @@ +module Main where +import Prim.Control as C +data F x = F Int (Int -> x) +data G x = G (Int -> x) +type Answer = { saved :: Int -> C.Control F Int, built :: C.Control F Int, value :: Int } +ret :: Int -> C.Control G Answer +ret n = C.return { saved: \x -> C.return x, built: C.return n, value: n } +inner :: forall x. F x -> (x -> C.Control F Int) -> C.Control G Answer +inner (F label k) raw = if intEq label 1 + then C.bind (C.perform (G (\x -> x))) (\n -> loop (raw (k n))) + else C.return { saved: \n -> raw (k n), built: raw (k 42), value: 21 } +loop :: C.Control F Int -> C.Control G Answer +loop = C.handle ret inner +forward :: forall x. G x -> (x -> C.Control G Answer) -> C.Control G Answer +forward request raw = C.bind (C.perform request) (\answer -> wrap (raw answer)) +wrap :: C.Control G Answer -> C.Control G Answer +wrap computation = C.handle C.return forward computation +root :: forall x. Int -> G x -> (x -> C.Control G Answer) -> { _1 :: Int, _2 :: C.Control G Answer } +root s (G k) raw = { _1: s, _2: C.bind (raw (k 0)) (\a -> AFTER) } +closed :: forall x. Int -> F x -> (x -> C.Control F Int) -> { _1 :: Int, _2 :: C.Control F Int } +closed s (F label k) raw = { _1: s, _2: raw (k 42) } +program = C.bind (C.perform (F 1 (\x -> x))) (\_ -> C.perform (F 2 (\x -> x))) +main = (C.run 0 root (wrap (loop program)))._2.value diff --git a/crates/psrs-driver/tests/control_surface.rs b/crates/psrs-driver/tests/control_surface.rs new file mode 100644 index 00000000..94f69282 --- /dev/null +++ b/crates/psrs-driver/tests/control_surface.rs @@ -0,0 +1,190 @@ +//! Frontend acceptance for the compiler-owned delimited-control surface. +//! +//! `Prim.Control` exposes the effect-agnostic `Control f a` carrier with +//! `return`/`bind`/`perform`/`run`/`handle` ([DEC-19](../../../docs/decision/DEC-19-compiler-generated-resumable-frames.md), +//! [delimited control and resumable frames](../../../docs/design/backend/fp/delimited-control-and-resumable-frames.md)). +//! Source acceptance/rejection and focused executor execution. +//! Open execution is covered in `control_open_runtime.rs`. + +/// A well-typed program over an opaque request functor, interpreted by the +/// closed state-threading `run`. `Op`'s answer type is `Int`, and the handler +/// threads an `Int` state, so `run 0 handler program` is `{ _1 :: Int, _2 :: Int }`. +const ACCEPTS: &str = r#"module Main where + +import Prim.Control as C + +data Op next = Op (Int -> next) + +program :: C.Control Op Int +program = C.perform (Op \x -> x) + +runProgram :: { _1 :: Int, _2 :: Int } +runProgram = C.run 0 handler program + where + handler :: forall x. Int -> Op x -> (x -> C.Control Op Int) -> { _1 :: Int, _2 :: C.Control Op Int } + handler s (Op k) cont = { _1: s, _2: cont (k s) } + +main :: Int +main = runProgram._2 +"#; + +/// A `Control f a` computation is not interchangeable with its result type. +const REJECTS_RESULT_TYPE: &str = r#"module Main where + +import Prim.Control as C + +data Op next = Op (Int -> next) + +program :: C.Control Op Int +program = C.perform (Op \x -> x) + +bad :: Int +bad = program +"#; + +/// `run`'s handler must be rank-2: a handler specialized to one answer type +/// cannot satisfy the polymorphic argument. +const REJECTS_MONOMORPHIC_HANDLER: &str = r#"module Main where + +import Prim.Control as C + +data Op next = Op (Int -> next) + +program :: C.Control Op Int +program = C.perform (Op \x -> x) + +mono :: Int -> Op Int -> (Int -> C.Control Op Int) -> { _1 :: Int, _2 :: C.Control Op Int } +mono s (Op k) cont = { _1: s, _2: cont (k s) } + +bad :: { _1 :: Int, _2 :: Int } +bad = C.run 0 mono program +"#; + +/// The open eliminator removes one effect: `handle` interprets the `F` requests +/// of a computation over the combined effect `U` and leaves the `G` requests as a +/// residual `Control G`. +const HANDLES_ONE_EFFECT: &str = r#"module Main where + +import Prim.Control as C + +data F next = F (Int -> next) +data G next = G (Int -> next) +data U next = UF (F next) | UG (G next) + +project :: forall x r. U x -> (F x -> r) -> (G x -> r) -> r +project u onF onG = case u of + UF f -> onF f + UG g -> onG g + +inject :: forall x. G x -> U x +inject = UG + +handler :: forall x. Int -> F x -> (x -> C.Control U Int) -> { _1 :: Int, _2 :: C.Control U Int } +handler s (F k) cont = { _1: s, _2: cont (k s) } + +program :: C.Control U Int +program = C.bind (C.perform (UF (F \x -> x))) \_ -> C.perform (UG (G \x -> x)) + +handled :: C.Control G { _1 :: Int, _2 :: Int } +handled = foldOpen 0 handler project inject program + +main :: Int +main = 0 +"#; + +mod control_support; +use control_support::execute; + +/// The `return`-only path lowers to a frame and executes: `main` reads the +/// `(state, value)` result of the closed `run`. +const RETURNS_AND_RUNS: &str = r#"module Main where + +import Prim.Control as C + +data Op next = Op (Int -> next) + +program :: C.Control Op Int +program = C.return 42 + +result :: { _1 :: Int, _2 :: Int } +result = C.run 0 handler program + where + handler :: forall x. Int -> Op x -> (x -> C.Control Op Int) -> { _1 :: Int, _2 :: C.Control Op Int } + handler s (Op k) cont = { _1: s, _2: cont (k s) } + +main :: Int +main = result._2 +"#; + +#[test] +fn control_surface_typechecks() { + if let Err(errors) = psrs_driver::check_source("Main.purs", ACCEPTS) { + panic!("control surface rejected: {errors:?}"); + } +} + +#[test] +fn executes_a_returning_computation() { + execute("return", RETURNS_AND_RUNS, 42); +} + +#[test] +fn executes_a_performed_request() { + execute("perform", &ACCEPTS.replace("C.run 0", "C.run 42"), 42); +} + +#[test] +fn control_surface_handles_one_effect() { + if let Err(errors) = psrs_driver::check_source( + "Main.purs", + &control_support::fold_source(HANDLES_ONE_EFFECT), + ) { + panic!("open handle rejected: {errors:?}"); + } +} + +#[test] +fn control_surface_rejects_result_type_confusion() { + assert!( + psrs_driver::check_source("Main.purs", REJECTS_RESULT_TYPE).is_err(), + "a computation used as its own result type must be rejected" + ); +} + +#[test] +fn control_surface_requires_a_rank_two_handler() { + assert!( + psrs_driver::check_source("Main.purs", REJECTS_MONOMORPHIC_HANDLER).is_err(), + "a monomorphic handler must not satisfy run's rank-2 argument" + ); +} + +#[test] +fn open_control_rejects_an_incompatible_residual_functor() { + let source = HANDLES_ONE_EFFECT.replace("handled :: C.Control G", "handled :: C.Control F"); + assert!( + psrs_driver::check_source("Main.purs", &control_support::fold_source(&source)).is_err() + ); +} + +#[test] +fn library_fold_rejects_an_incompatible_injection() { + let source = HANDLES_ONE_EFFECT.replace( + "inject :: forall x. G x -> U x\ninject = UG", + "inject :: forall x. F x -> U x\ninject = UF", + ); + assert!( + psrs_driver::check_source("Main.purs", &control_support::fold_source(&source)).is_err() + ); +} + +#[test] +fn library_fold_requires_a_rank_two_projection() { + let source = HANDLES_ONE_EFFECT.replace( + "project :: forall x r. U x -> (F x -> r) -> (G x -> r) -> r", + "project :: forall r. U Int -> (F Int -> r) -> (G Int -> r) -> r", + ); + assert!( + psrs_driver::check_source("Main.purs", &control_support::fold_source(&source)).is_err() + ); +} diff --git a/crates/psrs-driver/tests/effect_representation.rs b/crates/psrs-driver/tests/effect_representation.rs new file mode 100644 index 00000000..b767e111 --- /dev/null +++ b/crates/psrs-driver/tests/effect_representation.rs @@ -0,0 +1,115 @@ +//! Prototype acceptance for the library-owned algebraic-effects representation. +//! +//! Before the native handler mechanism selected by +//! [DEC-19](../../../docs/decision/DEC-19-compiler-generated-resumable-frames.md) +//! is lowered, the [algebraic-effects design](../../../docs/design/backend/fp/algebraic-effects-and-handlers.md) +//! requires validating the shared generic representation: a rank-N +//! heterogeneous request transport and the common computation type the official +//! Effect/ST facades share. This test pins that representation and its pure +//! interpretation. It does not claim the compiler-generated resumable-frame +//! mechanism, and it is not standard-library migration evidence. + +use std::process::Command; + +/// A rank-N eliminator whose method is itself rank-1 polymorphic transports a +/// typed payload and continuation. `Effect` and `ST` wrap the same `Free` field, +/// and `toEffect = unsafeCoerce` is the common Global conversion. +const COMMON_REPRESENTATION: &str = r#"module Main where + +import Unsafe.Coerce (unsafeCoerce) + +data Request next = Request + (forall z. { run :: forall a. a -> (a -> next) -> z } -> z) + +data Free a = Pure a | Step (Request (Free a)) + +newtype Effect a = Effect (Free a) +newtype ST a = ST (Free a) + +toEffect :: forall a. ST a -> Effect a +toEffect = unsafeCoerce + +mk :: forall a. a -> Request (Free a) +mk value = Request \ops -> ops.run value Pure + +step :: forall a. a -> Free a +step value = Step (mk value) + +interp :: forall a. Free a -> a +interp (Pure a) = a +interp (Step k) = case k of + Request f -> interp (f { run: \x next -> next x }) + +stProg :: ST Int +stProg = ST (step 42) + +main :: Int +main = case toEffect stProg of + Effect f -> interp f +"#; + +/// A request transports its payload and continuation at one result type; using +/// the request itself where its result type is wanted must be rejected. +const REQUEST_IS_NOT_ITS_RESULT: &str = r#"module Main where + +data Request next = Request + (forall z. { run :: forall a. a -> (a -> next) -> z } -> z) + +data Free a = Pure a | Step (Request (Free a)) + +mk :: forall a. a -> Request (Free a) +mk value = Request \ops -> ops.run value Pure + +bad :: Int +bad = Step (mk 42) +"#; + +#[test] +fn common_representation_checks() { + if let Err(errors) = psrs_driver::check_source("Main.purs", COMMON_REPRESENTATION) { + panic!("common representation rejected: {errors:?}"); + } +} + +#[test] +fn a_request_is_not_interchangeable_with_its_result() { + assert!( + psrs_driver::check_source("Main.purs", REQUEST_IS_NOT_ITS_RESULT).is_err(), + "a request used as its own result type must be rejected" + ); +} + +#[test] +fn executes_a_pure_handler_over_the_shared_representation() { + let available = Command::new("wasmtime") + .arg("--version") + .output() + .is_ok_and(|output| output.status.success()); + if !available { + assert_ne!( + std::env::var("PSRS_REQUIRE_WASMTIME").as_deref(), + Ok("1"), + "the pure-handler execution requires wasmtime" + ); + eprintln!("skipping effect-representation execution: wasmtime is unavailable"); + return; + } + let artifact = psrs_driver::compile_source("Main.purs", COMMON_REPRESENTATION) + .expect("compile the common representation"); + let path = std::env::temp_dir().join(format!( + "psrs-effect-representation-{}.wasm", + std::process::id() + )); + std::fs::write(&path, &artifact.wasm).expect("write component"); + let output = Command::new("wasmtime") + .arg("run") + .arg(&path) + .output() + .expect("run component"); + let _ = std::fs::remove_file(path); + assert_eq!( + output.status.code(), + Some(42), + "expected the pure handler to return 42, got {output:?}" + ); +} diff --git a/crates/psrs-driver/tests/fixtures/stdlib-array-bind/Golden.purs b/crates/psrs-driver/tests/fixtures/stdlib-array-bind/Golden.purs new file mode 100644 index 00000000..0ef6c1e8 --- /dev/null +++ b/crates/psrs-driver/tests/fixtures/stdlib-array-bind/Golden.purs @@ -0,0 +1,4 @@ +module Golden where +import PSRS.Array as Target.Array +arrayBind :: forall a b. Array a -> (a -> Array b) -> Array b +arrayBind = Target.Array.arrayBind diff --git a/crates/psrs-driver/tests/fixtures/stdlib-array-bind/Main.purs b/crates/psrs-driver/tests/fixtures/stdlib-array-bind/Main.purs new file mode 100644 index 00000000..a9864abc --- /dev/null +++ b/crates/psrs-driver/tests/fixtures/stdlib-array-bind/Main.purs @@ -0,0 +1,16 @@ +module Main where +import Golden as Golden +main = + let + r0 = Golden.arrayBind [4, 0, 9] ((\offset -> \x -> if intEq x 0 then [] else [intAdd offset x, intMul x 2]) 1) + r1 = Golden.arrayBind [4, 8] (\x -> [numberAdd (intToNumber x) 0.5]) + r2 = Golden.arrayBind [40] (\x -> [{ value: intAdd x 2 }]) + r3 = Golden.arrayBind [1, 2] (\x -> [[x], [intAdd x 10]]) + r4 = Golden.arrayBind [1, 2] (\x -> [if intEq x 1 then "λ" else "😀"]) + r5 = Golden.arrayBind [] (\x -> [arrayIndex ([] :: Array Int) x]) + r6 = Golden.arrayBind [1, 2] (\x -> ([] :: Array Int)) + curried = Golden.arrayBind [40] (\x -> [\y -> intAdd x y]) + shared = arrayFill 2 0 + mutate x = let first = arrayWrite shared 0 x in arrayWrite first 1 (intAdd (arrayIndex first 1) 1) + observed = Golden.arrayBind [10,20] mutate + in if (booleanAnd (booleanAnd (booleanAnd (booleanAnd (booleanAnd (intEq (arrayLength r0) 4) (intEq (arrayIndex r0 0) 5)) (booleanAnd (intEq (arrayIndex r0 1) 8) (intEq (arrayIndex r0 2) 10))) (booleanAnd (booleanAnd (intEq (arrayIndex r0 3) 18) (intEq (arrayLength r1) 2)) (booleanAnd (numberEq (arrayIndex r1 0) 4.5) (booleanAnd (numberEq (arrayIndex r1 1) 8.5) (intEq (arrayLength r2) 1))))) (booleanAnd (booleanAnd (booleanAnd (intEq ((arrayIndex r2 0)).value 42) (intEq (arrayLength r3) 4)) (booleanAnd (intEq (arrayLength (arrayIndex r3 0)) 1) (intEq (arrayIndex (arrayIndex r3 0) 0) 1))) (booleanAnd (booleanAnd (intEq (arrayLength (arrayIndex r3 1)) 1) (intEq (arrayIndex (arrayIndex r3 1) 0) 11)) (booleanAnd (intEq (arrayLength (arrayIndex r3 2)) 1) (booleanAnd (intEq (arrayIndex (arrayIndex r3 2) 0) 2) (intEq (arrayLength (arrayIndex r3 3)) 1)))))) (booleanAnd (booleanAnd (booleanAnd (booleanAnd (intEq (arrayIndex (arrayIndex r3 3) 0) 12) (intEq (arrayLength r4) 2)) (booleanAnd (intEq (arrayLength (stringToBytes (arrayIndex r4 0))) 2) (intEq (arrayIndex (stringToBytes (arrayIndex r4 0)) 0) 206))) (booleanAnd (booleanAnd (intEq (arrayIndex (stringToBytes (arrayIndex r4 0)) 1) 187) (intEq (arrayLength (stringToBytes (arrayIndex r4 1))) 4)) (booleanAnd (intEq (arrayIndex (stringToBytes (arrayIndex r4 1)) 0) 240) (booleanAnd (intEq (arrayIndex (stringToBytes (arrayIndex r4 1)) 1) 159) (intEq (arrayIndex (stringToBytes (arrayIndex r4 1)) 2) 152))))) (booleanAnd (booleanAnd (booleanAnd (intEq (arrayIndex (stringToBytes (arrayIndex r4 1)) 3) 128) (intEq (arrayLength r5) 0)) (booleanAnd (intEq (arrayLength r6) 0) (booleanAnd (intEq ((arrayIndex curried 0) 2) 42) (intEq (arrayLength observed) 4)))) (booleanAnd (booleanAnd (intEq (arrayIndex observed 0) 10) (intEq (arrayIndex observed 1) 1)) (booleanAnd (intEq (arrayIndex observed 2) 20) (booleanAnd (intEq (arrayIndex observed 3) 2) (intEq (arrayIndex shared 1) 2))))))) then 42 else 1 diff --git a/crates/psrs-driver/tests/fixtures/stdlib-array-bind/observations.json b/crates/psrs-driver/tests/fixtures/stdlib-array-bind/observations.json new file mode 100644 index 00000000..f4ce4bf3 --- /dev/null +++ b/crates/psrs-driver/tests/fixtures/stdlib-array-bind/observations.json @@ -0,0 +1,102 @@ +{ + "node": "v26.10.0", + "upstream_commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "official_js_sha256": "f05d5b14753a4b190b448fda0bfac66f97765bf5835f55c8e9c22dcbe5188543", + "observations": [ + { + "name": "variable_chunks_and_capture", + "values": [ + 4, + 0, + 9 + ], + "result": [ + 5, + 8, + 10, + 18 + ] + }, + { + "name": "number_results", + "values": [ + 4, + 8 + ], + "result": [ + 4.5, + 8.5 + ] + }, + { + "name": "records", + "values": [ + 40 + ], + "result": [ + { + "value": 42 + } + ] + }, + { + "name": "nested_arrays", + "values": [ + 1, + 2 + ], + "result": [ + [ + 1 + ], + [ + 11 + ], + [ + 2 + ], + [ + 12 + ] + ] + }, + { + "name": "utf8_results", + "values": [ + 1, + 2 + ], + "result": [ + "λ", + "😀" + ] + }, + { + "name": "empty_input", + "values": [], + "result": [] + }, + { + "name": "empty_chunks", + "values": [ + 1, + 2 + ], + "result": [] + }, + { + "name": "returned_function", + "result": 42 + }, + { + "name": "callback_count_and_shared_chunk_snapshot", + "result": [ + 10, + 1, + 20, + 2 + ], + "calls": 2 + } + ] +} diff --git a/crates/psrs-driver/tests/fixtures/stdlib-array-callbacks/Main.purs b/crates/psrs-driver/tests/fixtures/stdlib-array-callbacks/Main.purs new file mode 100644 index 00000000..550573f8 --- /dev/null +++ b/crates/psrs-driver/tests/fixtures/stdlib-array-callbacks/Main.purs @@ -0,0 +1,53 @@ +module Main where +import PSRS.Array as A +main = + let + state0 = arrayFill 2 0 + visit0 x = let counted = arrayWrite state0 0 (intAdd (arrayIndex state0 0) 1) in arrayWrite counted 1 (intAdd (intMul (arrayIndex counted 1) 10) x) + result0 = A.filterImpl (\x -> let next = visit0 x in if intLt 0 (arrayIndex next 0) then intLt 1 x else false) ([1, 2, 3] :: Array Int) + state1 = arrayFill 2 0 + visit1 x = let counted = arrayWrite state1 0 (intAdd (arrayIndex state1 0) 1) in arrayWrite counted 1 (intAdd (intMul (arrayIndex counted 1) 10) x) + result1 = A.filterImpl (\x -> let next = visit1 x in if intLt 0 (arrayIndex next 0) then intLt 0 x else false) ([1, 2, 3] :: Array Int) + state2 = arrayFill 2 0 + visit2 x = let counted = arrayWrite state2 0 (intAdd (arrayIndex state2 0) 1) in arrayWrite counted 1 (intAdd (intMul (arrayIndex counted 1) 10) x) + result2 = A.filterImpl (\x -> let next = visit2 x in if intLt 0 (arrayIndex next 0) then intLt 9 x else false) ([1, 2, 3] :: Array Int) + state3 = arrayFill 2 0 + visit3 x = let counted = arrayWrite state3 0 (intAdd (arrayIndex state3 0) 1) in arrayWrite counted 1 (intAdd (intMul (arrayIndex counted 1) 10) x) + result3 = A.filterImpl (\x -> let next = visit3 x in if intLt 0 (arrayIndex next 0) then intLt 0 x else false) ([] :: Array Int) + state4 = arrayFill 2 0 + visit4 x = let counted = arrayWrite state4 0 (intAdd (arrayIndex state4 0) 1) in arrayWrite counted 1 (intAdd (intMul (arrayIndex counted 1) 10) x) + result4 = A.partitionImpl (\x -> let next = visit4 x in if intLt 0 (arrayIndex next 0) then intLt 1 x else false) ([1, 2, 3] :: Array Int) + state5 = arrayFill 2 0 + visit5 x = let counted = arrayWrite state5 0 (intAdd (arrayIndex state5 0) 1) in arrayWrite counted 1 (intAdd (intMul (arrayIndex counted 1) 10) x) + result5 = A.partitionImpl (\x -> let next = visit5 x in if intLt 0 (arrayIndex next 0) then intLt 0 x else false) ([1, 2, 3] :: Array Int) + state6 = arrayFill 2 0 + visit6 x = let counted = arrayWrite state6 0 (intAdd (arrayIndex state6 0) 1) in arrayWrite counted 1 (intAdd (intMul (arrayIndex counted 1) 10) x) + result6 = A.partitionImpl (\x -> let next = visit6 x in if intLt 0 (arrayIndex next 0) then intLt 9 x else false) ([1, 2, 3] :: Array Int) + state7 = arrayFill 2 0 + visit7 x = let counted = arrayWrite state7 0 (intAdd (arrayIndex state7 0) 1) in arrayWrite counted 1 (intAdd (intMul (arrayIndex counted 1) 10) x) + result7 = A.partitionImpl (\x -> let next = visit7 x in if intLt 0 (arrayIndex next 0) then intLt 0 x else false) ([] :: Array Int) + state8 = arrayFill 2 0 + visit8 x = let counted = arrayWrite state8 0 (intAdd (arrayIndex state8 0) 1) in arrayWrite counted 1 (intAdd (intMul (arrayIndex counted 1) 10) x) + result8 = A.zipWithImpl (\x y -> let next = visit8 x in intAdd (intMul (arrayIndex next 0) 0) (intAdd x y)) ([1, 2, 3] :: Array Int) ([10, 20] :: Array Int) + state9 = arrayFill 2 0 + visit9 x = let counted = arrayWrite state9 0 (intAdd (arrayIndex state9 0) 1) in arrayWrite counted 1 (intAdd (intMul (arrayIndex counted 1) 10) x) + result9 = A.zipWithImpl (\x y -> let next = visit9 x in intAdd (intMul (arrayIndex next 0) 0) (intAdd x y)) ([1] :: Array Int) ([10, 20] :: Array Int) + state10 = arrayFill 2 0 + visit10 x = let counted = arrayWrite state10 0 (intAdd (arrayIndex state10 0) 1) in arrayWrite counted 1 (intAdd (intMul (arrayIndex counted 1) 10) x) + result10 = A.zipWithImpl (\x y -> let next = visit10 x in intAdd (intMul (arrayIndex next 0) 0) (intAdd x y)) ([] :: Array Int) ([10] :: Array Int) + state11 = arrayFill 2 0 + visit11 x = let counted = arrayWrite state11 0 (intAdd (arrayIndex state11 0) 1) in arrayWrite counted 1 (intAdd (intMul (arrayIndex counted 1) 10) x) + result11 = A.zipWithImpl (\x y -> let next = visit11 x in intAdd (intMul (arrayIndex next 0) 0) (intAdd x y)) ([1] :: Array Int) ([] :: Array Int) + state12 = arrayFill 2 0 + visit12 x = let counted = arrayWrite state12 0 (intAdd (arrayIndex state12 0) 1) in arrayWrite counted 1 (intAdd (intMul (arrayIndex counted 1) 10) x) + result12 = A.anyImpl (\x -> let next = visit12 x in if intLt 0 (arrayIndex next 0) then intLt 1 x else false) ([1, 2, 3] :: Array Int) + state13 = arrayFill 2 0 + visit13 x = let counted = arrayWrite state13 0 (intAdd (arrayIndex state13 0) 1) in arrayWrite counted 1 (intAdd (intMul (arrayIndex counted 1) 10) x) + result13 = A.allImpl (\x -> let next = visit13 x in if intLt 0 (arrayIndex next 0) then intLt 1 x else false) ([3, 1, 2] :: Array Int) + state14 = arrayFill 2 0 + visit14 x = let counted = arrayWrite state14 0 (intAdd (arrayIndex state14 0) 1) in arrayWrite counted 1 (intAdd (intMul (arrayIndex counted 1) 10) x) + result14 = A.anyImpl (\x -> let next = visit14 x in if intLt 0 (arrayIndex next 0) then intLt 0 x else false) ([] :: Array Int) + state15 = arrayFill 2 0 + visit15 x = let counted = arrayWrite state15 0 (intAdd (arrayIndex state15 0) 1) in arrayWrite counted 1 (intAdd (intMul (arrayIndex counted 1) 10) x) + result15 = A.allImpl (\x -> let next = visit15 x in if intLt 0 (arrayIndex next 0) then intLt 0 x else false) ([] :: Array Int) + in if (booleanAnd (booleanAnd (booleanAnd (booleanAnd (booleanAnd (booleanAnd (intEq (arrayLength result0) 2) (intEq (arrayIndex result0 0) 2)) (booleanAnd (intEq (arrayIndex result0 1) 3) (intEq (arrayIndex state0 0) 3))) (booleanAnd (booleanAnd (intEq (arrayIndex state0 1) 123) (intEq (arrayLength result1) 3)) (booleanAnd (intEq (arrayIndex result1 0) 1) (intEq (arrayIndex result1 1) 2)))) (booleanAnd (booleanAnd (booleanAnd (intEq (arrayIndex result1 2) 3) (intEq (arrayIndex state1 0) 3)) (booleanAnd (intEq (arrayIndex state1 1) 123) (intEq (arrayLength result2) 0))) (booleanAnd (booleanAnd (intEq (arrayIndex state2 0) 3) (intEq (arrayIndex state2 1) 123)) (booleanAnd (intEq (arrayLength result3) 0) (booleanAnd (intEq (arrayIndex state3 0) 0) (intEq (arrayIndex state3 1) 0)))))) (booleanAnd (booleanAnd (booleanAnd (booleanAnd (intEq (arrayLength (result4).yes) 2) (intEq (arrayIndex (result4).yes 0) 2)) (booleanAnd (intEq (arrayIndex (result4).yes 1) 3) (intEq (arrayLength (result4).no) 1))) (booleanAnd (booleanAnd (intEq (arrayIndex (result4).no 0) 1) (intEq (arrayIndex state4 0) 3)) (booleanAnd (intEq (arrayIndex state4 1) 123) (intEq (arrayLength (result5).yes) 3)))) (booleanAnd (booleanAnd (booleanAnd (intEq (arrayIndex (result5).yes 0) 1) (intEq (arrayIndex (result5).yes 1) 2)) (booleanAnd (intEq (arrayIndex (result5).yes 2) 3) (intEq (arrayLength (result5).no) 0))) (booleanAnd (booleanAnd (intEq (arrayIndex state5 0) 3) (intEq (arrayIndex state5 1) 123)) (booleanAnd (intEq (arrayLength (result6).yes) 0) (booleanAnd (intEq (arrayLength (result6).no) 3) (intEq (arrayIndex (result6).no 0) 1))))))) (booleanAnd (booleanAnd (booleanAnd (booleanAnd (booleanAnd (intEq (arrayIndex (result6).no 1) 2) (intEq (arrayIndex (result6).no 2) 3)) (booleanAnd (intEq (arrayIndex state6 0) 3) (intEq (arrayIndex state6 1) 123))) (booleanAnd (booleanAnd (intEq (arrayLength (result7).yes) 0) (intEq (arrayLength (result7).no) 0)) (booleanAnd (intEq (arrayIndex state7 0) 0) (intEq (arrayIndex state7 1) 0)))) (booleanAnd (booleanAnd (booleanAnd (intEq (arrayLength result8) 2) (intEq (arrayIndex result8 0) 11)) (booleanAnd (intEq (arrayIndex result8 1) 22) (intEq (arrayIndex state8 0) 2))) (booleanAnd (booleanAnd (intEq (arrayIndex state8 1) 12) (intEq (arrayLength result9) 1)) (booleanAnd (intEq (arrayIndex result9 0) 11) (booleanAnd (intEq (arrayIndex state9 0) 1) (intEq (arrayIndex state9 1) 1)))))) (booleanAnd (booleanAnd (booleanAnd (booleanAnd (intEq (arrayLength result10) 0) (intEq (arrayIndex state10 0) 0)) (booleanAnd (intEq (arrayIndex state10 1) 0) (intEq (arrayLength result11) 0))) (booleanAnd (booleanAnd (intEq (arrayIndex state11 0) 0) (intEq (arrayIndex state11 1) 0)) (booleanAnd (booleanEq result12 true) (booleanAnd (intEq (arrayIndex state12 0) 2) (intEq (arrayIndex state12 1) 12))))) (booleanAnd (booleanAnd (booleanAnd (booleanEq result13 false) (intEq (arrayIndex state13 0) 2)) (booleanAnd (intEq (arrayIndex state13 1) 31) (booleanEq result14 false))) (booleanAnd (booleanAnd (intEq (arrayIndex state14 0) 0) (intEq (arrayIndex state14 1) 0)) (booleanAnd (booleanEq result15 true) (booleanAnd (intEq (arrayIndex state15 0) 0) (intEq (arrayIndex state15 1) 0)))))))) then 42 else 1 diff --git a/crates/psrs-driver/tests/fixtures/stdlib-array-extend/Golden.purs b/crates/psrs-driver/tests/fixtures/stdlib-array-extend/Golden.purs new file mode 100644 index 00000000..c2c49899 --- /dev/null +++ b/crates/psrs-driver/tests/fixtures/stdlib-array-extend/Golden.purs @@ -0,0 +1,4 @@ +module Golden where +import PSRS.Array as Target.Array +arrayExtend :: forall a b. (Array a -> b) -> Array a -> Array b +arrayExtend = Target.Array.arrayExtend diff --git a/crates/psrs-driver/tests/fixtures/stdlib-array-extend/Main.purs b/crates/psrs-driver/tests/fixtures/stdlib-array-extend/Main.purs new file mode 100644 index 00000000..a02f58dd --- /dev/null +++ b/crates/psrs-driver/tests/fixtures/stdlib-array-extend/Main.purs @@ -0,0 +1,18 @@ +module Main where +import Golden as Golden +main = + let + r0 = Golden.arrayExtend (\suffix -> intAdd (arrayIndex suffix 0) (arrayLength suffix)) [10, 20, 30] + r1 = Golden.arrayExtend (\suffix -> numberAdd (intToNumber (arrayIndex suffix 0)) 0.5) [4, 8] + r2 = Golden.arrayExtend (\suffix -> { value: intAdd ((arrayIndex suffix 0).value) 2 }) [{ value: 40 }] + r3 = Golden.arrayExtend (\suffix -> [arrayLength suffix, arrayIndex (arrayIndex suffix 0) 0]) [[1], [2]] + r4 = Golden.arrayExtend (\suffix -> if intEq (arrayLength suffix) 2 then "λ" else "😀") [1, 2] + r5 = Golden.arrayExtend (\suffix -> arrayIndex ([] :: Array Int) 0) [] + closures = Golden.arrayExtend (\suffix -> \n -> intAdd (arrayIndex suffix 0) n) [40, 41] + counter = arrayFill 1 0 + bump suffix = let first = arrayWrite counter 0 (intAdd (arrayIndex counter 0) 1) in intAdd (arrayIndex first 0) (arrayLength suffix) + counted = Golden.arrayExtend bump [10, 20, 30] + sourceArray = [1, 2, 3] + scribble suffix = let first = arrayWrite suffix 0 (intAdd (arrayIndex suffix 0) 100) in arrayIndex first 0 + scribbled = Golden.arrayExtend scribble sourceArray + in if (booleanAnd (booleanAnd (booleanAnd (booleanAnd (booleanAnd (intEq (arrayLength r0) 3) (intEq (arrayIndex r0 0) 13)) (booleanAnd (intEq (arrayIndex r0 1) 22) (booleanAnd (intEq (arrayIndex r0 2) 31) (intEq (arrayLength r1) 2)))) (booleanAnd (booleanAnd (numberEq (arrayIndex r1 0) 4.5) (numberEq (arrayIndex r1 1) 8.5)) (booleanAnd (intEq (arrayLength r2) 1) (booleanAnd (intEq ((arrayIndex r2 0)).value 42) (intEq (arrayLength r3) 2))))) (booleanAnd (booleanAnd (booleanAnd (intEq (arrayLength (arrayIndex r3 0)) 2) (intEq (arrayIndex (arrayIndex r3 0) 0) 2)) (booleanAnd (intEq (arrayIndex (arrayIndex r3 0) 1) 1) (booleanAnd (intEq (arrayLength (arrayIndex r3 1)) 2) (intEq (arrayIndex (arrayIndex r3 1) 0) 1)))) (booleanAnd (booleanAnd (intEq (arrayIndex (arrayIndex r3 1) 1) 2) (intEq (arrayLength r4) 2)) (booleanAnd (intEq (arrayLength (stringToBytes (arrayIndex r4 0))) 2) (booleanAnd (intEq (arrayIndex (stringToBytes (arrayIndex r4 0)) 0) 206) (intEq (arrayIndex (stringToBytes (arrayIndex r4 0)) 1) 187)))))) (booleanAnd (booleanAnd (booleanAnd (booleanAnd (intEq (arrayLength (stringToBytes (arrayIndex r4 1))) 4) (intEq (arrayIndex (stringToBytes (arrayIndex r4 1)) 0) 240)) (booleanAnd (intEq (arrayIndex (stringToBytes (arrayIndex r4 1)) 1) 159) (booleanAnd (intEq (arrayIndex (stringToBytes (arrayIndex r4 1)) 2) 152) (intEq (arrayIndex (stringToBytes (arrayIndex r4 1)) 3) 128)))) (booleanAnd (booleanAnd (intEq (arrayLength r5) 0) (intEq ((arrayIndex closures 0) 2) 42)) (booleanAnd (intEq ((arrayIndex closures 1) 1) 42) (booleanAnd (intEq (arrayLength counted) 3) (intEq (arrayIndex counted 0) 4))))) (booleanAnd (booleanAnd (booleanAnd (intEq (arrayIndex counted 1) 4) (intEq (arrayIndex counted 2) 4)) (booleanAnd (intEq (arrayIndex counter 0) 3) (booleanAnd (intEq (arrayLength scribbled) 3) (intEq (arrayIndex scribbled 0) 101)))) (booleanAnd (booleanAnd (intEq (arrayIndex scribbled 1) 102) (booleanAnd (intEq (arrayIndex scribbled 2) 103) (intEq (arrayLength sourceArray) 3))) (booleanAnd (intEq (arrayIndex sourceArray 0) 1) (booleanAnd (intEq (arrayIndex sourceArray 1) 2) (intEq (arrayIndex sourceArray 2) 3))))))) then 42 else 1 diff --git a/crates/psrs-driver/tests/fixtures/stdlib-array-ops/Main.purs b/crates/psrs-driver/tests/fixtures/stdlib-array-ops/Main.purs new file mode 100644 index 00000000..4db55ffc --- /dev/null +++ b/crates/psrs-driver/tests/fixtures/stdlib-array-ops/Main.purs @@ -0,0 +1,70 @@ +module Main where +import PSRS.Array as A + +arrayFoldr :: forall a b. (a -> b -> b) -> b -> Array a -> b +arrayFoldr f b xs = go 0 + where + go i = if intLt i (A.lengthImpl xs) then f (A.unsafeIndexImpl xs i) (go (intAdd i 1)) else b + +c0 = A.rangeImpl 2 5 + +c1 = A.rangeImpl 5 2 + +c2 = A.rangeImpl 3 3 + +c3 = A.replicateImpl 3 7 + +c4 = A.replicateImpl 0 7 + +c5 = A.fromFoldableImpl arrayFoldr [1, 2, 3] + +c6 = A.lengthImpl [1, 2, 3] + +c7 = A.unconsImpl (\_ -> 0) (\x xs -> intAdd x (A.lengthImpl xs)) [1, 2, 3] + +c8 = A.unconsImpl (\_ -> 42) (\_ _ -> 0) ([] :: Array Int) + +c9 = A.reverseImpl [1, 2, 3] + +c10 = A.reverseImpl ([] :: Array Int) + +c11 = A.concatImpl [[1, 2], [], [3]] + +c12 = A.concatImpl ([] :: Array (Array Int)) + +c13 = A.filterImpl (\x -> intLt 1 x) [0, 2, 1, 3] + +c14 = A.filterImpl (\x -> intLt 9 x) [0, 2, 1, 3] + +c15 = A.partitionImpl (\x -> intLt 1 x) [0, 2, 1, 3] + +c16 = A.scanlImpl (\acc x -> intAdd acc x) 0 [1, 2, 3] + +c17 = A.scanlImpl (\acc x -> intAdd acc x) 0 ([] :: Array Int) + +c18 = A.scanrImpl (\x acc -> intAdd x acc) 0 [1, 2, 3] + +c19 = A.sortByImpl (\x y -> intSub x y) (\c -> c) [3, 1, 2] + +c20 = A.sortByImpl (\x y -> intSub x.key y.key) (\c -> c) [{ key: 1, id: 0 }, { key: 1, id: 1 }, { key: 0, id: 2 }] + +c21 = A.sliceImpl 1 3 [0, 1, 2, 3] + +c22 = A.sliceImpl (intSub 0 2) 4 [0, 1, 2, 3] + +c23 = A.sliceImpl 3 1 [0, 1, 2, 3] + +c24 = A.zipWithImpl (\x y -> intAdd x y) [1, 2, 3] [10, 20] + +c25 = A.anyImpl (\x -> intLt 2 x) [0, 1, 2, 3] + +c26 = A.anyImpl (\x -> intLt 2 x) [0, 1] + +c27 = A.allImpl (\x -> intLt 0 x) [1, 2] + +c28 = A.allImpl (\x -> intLt 0 x) [1, 0] + +c29 = A.unsafeIndexImpl [1, 2, 3] 1 + +main = + if (booleanAnd (booleanAnd (booleanAnd (booleanAnd (booleanAnd (booleanAnd (intEq (arrayLength c0) 4) (intEq (arrayIndex c0 0) 2)) (booleanAnd (intEq (arrayIndex c0 1) 3) (intEq (arrayIndex c0 2) 4))) (booleanAnd (booleanAnd (intEq (arrayIndex c0 3) 5) (intEq (arrayLength c1) 4)) (booleanAnd (intEq (arrayIndex c1 0) 5) (booleanAnd (intEq (arrayIndex c1 1) 4) (intEq (arrayIndex c1 2) 3))))) (booleanAnd (booleanAnd (booleanAnd (intEq (arrayIndex c1 3) 2) (intEq (arrayLength c2) 1)) (booleanAnd (intEq (arrayIndex c2 0) 3) (booleanAnd (intEq (arrayLength c3) 3) (intEq (arrayIndex c3 0) 7)))) (booleanAnd (booleanAnd (intEq (arrayIndex c3 1) 7) (intEq (arrayIndex c3 2) 7)) (booleanAnd (intEq (arrayLength c4) 0) (booleanAnd (intEq (arrayLength c5) 3) (intEq (arrayIndex c5 0) 1)))))) (booleanAnd (booleanAnd (booleanAnd (booleanAnd (intEq (arrayIndex c5 1) 2) (intEq (arrayIndex c5 2) 3)) (booleanAnd (intEq c6 3) (booleanAnd (intEq c7 3) (intEq c8 42)))) (booleanAnd (booleanAnd (intEq (arrayLength c9) 3) (intEq (arrayIndex c9 0) 3)) (booleanAnd (intEq (arrayIndex c9 1) 2) (booleanAnd (intEq (arrayIndex c9 2) 1) (intEq (arrayLength c10) 0))))) (booleanAnd (booleanAnd (booleanAnd (intEq (arrayLength c11) 3) (intEq (arrayIndex c11 0) 1)) (booleanAnd (intEq (arrayIndex c11 1) 2) (booleanAnd (intEq (arrayIndex c11 2) 3) (intEq (arrayLength c12) 0)))) (booleanAnd (booleanAnd (intEq (arrayLength c13) 2) (intEq (arrayIndex c13 0) 2)) (booleanAnd (intEq (arrayIndex c13 1) 3) (booleanAnd (intEq (arrayLength c14) 0) (intEq (arrayLength (c15).yes) 2))))))) (booleanAnd (booleanAnd (booleanAnd (booleanAnd (booleanAnd (intEq (arrayIndex (c15).yes 0) 2) (intEq (arrayIndex (c15).yes 1) 3)) (booleanAnd (intEq (arrayLength (c15).no) 2) (booleanAnd (intEq (arrayIndex (c15).no 0) 0) (intEq (arrayIndex (c15).no 1) 1)))) (booleanAnd (booleanAnd (intEq (arrayLength c16) 3) (intEq (arrayIndex c16 0) 1)) (booleanAnd (intEq (arrayIndex c16 1) 3) (booleanAnd (intEq (arrayIndex c16 2) 6) (intEq (arrayLength c17) 0))))) (booleanAnd (booleanAnd (booleanAnd (intEq (arrayLength c18) 3) (intEq (arrayIndex c18 0) 6)) (booleanAnd (intEq (arrayIndex c18 1) 5) (booleanAnd (intEq (arrayIndex c18 2) 3) (intEq (arrayLength c19) 3)))) (booleanAnd (booleanAnd (intEq (arrayIndex c19 0) 1) (intEq (arrayIndex c19 1) 2)) (booleanAnd (intEq (arrayIndex c19 2) 3) (booleanAnd (intEq (arrayLength c20) 3) (intEq ((arrayIndex c20 0)).key 0)))))) (booleanAnd (booleanAnd (booleanAnd (booleanAnd (intEq ((arrayIndex c20 0)).id 2) (intEq ((arrayIndex c20 1)).key 1)) (booleanAnd (intEq ((arrayIndex c20 1)).id 0) (booleanAnd (intEq ((arrayIndex c20 2)).key 1) (intEq ((arrayIndex c20 2)).id 1)))) (booleanAnd (booleanAnd (intEq (arrayLength c21) 2) (intEq (arrayIndex c21 0) 1)) (booleanAnd (intEq (arrayIndex c21 1) 2) (booleanAnd (intEq (arrayLength c22) 2) (intEq (arrayIndex c22 0) 2))))) (booleanAnd (booleanAnd (booleanAnd (intEq (arrayIndex c22 1) 3) (intEq (arrayLength c23) 0)) (booleanAnd (intEq (arrayLength c24) 2) (booleanAnd (intEq (arrayIndex c24 0) 11) (intEq (arrayIndex c24 1) 22)))) (booleanAnd (booleanAnd (booleanEq c25 true) (booleanEq c26 false)) (booleanAnd (booleanEq c27 true) (booleanAnd (booleanEq c28 false) (intEq c29 2)))))))) then 42 else 1 diff --git a/crates/psrs-driver/tests/fixtures/stdlib-array/Golden.purs b/crates/psrs-driver/tests/fixtures/stdlib-array/Golden.purs new file mode 100644 index 00000000..ed23a02d --- /dev/null +++ b/crates/psrs-driver/tests/fixtures/stdlib-array/Golden.purs @@ -0,0 +1,4 @@ +module Golden where +import PSRS.Array as Target.Array +arrayApply :: forall a b. Array (a -> b) -> Array a -> Array b +arrayApply = Target.Array.arrayApply diff --git a/crates/psrs-driver/tests/fixtures/stdlib-array/Main.purs b/crates/psrs-driver/tests/fixtures/stdlib-array/Main.purs new file mode 100644 index 00000000..59a6c07e --- /dev/null +++ b/crates/psrs-driver/tests/fixtures/stdlib-array/Main.purs @@ -0,0 +1,13 @@ +module Main where +import Golden as Golden +main = + let + r0 = Golden.arrayApply [((\offset -> \x -> intAdd offset x) 1), (\x -> intMul x 2)] [10, 20] + r1 = Golden.arrayApply [(\x -> numberAdd (intToNumber x) 0.5)] [4, 8] + r2 = Golden.arrayApply [(\x -> { value: intAdd x 2 })] [40] + r3 = Golden.arrayApply [(\x -> [x, intAdd x 1])] [5, 9] + r4 = Golden.arrayApply [(\x -> if intEq x 1 then "λ" else "😀")] [1, 2] + r5 = Golden.arrayApply [(\x -> arrayIndex ([] :: Array Int) x)] [] + r6 = Golden.arrayApply ([] :: Array (Int -> Int)) [1, 2] + curried = Golden.arrayApply [(\x -> \y -> intAdd x y)] [40] + in if (booleanAnd (booleanAnd (booleanAnd (booleanAnd (intEq (arrayLength r0) 4) (booleanAnd (intEq (arrayIndex r0 0) 11) (intEq (arrayIndex r0 1) 21))) (booleanAnd (booleanAnd (intEq (arrayIndex r0 2) 20) (intEq (arrayIndex r0 3) 40)) (booleanAnd (intEq (arrayLength r1) 2) (numberEq (arrayIndex r1 0) 4.5)))) (booleanAnd (booleanAnd (numberEq (arrayIndex r1 1) 8.5) (booleanAnd (intEq (arrayLength r2) 1) (intEq ((arrayIndex r2 0)).value 42))) (booleanAnd (booleanAnd (intEq (arrayLength r3) 2) (intEq (arrayLength (arrayIndex r3 0)) 2)) (booleanAnd (intEq (arrayIndex (arrayIndex r3 0) 0) 5) (intEq (arrayIndex (arrayIndex r3 0) 1) 6))))) (booleanAnd (booleanAnd (booleanAnd (intEq (arrayLength (arrayIndex r3 1)) 2) (booleanAnd (intEq (arrayIndex (arrayIndex r3 1) 0) 9) (intEq (arrayIndex (arrayIndex r3 1) 1) 10))) (booleanAnd (booleanAnd (intEq (arrayLength r4) 2) (intEq (arrayLength (stringToBytes (arrayIndex r4 0))) 2)) (booleanAnd (intEq (arrayIndex (stringToBytes (arrayIndex r4 0)) 0) 206) (intEq (arrayIndex (stringToBytes (arrayIndex r4 0)) 1) 187)))) (booleanAnd (booleanAnd (booleanAnd (intEq (arrayLength (stringToBytes (arrayIndex r4 1))) 4) (intEq (arrayIndex (stringToBytes (arrayIndex r4 1)) 0) 240)) (booleanAnd (intEq (arrayIndex (stringToBytes (arrayIndex r4 1)) 1) 159) (intEq (arrayIndex (stringToBytes (arrayIndex r4 1)) 2) 152))) (booleanAnd (booleanAnd (intEq (arrayIndex (stringToBytes (arrayIndex r4 1)) 3) 128) (intEq (arrayLength r5) 0)) (booleanAnd (intEq (arrayLength r6) 0) (intEq ((arrayIndex curried 0) 2) 42)))))) then 42 else 1 diff --git a/crates/psrs-driver/tests/fixtures/stdlib-array/observations.json b/crates/psrs-driver/tests/fixtures/stdlib-array/observations.json new file mode 100644 index 00000000..8838e0f6 --- /dev/null +++ b/crates/psrs-driver/tests/fixtures/stdlib-array/observations.json @@ -0,0 +1,87 @@ +{ + "node": "v26.10.0", + "upstream_commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "official_js_sha256": "ddebe14d1c07db99d7c2bf659d823c35fb65409c977be0f297be3215d5e01966", + "observations": [ + { + "name": "order_and_capture", + "values": [ + 10, + 20 + ], + "result": [ + 11, + 21, + 20, + 40 + ] + }, + { + "name": "number_results", + "values": [ + 4, + 8 + ], + "result": [ + 4.5, + 8.5 + ] + }, + { + "name": "records", + "values": [ + 40 + ], + "result": [ + { + "value": 42 + } + ] + }, + { + "name": "nested_arrays", + "values": [ + 5, + 9 + ], + "result": [ + [ + 5, + 6 + ], + [ + 9, + 10 + ] + ] + }, + { + "name": "utf8_results", + "values": [ + 1, + 2 + ], + "result": [ + "λ", + "😀" + ] + }, + { + "name": "empty_values", + "values": [], + "result": [] + }, + { + "name": "empty_functions", + "values": [ + 1, + 2 + ], + "result": [] + }, + { + "name": "returned_function", + "result": 42 + } + ] +} diff --git a/crates/psrs-driver/tests/fixtures/stdlib-scalar/Golden.purs b/crates/psrs-driver/tests/fixtures/stdlib-scalar/Golden.purs new file mode 100644 index 00000000..aff65a5b --- /dev/null +++ b/crates/psrs-driver/tests/fixtures/stdlib-scalar/Golden.purs @@ -0,0 +1,21 @@ +module Golden where +foreign import "psrs:intrinsic#intToNumber" toNumber :: Int -> Number +foreign import "psrs:intrinsic#intAnd" and :: Int -> Int -> Int +foreign import "psrs:intrinsic#intOr" or :: Int -> Int -> Int +foreign import "psrs:intrinsic#intXor" xor :: Int -> Int -> Int +foreign import "psrs:intrinsic#intShl" shl :: Int -> Int -> Int +foreign import "psrs:intrinsic#intShr" shr :: Int -> Int -> Int +foreign import "psrs:intrinsic#intZshr" zshr :: Int -> Int -> Int +foreign import "psrs:intrinsic#intComplement" complement :: Int -> Int +foreign import "psrs:intrinsic#booleanEq" eqBooleanImpl :: Boolean -> Boolean -> Boolean +foreign import "psrs:intrinsic#intEq" eqIntImpl :: Int -> Int -> Boolean +foreign import "psrs:intrinsic#numberEq" eqNumberImpl :: Number -> Number -> Boolean +foreign import "psrs:intrinsic#charEq" eqCharImpl :: Char -> Char -> Boolean +foreign import "psrs:intrinsic#intSub" intSub :: Int -> Int -> Int +foreign import "psrs:intrinsic#numberSub" numSub :: Number -> Number -> Number +foreign import "psrs:intrinsic#intAdd" intAdd :: Int -> Int -> Int +foreign import "psrs:intrinsic#numberAdd" numAdd :: Number -> Number -> Number +foreign import "psrs:intrinsic#numberMul" numMul :: Number -> Number -> Number +foreign import "psrs:intrinsic#booleanAnd" boolConj :: Boolean -> Boolean -> Boolean +foreign import "psrs:intrinsic#booleanOr" boolDisj :: Boolean -> Boolean -> Boolean +foreign import "psrs:intrinsic#booleanNot" boolNot :: Boolean -> Boolean diff --git a/crates/psrs-driver/tests/fixtures/stdlib-scalar/Main.purs b/crates/psrs-driver/tests/fixtures/stdlib-scalar/Main.purs new file mode 100644 index 00000000..5b683a2b --- /dev/null +++ b/crates/psrs-driver/tests/fixtures/stdlib-scalar/Main.purs @@ -0,0 +1,3 @@ +module Main where +import Golden as Golden +main = if (booleanAnd (booleanAnd (booleanAnd (booleanAnd (booleanAnd (numberEq (Golden.toNumber 42) 42.0) (numberEq (Golden.toNumber (intSub (intNeg 2147483647) 1)) (numberNeg 2147483648.0))) (booleanAnd (numberEq (Golden.toNumber 2147483647) 2147483647.0) (booleanAnd (intEq (Golden.and 63 42) 42) (intEq (Golden.and (intNeg 1) 42) 42)))) (booleanAnd (booleanAnd (intEq (Golden.or 32 10) 42) (booleanAnd (intEq (Golden.or (intNeg 1) 0) (intNeg 1)) (intEq (Golden.xor 63 21) 42))) (booleanAnd (intEq (Golden.xor (intNeg 1) 0) (intNeg 1)) (booleanAnd (intEq (Golden.shl 21 1) 42) (intEq (Golden.shl 21 33) 42))))) (booleanAnd (booleanAnd (booleanAnd (intEq (Golden.shl 1 (intNeg 1)) (intSub (intNeg 2147483647) 1)) (booleanAnd (intEq (Golden.shr (intNeg 84) 1) (intNeg 42)) (intEq (Golden.shr (intNeg 84) 33) (intNeg 42)))) (booleanAnd (intEq (Golden.shr 1 (intNeg 1)) 0) (booleanAnd (intEq (Golden.zshr (intNeg 1) 1) 2147483647) (intEq (Golden.zshr (intNeg 1) 0) (intNeg 1))))) (booleanAnd (booleanAnd (intEq (Golden.zshr (intNeg 1) 32) (intNeg 1)) (booleanAnd (intEq (Golden.complement (intNeg 43)) 42) (intEq (Golden.complement (intSub (intNeg 2147483647) 1)) 2147483647))) (booleanAnd (booleanEq (Golden.eqBooleanImpl true true) true) (booleanAnd (booleanEq (Golden.eqBooleanImpl true false) false) (booleanEq (Golden.eqIntImpl 42 42) true)))))) (booleanAnd (booleanAnd (booleanAnd (booleanAnd (booleanEq (Golden.eqIntImpl (intSub (intNeg 2147483647) 1) 2147483647) false) (booleanEq (Golden.eqNumberImpl 0.0 (numberNeg 0.0)) true)) (booleanAnd (booleanEq (Golden.eqNumberImpl (numberDiv 0.0 0.0) (numberDiv 0.0 0.0)) false) (booleanAnd (booleanEq (Golden.eqNumberImpl (numberDiv 1.0 0.0) (numberDiv 1.0 0.0)) true) (booleanEq (Golden.eqCharImpl 'λ' 'λ') true)))) (booleanAnd (booleanAnd (booleanEq (Golden.eqCharImpl '😀' '😀') true) (booleanAnd (booleanEq (Golden.eqCharImpl 'λ' '😀') false) (intEq (Golden.intSub 44 2) 42))) (booleanAnd (intEq (Golden.intSub (intSub (intNeg 2147483647) 1) 1) 2147483647) (booleanAnd (numberEq (Golden.numSub 44.0 2.0) 42.0) (numberEq (numberDiv 1.0 (Golden.numSub (numberNeg 0.0) 0.0)) (numberDiv (numberNeg 1.0) 0.0)))))) (booleanAnd (booleanAnd (booleanAnd (intEq (Golden.intAdd 40 2) 42) (booleanAnd (intEq (Golden.intAdd 2147483647 1) (intSub (intNeg 2147483647) 1)) (numberEq (Golden.numAdd 40.0 2.0) 42.0))) (booleanAnd (booleanNot (numberEq (Golden.numAdd (numberDiv 1.0 0.0) (numberDiv (numberNeg 1.0) 0.0)) (Golden.numAdd (numberDiv 1.0 0.0) (numberDiv (numberNeg 1.0) 0.0)))) (booleanAnd (numberEq (Golden.numMul 21.0 2.0) 42.0) (numberEq (numberDiv 1.0 (Golden.numMul (numberNeg 0.0) 2.0)) (numberDiv (numberNeg 1.0) 0.0))))) (booleanAnd (booleanAnd (booleanEq (Golden.boolConj true true) true) (booleanAnd (booleanEq (Golden.boolConj true false) false) (booleanEq (Golden.boolDisj false true) true))) (booleanAnd (booleanEq (Golden.boolDisj false false) false) (booleanAnd (booleanEq (Golden.boolNot true) false) (booleanEq (Golden.boolNot false) true))))))) then 42 else 1 diff --git a/crates/psrs-driver/tests/fixtures/stdlib-scalar/observations.json b/crates/psrs-driver/tests/fixtures/stdlib-scalar/observations.json new file mode 100644 index 00000000..39fa41a4 --- /dev/null +++ b/crates/psrs-driver/tests/fixtures/stdlib-scalar/observations.json @@ -0,0 +1,530 @@ +{ + "node": "v26.10.0", + "inputs": [ + { + "path": "/tmp/ps-pkgs/purescript-integers/src/Data/Int.js", + "sha256": "35ff31f29f669c4c5d2ab3baee06b74a30b8cf5fcad77d25390158593ab9ac26" + }, + { + "path": "/tmp/ps-pkgs/purescript-integers/src/Data/Int/Bits.js", + "sha256": "4e253dbde0792507b4ed73f5f7c6409f1e297e84a4c481fda159a66842159343" + }, + { + "path": "/tmp/purescript-prelude/src/Data/Eq.js", + "sha256": "0a322f9252ba18de27c1dbb8fec560b44dfd4f2b4c82b3a51fde82af4d871ffe" + }, + { + "path": "/tmp/purescript-prelude/src/Data/Ring.js", + "sha256": "cbddf7ed3d4c34c775dffd9534719fc20f980d6063edbffcf1ac2bd436b59131" + }, + { + "path": "/tmp/purescript-prelude/src/Data/Semiring.js", + "sha256": "fb5e09243e90fcde78f52b15d56a07216a8bea1267db2e12d46ff07c8e0334c9" + }, + { + "path": "/tmp/purescript-prelude/src/Data/HeytingAlgebra.js", + "sha256": "c047b0c397ee4127c2e14ac297f97f9e8deddcb1bace7bcecb2ee1d3117bb70b" + } + ], + "observations": [ + { + "module": "Data/Int.purs", + "name": "toNumber", + "arguments": [ + 42 + ], + "official_result": 42, + "target_result": 42, + "representation_difference": false + }, + { + "module": "Data/Int.purs", + "name": "toNumber", + "arguments": [ + -2147483648 + ], + "official_result": -2147483648, + "target_result": -2147483648, + "representation_difference": false + }, + { + "module": "Data/Int.purs", + "name": "toNumber", + "arguments": [ + 2147483647 + ], + "official_result": 2147483647, + "target_result": 2147483647, + "representation_difference": false + }, + { + "module": "Data/Int/Bits.purs", + "name": "and", + "arguments": [ + 63, + 42 + ], + "official_result": 42, + "target_result": 42, + "representation_difference": false + }, + { + "module": "Data/Int/Bits.purs", + "name": "and", + "arguments": [ + -1, + 42 + ], + "official_result": 42, + "target_result": 42, + "representation_difference": false + }, + { + "module": "Data/Int/Bits.purs", + "name": "or", + "arguments": [ + 32, + 10 + ], + "official_result": 42, + "target_result": 42, + "representation_difference": false + }, + { + "module": "Data/Int/Bits.purs", + "name": "or", + "arguments": [ + -1, + 0 + ], + "official_result": -1, + "target_result": -1, + "representation_difference": false + }, + { + "module": "Data/Int/Bits.purs", + "name": "xor", + "arguments": [ + 63, + 21 + ], + "official_result": 42, + "target_result": 42, + "representation_difference": false + }, + { + "module": "Data/Int/Bits.purs", + "name": "xor", + "arguments": [ + -1, + 0 + ], + "official_result": -1, + "target_result": -1, + "representation_difference": false + }, + { + "module": "Data/Int/Bits.purs", + "name": "shl", + "arguments": [ + 21, + 1 + ], + "official_result": 42, + "target_result": 42, + "representation_difference": false + }, + { + "module": "Data/Int/Bits.purs", + "name": "shl", + "arguments": [ + 21, + 33 + ], + "official_result": 42, + "target_result": 42, + "representation_difference": false + }, + { + "module": "Data/Int/Bits.purs", + "name": "shl", + "arguments": [ + 1, + -1 + ], + "official_result": -2147483648, + "target_result": -2147483648, + "representation_difference": false + }, + { + "module": "Data/Int/Bits.purs", + "name": "shr", + "arguments": [ + -84, + 1 + ], + "official_result": -42, + "target_result": -42, + "representation_difference": false + }, + { + "module": "Data/Int/Bits.purs", + "name": "shr", + "arguments": [ + -84, + 33 + ], + "official_result": -42, + "target_result": -42, + "representation_difference": false + }, + { + "module": "Data/Int/Bits.purs", + "name": "shr", + "arguments": [ + 1, + -1 + ], + "official_result": 0, + "target_result": 0, + "representation_difference": false + }, + { + "module": "Data/Int/Bits.purs", + "name": "zshr", + "arguments": [ + -1, + 1 + ], + "official_result": 2147483647, + "target_result": 2147483647, + "representation_difference": false + }, + { + "module": "Data/Int/Bits.purs", + "name": "zshr", + "arguments": [ + -1, + 0 + ], + "official_result": 4294967295, + "target_result": -1, + "representation_difference": true + }, + { + "module": "Data/Int/Bits.purs", + "name": "zshr", + "arguments": [ + -1, + 32 + ], + "official_result": 4294967295, + "target_result": -1, + "representation_difference": true + }, + { + "module": "Data/Int/Bits.purs", + "name": "complement", + "arguments": [ + -43 + ], + "official_result": 42, + "target_result": 42, + "representation_difference": false + }, + { + "module": "Data/Int/Bits.purs", + "name": "complement", + "arguments": [ + -2147483648 + ], + "official_result": 2147483647, + "target_result": 2147483647, + "representation_difference": false + }, + { + "module": "Data/Eq.purs", + "name": "eqBooleanImpl", + "arguments": [ + true, + true + ], + "official_result": true, + "target_result": true, + "representation_difference": false + }, + { + "module": "Data/Eq.purs", + "name": "eqBooleanImpl", + "arguments": [ + true, + false + ], + "official_result": false, + "target_result": false, + "representation_difference": false + }, + { + "module": "Data/Eq.purs", + "name": "eqIntImpl", + "arguments": [ + 42, + 42 + ], + "official_result": true, + "target_result": true, + "representation_difference": false + }, + { + "module": "Data/Eq.purs", + "name": "eqIntImpl", + "arguments": [ + -2147483648, + 2147483647 + ], + "official_result": false, + "target_result": false, + "representation_difference": false + }, + { + "module": "Data/Eq.purs", + "name": "eqNumberImpl", + "arguments": [ + 0, + "-0" + ], + "official_result": true, + "target_result": true, + "representation_difference": false + }, + { + "module": "Data/Eq.purs", + "name": "eqNumberImpl", + "arguments": [ + "NaN", + "NaN" + ], + "official_result": false, + "target_result": false, + "representation_difference": false + }, + { + "module": "Data/Eq.purs", + "name": "eqNumberImpl", + "arguments": [ + "Infinity", + "Infinity" + ], + "official_result": true, + "target_result": true, + "representation_difference": false + }, + { + "module": "Data/Eq.purs", + "name": "eqCharImpl", + "arguments": [ + "λ", + "λ" + ], + "official_result": true, + "target_result": true, + "representation_difference": false + }, + { + "module": "Data/Eq.purs", + "name": "eqCharImpl", + "arguments": [ + "😀", + "😀" + ], + "official_result": true, + "target_result": true, + "representation_difference": false + }, + { + "module": "Data/Eq.purs", + "name": "eqCharImpl", + "arguments": [ + "λ", + "😀" + ], + "official_result": false, + "target_result": false, + "representation_difference": false + }, + { + "module": "Data/Ring.purs", + "name": "intSub", + "arguments": [ + 44, + 2 + ], + "official_result": 42, + "target_result": 42, + "representation_difference": false + }, + { + "module": "Data/Ring.purs", + "name": "intSub", + "arguments": [ + -2147483648, + 1 + ], + "official_result": 2147483647, + "target_result": 2147483647, + "representation_difference": false + }, + { + "module": "Data/Ring.purs", + "name": "numSub", + "arguments": [ + 44, + 2 + ], + "official_result": 42, + "target_result": 42, + "representation_difference": false + }, + { + "module": "Data/Ring.purs", + "name": "numSub", + "arguments": [ + "-0", + 0 + ], + "official_result": "-0", + "target_result": "-0", + "representation_difference": false + }, + { + "module": "Data/Semiring.purs", + "name": "intAdd", + "arguments": [ + 40, + 2 + ], + "official_result": 42, + "target_result": 42, + "representation_difference": false + }, + { + "module": "Data/Semiring.purs", + "name": "intAdd", + "arguments": [ + 2147483647, + 1 + ], + "official_result": -2147483648, + "target_result": -2147483648, + "representation_difference": false + }, + { + "module": "Data/Semiring.purs", + "name": "numAdd", + "arguments": [ + 40, + 2 + ], + "official_result": 42, + "target_result": 42, + "representation_difference": false + }, + { + "module": "Data/Semiring.purs", + "name": "numAdd", + "arguments": [ + "Infinity", + "-Infinity" + ], + "official_result": "NaN", + "target_result": "NaN", + "representation_difference": false + }, + { + "module": "Data/Semiring.purs", + "name": "numMul", + "arguments": [ + 21, + 2 + ], + "official_result": 42, + "target_result": 42, + "representation_difference": false + }, + { + "module": "Data/Semiring.purs", + "name": "numMul", + "arguments": [ + "-0", + 2 + ], + "official_result": "-0", + "target_result": "-0", + "representation_difference": false + }, + { + "module": "Data/HeytingAlgebra.purs", + "name": "boolConj", + "arguments": [ + true, + true + ], + "official_result": true, + "target_result": true, + "representation_difference": false + }, + { + "module": "Data/HeytingAlgebra.purs", + "name": "boolConj", + "arguments": [ + true, + false + ], + "official_result": false, + "target_result": false, + "representation_difference": false + }, + { + "module": "Data/HeytingAlgebra.purs", + "name": "boolDisj", + "arguments": [ + false, + true + ], + "official_result": true, + "target_result": true, + "representation_difference": false + }, + { + "module": "Data/HeytingAlgebra.purs", + "name": "boolDisj", + "arguments": [ + false, + false + ], + "official_result": false, + "target_result": false, + "representation_difference": false + }, + { + "module": "Data/HeytingAlgebra.purs", + "name": "boolNot", + "arguments": [ + true + ], + "official_result": false, + "target_result": false, + "representation_difference": false + }, + { + "module": "Data/HeytingAlgebra.purs", + "name": "boolNot", + "arguments": [ + false + ], + "official_result": true, + "target_result": true, + "representation_difference": false + } + ] +} diff --git a/crates/psrs-driver/tests/fixtures/stdlib-st/Main.purs b/crates/psrs-driver/tests/fixtures/stdlib-st/Main.purs new file mode 100644 index 00000000..0ee6c4cd --- /dev/null +++ b/crates/psrs-driver/tests/fixtures/stdlib-st/Main.purs @@ -0,0 +1,35 @@ +module Main where +import PSRS.ST as S + +c0 :: forall r. S.Action r Int +c0 = S.bindAction (S.mapAction (\x -> intAdd 1 x) (S.pureAction 40)) (\x -> S.pureAction (intAdd x 1)) + +c1 :: forall r. S.Action r Int +c1 = S.bindAction (S.newCell 40) \ref -> S.bindAction (S.modifyCell (\x -> { state: intAdd x 1, value: intAdd x 2 }) ref) \_ -> S.readCell ref + +c2 :: forall r. S.Action r Int +c2 = S.bindAction (S.newCell 0) \ref -> S.bindAction (S.writeCell 7 ref) \w -> S.bindAction (S.readCell ref) \r -> S.pureAction (intAdd w r) + +c3 :: forall r. S.Action r Int +c3 = S.bindAction (S.newCell 1) \a -> S.bindAction (S.newCell 2) \b -> S.bindAction (S.writeCell 10 a) \_ -> S.bindAction (S.writeCell 20 b) \_ -> S.bindAction (S.readCell a) \x -> S.bindAction (S.readCell b) \y -> S.pureAction (intAdd x y) + +c4 :: forall r. S.Action r Int +c4 = S.bindAction (S.newCell 0) \count -> S.bindAction (S.whileAction (S.mapAction (\c -> intLt c 3) (S.readCell count)) (S.bindAction (S.readCell count) \c -> S.writeCell (intAdd c 1) count)) \_ -> S.readCell count + +c5 :: forall r. S.Action r Int +c5 = S.bindAction (S.newCell 0) \total -> S.bindAction (S.forAction 0 5 (\i -> S.bindAction (S.readCell total) \t -> S.writeCell (intAdd t i) total)) \_ -> S.readCell total + +c6 :: forall r. S.Action r Int +c6 = S.bindAction (S.newCell 0) \total -> S.bindAction (S.foreachAction [1, 2, 3] (\i -> S.bindAction (S.readCell total) \t -> S.bindAction (S.writeCell (intAdd t i) total) \_ -> S.pureAction unit)) \_ -> S.readCell total + +c7 :: forall r. S.Action r Int +c7 = S.bindAction (S.newCell 5) \total -> S.bindAction (S.foreachAction [] (\_ -> S.bindAction (S.pureAction unit) (\_ -> S.bindAction (S.pureAction (arrayIndex ([] :: Array Int) 0)) \_ -> S.pureAction unit))) \_ -> S.readCell total + +c8 :: forall r. S.Action r Int +c8 = S.bindAction (S.newCell 5) \total -> S.bindAction (S.forAction 5 5 (\_ -> S.bindAction (S.pureAction unit) (\_ -> S.bindAction (S.pureAction (arrayIndex ([] :: Array Int) 0)) \_ -> S.pureAction unit))) \_ -> S.readCell total + +c9 :: forall r. S.Action r Int +c9 = S.bindAction (S.newCell 5) \total -> S.bindAction (S.whileAction (S.pureAction false) (S.bindAction (S.pureAction unit) (\_ -> S.bindAction (S.pureAction (arrayIndex ([] :: Array Int) 0)) \_ -> S.pureAction unit))) \_ -> S.readCell total + +main = + if (booleanAnd (booleanAnd (booleanAnd (intEq (S.runAction c0) 42) (intEq (S.runAction c1) 41)) (booleanAnd (intEq (S.runAction c2) 14) (booleanAnd (intEq (S.runAction c3) 30) (intEq (S.runAction c4) 3)))) (booleanAnd (booleanAnd (intEq (S.runAction c5) 10) (intEq (S.runAction c6) 6)) (booleanAnd (intEq (S.runAction c7) 5) (booleanAnd (intEq (S.runAction c8) 5) (intEq (S.runAction c9) 5))))) then 42 else 1 diff --git a/crates/psrs-driver/tests/fixtures/stdlib-uncurried/Main.purs b/crates/psrs-driver/tests/fixtures/stdlib-uncurried/Main.purs new file mode 100644 index 00000000..0994d550 --- /dev/null +++ b/crates/psrs-driver/tests/fixtures/stdlib-uncurried/Main.purs @@ -0,0 +1,40 @@ +module Main where +import Data.Function.Uncurried as S + +c0 :: Int +c0 = S.runFn0 (S.mkFn0 (\_ -> 42)) + +c1 :: Int +c1 = S.runFn2 (S.mkFn2 (\a1 a2 -> intAdd (a1) a2)) 1 2 + +c2 :: Int +c2 = S.runFn3 (S.mkFn3 (\a1 a2 a3 -> intAdd (intAdd (a1) a2) a3)) 1 2 3 + +c3 :: Int +c3 = S.runFn4 (S.mkFn4 (\a1 a2 a3 a4 -> intAdd (intAdd (intAdd (a1) a2) a3) a4)) 1 2 3 4 + +c4 :: Int +c4 = S.runFn5 (S.mkFn5 (\a1 a2 a3 a4 a5 -> intAdd (intAdd (intAdd (intAdd (a1) a2) a3) a4) a5)) 1 2 3 4 5 + +c5 :: Int +c5 = S.runFn6 (S.mkFn6 (\a1 a2 a3 a4 a5 a6 -> intAdd (intAdd (intAdd (intAdd (intAdd (a1) a2) a3) a4) a5) a6)) 1 2 3 4 5 6 + +c6 :: Int +c6 = S.runFn7 (S.mkFn7 (\a1 a2 a3 a4 a5 a6 a7 -> intAdd (intAdd (intAdd (intAdd (intAdd (intAdd (a1) a2) a3) a4) a5) a6) a7)) 1 2 3 4 5 6 7 + +c7 :: Int +c7 = S.runFn8 (S.mkFn8 (\a1 a2 a3 a4 a5 a6 a7 a8 -> intAdd (intAdd (intAdd (intAdd (intAdd (intAdd (intAdd (a1) a2) a3) a4) a5) a6) a7) a8)) 1 2 3 4 5 6 7 8 + +c8 :: Int +c8 = S.runFn9 (S.mkFn9 (\a1 a2 a3 a4 a5 a6 a7 a8 a9 -> intAdd (intAdd (intAdd (intAdd (intAdd (intAdd (intAdd (intAdd (a1) a2) a3) a4) a5) a6) a7) a8) a9)) 1 2 3 4 5 6 7 8 9 + +c9 :: Int +c9 = S.runFn10 (S.mkFn10 (\a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 -> intAdd (intAdd (intAdd (intAdd (intAdd (intAdd (intAdd (intAdd (intAdd (a1) a2) a3) a4) a5) a6) a7) a8) a9) a10)) 1 2 3 4 5 6 7 8 9 10 + +apply2 :: forall a b c. S.Fn2 a b c -> a -> b -> c +apply2 f a b = S.runFn2 f a b +c10 :: Int +c10 = apply2 (S.mkFn2 (\a b -> intAdd a b)) 40 2 + +main = + if (booleanAnd (booleanAnd (booleanAnd (intEq c0 42) (intEq c1 3)) (booleanAnd (intEq c2 6) (booleanAnd (intEq c3 10) (intEq c4 15)))) (booleanAnd (booleanAnd (intEq c5 21) (booleanAnd (intEq c6 28) (intEq c7 36))) (booleanAnd (intEq c8 45) (booleanAnd (intEq c9 55) (intEq c10 42))))) then 42 else 1 diff --git a/crates/psrs-driver/tests/fixtures/upstream-prelude/Symbol.js b/crates/psrs-driver/tests/fixtures/upstream-prelude/Symbol.js new file mode 100644 index 00000000..72041597 --- /dev/null +++ b/crates/psrs-driver/tests/fixtures/upstream-prelude/Symbol.js @@ -0,0 +1,5 @@ +// module Data.Symbol + +export const unsafeCoerce = function (arg) { + return arg; +}; diff --git a/crates/psrs-driver/tests/pattern_support/support.rs b/crates/psrs-driver/tests/pattern_support/support.rs index cef995de..645d1500 100644 --- a/crates/psrs-driver/tests/pattern_support/support.rs +++ b/crates/psrs-driver/tests/pattern_support/support.rs @@ -1,4 +1,16 @@ -use psrs_ast::{Expr, ExprKind, Guard, Pattern, PatternKind, Type, TypeKind}; +use psrs_ast::{ + Expr, ExprKind, Guard, Pattern, PatternKind, RecordUpdateField, RecordUpdateValue, Type, + TypeKind, +}; + +fn visit_update_expressions<'a>(fields: &'a [RecordUpdateField], visit: &mut impl FnMut(&'a Expr)) { + for field in fields { + match &field.value { + RecordUpdateValue::Expression(value) => visit(value), + RecordUpdateValue::Nested { fields, .. } => visit_update_expressions(fields, visit), + } + } +} pub(super) fn type_contains_wildcard(ty: &Type) -> bool { match &ty.kind { @@ -52,16 +64,14 @@ pub(super) fn visit_expr_patterns<'a>(expression: &'a Expr, output: &mut Vec<&'a visit_expr_patterns(element, output); } } - ExprKind::Record(fields) => { + ExprKind::Record(fields) | ExprKind::MatchProduct(fields) => { for (_, value) in fields { visit_expr_patterns(value, output); } } ExprKind::RecordUpdate { expression, fields } => { visit_expr_patterns(expression, output); - for (_, value) in fields { - visit_expr_patterns(value, output); - } + visit_update_expressions(fields, &mut |value| visit_expr_patterns(value, output)); } ExprKind::Application(function, argument) | ExprKind::Operator { @@ -140,16 +150,14 @@ pub(super) fn visit_expr_guards<'a>(expression: &'a Expr, output: &mut Vec<&'a G visit_expr_guards(element, output); } } - ExprKind::Record(fields) => { + ExprKind::Record(fields) | ExprKind::MatchProduct(fields) => { for (_, value) in fields { visit_expr_guards(value, output); } } ExprKind::RecordUpdate { expression, fields } => { visit_expr_guards(expression, output); - for (_, value) in fields { - visit_expr_guards(value, output); - } + visit_update_expressions(fields, &mut |value| visit_expr_guards(value, output)); } ExprKind::Application(function, argument) | ExprKind::Operator { diff --git a/crates/psrs-driver/tests/residual_constraints.rs b/crates/psrs-driver/tests/residual_constraints.rs index 8617ba03..57d81064 100644 --- a/crates/psrs-driver/tests/residual_constraints.rs +++ b/crates/psrs-driver/tests/residual_constraints.rs @@ -21,6 +21,27 @@ fn rejection(source_name: &str, source: &str) -> Vec<(u32, &'static str)> { .collect() } +/// The Core dump names every type variable, so `name: "f"` also matches a +/// functor parameter. The declaration is the occurrence whose own body selects +/// `method` from the abstracted dictionary. +fn declaration_named<'a>(core: &'a str, name: &str) -> &'a str { + let needle = format!("name: \"{name}\""); + let mut rest = core; + while let Some(index) = rest.find(&needle) { + let body = &rest[index..]; + let end = body[needle.len()..] + .find("\n Declaration {") + .map(|offset| needle.len() + offset) + .unwrap_or(body.len()); + let declaration = &body[..end]; + if declaration.contains("name: \"dict\"") && declaration.contains("field: \"method\"") { + return declaration; + } + rest = &body[needle.len()..]; + } + panic!("declaration `{name}` did not abstract a dictionary parameter"); +} + fn accepts(source_name: &str, source: &str) { if let Err(errors) = check_source(source_name, source) { panic!("{source_name} was rejected: {errors:#?}"); @@ -67,11 +88,7 @@ fn abstracts_one_dictionary_parameter_per_retained_constraint() { // evidence the body used is a field of that parameter rather than a global // instance dictionary. let core = compilation.dumps.get("core").expect("a core dump"); - let declaration = core - .split_once("name: \"f\"") - .expect("the declaration is in the core dump") - .1; - let declaration = &declaration[..declaration.len().min(4000)]; + let declaration = declaration_named(core, "f"); assert!( declaration.contains("name: \"dict\""), "the retained constraint did not become a dictionary parameter:\n{declaration}" diff --git a/crates/psrs-driver/tests/suite/corpus/mod.rs b/crates/psrs-driver/tests/suite/corpus/mod.rs index c9c8bfc4..ea883332 100644 --- a/crates/psrs-driver/tests/suite/corpus/mod.rs +++ b/crates/psrs-driver/tests/suite/corpus/mod.rs @@ -48,17 +48,7 @@ pub fn collected_files(dir: &Path, limit: Option) -> Vec { } pub fn collect_purs_files(dir: &Path, out: &mut Vec) { - let Ok(entries) = std::fs::read_dir(dir) else { - return; - }; - for entry in entries.filter_map(|entry| entry.ok()) { - let path = entry.path(); - if path.is_dir() { - collect_purs_files(&path, out); - } else if path.extension().is_some_and(|ext| ext == "purs") { - out.push(path); - } - } + psrs_driver::collect_purs_files(dir, out); } /// One corpus case, assembled the way the compiler loads a program. @@ -127,53 +117,17 @@ impl Case { /// dependencies there, and that subdirectory is searched first, so its support /// modules win over anything found beside the case. pub fn load_case(path: &Path, category_dir: &Path, text: &str) -> Case { - let own = own_sources(path, text); let own_directory = path.parent() != Some(category_dir); - let loaded = if !own_directory { - Vec::new() - } else { - let entry = own - .iter() - .skip(1) - .map(|(path, _)| path.clone()) - .chain(std::iter::once(path.to_string_lossy().into_owned())) - .collect::>(); - match psrs_driver::load_program_files(&entry) { - Ok(sources) => sources - .into_iter() - .filter(|(loaded, _)| !own.iter().any(|(own, _)| own == loaded)) - .collect(), - // The loader only fails on an unreadable entry file, which the case - // sources above have already read. - Err(_) => Vec::new(), - } - }; + let sources = psrs_driver::load_program_case_sources(path, category_dir, text) + .expect("a previously read suite case and its support files remain readable"); Case { - own, - loaded, + own: sources.own, + loaded: sources.loaded, directory: path.parent().unwrap_or(Path::new(".")).to_path_buf(), own_directory, } } -/// The main source plus the modules in a sibling directory named after the file -/// stem, which is how the corpus supplies a case's support modules. -pub fn own_sources(path: &Path, text: &str) -> Vec<(String, String)> { - let mut sources = vec![(path.to_string_lossy().into_owned(), text.to_owned())]; - let support_dir = path.with_extension(""); - if support_dir.is_dir() { - let mut files = Vec::new(); - collect_purs_files(&support_dir, &mut files); - files.sort(); - for support in files { - if let Ok(text) = std::fs::read_to_string(&support) { - sources.push((support.to_string_lossy().into_owned(), text)); - } - } - } - sources -} - /// Why a case is blocked, split so the phase that recovers it is visible. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Blocker { diff --git a/crates/psrs-driver/tests/support/rank_n/mod.rs b/crates/psrs-driver/tests/support/rank_n/mod.rs index 51f8392a..7bb4a96c 100644 --- a/crates/psrs-driver/tests/support/rank_n/mod.rs +++ b/crates/psrs-driver/tests/support/rank_n/mod.rs @@ -1,5 +1,7 @@ //! Source cases shared by semantic, differential, and execution acceptance. +mod reify; + pub const CHECK_ONLY: &[(&str, &str)] = &[( "annotated_polymorphic_array", r#"module Main where @@ -32,6 +34,9 @@ main = case Holder (make 0) of ]; pub const ACCEPT: &[(&str, &str)] = &[ + ("rank_n_separate_equations", reify::EQUATIONS), + ("rank_n_multiple_scrutinees", reify::MULTIPLE), + ("rank_n_guarded_equations", reify::GUARDED), ( "returned_constraint", r#"module Main where diff --git a/crates/psrs-driver/tests/support/rank_n/reify.rs b/crates/psrs-driver/tests/support/rank_n/reify.rs new file mode 100644 index 00000000..2be2cc95 --- /dev/null +++ b/crates/psrs-driver/tests/support/rank_n/reify.rs @@ -0,0 +1,77 @@ +pub const EQUATIONS: &str = r#"module Main where +data First = First +data Second = Second +data Proxy a = Proxy +class Tag a where + tag :: Proxy a -> Int +instance tagFirst :: Tag First where + tag _ = 21 +instance tagSecond :: Tag Second where + tag _ = 42 +reify :: forall r. Boolean -> (forall a. Tag a => Proxy a -> r) -> r +reify true f = f (Proxy :: Proxy First) +reify false f = f (Proxy :: Proxy Second) +main :: Int +main = case reify true tag, reify false tag of + 21, 42 -> 42 + _, _ -> 1 +"#; + +pub const MULTIPLE: &str = r#"module Main where +data First = First +data Second = Second +data Proxy a = Proxy +class Tag a where + tag :: Proxy a -> Int +instance tagFirst :: Tag First where + tag _ = 21 +instance tagSecond :: Tag Second where + tag _ = 42 +reify :: forall r. Boolean -> (forall a. Tag a => Proxy a -> r) -> r +reify flag f = case flag, f of + true, continuation -> continuation (Proxy :: Proxy First) + false, continuation -> continuation (Proxy :: Proxy Second) +main :: Int +main = case reify true tag, reify false tag of + 21, 42 -> 42 + _, _ -> 1 +"#; + +pub const GUARDED: &str = r#"module Main where +data First = First +data Second = Second +data Proxy a = Proxy +class Tag a where + tag :: Proxy a -> Int +instance tagFirst :: Tag First where + tag _ = 21 +instance tagSecond :: Tag Second where + tag _ = 42 +reify :: forall r. Boolean -> (forall a. Tag a => Proxy a -> r) -> r +reify true f | true = f (Proxy :: Proxy First) +reify false f | true = f (Proxy :: Proxy Second) +main :: Int +main = case reify true tag, reify false tag of + 21, 42 -> 42 + _, _ -> 1 +"#; + +pub const RECORD: &str = r#"module Main where +data First = First +data Second = Second +data Proxy a = Proxy +class Tag a where + tag :: Proxy a -> Int +instance tagFirst :: Tag First where + tag _ = 21 +instance tagSecond :: Tag Second where + tag _ = 42 +reify :: forall r. Boolean -> (forall a. Tag a => Proxy a -> r) -> r +reify flag f = case { flag, f } of + { flag: true, f: continuation } -> continuation (Proxy :: Proxy First) + { flag: false, f: continuation } -> continuation (Proxy :: Proxy Second) +main :: Int +main = case reify true tag, reify false tag of + 21, 42 -> 42 + _, _ -> 1 +"#; diff --git a/crates/psrs-driver/tests/support/rank_n/reject.rs b/crates/psrs-driver/tests/support/rank_n/reject.rs index 6f3e378a..a7a91851 100644 --- a/crates/psrs-driver/tests/support/rank_n/reject.rs +++ b/crates/psrs-driver/tests/support/rank_n/reject.rs @@ -1,4 +1,5 @@ pub const REJECT: &[(&str, &str)] = &[ + ("rank_n_record_shares_instantiation", super::reify::RECORD), ( "ambiguous_unresolved_main_use", r#"module Main where diff --git a/crates/psrs-driver/tests/upstream/deriving/diagnostics.rs b/crates/psrs-driver/tests/upstream/deriving/diagnostics.rs new file mode 100644 index 00000000..27633d35 --- /dev/null +++ b/crates/psrs-driver/tests/upstream/deriving/diagnostics.rs @@ -0,0 +1,102 @@ +use super::*; + +#[test] +fn differential_deriving_error_codes_against_purs() { + if !purs_available() { + eprintln!("skipping: purs is not installed"); + return; + } + let functor_module = r#"module Data.Functor where + +class Functor f where + map :: forall a b. (a -> b) -> f a -> f b +"#; + let functor_contravariant = r#"module Main where + +import Data.Functor (class Functor) + +data Contra a = Contra (a -> Int) + +derive instance functorContra :: Functor Contra + +main :: Int +main = 0 +"#; + let unknown_class = r#"module Main where + +class Marker a + +data Box = Box + +derive instance markerBox :: Marker Box + +main :: Int +main = 0 +"#; + let newtype_on_data = r#"module Main where + +class ToInt a where + toInt :: a -> Int + +data Box = Box Int + +derive newtype instance toIntBox :: ToInt Box + +main :: Int +main = 0 +"#; + let eq_builtin = "module Data.Eq where\nclass Eq a where\n eq :: a -> a -> Boolean\nderive instance eqInt :: Eq Int\n"; + let eq_binary = "module Data.Eq where\nclass Eq a b where\n eq :: a -> b -> Boolean\n"; + let invalid_arity = "module Main where\nimport Data.Eq\ndata Box = Box\nderive instance eqBox :: Eq Box Box\nmain = 0\n"; + let class_arity = "module Main where\nimport Data.Functor\ndata Box a = Box a\nderive instance functorBox :: Functor Box Int\nmain = 0\n"; + let cases: [(&str, &[(&str, &str)]); 6] = [ + ( + "functor-contravariant-field", + &[ + ("Data.Functor.purs", functor_module), + ("Main.purs", functor_contravariant), + ], + ), + ("builtin-head", &[("Data.Eq.purs", eq_builtin)]), + ( + "invalid-class-arity", + &[("Data.Eq.purs", eq_binary), ("Main.purs", invalid_arity)], + ), + ( + "class-instance-arity", + &[ + ("Data.Functor.purs", functor_module), + ("Main.purs", class_arity), + ], + ), + ("unknown-class", &[("Main.purs", unknown_class)]), + ("newtype-on-data", &[("Main.purs", newtype_on_data)]), + ]; + let mut failures = Vec::new(); + for (name, sources) in cases { + let purs_output = purs_sources_output(name, sources); + let purs_codes = purs_error_codes(&purs_output); + let psrs_codes: Vec = match psrs_driver::check_program(sources) { + Ok(()) => Vec::new(), + Err(errors) => errors + .into_iter() + .filter_map(|error| error.diagnostic.code.map(str::to_owned)) + .collect(), + }; + if psrs_codes.is_empty() { + failures.push(format!("`{name}`: psrs reported no errorCode")); + } + for code in &psrs_codes { + if !purs_codes.contains(code) { + failures.push(format!( + "`{name}`: psrs code `{code}` not among purs codes {purs_codes:?}" + )); + } + } + } + assert!( + failures.is_empty(), + "deriving error-code differential failures:\n{}", + failures.join("\n") + ); +} diff --git a/crates/psrs-driver/tests/upstream/deriving/mod.rs b/crates/psrs-driver/tests/upstream/deriving/mod.rs new file mode 100644 index 00000000..daa84dcf --- /dev/null +++ b/crates/psrs-driver/tests/upstream/deriving/mod.rs @@ -0,0 +1,5 @@ +use super::*; + +mod diagnostics; +mod rules; +mod traversals; diff --git a/crates/psrs-driver/tests/upstream/deriving.rs b/crates/psrs-driver/tests/upstream/deriving/rules.rs similarity index 100% rename from crates/psrs-driver/tests/upstream/deriving.rs rename to crates/psrs-driver/tests/upstream/deriving/rules.rs diff --git a/crates/psrs-driver/tests/upstream/deriving/traversals.rs b/crates/psrs-driver/tests/upstream/deriving/traversals.rs new file mode 100644 index 00000000..460da7bf --- /dev/null +++ b/crates/psrs-driver/tests/upstream/deriving/traversals.rs @@ -0,0 +1,291 @@ +use super::*; + +#[test] +fn differential_remaining_deriving_rules_against_purs() { + if !purs_available() { + return; + } + let libraries = [ + ( + "Control.Category.purs", + "module Control.Category where\nidentity :: forall a. a -> a\nidentity x = x\n", + ), + ( + "Data.Function.purs", + r#"module Data.Function where + +identity :: forall a. a -> a +identity x = x +"#, + ), + ( + "Data.Semigroup.purs", + r#"module Data.Semigroup where + +class Semigroup a where + append :: a -> a -> a +"#, + ), + ( + "Data.Monoid.purs", + r#"module Data.Monoid where + +import Data.Semigroup + +class Semigroup a <= Monoid a where + mempty :: a +"#, + ), + ( + "Data.Functor.purs", + r#"module Data.Functor where + +class Functor f where + map :: forall a b. (a -> b) -> f a -> f b + +instance functorFunction :: Functor ((->) r) where + map f g x = f (g x) +"#, + ), + ( + "Control.Apply.purs", + r#"module Control.Apply where + +import Data.Functor + +class Functor f <= Apply f where + apply :: forall a b. f (a -> b) -> f a -> f b +"#, + ), + ( + "Control.Applicative.purs", + r#"module Control.Applicative where + +import Control.Apply + +class Apply f <= Applicative f where + pure :: forall a. a -> f a +"#, + ), + ( + "Data.Foldable.purs", + r#"module Data.Foldable where + +import Data.Monoid + +class Foldable t where + foldr :: forall a b. (a -> b -> b) -> b -> t a -> b + foldl :: forall a b. (b -> a -> b) -> b -> t a -> b + foldMap :: forall a m. Monoid m => (a -> m) -> t a -> m +"#, + ), + ( + "Data.Bifoldable.purs", + r#"module Data.Bifoldable where + +import Data.Monoid + +class Bifoldable p where + bifoldr :: forall a b c. (a -> c -> c) -> (b -> c -> c) -> c -> p a b -> c + bifoldl :: forall a b c. (c -> a -> c) -> (c -> b -> c) -> c -> p a b -> c + bifoldMap :: forall m a b. Monoid m => (a -> m) -> (b -> m) -> p a b -> m +"#, + ), + ( + "Data.Traversable.purs", + r#"module Data.Traversable where + +import Control.Applicative +import Data.Functor +import Data.Function + +class Traversable t where + traverse :: forall a b f. Applicative f => (a -> f b) -> t a -> f (t b) + sequence :: forall a f. Applicative f => t (f a) -> f (t a) +"#, + ), + ( + "Data.Bitraversable.purs", + r#"module Data.Bitraversable where + +import Control.Applicative +import Data.Functor +import Data.Function + +class Bitraversable t where + bitraverse :: forall f a b c d. Applicative f => (a -> f c) -> (b -> f d) -> t a b -> f (t c d) + bisequence :: forall f a b. Applicative f => t (f a) (f b) -> f (t a b) +"#, + ), + ( + "Data.Profunctor.purs", + r#"module Data.Profunctor where + +class Profunctor p where + dimap :: forall a b c d. (a -> b) -> (c -> d) -> p b c -> p a d +"#, + ), + ( + "Data.Functor.Contravariant.purs", + r#"module Data.Functor.Contravariant where + +class Contravariant f where + cmap :: forall a b. (b -> a) -> f a -> f b +"#, + ), + ( + "Data.Bifunctor.purs", + r#"module Data.Bifunctor where + +class Bifunctor f where + bimap :: forall a b c d. (a -> b) -> (c -> d) -> f a c -> f b d +"#, + ), + ]; + let cases = [ + ( + "Functor", + "Data.Functor", + "a", + "{ field :: a, inert :: Int }", + true, + ), + ( + "Bifunctor", + "Data.Bifunctor", + "a b", + "{ left :: a, right :: b, inert :: Int }", + true, + ), + ( + "Contravariant", + "Data.Functor.Contravariant", + "a", + "a", + false, + ), + ("Profunctor", "Data.Profunctor", "a b", "b", true), + ("Profunctor", "Data.Profunctor", "a b", "a", false), + ( + "Foldable", + "Data.Foldable", + "a", + "{ field :: a, inert :: Int }", + true, + ), + ("Foldable", "Data.Foldable", "a", "(a -> Int)", false), + ( + "Bifoldable", + "Data.Bifoldable", + "a b", + "{ left :: a, right :: b }", + true, + ), + ("Bifoldable", "Data.Bifoldable", "a b", "(a -> Int)", false), + ( + "Traversable", + "Data.Traversable", + "a", + "{ field :: a, inert :: Int }", + true, + ), + ("Traversable", "Data.Traversable", "a", "(a -> Int)", false), + ( + "Bitraversable", + "Data.Bitraversable", + "a b", + "{ left :: a, right :: b }", + true, + ), + ( + "Bitraversable", + "Data.Bitraversable", + "a b", + "(a -> Int)", + false, + ), + ]; + let mut failures = Vec::new(); + for (index, (class, module, parameters, field, accepted)) in cases.into_iter().enumerate() { + let main = format!( + "module Main where\nimport Data.Function as Function\nimport Control.Category (identity)\nimport Data.Functor\nimport Control.Apply\nimport Control.Applicative\nimport {module}\ndata Box {parameters} = Box {field}\nderive instance derivedBox :: {class} Box\nmain = 0\n" + ); + let mut sources = libraries.to_vec(); + sources.push(("Main.purs", &main)); + let name = format!("deriving-{class}-{index}"); + let purs_output = purs_sources_output(&name, &sources); + let purs_accepted = purs_output.status.success(); + let psrs = psrs_driver::check_program(&sources); + if purs_accepted != accepted || psrs.is_ok() != accepted { + failures.push(format!( + "{name}: expected={accepted}, purs={purs_accepted}, psrs={psrs:?}, purs stderr={}, stdout={}", + String::from_utf8_lossy(&purs_output.stderr), + String::from_utf8_lossy(&purs_output.stdout) + )); + } + if !accepted { + let codes = purs_error_codes(&purs_output); + if !codes + .iter() + .any(|code| code == "CannotDeriveInvalidConstructorArg") + || !psrs.as_ref().err().is_some_and(|errors| { + errors.iter().any(|error| { + error.diagnostic.code == Some("CannotDeriveInvalidConstructorArg") + }) + }) + { + failures.push(format!( + "{name}: expected field-usage errorCode, purs={codes:?}, psrs={psrs:?}" + )); + } + } + } + for (class, module) in [ + ("Functor", "Data.Functor"), + ("Foldable", "Data.Foldable"), + ("Traversable", "Data.Traversable"), + ] { + let main = format!( + "module Main where\nimport Data.Function as Function\nimport Control.Category (identity)\nimport Data.Functor\nimport Control.Apply\nimport Control.Applicative\nimport {module}\ndata Box f a = Box (f a)\nderive instance boxInstance :: {class} f => {class} (Box f)\nmain = 0\n" + ); + let mut sources = libraries.to_vec(); + sources.push(("Main.purs", &main)); + let output = purs_sources_output(&format!("deriving-context-{class}"), &sources); + let psrs = psrs_driver::check_program(&sources); + if !output.status.success() || psrs.is_err() { + failures.push(format!("{class} context: purs={output:?}, psrs={psrs:?}")); + } + } + let scoped = "module Main where\nimport Data.Bifunctor\nimport Data.Functor\ndata Box a b = Box (forall a. a -> b)\nderive instance bifunctorBox :: Bifunctor Box\nmain = 0\n"; + let mut sources = libraries.to_vec(); + sources.push(("Main.purs", scoped)); + let output = purs_sources_output("deriving-scoped-parameter", &sources); + let psrs = psrs_driver::check_program(&sources); + if !output.status.success() || psrs.is_err() { + failures.push(format!("scoped parameter: purs={output:?}, psrs={psrs:?}")); + } + assert!(failures.is_empty(), "{}", failures.join("\n")); +} + +#[test] +fn differential_record_postfix_precedence_against_purs() { + if !purs_available() { + return; + } + let source = r#"module Main where +consume :: Int -> Int +consume x = x +project :: { field :: Int } -> Int +project r = consume r.field +consumeRecord :: { field :: Int } -> { field :: Int } +consumeRecord r = r +update :: { field :: Int } -> { field :: Int } +update r = consumeRecord r { field = 42 } +main :: Int +main = project (update { field: 0 }) +"#; + let sources = [("Main.purs", source)]; + let output = purs_sources_output("record-postfix-precedence", &sources); + assert!(output.status.success(), "{output:?}"); + psrs_driver::check_program(&sources).expect("record postfix operands agree with purs"); +} diff --git a/crates/psrs-driver/tests/upstream/guard_continuations.rs b/crates/psrs-driver/tests/upstream/guard_continuations.rs new file mode 100644 index 00000000..120ce730 --- /dev/null +++ b/crates/psrs-driver/tests/upstream/guard_continuations.rs @@ -0,0 +1,52 @@ +use super::*; + +#[test] +fn guarded_polymorphic_results_match_official_purs() { + if !purs_available() { + eprintln!("skipping: purs is not installed"); + return; + } + for (name, constraint, body, accepted) in [ + ( + "single-row", + "Build f => ", + "choose = case _, _ of\n flag, value\n | flag -> build value\n | true -> build value\n", + true, + ), + ( + "later-guard", + "Build f => ", + "choose flag value = case flag of\n false -> build value\n true | flag -> build value\n true -> build value\n", + true, + ), + ( + "missing-dictionary", + "", + "choose = case _, _ of\n flag, value\n | flag -> build value\n | true -> build value\n", + false, + ), + ] { + let source = format!( + "module Main where\nclass Build f where\n build :: forall a. a -> f a\nchoose :: forall f a. {constraint}Boolean -> a -> f a\n{body}main :: Int\nmain = 42\n" + ); + let sources = [("Main.purs", source.as_str())]; + let purs = purs_sources_output(&format!("guard-result-{name}"), &sources); + assert_eq!(purs.status.success(), accepted, "{name}: {purs:?}"); + let ours = psrs_driver::check_program_types_lenient(&sources); + assert_eq!(ours.is_ok(), accepted, "{name}: {ours:?}"); + if !accepted { + assert!( + purs_error_codes(&purs) + .iter() + .any(|code| code == "NoInstanceFound"), + "{name}: {purs:?}" + ); + assert!( + ours.unwrap_err() + .iter() + .any(|error| error.diagnostic.code == Some("NoInstanceFound")), + "{name}" + ); + } + } +} diff --git a/crates/psrs-driver/tests/upstream/import_aliases.rs b/crates/psrs-driver/tests/upstream/import_aliases.rs new file mode 100644 index 00000000..95208908 --- /dev/null +++ b/crates/psrs-driver/tests/upstream/import_aliases.rs @@ -0,0 +1,71 @@ +use super::*; + +#[test] +fn shared_import_aliases_match_official_purs() { + if !purs_available() { + eprintln!("skipping: purs is not installed"); + return; + } + let a = "module A where\ndata Item = Item\nthing :: Item\nthing = Item\n"; + let disjoint = "module B where\ndata Other = Other\nother :: Other\nother = Other\n"; + let canonical = "module B (module A) where\nimport A\n"; + let conflicting = "module B where\ndata Item = Other\nthing :: Item\nthing = Other\n"; + for (name, b, main, expected) in [ + ( + "disjoint-reexport", + disjoint, + "module Main (module X, value) where\nimport A as X\nimport B as X\nvalue :: X.Other\nvalue = X.other\n", + true, + ), + ( + "canonical-reexport", + canonical, + "module Main (module X, value) where\nimport A as X\nimport B as X\nvalue :: X.Item\nvalue = X.thing\n", + true, + ), + ( + "unused-conflict", + conflicting, + "module Main where\nimport A as X\nimport B as X\nmain :: Int\nmain = 42\n", + true, + ), + ( + "value-conflict", + conflicting, + "module Main where\nimport A as X\nimport B as X\nmain = X.thing\n", + false, + ), + ( + "type-conflict", + conflicting, + "module Main where\nimport A as X\nimport B as X\nmain :: X.Item\nmain = X.Item\n", + false, + ), + ( + "reexport-conflict", + conflicting, + "module Main (module X) where\nimport A as X\nimport B as X\n", + false, + ), + ] { + let sources = [("A.purs", a), ("B.purs", b), ("Main.purs", main)]; + let purs = purs_sources_output(&format!("import-alias-{name}"), &sources); + assert_eq!(purs.status.success(), expected, "{name}: {purs:?}"); + let ours = psrs_driver::resolve_program_sources(&sources); + assert_eq!(ours.is_ok(), expected, "{name}: {ours:?}"); + if !expected { + assert!( + purs_error_codes(&purs) + .iter() + .any(|code| code == "ScopeConflict"), + "{name}: {purs:?}" + ); + assert!( + ours.unwrap_err() + .iter() + .any(|error| error.diagnostic.code == Some("ScopeConflict")), + "{name}" + ); + } + } +} diff --git a/crates/psrs-driver/tests/upstream/library_foreign.rs b/crates/psrs-driver/tests/upstream/library_foreign.rs new file mode 100644 index 00000000..6913c4ed --- /dev/null +++ b/crates/psrs-driver/tests/upstream/library_foreign.rs @@ -0,0 +1,36 @@ +use super::*; + +#[test] +fn differential_library_foreign_declarations_against_purs() { + if !purs_available() { + eprintln!("skipping: purs is not installed"); + return; + } + let native = "module Native where\nforeign import intAdd :: Int -> Int\nforeign import operation :: Int -> Int -> Int\ninfixl 4 operation as %%\n"; + let main = "module Main where\nimport Native\nmain :: Int\nmain = intAdd (1 %% 2)\n"; + let sources = [("Native.purs", native), ("Main.purs", main)]; + psrs_driver::check_program(&sources).expect("source foreign declarations should type check"); + let directory = std::env::temp_dir().join(format!( + "psrs-official-library-foreign-{}", + std::process::id() + )); + std::fs::create_dir_all(&directory).unwrap(); + for (name, source) in sources { + std::fs::write(directory.join(name), source).unwrap(); + } + std::fs::write( + directory.join("Native.js"), + "export const intAdd = x => x;\nexport const operation = x => y => x + y;\n", + ) + .unwrap(); + let output = Command::new("purs") + .arg("compile") + .arg(directory.join("Native.purs")) + .arg(directory.join("Main.purs")) + .arg("-o") + .arg(directory.join("output")) + .output() + .expect("run the official compiler"); + let _ = std::fs::remove_dir_all(directory); + assert!(output.status.success(), "{output:?}"); +} diff --git a/crates/psrs-driver/tests/upstream/mod.rs b/crates/psrs-driver/tests/upstream/mod.rs index 517c0319..afed3bb3 100644 --- a/crates/psrs-driver/tests/upstream/mod.rs +++ b/crates/psrs-driver/tests/upstream/mod.rs @@ -3,9 +3,14 @@ use std::process::Command; mod coercion; mod deriving; +mod guard_continuations; +mod import_aliases; +mod library_foreign; mod rank_n; mod reports; +mod residual_constraints; mod rows; +mod symbol_reflection; type SourceFile = (&'static str, &'static str); type SourceSet<'a> = &'a [SourceFile]; @@ -45,6 +50,20 @@ fn purs_accepts(source: &Path) -> bool { } fn purs_accepts_sources(name: &str, sources: &[SourceFile]) -> bool { + purs_sources_output(name, sources).status.success() +} + +/// Runs `purs` on the sources and returns its captured output. Used to compare +/// diagnostic codes, not only acceptance. +fn purs_sources_output(name: &str, sources: &[(&str, &str)]) -> std::process::Output { + purs_sources_with_foreign_output(name, sources, &[]) +} + +fn purs_sources_with_foreign_output( + name: &str, + sources: &[(&str, &str)], + foreign_sources: &[(&str, &str)], +) -> std::process::Output { let case_dir = std::env::temp_dir().join(format!("psrs-purs-upstream-{}-{name}", std::process::id())); let _ = std::fs::remove_dir_all(&case_dir); @@ -64,18 +83,35 @@ fn purs_accepts_sources(name: &str, sources: &[SourceFile]) -> bool { path }) .collect::>(); + for (name, source) in foreign_sources { + std::fs::write(case_dir.join(name), source).expect("write supplied official FFI fixture"); + } let output_dir = case_dir.join("output"); - let accepted = Command::new("purs") + let output = Command::new("purs") .arg("compile") .args(&paths) .arg("-o") .arg(output_dir) .output() - .expect("failed to run purs") - .status - .success(); + .expect("failed to run purs"); let _ = std::fs::remove_dir_all(case_dir); - accepted + output +} + +/// The `errorCode`s in a `purs` output, read from the `.../errors/.md` +/// links each diagnostic prints. +fn purs_error_codes(output: &std::process::Output) -> Vec { + let text = String::from_utf8_lossy(&output.stdout).into_owned() + + &String::from_utf8_lossy(&output.stderr); + text.lines() + .filter_map(|line| { + let marker = "/errors/"; + let start = line.find(marker)? + marker.len(); + let rest = &line[start..]; + let end = rest.find(".md")?; + Some(rest[..end].to_owned()) + }) + .collect() } #[test] diff --git a/crates/psrs-driver/tests/upstream/residual_constraints.rs b/crates/psrs-driver/tests/upstream/residual_constraints.rs new file mode 100644 index 00000000..37287b64 --- /dev/null +++ b/crates/psrs-driver/tests/upstream/residual_constraints.rs @@ -0,0 +1,81 @@ +use super::{purs_available, purs_error_codes, purs_sources_output}; +use psrs_driver::check_source; + +#[test] +fn local_binding_constraints_and_annotations_match_official_purs() { + if !purs_available() { + eprintln!("skipping: purs is not installed"); + return; + } + for (name, body, expected) in [ + ( + "unused-constrained-local", + "class C a where\n method :: a -> a\nf y = let g x = method x in y\n", + Some("AmbiguousTypeVariables"), + ), + ( + "used-constrained-local", + "class C a where\n method :: a -> a\nf y = let g x = method x in g y\n", + None, + ), + ( + "unannotated-local", + "f = let ident x = x in { a: ident 1, b: ident true }\n", + Some("TypesDoNotUnify"), + ), + ( + "annotated-local", + "f = let ident = (\\x -> x) :: forall a. a -> a in { a: ident 1, b: ident true }\n", + None, + ), + ( + "annotated-constrained-local", + "class C a where\n method :: a -> a\nf y = let g = (\\x -> method x) :: forall a. C a => a -> a in y\n", + None, + ), + ( + "fundep-determined-local", + "class C a b | a -> b where\n method :: a -> b\nf x = let g y = method y in g x\n", + None, + ), + ] { + let source = format!("module Main where\n{body}"); + let official = purs_sources_output(name, &[("Main.purs", &source)]); + let local = check_source("Main.purs", &source); + assert_eq!( + official.status.success(), + expected.is_none(), + "{name}: {official:?}" + ); + match expected { + Some(code) => { + assert!( + purs_error_codes(&official) + .iter() + .any(|found| found == code), + "{name}: {official:?}" + ); + let errors = local.expect_err(name); + assert!( + errors.iter().any(|error| error.code == Some(code)), + "{name}: {errors:?}" + ); + } + None => local.unwrap_or_else(|errors| panic!("{name}: {errors:?}")), + } + } +} + +#[test] +fn official_constraint_inference_case_reports_its_annotated_ambiguity() { + let source = include_str!("../../../../tests/upstream/failing/ConstraintInference.purs"); + assert!(source.contains("@shouldFailWith AmbiguousTypeVariables")); + let errors = check_source("ConstraintInference.purs", source) + .expect_err("the inferred result does not determine Show's argument"); + assert!( + errors + .iter() + .any(|error| error.code == Some("AmbiguousTypeVariables")), + "{errors:?}" + ); +} diff --git a/crates/psrs-driver/tests/upstream/symbol_reflection.rs b/crates/psrs-driver/tests/upstream/symbol_reflection.rs new file mode 100644 index 00000000..e1cad71a --- /dev/null +++ b/crates/psrs-driver/tests/upstream/symbol_reflection.rs @@ -0,0 +1,39 @@ +use super::*; + +#[test] +fn differential_symbol_reflection_against_purs() { + if !purs_available() { + eprintln!("skipping: purs is not installed"); + return; + } + let root = psrs_driver::standard_library_info() + .unwrap() + .root + .join("lib"); + let proxy = std::fs::read_to_string(root.join("Type/Proxy.purs")).unwrap(); + let symbol = std::fs::read_to_string(root.join("Data/Symbol.purs")).unwrap(); + let main = r#"module Main where +import Data.Symbol as S +import Type.Proxy (Proxy(..)) +reflect :: forall s. S.IsSymbol s => Proxy s -> String +reflect proxy = S.reflectSymbol proxy +main :: String +main = reflect (Proxy :: Proxy "λ😀") +"#; + let sources = [ + ("Proxy.purs", proxy.as_str()), + ("Symbol.purs", symbol.as_str()), + ("Main.purs", main), + ]; + // Official FFI implementation: purescript-prelude v6.0.1, f4cad0ae8106185c9ab407f43cf9abf05c256af4. + let official = purs_sources_with_foreign_output( + "symbol-reflection", + &sources, + &[( + "Symbol.js", + include_str!("../fixtures/upstream-prelude/Symbol.js"), + )], + ); + assert!(official.status.success(), "{official:?}"); + psrs_driver::check_program(&sources).expect("accepts official symbol reflection"); +} diff --git a/crates/psrs-hir/src/compiler_class.rs b/crates/psrs-hir/src/compiler_class.rs new file mode 100644 index 00000000..1c6bdf72 --- /dev/null +++ b/crates/psrs-hir/src/compiler_class.rs @@ -0,0 +1,16 @@ +//! Canonical library interfaces whose dictionaries the compiler can construct. +//! Resolution attaches this identity once; aliases and re-exports preserve the +//! declaration's TypeId and consumers never dispatch on source name spelling. + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum CompilerClass { + IsSymbol, +} + +impl CompilerClass { + pub fn method(self) -> &'static str { + match self { + Self::IsSymbol => "reflectSymbol", + } + } +} diff --git a/crates/psrs-hir/src/compiler_interface.rs b/crates/psrs-hir/src/compiler_interface.rs new file mode 100644 index 00000000..77ea4ab7 --- /dev/null +++ b/crates/psrs-hir/src/compiler_interface.rs @@ -0,0 +1,69 @@ +//! Canonical compiler/library interface bindings, owned at name resolution. +//! This registry declares identities, not typechecker or runtime behavior. +//! Source implementations keep their declarations and ordinary module TypeIds. +use super::{CompilerClass, Intrinsic, TypeId}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum InterfaceImplementation { + Compiler, + Source, +} + +pub struct CompilerInterface { + pub module: &'static str, + pub implementation: InterfaceImplementation, + pub values: &'static [(&'static str, Intrinsic)], + pub types: &'static [(&'static str, TypeId)], + pub classes: &'static [(&'static str, CompilerClass)], +} + +pub const COMPILER_INTERFACES: &[CompilerInterface] = &[ + CompilerInterface { + module: "Safe.Coerce", + implementation: InterfaceImplementation::Compiler, + values: &[("coerce", Intrinsic::Coerce)], + types: &[("Coercible", TypeId::COERCIBLE)], + classes: &[], + }, + CompilerInterface { + module: "Unsafe.Coerce", + implementation: InterfaceImplementation::Compiler, + values: &[("unsafeCoerce", Intrinsic::UnsafeCoerce)], + types: &[], + classes: &[], + }, + CompilerInterface { + module: "Data.Symbol", + implementation: InterfaceImplementation::Source, + values: &[], + types: &[], + classes: &[("IsSymbol", CompilerClass::IsSymbol)], + }, + // The effect-agnostic delimited-control surface. The carrier and the control + // primitives are compiler-owned; no pass recognizes a concrete operation. + CompilerInterface { + module: "Prim.Control", + implementation: InterfaceImplementation::Compiler, + values: &[ + ("return", Intrinsic::ControlReturn), + ("bind", Intrinsic::ControlBind), + ("perform", Intrinsic::ControlPerform), + ("run", Intrinsic::ControlRun), + ("handle", Intrinsic::ControlHandle), + ("once", Intrinsic::ControlOnce), + ("resume", Intrinsic::ControlResume), + ("cancel", Intrinsic::ControlCancel), + ], + types: &[ + ("Control", TypeId::CONTROL), + ("Resumption", TypeId::RESUMPTION), + ], + classes: &[], + }, +]; + +pub fn compiler_interface(module: &str) -> Option<&'static CompilerInterface> { + COMPILER_INTERFACES + .iter() + .find(|interface| interface.module == module) +} diff --git a/crates/psrs-hir/src/expr.rs b/crates/psrs-hir/src/expr.rs index fb7218d6..dd11f667 100644 --- a/crates/psrs-hir/src/expr.rs +++ b/crates/psrs-hir/src/expr.rs @@ -25,7 +25,7 @@ pub struct LocalBinding { pub span: TextRange, } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Debug, Eq)] pub struct Expr { pub kind: ExprKind, pub span: TextRange, @@ -41,6 +41,10 @@ pub enum ExprKind { Char(char), Array(Vec), Record(Vec<(String, Expr)>), + /// An ordered product of independent match scrutinees. Unlike a source + /// record literal, its fields retain polymorphism until pattern checking. + /// P5 checks the fields and converts the product to an ordinary typed record. + MatchProduct(Vec<(String, Expr)>), RecordUpdate { expression: Box, fields: Vec<(String, Expr)>, @@ -108,9 +112,38 @@ pub enum ExprKind { Guarded(Vec), } +impl Clone for Expr { + fn clone(&self) -> Self { + psrs_span::with_sufficient_stack(|| clone_expr(self)) + } +} + +impl PartialEq for Expr { + fn eq(&self, other: &Self) -> bool { + psrs_span::with_sufficient_stack(|| expr_eq(self, other)) + } +} + +#[inline(never)] +fn clone_expr(expression: &Expr) -> Expr { + Expr { + kind: expression.kind.clone(), + span: expression.span, + } +} + +#[inline(never)] +fn expr_eq(left: &Expr, right: &Expr) -> bool { + left.span == right.span && left.kind == right.kind +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct ResolvedOperator { pub symbol: SymbolId, + /// A backticked value in scope, such as `` `f` ``. The CST grammar binds + /// that form tighter than every symbolic operator. `symbol` is unused + /// when this is set. + pub local: Option, pub operator_span: TextRange, pub associativity: Associativity, pub precedence: u32, @@ -140,6 +173,9 @@ pub enum CaseBranchCoverage { Guarded, /// A generated fallthrough or guard test is excluded from diagnostics. Generated, + /// A checked explicit `Partial` scope authorizes this generated trap + /// fallback. Source syntax and desugaring never produce this marker. + PartialFallback, } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/crates/psrs-hir/src/intrinsic/effects.rs b/crates/psrs-hir/src/intrinsic/effects.rs new file mode 100644 index 00000000..60ba6260 --- /dev/null +++ b/crates/psrs-hir/src/intrinsic/effects.rs @@ -0,0 +1,46 @@ +//! Conservative semantic effects, independent of a target provider. +use super::Intrinsic; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct IntrinsicEffects { + pub may_trap: bool, + pub may_write: bool, +} + +impl IntrinsicEffects { + pub(super) fn for_intrinsic(intrinsic: Intrinsic) -> Self { + use Intrinsic::*; + match intrinsic { + ArrayWrite | ControlCancel => Self { + may_trap: true, + may_write: true, + }, + IntQuot | IntRem | ArrayIndex | ArrayUpdate | StringToBytes | BytesToString + | Undefined | ArrayAppend | UnsafeCoerce | ArrayFill | NumberToString + | NumberFromDecimal => Self { + may_trap: true, + may_write: false, + }, + // Executing a delimiter runs the handler and the continuation, which + // can trap or mutate storage; the summary is deliberately + // conservative. `return`/`bind`/`perform`/`handle` only build an inert + // computation, so they carry no effect of their own. + ControlRun => Self { + may_trap: true, + may_write: true, + }, + BoolTrue | BoolFalse | IntAdd | IntSub | IntMul | IntEq | IntNe | IntLt | IntLe + | IntGt | IntGe | ArrayLength | IntNeg | IntComplement | NumberNeg | BooleanNot + | IntToNumber | NumberToInt | BooleanToInt | IntToBoolean | CharToInt | IntToChar + | IntAnd | IntOr | IntXor | IntShl | IntShr | IntZshr | NumberAdd | NumberSub + | NumberMul | NumberDiv | NumberEq | NumberNe | NumberLt | NumberLe | NumberGt + | NumberGe | BooleanAnd | BooleanOr | BooleanEq | BooleanNe | CharEq | CharNe + | CharLt | CharLe | CharGt | CharGe | Coerce | Unit | NumberTrunc | NumberFloor + | NumberCeil | NumberAbs | NumberSqrt | NumberAcos | NumberAsin | NumberAtan + | NumberAtan2 | NumberSin | NumberCos | NumberTan | NumberExp | NumberLog + | NumberPow | NumberMin | NumberMax | NumberSign | NumberRemainder | NumberIsNaN + | NumberNaN | NumberInfinity | ControlReturn | ControlBind | ControlPerform + | ControlHandle | ControlOnce | ControlResume => Self::default(), + } + } +} diff --git a/crates/psrs-hir/src/intrinsic/mod.rs b/crates/psrs-hir/src/intrinsic/mod.rs index 7127e899..7b6c8078 100644 --- a/crates/psrs-hir/src/intrinsic/mod.rs +++ b/crates/psrs-hir/src/intrinsic/mod.rs @@ -6,8 +6,10 @@ //! category, and type scheme — lives in [`registry`], and each pass interprets //! it in its own representation. +mod effects; mod registry; +pub use effects::IntrinsicEffects; pub use registry::{IntrinsicCategory, IntrinsicDescriptor}; use crate::{ModuleId, SymbolId}; @@ -15,83 +17,174 @@ use crate::{ModuleId, SymbolId}; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[repr(u32)] pub enum Intrinsic { - BoolTrue, - BoolFalse, - I32Add, - I32Sub, - I32Mul, - I32DivS, - I32RemS, - I32Eq, - I32Ne, - I32LtS, - I32LeS, - I32GtS, - I32GeS, - ArrayLength, - ArrayIndex, - ArrayUpdate, - IntNeg, - IntComplement, - NumberNeg, - BooleanNot, - IntToNumber, - NumberToInt, - BooleanToInt, - IntToBoolean, - CharToInt, - IntToChar, - IntDiv, - IntMod, - IntAnd, - IntOr, - IntXor, - IntShl, - IntShr, - IntZshr, - NumberAdd, - NumberSub, - NumberMul, - NumberDiv, - NumberEq, - NumberNe, - NumberLt, - NumberLe, - NumberGt, - NumberGe, - BooleanAnd, - BooleanOr, - BooleanEq, - BooleanNe, - CharEq, - CharNe, - CharLt, - CharLe, - CharGt, - CharGe, + BoolTrue = 0, + BoolFalse = 1, + IntAdd = 2, + IntSub = 3, + IntMul = 4, + IntQuot = 5, + IntRem = 6, + IntEq = 7, + IntNe = 8, + IntLt = 9, + IntLe = 10, + IntGt = 11, + IntGe = 12, + ArrayLength = 13, + ArrayIndex = 14, + ArrayUpdate = 15, + IntNeg = 16, + IntComplement = 17, + NumberNeg = 18, + BooleanNot = 19, + IntToNumber = 20, + NumberToInt = 21, + BooleanToInt = 22, + IntToBoolean = 23, + CharToInt = 24, + IntToChar = 25, + IntAnd = 28, + IntOr = 29, + IntXor = 30, + IntShl = 31, + IntShr = 32, + IntZshr = 33, + NumberAdd = 34, + NumberSub = 35, + NumberMul = 36, + NumberDiv = 37, + NumberEq = 38, + NumberNe = 39, + NumberLt = 40, + NumberLe = 41, + NumberGt = 42, + NumberGe = 43, + BooleanAnd = 44, + BooleanOr = 45, + BooleanEq = 46, + BooleanNe = 47, + CharEq = 48, + CharNe = 49, + CharLt = 50, + CharLe = 51, + CharGt = 52, + CharGe = 53, /// A source `String`'s canonical UTF-8 bytes as an `Array Int`. A source /// string is a sequence of Unicode scalar values, so this is lossless and /// never fails /// ([DEC-16](../../decision/DEC-16-scalar-strings-and-utf8-storage.md)). - StringToBytes, + StringToBytes = 54, /// An `Array Int` as a source `String`. Each element must be a canonical /// byte and the bytes must be well-formed UTF-8; either violation traps /// rather than producing replacement text. - BytesToString, + BytesToString = 55, /// Source-level `Safe.Coerce.coerce`, elaborated to a checked coercion. - Coerce, + Coerce = 56, /// The compiler-provided partial value `Prim.undefined`, whose type is /// `forall a. a`. It has no runtime representation yet, so the stages that /// would have to choose one report it instead of inventing it. - Undefined, + Undefined = 57, /// The one `Unit` value, written `unit` or `()`. A compiler primitive rather /// than a nullary constructor, because `Unit` is a builtin type here. - Unit, + Unit = 58, /// Source-level `Array.append`: concatenates two arrays of the same element /// type into a fresh array. A compiler primitive because building an array /// of a computed length has no source spelling; the library's /// `Semigroup (Array a)` instance and `Semigroup String` are its users. - ArrayAppend, + ArrayAppend = 59, + /// Source-level `Unsafe.Coerce.unsafeCoerce`, the unchecked representation + /// coercion (`unsafeCoerce#`). Unlike `Coerce` it carries no `Coercible` + /// proof; it is a representation-preserving cast at the value's erased + /// boundary. + UnsafeCoerce = 60, + /// Allocate a fresh array fully initialized with one checked element. + ArrayFill = 61, + /// Unsafe in-place write; returns the same array. Library internals only. + ArrayWrite = 62, + NumberToString = 63, + /// Truncate an IEEE-754 Number toward zero, retaining its Number representation. + NumberTrunc = 64, + /// Round a Number toward negative infinity. + NumberFloor = 65, + /// Round a Number toward positive infinity. + NumberCeil = 66, + /// Convert a complete ASCII decimal token to binary64; invalid tokens return NaN. + NumberFromDecimal = 67, + /// Clear the binary64 sign bit, including negative zero and NaN. + NumberAbs = 68, + /// IEEE-754 square root. Negative finite inputs and NaN produce NaN. + /// Negative zero remains negative zero. The operation does not trap. + NumberSqrt = 69, + /// Inverse cosine in radians. Finite inputs outside [-1, 1], infinities, + /// and NaN produce NaN. The operation does not trap. + NumberAcos = 70, + /// Inverse sine in radians. Finite inputs outside [-1, 1], infinities, + /// and NaN produce NaN. Negative zero remains negative zero. The operation + /// does not trap. + NumberAsin = 71, + /// Inverse tangent in radians. Positive and negative infinity produce + /// positive and negative pi/2. Negative zero remains negative zero. NaN + /// produces NaN. The operation does not trap. + NumberAtan = 72, + /// Four-quadrant inverse tangent of `y` then `x`, in radians. The signs + /// of both arguments select the quadrant. Negative zero is preserved when + /// the result is zero. Either NaN produces NaN. The operation does not trap. + NumberAtan2 = 73, + /// Sine in radians. Infinities produce NaN. The operation does not trap. + NumberSin = 74, + /// Cosine in radians. Infinities produce NaN. The operation does not trap. + NumberCos = 75, + /// Tangent in radians. Infinities produce NaN. The operation does not trap. + NumberTan = 76, + /// Base-e exponential. Overflow produces infinity and underflow produces + /// zero. NaN produces NaN. The operation does not trap. + NumberExp = 77, + /// Natural logarithm. A negative finite input produces NaN, and + /// either signed zero produces negative infinity. The operation does not trap. + NumberLog = 78, + /// Exponentiation. A NaN exponent, or ±1 raised to an infinity, produces + /// NaN. An exponent of zero produces 1, including a NaN base. The + /// operation does not trap. + NumberPow = 79, + /// The smaller number. Either NaN produces NaN. A zero result is negative + /// when either zero is negative. The operation does not trap. + NumberMin = 80, + /// The larger number. Either NaN produces NaN. A zero result is negative + /// only when both zeros are negative. The operation does not trap. + NumberMax = 81, + /// Sign: NaN and both zeros are unchanged, and every other value becomes + /// ±1. The operation does not trap. + NumberSign = 82, + /// JavaScript remainder. The sign follows the dividend. Division by zero + /// or an infinite dividend produces NaN. The operation does not trap. + NumberRemainder = 83, + /// NaN predicate. The Boolean result is true for every NaN payload. + NumberIsNaN = 84, + /// Canonical NaN value. + NumberNaN = 85, + /// Positive infinity. + NumberInfinity = 86, + /// `Prim.Control.return`: lift a value into the control computation. + ControlReturn = 92, + /// `Prim.Control.bind`: sequence two control computations. + ControlBind = 93, + /// `Prim.Control.perform`: suspend at a request, delivering it to the + /// nearest `run` delimiter. + ControlPerform = 94, + /// `Prim.Control.run`: install a delimiter and interpret requests. The + /// handler is rank-2 in the request's answer type and threads a handler + /// state. + ControlRun = 95, + /// `Prim.Control.handle`: the open eliminator. It interprets the `f` + /// operations of a computation over a combined effect `u` and leaves the + /// rest as a residual `Control g (s, a)`. + ControlHandle = 96, + /// Allocate opaque one-shot continuation ownership. + ControlOnce = 97, + /// Build an inert computation that consumes ownership at interpretation. + ControlResume = 98, + /// Cancel a pending one-shot owner. + ControlCancel = 99, } impl Intrinsic { @@ -105,22 +198,34 @@ impl Intrinsic { registry::descriptor(self) } - /// Every variant, in discriminant order. `bootstrap_externals` builds the - /// compiler-known externals from it; the assertion below keeps it exact. - pub const ALL: [Intrinsic; 60] = [ + /// Resolves an explicit primitive binding using the authoritative registry. + pub fn from_binding(name: &str) -> Option { + Self::ALL + .into_iter() + .find(|value| value.descriptor().name == name) + } + + /// Retired floor division, experimental storage, and State runner + /// intrinsic identities. Storage uses ordinary runtime bindings and the + /// State mechanism was removed; these slots must never be reused. + pub const RESERVED_IDS: [u32; 7] = [26, 27, 87, 88, 89, 90, 91]; + + /// Every active variant, in discriminant order. Binding resolution uses the + /// complete list. + pub const ALL: [Intrinsic; 93] = [ Intrinsic::BoolTrue, Intrinsic::BoolFalse, - Intrinsic::I32Add, - Intrinsic::I32Sub, - Intrinsic::I32Mul, - Intrinsic::I32DivS, - Intrinsic::I32RemS, - Intrinsic::I32Eq, - Intrinsic::I32Ne, - Intrinsic::I32LtS, - Intrinsic::I32LeS, - Intrinsic::I32GtS, - Intrinsic::I32GeS, + Intrinsic::IntAdd, + Intrinsic::IntSub, + Intrinsic::IntMul, + Intrinsic::IntQuot, + Intrinsic::IntRem, + Intrinsic::IntEq, + Intrinsic::IntNe, + Intrinsic::IntLt, + Intrinsic::IntLe, + Intrinsic::IntGt, + Intrinsic::IntGe, Intrinsic::ArrayLength, Intrinsic::ArrayIndex, Intrinsic::ArrayUpdate, @@ -134,8 +239,6 @@ impl Intrinsic { Intrinsic::IntToBoolean, Intrinsic::CharToInt, Intrinsic::IntToChar, - Intrinsic::IntDiv, - Intrinsic::IntMod, Intrinsic::IntAnd, Intrinsic::IntOr, Intrinsic::IntXor, @@ -168,19 +271,64 @@ impl Intrinsic { Intrinsic::Undefined, Intrinsic::Unit, Intrinsic::ArrayAppend, + Intrinsic::UnsafeCoerce, + Intrinsic::ArrayFill, + Intrinsic::ArrayWrite, + Intrinsic::NumberToString, + Intrinsic::NumberTrunc, + Intrinsic::NumberFloor, + Intrinsic::NumberCeil, + Intrinsic::NumberFromDecimal, + Intrinsic::NumberAbs, + Intrinsic::NumberSqrt, + Intrinsic::NumberAcos, + Intrinsic::NumberAsin, + Intrinsic::NumberAtan, + Intrinsic::NumberAtan2, + Intrinsic::NumberSin, + Intrinsic::NumberCos, + Intrinsic::NumberTan, + Intrinsic::NumberExp, + Intrinsic::NumberLog, + Intrinsic::NumberPow, + Intrinsic::NumberMin, + Intrinsic::NumberMax, + Intrinsic::NumberSign, + Intrinsic::NumberRemainder, + Intrinsic::NumberIsNaN, + Intrinsic::NumberNaN, + Intrinsic::NumberInfinity, + Intrinsic::ControlReturn, + Intrinsic::ControlBind, + Intrinsic::ControlPerform, + Intrinsic::ControlRun, + Intrinsic::ControlHandle, + Intrinsic::ControlOnce, + Intrinsic::ControlResume, + Intrinsic::ControlCancel, ]; } -// `ALL` lists every discriminant exactly once: its length is the highest -// discriminant plus one, and the values are unique. A new variant therefore +// Active and reserved identities cover every stable discriminant exactly once. +// Retired identities cannot be reused by a later operation. A new variant therefore // cannot be added and silently left out of the bootstrap name table. const _: () = { assert!( - Intrinsic::ALL.len() == Intrinsic::ArrayAppend as u32 as usize + 1, + Intrinsic::ALL.len() + Intrinsic::RESERVED_IDS.len() == 100, "Intrinsic::ALL is out of date: update it when adding a variant", ); let mut seen: u128 = 0; let mut index = 0; + while index < Intrinsic::RESERVED_IDS.len() { + let bit = 1u128 << Intrinsic::RESERVED_IDS[index]; + assert!( + seen & bit == 0, + "Intrinsic::RESERVED_IDS repeats an identity" + ); + seen |= bit; + index += 1; + } + index = 0; while index < Intrinsic::ALL.len() { let bit = 1u128 << (Intrinsic::ALL[index] as u32); assert!(seen & bit == 0, "Intrinsic::ALL repeats a variant"); diff --git a/crates/psrs-hir/src/intrinsic/registry.rs b/crates/psrs-hir/src/intrinsic/registry.rs deleted file mode 100644 index a252cfa5..00000000 --- a/crates/psrs-hir/src/intrinsic/registry.rs +++ /dev/null @@ -1,381 +0,0 @@ -//! Metadata for each [`Intrinsic`]: its name, arity, category, and type scheme. -//! -//! This module is data, not lowering. Every pass reads the same descriptor and -//! interprets it in its own representation; the registry depends only on -//! `psrs-hir`, so no pass gains a dependency and no cycle is possible. -//! -//! Adding a variant to [`Intrinsic`](super::Intrinsic) forces an entry here -//! through the exhaustive `descriptor` match, so a variant cannot be registered -//! silently. - -use super::Intrinsic; -use crate::{BuiltinType, Type, TypeId, TypeKind, TypeParameter}; -use psrs_span::TextRange; - -/// A representation-independent classification of an intrinsic: what kind of -/// term it is, not how any one stage emits it. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum IntrinsicCategory { - /// A nullary value: `true`, `false`, `unit`. - Nullary, - /// One scalar argument and a scalar result. - UnaryScalar, - /// Two scalar arguments and a scalar result. - BinaryScalar, - /// `Array.length`: `forall a. Array a -> Int`. - ArrayLength, - /// `Array.index`: `forall a. Array a -> Int -> a`. - ArrayIndex, - /// `Array.update`: `forall a. Array a -> Int -> a -> Array a`. - ArrayUpdate, - /// `Array.append`: `forall a. Array a -> Array a -> Array a`. - ArrayAppend, - /// A source `String` to its canonical UTF-8 bytes. - StringToBytes, - /// Canonical UTF-8 bytes back to a source `String`. - BytesToString, - /// `Safe.Coerce.coerce`: checked evidence with no runtime operation of its - /// own. - Coercion, - /// `Prim.undefined`: a value with no runtime representation. - PartialValue, -} - -/// The metadata for one intrinsic. -#[derive(Clone, Copy, Debug)] -pub struct IntrinsicDescriptor { - pub intrinsic: Intrinsic, - /// The name `resolve` binds for this intrinsic. It is the compiler-internal - /// primitive name; a surface operator spelling and its fixity belong to the - /// library. Until the library classes land, the `Int` operators are still - /// bound directly here (see the implementation notes in - /// `docs/design/D-15-compiler-builtins.md`). - pub name: &'static str, - /// The number of arguments a saturated call supplies: the leading-arrow - /// count of `scheme`. - pub arity: u8, - pub category: IntrinsicCategory, - /// A non-capturing constructor, so the descriptor table can be `const`. - pub scheme: fn() -> Type, -} - -/// Builds [`descriptor`] from one line per intrinsic. The generated `match` is -/// exhaustive, so a variant with no entry is a compile error. -macro_rules! descriptors { - ($( $variant:ident => $name:expr, $arity:expr, $category:ident, $scheme:path; )*) => { - /// The descriptor for an intrinsic. The match is exhaustive over - /// [`Intrinsic`], so every variant has exactly one entry. - pub fn descriptor(intrinsic: Intrinsic) -> IntrinsicDescriptor { - match intrinsic { - $( - Intrinsic::$variant => IntrinsicDescriptor { - intrinsic: Intrinsic::$variant, - name: $name, - arity: $arity, - category: IntrinsicCategory::$category, - scheme: $scheme, - }, - )* - } - } - }; -} - -descriptors! { - BoolTrue => "true", 0, Nullary, scheme::boolean; - BoolFalse => "false", 0, Nullary, scheme::boolean; - I32Add => "intAdd", 2, BinaryScalar, scheme::int_int_int; - I32Sub => "-", 2, BinaryScalar, scheme::int_int_int; - I32Mul => "intMul", 2, BinaryScalar, scheme::int_int_int; - I32DivS => "/", 2, BinaryScalar, scheme::int_int_int; - I32RemS => "%", 2, BinaryScalar, scheme::int_int_int; - I32Eq => "intEq", 2, BinaryScalar, scheme::int_int_bool; - I32Ne => "intNe", 2, BinaryScalar, scheme::int_int_bool; - I32LtS => "intLt", 2, BinaryScalar, scheme::int_int_bool; - I32LeS => "intLe", 2, BinaryScalar, scheme::int_int_bool; - I32GtS => "intGt", 2, BinaryScalar, scheme::int_int_bool; - I32GeS => "intGe", 2, BinaryScalar, scheme::int_int_bool; - ArrayLength => "arrayLength", 1, ArrayLength, scheme::array_length; - ArrayIndex => "arrayIndex", 2, ArrayIndex, scheme::array_index; - ArrayUpdate => "arrayUpdate", 3, ArrayUpdate, scheme::array_update; - IntNeg => "intNeg", 1, UnaryScalar, scheme::int_int; - IntComplement => "intComplement", 1, UnaryScalar, scheme::int_int; - NumberNeg => "numberNeg", 1, UnaryScalar, scheme::number_number; - BooleanNot => "booleanNot", 1, UnaryScalar, scheme::boolean_boolean; - IntToNumber => "intToNumber", 1, UnaryScalar, scheme::int_number; - NumberToInt => "numberToInt", 1, UnaryScalar, scheme::number_int; - BooleanToInt => "booleanToInt", 1, UnaryScalar, scheme::boolean_int; - IntToBoolean => "intToBoolean", 1, UnaryScalar, scheme::int_boolean; - CharToInt => "charToInt", 1, UnaryScalar, scheme::char_int; - IntToChar => "intToChar", 1, UnaryScalar, scheme::int_char; - IntDiv => "intDiv", 2, BinaryScalar, scheme::int_int_int; - IntMod => "intMod", 2, BinaryScalar, scheme::int_int_int; - IntAnd => "intAnd", 2, BinaryScalar, scheme::int_int_int; - IntOr => "intOr", 2, BinaryScalar, scheme::int_int_int; - IntXor => "intXor", 2, BinaryScalar, scheme::int_int_int; - IntShl => "intShl", 2, BinaryScalar, scheme::int_int_int; - IntShr => "intShr", 2, BinaryScalar, scheme::int_int_int; - IntZshr => "intZshr", 2, BinaryScalar, scheme::int_int_int; - NumberAdd => "numberAdd", 2, BinaryScalar, scheme::number_number_number; - NumberSub => "numberSub", 2, BinaryScalar, scheme::number_number_number; - NumberMul => "numberMul", 2, BinaryScalar, scheme::number_number_number; - NumberDiv => "numberDiv", 2, BinaryScalar, scheme::number_number_number; - NumberEq => "numberEq", 2, BinaryScalar, scheme::number_number_bool; - NumberNe => "numberNe", 2, BinaryScalar, scheme::number_number_bool; - NumberLt => "numberLt", 2, BinaryScalar, scheme::number_number_bool; - NumberLe => "numberLe", 2, BinaryScalar, scheme::number_number_bool; - NumberGt => "numberGt", 2, BinaryScalar, scheme::number_number_bool; - NumberGe => "numberGe", 2, BinaryScalar, scheme::number_number_bool; - BooleanAnd => "booleanAnd", 2, BinaryScalar, scheme::boolean_boolean_boolean; - BooleanOr => "booleanOr", 2, BinaryScalar, scheme::boolean_boolean_boolean; - BooleanEq => "booleanEq", 2, BinaryScalar, scheme::boolean_boolean_boolean; - BooleanNe => "booleanNe", 2, BinaryScalar, scheme::boolean_boolean_boolean; - CharEq => "charEq", 2, BinaryScalar, scheme::char_char_bool; - CharNe => "charNe", 2, BinaryScalar, scheme::char_char_bool; - CharLt => "charLt", 2, BinaryScalar, scheme::char_char_bool; - CharLe => "charLe", 2, BinaryScalar, scheme::char_char_bool; - CharGt => "charGt", 2, BinaryScalar, scheme::char_char_bool; - CharGe => "charGe", 2, BinaryScalar, scheme::char_char_bool; - StringToBytes => "stringToBytes", 1, StringToBytes, scheme::string_to_bytes; - BytesToString => "bytesToString", 1, BytesToString, scheme::bytes_to_string; - Coerce => "__psrs_coerce", 1, Coercion, scheme::coerce; - Undefined => "__psrs_undefined", 0, PartialValue, scheme::undefined; - Unit => "unit", 0, Nullary, scheme::unit; - ArrayAppend => "arrayAppend", 2, ArrayAppend, scheme::array_append; -} - -/// The HIR type schemes. Each returns a fresh [`Type`], so a caller that -/// instantiates it at a use site gets its own type variables. -mod scheme { - use super::*; - - fn empty_span() -> TextRange { - TextRange::new(0, 0) - } - - fn builtin(builtin: BuiltinType) -> Type { - Type { - kind: TypeKind::Constructor(builtin), - span: empty_span(), - } - } - - fn named(id: TypeId) -> Type { - Type { - kind: TypeKind::Named(id), - span: empty_span(), - } - } - - fn variable(name: &str) -> Type { - Type { - kind: TypeKind::Variable(name.to_owned()), - span: empty_span(), - } - } - - fn apply(function: Type, argument: Type) -> Type { - Type { - kind: TypeKind::Application(Box::new(function), Box::new(argument)), - span: empty_span(), - } - } - - fn arrow(parameter: Type, result: Type) -> Type { - Type { - kind: TypeKind::Function { - parameter: Box::new(parameter), - result: Box::new(result), - }, - span: empty_span(), - } - } - - fn forall(names: &[&str], body: Type) -> Type { - Type { - kind: TypeKind::Forall { - variables: names - .iter() - .map(|name| TypeParameter { - name: (*name).to_owned(), - name_span: empty_span(), - kind: None, - }) - .collect(), - body: Box::new(body), - }, - span: empty_span(), - } - } - - fn array(element: Type) -> Type { - apply(builtin(BuiltinType::Array), element) - } - - fn int() -> Type { - builtin(BuiltinType::Int) - } - - fn scalar(operand: BuiltinType, result: BuiltinType) -> Type { - let operand = builtin(operand); - arrow(operand.clone(), arrow(operand, builtin(result))) - } - - fn unary(operand: BuiltinType, result: BuiltinType) -> Type { - arrow(builtin(operand), builtin(result)) - } - - pub(super) fn boolean() -> Type { - builtin(BuiltinType::Boolean) - } - - pub(super) fn unit() -> Type { - builtin(BuiltinType::Unit) - } - - pub(super) fn int_int_int() -> Type { - scalar(BuiltinType::Int, BuiltinType::Int) - } - - pub(super) fn int_int_bool() -> Type { - scalar(BuiltinType::Int, BuiltinType::Boolean) - } - - pub(super) fn number_number_number() -> Type { - scalar(BuiltinType::Number, BuiltinType::Number) - } - - pub(super) fn number_number_bool() -> Type { - scalar(BuiltinType::Number, BuiltinType::Boolean) - } - - pub(super) fn boolean_boolean_boolean() -> Type { - scalar(BuiltinType::Boolean, BuiltinType::Boolean) - } - - pub(super) fn char_char_bool() -> Type { - scalar(BuiltinType::Char, BuiltinType::Boolean) - } - - pub(super) fn int_int() -> Type { - unary(BuiltinType::Int, BuiltinType::Int) - } - - pub(super) fn number_number() -> Type { - unary(BuiltinType::Number, BuiltinType::Number) - } - - pub(super) fn boolean_boolean() -> Type { - unary(BuiltinType::Boolean, BuiltinType::Boolean) - } - - pub(super) fn int_number() -> Type { - unary(BuiltinType::Int, BuiltinType::Number) - } - - pub(super) fn number_int() -> Type { - unary(BuiltinType::Number, BuiltinType::Int) - } - - pub(super) fn boolean_int() -> Type { - unary(BuiltinType::Boolean, BuiltinType::Int) - } - - pub(super) fn int_boolean() -> Type { - unary(BuiltinType::Int, BuiltinType::Boolean) - } - - pub(super) fn char_int() -> Type { - unary(BuiltinType::Char, BuiltinType::Int) - } - - pub(super) fn int_char() -> Type { - unary(BuiltinType::Int, BuiltinType::Char) - } - - pub(super) fn array_length() -> Type { - forall(&["a"], arrow(array(variable("a")), int())) - } - - pub(super) fn array_index() -> Type { - forall( - &["a"], - arrow(array(variable("a")), arrow(int(), variable("a"))), - ) - } - - pub(super) fn array_update() -> Type { - forall( - &["a"], - arrow( - array(variable("a")), - arrow(int(), arrow(variable("a"), array(variable("a")))), - ), - ) - } - - pub(super) fn array_append() -> Type { - forall( - &["a"], - arrow( - array(variable("a")), - arrow(array(variable("a")), array(variable("a"))), - ), - ) - } - - pub(super) fn string_to_bytes() -> Type { - arrow(builtin(BuiltinType::String), array(int())) - } - - pub(super) fn bytes_to_string() -> Type { - arrow(array(int()), builtin(BuiltinType::String)) - } - - pub(super) fn undefined() -> Type { - forall(&["a"], variable("a")) - } - - pub(super) fn coerce() -> Type { - let constraint = apply( - apply(named(TypeId::COERCIBLE), variable("a")), - variable("b"), - ); - Type { - kind: TypeKind::Constrained { - constraint: Box::new(constraint), - body: Box::new(arrow(variable("a"), variable("b"))), - }, - span: empty_span(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// `arity` is the leading-arrow count of `scheme`, so the two cannot drift. - #[test] - fn arity_matches_the_scheme() { - for intrinsic in Intrinsic::ALL { - let descriptor = intrinsic.descriptor(); - assert_eq!( - descriptor.arity as usize, - leading_arrows(&(descriptor.scheme)()), - "arity disagrees with the scheme for {intrinsic:?}", - ); - assert_eq!(descriptor.intrinsic, intrinsic); - } - } - - fn leading_arrows(ty: &Type) -> usize { - match &ty.kind { - TypeKind::Forall { body, .. } | TypeKind::Constrained { body, .. } => { - leading_arrows(body) - } - TypeKind::Function { result, .. } => 1 + leading_arrows(result), - _ => 0, - } - } -} diff --git a/crates/psrs-hir/src/intrinsic/registry/mod.rs b/crates/psrs-hir/src/intrinsic/registry/mod.rs new file mode 100644 index 00000000..9de21caf --- /dev/null +++ b/crates/psrs-hir/src/intrinsic/registry/mod.rs @@ -0,0 +1,219 @@ +//! Metadata for each [`Intrinsic`]: its name, arity, category, and type scheme. +//! +//! This module is data, not lowering. Every pass reads the same descriptor and +//! interprets it in its own representation; the registry depends only on +//! `psrs-hir`, so no pass gains a dependency and no cycle is possible. +//! +//! Adding a variant to [`Intrinsic`](super::Intrinsic) forces an entry here +//! through the exhaustive `descriptor` match, so a variant cannot be registered +//! silently. + +use super::{Intrinsic, IntrinsicEffects}; +use crate::{BuiltinType, Type, TypeField, TypeId, TypeKind, TypeParameter}; +use psrs_span::TextRange; + +/// A representation-independent classification of an intrinsic: what kind of +/// term it is, not how any one stage emits it. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum IntrinsicCategory { + /// A nullary value: `true`, `false`, `unit`, or a numeric constant. + Nullary, + /// One language argument and a language result. + Unary, + /// Two scalar arguments and a scalar result. + BinaryScalar, + /// `Array.length`: `forall a. Array a -> Int`. + ArrayLength, + /// `Array.index`: `forall a. Array a -> Int -> a`. + ArrayIndex, + /// `Array.update`: `forall a. Array a -> Int -> a -> Array a`. + ArrayUpdate, + /// `Array.append`: `forall a. Array a -> Array a -> Array a`. + ArrayAppend, + /// `forall a. Int -> a -> Array a`. + ArrayFill, + /// `forall a. Array a -> Int -> a -> Array a`, in place. + ArrayWrite, + /// A source `String` to its canonical UTF-8 bytes. + StringToBytes, + /// Canonical UTF-8 bytes back to a source `String`. + BytesToString, + /// `Safe.Coerce.coerce`: checked evidence with no runtime operation of its + /// own. + Coercion, + /// `Prim.undefined`: a value with no runtime representation. + PartialValue, + /// A compiler-owned delimited-control primitive: `Control`'s + /// construction, execution, typed elimination and explicit ownership operations. + Control, +} + +/// The metadata for one intrinsic. +#[derive(Clone, Copy, Debug)] +pub struct IntrinsicDescriptor { + pub intrinsic: Intrinsic, + /// The name `resolve` binds for this intrinsic. It is the compiler-internal + /// primitive name. Surface operator spellings and their fixities belong to + /// the library (`Data.Ring` owns `-`, `Data.EuclideanRing` owns `/`). + /// `%` is still the truncating remainder primitive. + pub name: &'static str, + /// The number of arguments a saturated call supplies: the leading-arrow + /// count of `scheme`. + pub arity: u8, + pub category: IntrinsicCategory, + pub effects: IntrinsicEffects, + /// A non-capturing constructor, so the descriptor table can be `const`. + pub scheme: fn() -> Type, +} + +/// Builds [`descriptor`] from one line per intrinsic. The generated `match` is +/// exhaustive, so a variant with no entry is a compile error. +macro_rules! descriptors { + ($( $variant:ident => $name:expr, $arity:expr, $category:ident, $scheme:path; )*) => { + /// The descriptor for an intrinsic. The match is exhaustive over + /// [`Intrinsic`], so every variant has exactly one entry. + pub fn descriptor(intrinsic: Intrinsic) -> IntrinsicDescriptor { + match intrinsic { + $( + Intrinsic::$variant => IntrinsicDescriptor { + intrinsic: Intrinsic::$variant, + name: $name, + arity: $arity, + category: IntrinsicCategory::$category, + effects: IntrinsicEffects::for_intrinsic(intrinsic), + scheme: $scheme, + }, + )* + } + } + }; +} + +descriptors! { + BoolTrue => "true", 0, Nullary, scheme::boolean; + BoolFalse => "false", 0, Nullary, scheme::boolean; + IntAdd => "intAdd", 2, BinaryScalar, scheme::int_int_int; + IntSub => "intSub", 2, BinaryScalar, scheme::int_int_int; + IntMul => "intMul", 2, BinaryScalar, scheme::int_int_int; + IntQuot => "intQuot", 2, BinaryScalar, scheme::int_int_int; + IntRem => "%", 2, BinaryScalar, scheme::int_int_int; + IntEq => "intEq", 2, BinaryScalar, scheme::int_int_bool; + IntNe => "intNe", 2, BinaryScalar, scheme::int_int_bool; + IntLt => "intLt", 2, BinaryScalar, scheme::int_int_bool; + IntLe => "intLe", 2, BinaryScalar, scheme::int_int_bool; + IntGt => "intGt", 2, BinaryScalar, scheme::int_int_bool; + IntGe => "intGe", 2, BinaryScalar, scheme::int_int_bool; + ArrayLength => "arrayLength", 1, ArrayLength, scheme::array_length; + ArrayIndex => "arrayIndex", 2, ArrayIndex, scheme::array_index; + ArrayUpdate => "arrayUpdate", 3, ArrayUpdate, scheme::array_update; + IntNeg => "intNeg", 1, Unary, scheme::int_int; + IntComplement => "intComplement", 1, Unary, scheme::int_int; + NumberNeg => "numberNeg", 1, Unary, scheme::number_number; + NumberTrunc => "numberTrunc", 1, Unary, scheme::number_number; + NumberFloor => "numberFloor", 1, Unary, scheme::number_number; + NumberAbs => "numberAbs", 1, Unary, scheme::number_number; + NumberSqrt => "numberSqrt", 1, Unary, scheme::number_number; + NumberAcos => "numberAcos", 1, Unary, scheme::number_number; + NumberAsin => "numberAsin", 1, Unary, scheme::number_number; + NumberAtan => "numberAtan", 1, Unary, scheme::number_number; + NumberAtan2 => "numberAtan2", 2, BinaryScalar, scheme::number_number_number; + NumberSin => "numberSin", 1, Unary, scheme::number_number; + NumberCos => "numberCos", 1, Unary, scheme::number_number; + NumberTan => "numberTan", 1, Unary, scheme::number_number; + NumberExp => "numberExp", 1, Unary, scheme::number_number; + NumberLog => "numberLog", 1, Unary, scheme::number_number; + NumberPow => "numberPow", 2, BinaryScalar, scheme::number_number_number; + NumberMin => "numberMin", 2, BinaryScalar, scheme::number_number_number; + NumberMax => "numberMax", 2, BinaryScalar, scheme::number_number_number; + NumberSign => "numberSign", 1, Unary, scheme::number_number; + NumberRemainder => "numberRemainder", 2, BinaryScalar, scheme::number_number_number; + NumberIsNaN => "numberIsNaN", 1, Unary, scheme::number_bool; + NumberNaN => "numberNan", 0, Nullary, scheme::number; + NumberInfinity => "numberInfinity", 0, Nullary, scheme::number; + NumberCeil => "numberCeil", 1, Unary, scheme::number_number; + BooleanNot => "booleanNot", 1, Unary, scheme::boolean_boolean; + IntToNumber => "intToNumber", 1, Unary, scheme::int_number; + NumberToInt => "numberToInt", 1, Unary, scheme::number_int; + BooleanToInt => "booleanToInt", 1, Unary, scheme::boolean_int; + IntToBoolean => "intToBoolean", 1, Unary, scheme::int_boolean; + CharToInt => "charToInt", 1, Unary, scheme::char_int; + IntToChar => "intToChar", 1, Unary, scheme::int_char; + IntAnd => "intAnd", 2, BinaryScalar, scheme::int_int_int; + IntOr => "intOr", 2, BinaryScalar, scheme::int_int_int; + IntXor => "intXor", 2, BinaryScalar, scheme::int_int_int; + IntShl => "intShl", 2, BinaryScalar, scheme::int_int_int; + IntShr => "intShr", 2, BinaryScalar, scheme::int_int_int; + IntZshr => "intZshr", 2, BinaryScalar, scheme::int_int_int; + NumberAdd => "numberAdd", 2, BinaryScalar, scheme::number_number_number; + NumberSub => "numberSub", 2, BinaryScalar, scheme::number_number_number; + NumberMul => "numberMul", 2, BinaryScalar, scheme::number_number_number; + NumberDiv => "numberDiv", 2, BinaryScalar, scheme::number_number_number; + NumberEq => "numberEq", 2, BinaryScalar, scheme::number_number_bool; + NumberNe => "numberNe", 2, BinaryScalar, scheme::number_number_bool; + NumberLt => "numberLt", 2, BinaryScalar, scheme::number_number_bool; + NumberLe => "numberLe", 2, BinaryScalar, scheme::number_number_bool; + NumberGt => "numberGt", 2, BinaryScalar, scheme::number_number_bool; + NumberGe => "numberGe", 2, BinaryScalar, scheme::number_number_bool; + BooleanAnd => "booleanAnd", 2, BinaryScalar, scheme::boolean_boolean_boolean; + BooleanOr => "booleanOr", 2, BinaryScalar, scheme::boolean_boolean_boolean; + BooleanEq => "booleanEq", 2, BinaryScalar, scheme::boolean_boolean_boolean; + BooleanNe => "booleanNe", 2, BinaryScalar, scheme::boolean_boolean_boolean; + CharEq => "charEq", 2, BinaryScalar, scheme::char_char_bool; + CharNe => "charNe", 2, BinaryScalar, scheme::char_char_bool; + CharLt => "charLt", 2, BinaryScalar, scheme::char_char_bool; + CharLe => "charLe", 2, BinaryScalar, scheme::char_char_bool; + CharGt => "charGt", 2, BinaryScalar, scheme::char_char_bool; + CharGe => "charGe", 2, BinaryScalar, scheme::char_char_bool; + StringToBytes => "stringToBytes", 1, StringToBytes, scheme::string_to_bytes; + BytesToString => "bytesToString", 1, BytesToString, scheme::bytes_to_string; + Coerce => "__psrs_coerce", 1, Coercion, scheme::coerce; + Undefined => "__psrs_undefined", 0, PartialValue, scheme::undefined; + Unit => "unit", 0, Nullary, scheme::unit; + ArrayAppend => "arrayAppend", 2, ArrayAppend, scheme::array_append; + UnsafeCoerce => "__psrs_unsafe_coerce", 1, Coercion, scheme::unsafe_coerce; + ArrayFill => "arrayFill", 2, ArrayFill, scheme::array_fill; + ArrayWrite => "arrayWrite", 3, ArrayWrite, scheme::array_update; + NumberToString => "numberToString", 1, Unary, scheme::number_string; + NumberFromDecimal => "numberFromDecimal", 1, Unary, scheme::string_number; + ControlReturn => "controlReturn", 1, Control, scheme::control_return; + ControlBind => "controlBind", 2, Control, scheme::control_bind; + ControlPerform => "controlPerform", 1, Control, scheme::control_perform; + ControlRun => "controlRun", 3, Control, scheme::control_run; + ControlHandle => "controlHandle", 3, Control, scheme::control_handle; + ControlOnce => "controlOnce", 1, Control, scheme::control_once; + ControlResume => "controlResume", 2, Control, scheme::control_resume; + ControlCancel => "controlCancel", 1, Control, scheme::control_cancel; +} + +/// The HIR type schemes. Each returns a fresh [`Type`], so a caller that +/// instantiates it at a use site gets its own type variables. +mod scheme; + +#[cfg(test)] +mod tests { + use super::*; + + /// `arity` is the leading-arrow count of `scheme`, so the two cannot drift. + #[test] + fn arity_matches_the_scheme() { + for intrinsic in Intrinsic::ALL { + let descriptor = intrinsic.descriptor(); + assert_eq!( + descriptor.arity as usize, + leading_arrows(&(descriptor.scheme)()), + "arity disagrees with the scheme for {intrinsic:?}", + ); + assert_eq!(descriptor.intrinsic, intrinsic); + } + } + + fn leading_arrows(ty: &Type) -> usize { + match &ty.kind { + TypeKind::Forall { body, .. } | TypeKind::Constrained { body, .. } => { + leading_arrows(body) + } + TypeKind::Function { result, .. } => 1 + leading_arrows(result), + _ => 0, + } + } +} diff --git a/crates/psrs-hir/src/intrinsic/registry/scheme/control.rs b/crates/psrs-hir/src/intrinsic/registry/scheme/control.rs new file mode 100644 index 00000000..ea69998c --- /dev/null +++ b/crates/psrs-hir/src/intrinsic/registry/scheme/control.rs @@ -0,0 +1,157 @@ +use super::*; + +/// `Control f a`: a computation over the opaque request functor `f` that +/// yields an `a`, possibly after suspending at `perform`. +fn control(of: Type, value: Type) -> Type { + apply(apply(named(TypeId::CONTROL), of), value) +} + +/// `Prim.Control.return :: forall f a. a -> Control f a`. +pub(in crate::intrinsic::registry) fn control_return() -> Type { + forall( + &["f", "a"], + arrow(variable("a"), control(variable("f"), variable("a"))), + ) +} + +/// `Prim.Control.bind :: forall f a b. Control f a -> (a -> Control f b) -> Control f b`. +pub(in crate::intrinsic::registry) fn control_bind() -> Type { + forall( + &["f", "a", "b"], + arrow( + control(variable("f"), variable("a")), + arrow( + arrow(variable("a"), control(variable("f"), variable("b"))), + control(variable("f"), variable("b")), + ), + ), + ) +} + +/// `Prim.Control.perform :: forall f a. f a -> Control f a`. +pub(in crate::intrinsic::registry) fn control_perform() -> Type { + forall( + &["f", "a"], + arrow( + apply(variable("f"), variable("a")), + control(variable("f"), variable("a")), + ), + ) +} + +/// A closed two-field record, used for the `(state, value)` result. +fn record(fields: &[(&str, Type)]) -> Type { + Type { + kind: TypeKind::Record { + fields: fields + .iter() + .map(|(label, ty)| TypeField { + label: (*label).to_owned(), + label_span: empty_span(), + ty: ty.clone(), + span: empty_span(), + }) + .collect(), + tail: None, + }, + span: empty_span(), + } +} + +/// `Prim.Control.run`, the closed state-threading fold: +/// `forall f a s. s -> (forall x. s -> f x -> (x -> Control f a) -> +/// (s, Control f a)) -> Control f a -> (s, a)`. +pub(in crate::intrinsic::registry) fn control_run() -> Type { + let handler = forall( + &["x"], + arrow( + variable("s"), + arrow( + apply(variable("f"), variable("x")), + arrow( + arrow(variable("x"), control(variable("f"), variable("a"))), + record(&[ + ("_1", variable("s")), + ("_2", control(variable("f"), variable("a"))), + ]), + ), + ), + ), + ); + forall( + &["f", "a", "s"], + arrow( + variable("s"), + arrow( + handler, + arrow( + control(variable("f"), variable("a")), + record(&[("_1", variable("s")), ("_2", variable("a"))]), + ), + ), + ), + ) +} + +/// Eliminate a source computation into an independently typed target computation. +/// Clauses execute outside source dispatch; the raw continuation retains `u`/`a`. +pub(in crate::intrinsic::registry) fn control_handle() -> Type { + let target = control(variable("g"), variable("b")); + let handler = forall( + &["x"], + arrow( + apply(variable("u"), variable("x")), + arrow( + arrow(variable("x"), control(variable("u"), variable("a"))), + target.clone(), + ), + ), + ); + forall( + &["u", "g", "a", "b"], + arrow( + arrow(variable("a"), target.clone()), + arrow( + handler, + arrow(control(variable("u"), variable("a")), target), + ), + ), + ) +} + +fn resumption(f: Type, a: Type, b: Type) -> Type { + apply(apply(apply(named(TypeId::RESUMPTION), f), a), b) +} + +/// Wrap an ordinary continuation with independently checked one-shot ownership. +pub(in crate::intrinsic::registry) fn control_once() -> Type { + forall( + &["f", "a", "b"], + arrow( + arrow(variable("a"), control(variable("f"), variable("b"))), + resumption(variable("f"), variable("a"), variable("b")), + ), + ) +} + +/// Construct a resume node; ownership is consumed when it is interpreted. +pub(in crate::intrinsic::registry) fn control_resume() -> Type { + forall( + &["f", "a", "b"], + arrow( + resumption(variable("f"), variable("a"), variable("b")), + arrow(variable("a"), control(variable("f"), variable("b"))), + ), + ) +} + +/// Cancel pending ownership immediately; cancelling an active/used owner traps. +pub(in crate::intrinsic::registry) fn control_cancel() -> Type { + forall( + &["f", "a", "b"], + arrow( + resumption(variable("f"), variable("a"), variable("b")), + builtin(BuiltinType::Unit), + ), + ) +} diff --git a/crates/psrs-hir/src/intrinsic/registry/scheme/mod.rs b/crates/psrs-hir/src/intrinsic/registry/scheme/mod.rs new file mode 100644 index 00000000..a215d14c --- /dev/null +++ b/crates/psrs-hir/src/intrinsic/registry/scheme/mod.rs @@ -0,0 +1,237 @@ +use super::*; + +fn empty_span() -> TextRange { + TextRange::new(0, 0) +} + +fn builtin(builtin: BuiltinType) -> Type { + Type { + kind: TypeKind::Constructor(builtin), + span: empty_span(), + } +} + +fn named(id: TypeId) -> Type { + Type { + kind: TypeKind::Named(id), + span: empty_span(), + } +} + +fn variable(name: &str) -> Type { + Type { + kind: TypeKind::Variable(name.to_owned()), + span: empty_span(), + } +} + +fn apply(function: Type, argument: Type) -> Type { + Type { + kind: TypeKind::Application(Box::new(function), Box::new(argument)), + span: empty_span(), + } +} + +fn arrow(parameter: Type, result: Type) -> Type { + Type { + kind: TypeKind::Function { + parameter: Box::new(parameter), + result: Box::new(result), + }, + span: empty_span(), + } +} + +fn forall(names: &[&str], body: Type) -> Type { + Type { + kind: TypeKind::Forall { + variables: names + .iter() + .map(|name| TypeParameter { + name: (*name).to_owned(), + name_span: empty_span(), + kind: None, + }) + .collect(), + body: Box::new(body), + }, + span: empty_span(), + } +} + +fn array(element: Type) -> Type { + apply(builtin(BuiltinType::Array), element) +} + +fn int() -> Type { + builtin(BuiltinType::Int) +} + +fn scalar(operand: BuiltinType, result: BuiltinType) -> Type { + let operand = builtin(operand); + arrow(operand.clone(), arrow(operand, builtin(result))) +} + +fn unary(operand: BuiltinType, result: BuiltinType) -> Type { + arrow(builtin(operand), builtin(result)) +} + +pub(super) fn boolean() -> Type { + builtin(BuiltinType::Boolean) +} + +pub(super) fn unit() -> Type { + builtin(BuiltinType::Unit) +} + +pub(super) fn int_int_int() -> Type { + scalar(BuiltinType::Int, BuiltinType::Int) +} + +pub(super) fn int_int_bool() -> Type { + scalar(BuiltinType::Int, BuiltinType::Boolean) +} + +pub(super) fn number_number_number() -> Type { + scalar(BuiltinType::Number, BuiltinType::Number) +} + +pub(super) fn number_number_bool() -> Type { + scalar(BuiltinType::Number, BuiltinType::Boolean) +} + +pub(super) fn boolean_boolean_boolean() -> Type { + scalar(BuiltinType::Boolean, BuiltinType::Boolean) +} + +pub(super) fn char_char_bool() -> Type { + scalar(BuiltinType::Char, BuiltinType::Boolean) +} + +pub(super) fn int_int() -> Type { + unary(BuiltinType::Int, BuiltinType::Int) +} + +pub(super) fn number_number() -> Type { + unary(BuiltinType::Number, BuiltinType::Number) +} + +pub(super) fn number_bool() -> Type { + unary(BuiltinType::Number, BuiltinType::Boolean) +} + +pub(super) fn number() -> Type { + builtin(BuiltinType::Number) +} + +pub(super) fn boolean_boolean() -> Type { + unary(BuiltinType::Boolean, BuiltinType::Boolean) +} + +pub(super) fn int_number() -> Type { + unary(BuiltinType::Int, BuiltinType::Number) +} + +pub(super) fn string_number() -> Type { + unary(BuiltinType::String, BuiltinType::Number) +} + +pub(super) fn number_string() -> Type { + unary(BuiltinType::Number, BuiltinType::String) +} + +pub(super) fn number_int() -> Type { + unary(BuiltinType::Number, BuiltinType::Int) +} + +pub(super) fn boolean_int() -> Type { + unary(BuiltinType::Boolean, BuiltinType::Int) +} + +pub(super) fn int_boolean() -> Type { + unary(BuiltinType::Int, BuiltinType::Boolean) +} + +pub(super) fn char_int() -> Type { + unary(BuiltinType::Char, BuiltinType::Int) +} + +pub(super) fn int_char() -> Type { + unary(BuiltinType::Int, BuiltinType::Char) +} + +pub(super) fn array_length() -> Type { + forall(&["a"], arrow(array(variable("a")), int())) +} + +pub(super) fn array_index() -> Type { + forall( + &["a"], + arrow(array(variable("a")), arrow(int(), variable("a"))), + ) +} + +pub(super) fn array_update() -> Type { + forall( + &["a"], + arrow( + array(variable("a")), + arrow(int(), arrow(variable("a"), array(variable("a")))), + ), + ) +} + +pub(super) fn array_append() -> Type { + forall( + &["a"], + arrow( + array(variable("a")), + arrow(array(variable("a")), array(variable("a"))), + ), + ) +} + +pub(super) fn array_fill() -> Type { + forall( + &["a"], + arrow(int(), arrow(variable("a"), array(variable("a")))), + ) +} + +pub(super) fn string_to_bytes() -> Type { + arrow(builtin(BuiltinType::String), array(int())) +} + +pub(super) fn bytes_to_string() -> Type { + arrow(array(int()), builtin(BuiltinType::String)) +} + +pub(super) fn undefined() -> Type { + forall(&["a"], variable("a")) +} + +pub(super) fn coerce() -> Type { + let constraint = apply( + apply(named(TypeId::COERCIBLE), variable("a")), + variable("b"), + ); + Type { + kind: TypeKind::Constrained { + constraint: Box::new(constraint), + body: Box::new(arrow(variable("a"), variable("b"))), + }, + span: empty_span(), + } +} + +/// `Unsafe.Coerce.unsafeCoerce`: an unconstrained identity cast. It has no +/// `Coercible` proof; the value crosses its erased representation unchanged. +pub(super) fn unsafe_coerce() -> Type { + forall(&["a", "b"], arrow(variable("a"), variable("b"))) +} + +mod control; +pub(super) use control::{ + control_bind, control_cancel, control_handle, control_once, control_perform, control_resume, + control_return, control_run, +}; diff --git a/crates/psrs-hir/src/lib.rs b/crates/psrs-hir/src/lib.rs index c1dfc8da..d9002fd7 100644 --- a/crates/psrs-hir/src/lib.rs +++ b/crates/psrs-hir/src/lib.rs @@ -2,6 +2,8 @@ use psrs_span::TextRange; use std::collections::{HashMap, HashSet}; use verify::verify_expr; +mod compiler_class; +mod compiler_interface; mod expr; mod module; mod primitives; @@ -9,6 +11,10 @@ mod substitution; mod ty; mod types; +pub use compiler_class::CompilerClass; +pub use compiler_interface::{ + COMPILER_INTERFACES, CompilerInterface, InterfaceImplementation, compiler_interface, +}; pub use expr::{ CaseBranch, CaseBranchCoverage, Declaration, Expr, ExprKind, Guard, GuardedExpr, LocalBinder, LocalBinding, Pattern, PatternKind, RecordPatternMode, ResolvedOperator, SectionSide, @@ -100,6 +106,11 @@ impl TypeId { pub const PRIM_TYPE_ERROR_QUOTE_LABEL: Self = Self::new(ModuleId::INTRINSICS, 28); pub const PRIM_TYPE_ERROR_BESIDE: Self = Self::new(ModuleId::INTRINSICS, 29); pub const PRIM_TYPE_ERROR_ABOVE: Self = Self::new(ModuleId::INTRINSICS, 30); + /// Stable identity of the compiler-owned `Prim.Control.Control` carrier, the + /// effect-agnostic delimited-control computation type. + pub const CONTROL: Self = Self::new(ModuleId::INTRINSICS, 31); + /// Opaque owner of one continuation use; argument roles are nominal. + pub const RESUMPTION: Self = Self::new(ModuleId::INTRINSICS, 32); } mod intrinsic; @@ -119,10 +130,31 @@ pub enum ExternalKind { /// resolves the canonical signature from the vendored WIT and lowers calls /// generically. See `docs/design/backend/wasm/canonical-abi-and-wit.md`. Wit { interface: String, function: String }, + /// An ordinary library foreign declaration awaiting target implementation. + /// Its identity is the declaring module and the external value's name; + /// absence of an implementation must remain an explicit linking failure. + Library { module: String }, + /// A source value explicitly bound to a compiler primitive. Its checked + /// declaration type remains authoritative until target linking verifies it. + Primitive(Intrinsic), + /// An explicitly named runtime provider/export. Source resolution records + /// identity only; target linking owns the checked ABI and implementation. + Runtime { module: String, function: String }, +} + +impl ExternalKind { + /// Source declarations must retain a checked signature across IR boundaries. + pub fn requires_checked_signature(&self) -> bool { + !matches!(self, Self::Intrinsic(_)) + } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct ExternalSymbol { + /// The declaring source module, retained when an import copies this symbol. + /// Compiler-provided intrinsics and post-checking synthetic externals have + /// no source declaration. + pub source_module: Option, pub symbol: SymbolId, pub name: String, pub kind: ExternalKind, @@ -155,6 +187,20 @@ pub struct Module { } impl Module { + /// Value annotations declared by this source module. Imported external + /// signatures retain their original owner and are checked there. + pub fn declared_value_signatures(&self) -> impl Iterator { + self.declarations + .iter() + .filter_map(|declaration| declaration.signature.as_ref()) + .chain( + self.externals + .iter() + .filter(|external| external.source_module == Some(self.id)) + .filter_map(|external| external.signature.as_ref()), + ) + } + /// Checks that every reference targets a declaration visible in this module. pub fn verify(&self) -> Result<(), Vec> { let mut errors = Vec::new(); @@ -180,6 +226,18 @@ impl Module { } } for external in &self.externals { + if external.kind.requires_checked_signature() && external.source_module.is_none() { + errors.push(VerifyError { + span: self.span, + message: "source external has no declaring module", + }); + } + if external.kind.requires_checked_signature() && external.signature.is_none() { + errors.push(VerifyError { + span: self.span, + message: "source external has no declared signature", + }); + } if external.symbol.module != ModuleId::INTRINSICS { errors.push(VerifyError { span: self.span, diff --git a/crates/psrs-hir/src/module.rs b/crates/psrs-hir/src/module.rs index 29706a7d..cd4d78ca 100644 --- a/crates/psrs-hir/src/module.rs +++ b/crates/psrs-hir/src/module.rs @@ -25,6 +25,9 @@ pub struct ImportedType { /// Importers must keep that nominal identity; the declaring module is not /// consulted again when a signature mentions the type. pub opaque: bool, + /// Data constructors brought in with this type. A `module` re-export + /// publishes these, so `Ordering(..)` stays available through `Data.Ord`. + pub constructors: Vec<(String, SymbolId)>, } /// A resolved import declaration. The imported module is identified by ID and diff --git a/crates/psrs-hir/src/primitives/control.rs b/crates/psrs-hir/src/primitives/control.rs new file mode 100644 index 00000000..3e227203 --- /dev/null +++ b/crates/psrs-hir/src/primitives/control.rs @@ -0,0 +1,41 @@ +use super::*; + +/// The `Prim.Control` interface's carrier type. `Control f a` is an opaque +/// computation over the request functor `f` that yields an `a`, possibly after +/// suspending at `perform`. Its representation is owned by the control lowering; +/// this declaration fixes only its identity, kind, and roles. +pub(super) fn declarations() -> Vec<(&'static str, TypeDeclaration)> { + vec![ + ( + "Prim.Control", + foreign_type( + TypeId::CONTROL, + "Control", + arrow_kind( + vec![ + arrow_kind(vec![builtin(BuiltinType::Type)], builtin(BuiltinType::Type)), + builtin(BuiltinType::Type), + ], + builtin(BuiltinType::Type), + ), + &[Role::Representational, Role::Representational], + ), + ), + ( + "Prim.Control", + foreign_type( + TypeId::RESUMPTION, + "Resumption", + arrow_kind( + vec![ + arrow_kind(vec![builtin(BuiltinType::Type)], builtin(BuiltinType::Type)), + builtin(BuiltinType::Type), + builtin(BuiltinType::Type), + ], + builtin(BuiltinType::Type), + ), + &[Role::Nominal, Role::Nominal, Role::Nominal], + ), + ), + ] +} diff --git a/crates/psrs-hir/src/primitives/mod.rs b/crates/psrs-hir/src/primitives/mod.rs index 8e95729f..959dc5c3 100644 --- a/crates/psrs-hir/src/primitives/mod.rs +++ b/crates/psrs-hir/src/primitives/mod.rs @@ -4,6 +4,7 @@ use crate::{ }; use psrs_span::TextRange; +mod control; mod core; mod numbers; mod rows; @@ -16,6 +17,7 @@ mod tests; /// shared type spine. Each declaration is owned by its virtual interface. pub fn primitive_type_declarations() -> Vec<(&'static str, TypeDeclaration)> { [ + control::declarations(), core::declarations(), rows::declarations(), numbers::declarations(), @@ -38,6 +40,7 @@ fn class( name: name.to_owned(), name_span: empty_span(), kind: TypeDeclarationKind::Class, + compiler_class: None, parameters: parameters .iter() .map(|name| TypeParameter { @@ -63,6 +66,7 @@ fn foreign_type(id: TypeId, name: &str, declared_kind: Type, roles: &[Role]) -> name: name.to_owned(), name_span: empty_span(), kind: TypeDeclarationKind::Foreign, + compiler_class: None, parameters: Vec::new(), constructors: Vec::new(), members: Vec::new(), diff --git a/crates/psrs-hir/src/primitives/tests.rs b/crates/psrs-hir/src/primitives/tests.rs index 09b0c791..2157c48c 100644 --- a/crates/psrs-hir/src/primitives/tests.rs +++ b/crates/psrs-hir/src/primitives/tests.rs @@ -43,6 +43,12 @@ const OFFICIAL_MEMBERS: &[(&str, &str)] = &[ ("Prim.TypeError", "Text"), ]; +/// Compiler-owned members that extend the official `Prim.*` environment. They +/// are not part of PureScript's `primModules`, so they are recorded separately +/// and never folded into the official transcription above. +const COMPILER_EXTENSIONS: &[(&str, &str)] = + &[("Prim.Control", "Control"), ("Prim.Control", "Resumption")]; + fn declared(owner: &str, name: &str) -> TypeDeclaration { primitive_type_declarations() .into_iter() @@ -60,12 +66,13 @@ fn every_official_primitive_member_is_declared_exactly_once() { declared.sort(); let mut official = OFFICIAL_MEMBERS .iter() + .chain(COMPILER_EXTENSIONS.iter()) .map(|(owner, name)| ((*owner).to_owned(), (*name).to_owned())) .collect::>(); official.sort(); assert_eq!( declared, official, - "the registry must declare exactly the official `Prim` members" + "the registry must declare exactly the official `Prim` members plus the recorded compiler extensions" ); } diff --git a/crates/psrs-hir/src/tests.rs b/crates/psrs-hir/src/tests.rs index 07c892b6..98a6f5c8 100644 --- a/crates/psrs-hir/src/tests.rs +++ b/crates/psrs-hir/src/tests.rs @@ -1,11 +1,38 @@ use super::*; +/// Clone and equality follow a deep application spine on a heap stack. +/// Ordinary recursive destruction also fits the worker stack at this depth. +#[test] +fn deep_application_clone_equality_and_drop_stay_within_a_worker_stack() { + let mut expression = Expr { + kind: ExprKind::Char('t'), + span: TextRange::default(), + }; + for _ in 0..800 { + expression = Expr { + kind: ExprKind::Application( + Box::new(Expr { + kind: ExprKind::Char('f'), + span: TextRange::default(), + }), + Box::new(expression), + ), + span: TextRange::default(), + }; + } + let cloned = expression.clone(); + assert_eq!(expression, cloned); + drop(cloned); + drop(expression); +} + fn class_type(module: ModuleId, index: u32) -> TypeDeclaration { TypeDeclaration { id: TypeId::new(module, index), name: format!("Class{index}"), name_span: TextRange::default(), kind: TypeDeclarationKind::Class, + compiler_class: None, parameters: Vec::new(), constructors: Vec::new(), members: Vec::new(), diff --git a/crates/psrs-hir/src/types.rs b/crates/psrs-hir/src/types.rs index 16feae4d..10f6230e 100644 --- a/crates/psrs-hir/src/types.rs +++ b/crates/psrs-hir/src/types.rs @@ -35,6 +35,9 @@ pub struct TypeDeclaration { pub name: String, pub name_span: TextRange, pub kind: TypeDeclarationKind, + /// A canonical library interface identity attached by resolution. P5 + /// validates the declared dictionary contract before enabling its rule. + pub compiler_class: Option, pub parameters: Vec, pub constructors: Vec, pub members: Vec, diff --git a/crates/psrs-hir/src/verify/mod.rs b/crates/psrs-hir/src/verify/mod.rs index 936608bb..b917c2c3 100644 --- a/crates/psrs-hir/src/verify/mod.rs +++ b/crates/psrs-hir/src/verify/mod.rs @@ -17,6 +17,18 @@ pub(crate) fn verify_expr( visible_locals: &mut HashSet, declared_locals: &mut HashSet, errors: &mut Vec, +) { + psrs_span::with_sufficient_stack(|| { + verify_expr_inner(expression, globals, visible_locals, declared_locals, errors) + }) +} + +fn verify_expr_inner( + expression: &Expr, + globals: &HashSet, + visible_locals: &mut HashSet, + declared_locals: &mut HashSet, + errors: &mut Vec, ) { match &expression.kind { ExprKind::Local(id) if !visible_locals.contains(id) => errors.push(VerifyError { @@ -38,7 +50,7 @@ pub(crate) fn verify_expr( verify_expr(element, globals, visible_locals, declared_locals, errors); } } - ExprKind::Record(fields) => { + ExprKind::Record(fields) | ExprKind::MatchProduct(fields) => { for (_, value) in fields { verify_expr(value, globals, visible_locals, declared_locals, errors); } @@ -93,7 +105,14 @@ pub(crate) fn verify_expr( }); } for operator in operators { - if !globals.contains(&operator.symbol) { + if let Some(local) = operator.local { + if !visible_locals.contains(&local) { + errors.push(VerifyError { + span: operator.operator_span, + message: "backticked operator is not in scope", + }); + } + } else if !globals.contains(&operator.symbol) { errors.push(VerifyError { span: operator.operator_span, message: "operator symbol is not declared in the module or intrinsic set", @@ -110,7 +129,14 @@ pub(crate) fn verify_expr( binder, .. } => { - if !globals.contains(&operator.symbol) { + if let Some(local) = operator.local { + if !visible_locals.contains(&local) { + errors.push(VerifyError { + span: operator.operator_span, + message: "backticked operator is not in scope", + }); + } + } else if !globals.contains(&operator.symbol) { errors.push(VerifyError { span: operator.operator_span, message: "operator symbol is not declared in the module or intrinsic set", diff --git a/crates/psrs-hir/src/verify/normalized.rs b/crates/psrs-hir/src/verify/normalized.rs index bf0be858..18dcc228 100644 --- a/crates/psrs-hir/src/verify/normalized.rs +++ b/crates/psrs-hir/src/verify/normalized.rs @@ -19,6 +19,10 @@ pub(crate) fn normalized(module: &crate::Module) -> Result<(), Vec> } fn check_normalized_expr(expression: &Expr, errors: &mut Vec) { + psrs_span::with_sufficient_stack(|| check_normalized_expr_inner(expression, errors)) +} + +fn check_normalized_expr_inner(expression: &Expr, errors: &mut Vec) { match &expression.kind { ExprKind::Guarded(clauses) => { errors.push(VerifyError { @@ -88,7 +92,7 @@ fn check_normalized_expr(expression: &Expr, errors: &mut Vec) { check_normalized_expr(item, errors); } } - ExprKind::Record(fields) => { + ExprKind::Record(fields) | ExprKind::MatchProduct(fields) => { for (_, value) in fields { check_normalized_expr(value, errors); } diff --git a/crates/psrs-kind/src/check/infer/mod.rs b/crates/psrs-kind/src/check/infer/mod.rs index 4b231e4c..9b4770fb 100644 --- a/crates/psrs-kind/src/check/infer/mod.rs +++ b/crates/psrs-kind/src/check/infer/mod.rs @@ -170,12 +170,10 @@ impl Checker<'_> { } fn check_module_definitions(&mut self, module: &hir::Module) { - for declaration in &module.declarations { - if let Some(signature) = &declaration.signature { - let mut scope = HashMap::new(); - let kind = self.kind_of_type(signature, &mut scope); - self.unify(kind, type_kind(), signature.span); - } + for signature in module.declared_value_signatures() { + let mut scope = HashMap::new(); + let kind = self.kind_of_type(signature, &mut scope); + self.unify(kind, type_kind(), signature.span); } for declaration in &module.types { let Some(scheme) = self.schemes.get(&declaration.id).cloned() else { diff --git a/crates/psrs-kind/src/check/infer/pattern_annotations.rs b/crates/psrs-kind/src/check/infer/pattern_annotations.rs index 31777c82..aefc54d9 100644 --- a/crates/psrs-kind/src/check/infer/pattern_annotations.rs +++ b/crates/psrs-kind/src/check/infer/pattern_annotations.rs @@ -19,6 +19,10 @@ impl Checker<'_> { } fn check_expression_annotations(&mut self, expression: &psrs_hir::Expr) { + psrs_span::with_sufficient_stack(|| self.check_expression_annotations_inner(expression)) + } + + fn check_expression_annotations_inner(&mut self, expression: &psrs_hir::Expr) { use psrs_hir::ExprKind; match &expression.kind { ExprKind::Typed { expression, ty } | ExprKind::TypeApplication { expression, ty } => { @@ -30,7 +34,7 @@ impl Checker<'_> { self.check_expression_annotations(element); } } - ExprKind::Record(fields) => { + ExprKind::Record(fields) | ExprKind::MatchProduct(fields) => { for (_, value) in fields { self.check_expression_annotations(value); } diff --git a/crates/psrs-kind/src/check/infer/type_kind.rs b/crates/psrs-kind/src/check/infer/type_kind.rs index 86d68bdb..0db195ce 100644 --- a/crates/psrs-kind/src/check/infer/type_kind.rs +++ b/crates/psrs-kind/src/check/infer/type_kind.rs @@ -23,26 +23,31 @@ impl Checker<'_> { /// is the case that matters: each of its arguments is checked against the /// parameter kind its class declares, and `(->)` applied to one argument is /// how a higher-kinded class parameter is written — `Functor ((->) r)` is - /// the standard library's own instance, which `purs` accepts. Read on its - /// own, that same argument is the function arrow partway applied, which is - /// the `PartiallyAppliedSynonym` of `failing/TypeSynonyms9.purs`. + /// the standard library's own instance, which `purs` accepts. The function + /// arrow is a builtin type constructor, not a synonym, so a partial + /// application of it is never a `PartiallyAppliedSynonym`. pub(super) fn kind_of_type_against( &mut self, ty: &hir::Type, scope: &mut HashMap, report_partial_synonym: bool, ) -> Kind { + if report_partial_synonym { + self.check_annotation_saturation(ty); + } + self.infer_type_kind(ty, scope) + } + + // Infer the original application so even arguments discarded by synonym + // expansion must satisfy the declaration's checked parameter kinds. + fn infer_type_kind(&mut self, ty: &hir::Type, scope: &mut HashMap) -> Kind { match &ty.kind { TypeKind::Wildcard => self.fresh(), TypeKind::Application(..) => { let (head, arguments) = flatten_spine(ty); - if report_partial_synonym { - self.check_partial_synonym(head, arguments.len(), ty.span); - } let mut function = self.head_kind(head, scope); for argument in arguments { - let argument = - self.kind_of_type_against(argument, scope, report_partial_synonym); + let argument = self.infer_type_kind(argument, scope); let result = self.fresh(); self.unify( function, @@ -61,10 +66,7 @@ impl Checker<'_> { ); self.fresh() } - TypeKind::Named(id) | TypeKind::Opaque(id) => { - self.check_partial_synonym(ty, 0, ty.span); - self.instantiate_named(*id, ty.span) - } + TypeKind::Named(id) | TypeKind::Opaque(id) => self.instantiate_named(*id, ty.span), TypeKind::Constructor(BuiltinType::Function) => { // A bare `(->)` is the function type constructor itself, of kind // `Type -> Type -> Type`, and applies nothing. It is how a @@ -108,7 +110,7 @@ impl Checker<'_> { }), TypeKind::Constructor(builtin) => primitive_kind(*builtin), TypeKind::Named(id) | TypeKind::Opaque(id) => self.instantiate_named(*id, ty.span), - TypeKind::Application(..) => self.kind_of_type(ty, scope), + TypeKind::Application(..) => self.infer_type_kind(ty, scope), TypeKind::OperatorChain { .. } => { self.report( UNLOWERED_TYPE_OPERATOR, @@ -118,9 +120,9 @@ impl Checker<'_> { self.fresh() } TypeKind::Function { parameter, result } => { - let parameter_kind = self.kind_of_type(parameter, scope); + let parameter_kind = self.infer_type_kind(parameter, scope); self.unify(parameter_kind, type_kind(), parameter.span); - let result_kind = self.kind_of_type(result, scope); + let result_kind = self.infer_type_kind(result, scope); self.unify(result_kind, type_kind(), result.span); type_kind() } @@ -133,23 +135,23 @@ impl Checker<'_> { }; scope.insert(variable.name.clone(), kind); } - let kind = self.kind_of_type(body, scope); + let kind = self.infer_type_kind(body, scope); *scope = saved; kind } TypeKind::Constrained { constraint, body } => { - let constraint_kind = self.kind_of_type(constraint, scope); + let constraint_kind = self.infer_type_kind(constraint, scope); self.unify(constraint_kind, super::constraint_kind(), constraint.span); - self.kind_of_type(body, scope) + self.infer_type_kind(body, scope) } TypeKind::Row { fields, tail } => { let field_kind = self.fresh(); for field in fields { - let kind = self.kind_of_type(&field.ty, scope); + let kind = self.infer_type_kind(&field.ty, scope); self.unify(kind, field_kind.clone(), field.ty.span); } if let Some(tail) = tail { - let tail_kind = self.kind_of_type(tail, scope); + let tail_kind = self.infer_type_kind(tail, scope); self.unify(tail_kind, Kind::row(field_kind.clone()), tail.span); } Kind::row(field_kind) @@ -157,11 +159,11 @@ impl Checker<'_> { TypeKind::Record { fields, tail } => { let field_kind = self.fresh(); for field in fields { - let kind = self.kind_of_type(&field.ty, scope); + let kind = self.infer_type_kind(&field.ty, scope); self.unify(kind, field_kind.clone(), field.ty.span); } if let Some(tail) = tail { - let tail_kind = self.kind_of_type(tail, scope); + let tail_kind = self.infer_type_kind(tail, scope); self.unify(tail_kind, Kind::row(field_kind), tail.span); } type_kind() @@ -178,44 +180,51 @@ impl Checker<'_> { /// `newtype C (a :: (->) (Type -> Type) -> Type)` is a partially applied /// synonym whether the expression is read as a kind or as a type. pub(in crate::check) fn check_annotation_saturation(&mut self, ty: &hir::Type) { + // A saturated outer alias can apply or discard a partial alias + // argument. Only aliases surviving substitution require saturation. + let expanded = super::super::synonyms::expand(ty, &self.synonyms); + self.check_normalized_saturation(expanded.as_ref().unwrap_or(ty), ty.span); + } + + fn check_normalized_saturation(&mut self, ty: &hir::Type, use_span: TextRange) { match &ty.kind { TypeKind::Application(..) => { let (head, arguments) = flatten_spine(ty); - self.check_partial_synonym(head, arguments.len(), ty.span); - self.check_annotation_saturation(head); + self.check_partial_synonym(head, arguments.len(), use_span); for argument in arguments { - self.check_annotation_saturation(argument); + self.check_normalized_saturation(argument, use_span); } } TypeKind::Function { parameter, result } => { - self.check_annotation_saturation(parameter); - self.check_annotation_saturation(result); + self.check_normalized_saturation(parameter, use_span); + self.check_normalized_saturation(result, use_span); } TypeKind::Forall { variables, body } => { for variable in variables { if let Some(annotation) = &variable.kind { - self.check_annotation_saturation(annotation); + self.check_normalized_saturation(annotation, use_span); } } - self.check_annotation_saturation(body); + self.check_normalized_saturation(body, use_span); } TypeKind::Constrained { constraint, body } => { - self.check_annotation_saturation(constraint); - self.check_annotation_saturation(body); + self.check_normalized_saturation(constraint, use_span); + self.check_normalized_saturation(body, use_span); } TypeKind::Row { fields, tail } | TypeKind::Record { fields, tail } => { for field in fields { - self.check_annotation_saturation(&field.ty); + self.check_normalized_saturation(&field.ty, use_span); } if let Some(tail) = tail { - self.check_annotation_saturation(tail); + self.check_normalized_saturation(tail, use_span); } } + TypeKind::Named(_) | TypeKind::Opaque(_) => { + self.check_partial_synonym(ty, 0, use_span); + } TypeKind::Variable(_) | TypeKind::Wildcard | TypeKind::Constructor(_) - | TypeKind::Named(_) - | TypeKind::Opaque(_) | TypeKind::OperatorChain { .. } | TypeKind::Integer(_) | TypeKind::String(_) => {} @@ -228,26 +237,15 @@ impl Checker<'_> { arguments: usize, span: TextRange, ) { - match &head.kind { - TypeKind::Named(id) => { - if let Some(arity) = self.synonym_arity.get(id) - && arguments < *arity - { - self.report( - PARTIALLY_APPLIED_SYNONYM, - span, - "a type synonym must be fully applied", - ); - } - } - TypeKind::Constructor(BuiltinType::Function) if arguments < 2 => { - self.report( - PARTIALLY_APPLIED_SYNONYM, - span, - "a type synonym must be fully applied", - ); - } - _ => {} + if let TypeKind::Named(id) = &head.kind + && let Some(synonym) = self.synonyms.get(id) + && arguments < synonym.parameters.len() + { + self.report( + PARTIALLY_APPLIED_SYNONYM, + span, + "a type synonym must be fully applied", + ); } } } diff --git a/crates/psrs-kind/src/check/mod.rs b/crates/psrs-kind/src/check/mod.rs index 20fa303f..6c0418ce 100644 --- a/crates/psrs-kind/src/check/mod.rs +++ b/crates/psrs-kind/src/check/mod.rs @@ -10,6 +10,7 @@ use std::collections::HashMap; mod infer; mod roles; +pub(crate) mod synonyms; /// Official `errorCode`s this pass reports. pub const KINDS_DO_NOT_UNIFY: &str = "KindsDoNotUnify"; @@ -71,7 +72,7 @@ struct Checker<'a> { /// The schemes this run knows: the program's checked schemes, overwritten by /// the declarations this run checks. schemes: HashMap, - synonym_arity: HashMap, + synonyms: synonyms::SynonymMap, state: KindState, errors: Vec, } @@ -88,7 +89,7 @@ impl<'a> Checker<'a> { declaring, names, schemes: HashMap::new(), - synonym_arity: synonym_arities(modules), + synonyms: synonyms::collect(modules), state: coercible_state(), errors: Vec::new(), } @@ -99,13 +100,15 @@ impl<'a> Checker<'a> { let (declaring, names) = declaration_index(modules); let mut schemes = imported.kinds.clone(); schemes.extend(coercible_scheme()); + let mut synonyms = imported.synonyms.clone(); + synonyms.extend(synonyms::collect(modules)); Self { current: module.id, modules, declaring, names, schemes, - synonym_arity: synonym_arities(modules), + synonyms, state: coercible_state(), errors: Vec::new(), } @@ -158,6 +161,7 @@ impl<'a> Checker<'a> { roles, kinds: self.checked_schemes(), declaring_module, + synonyms: self.synonyms.clone(), } } @@ -193,11 +197,9 @@ impl<'a> Checker<'a> { fn check_defined_variables(&mut self) { for module in self.modules { self.current = module.id; - for declaration in &module.declarations { - if let Some(signature) = &declaration.signature { - let mut bound = Vec::new(); - self.check_references(signature, &mut bound); - } + for signature in module.declared_value_signatures() { + let mut bound = Vec::new(); + self.check_references(signature, &mut bound); } for declaration in &module.types { let mut bound: Vec = Vec::new(); @@ -389,15 +391,3 @@ fn declaration_index( } (declaring, names) } - -fn synonym_arities(modules: &[hir::Module]) -> HashMap { - let mut arities = HashMap::new(); - for module in modules { - for declaration in &module.types { - if declaration.kind == TypeDeclarationKind::TypeSynonym { - arities.insert(declaration.id, declaration.parameters.len()); - } - } - } - arities -} diff --git a/crates/psrs-kind/src/check/roles/mod.rs b/crates/psrs-kind/src/check/roles/mod.rs index 98dc6d4f..479e3508 100644 --- a/crates/psrs-kind/src/check/roles/mod.rs +++ b/crates/psrs-kind/src/check/roles/mod.rs @@ -2,7 +2,7 @@ use super::*; use psrs_hir::{BuiltinType, ModuleId}; use std::collections::HashSet; -mod synonyms; +use super::synonyms; /// Infers representation roles for every data, newtype, and foreign-data type in a /// resolved program. Explicit annotations constrain the published roles and diff --git a/crates/psrs-kind/src/check/roles/synonyms.rs b/crates/psrs-kind/src/check/synonyms.rs similarity index 65% rename from crates/psrs-kind/src/check/roles/synonyms.rs rename to crates/psrs-kind/src/check/synonyms.rs index 1a7ba2f5..7b4267c5 100644 --- a/crates/psrs-kind/src/check/roles/synonyms.rs +++ b/crates/psrs-kind/src/check/synonyms.rs @@ -1,16 +1,17 @@ use psrs_hir::{self as hir, TypeId, TypeKind}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; -#[derive(Clone)] -pub(super) struct Synonym { - parameters: Vec, +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct Synonym { + pub(super) parameters: Vec, body: hir::Type, + cyclic: bool, } -pub(super) type SynonymMap = HashMap; +pub(crate) type SynonymMap = HashMap; pub(super) fn collect(modules: &[hir::Module]) -> SynonymMap { - modules + let mut definitions: SynonymMap = modules .iter() .flat_map(|module| &module.types) .filter(|declaration| declaration.kind == hir::TypeDeclarationKind::TypeSynonym) @@ -24,20 +25,41 @@ pub(super) fn collect(modules: &[hir::Module]) -> SynonymMap { .map(|parameter| parameter.name.clone()) .collect(), body: declaration.body.clone()?, + cyclic: false, }, )) }) - .collect() + .collect(); + let edges = definitions + .iter() + .map(|(&id, item)| { + let mut references = Vec::new(); + crate::kind::collect_type_ids(&item.body, &mut references); + ( + id, + references + .into_iter() + .map(|id| (id, item.body.span)) + .collect(), + ) + }) + .collect(); + for (id, _) in crate::kind::detect_cycle(definitions.keys().copied(), &edges) { + if let Some(item) = definitions.get_mut(&id) { + item.cyclic = true; + } + } + definitions } -pub(super) fn expand(ty: &hir::Type, synonyms: &SynonymMap) -> Option { - expand_inner(ty, synonyms, &mut HashSet::new(), &mut 0) +pub(crate) fn expand(ty: &hir::Type, synonyms: &SynonymMap) -> Option { + expand_inner(ty, synonyms, &mut 65_536, &mut 0) } fn expand_inner( ty: &hir::Type, synonyms: &SynonymMap, - active: &mut HashSet, + fuel: &mut usize, next_fresh: &mut u32, ) -> Option { let kind = match &ty.kind { @@ -46,11 +68,11 @@ fn expand_inner( .get(id) .is_some_and(|item| item.parameters.is_empty()) => { - return expand_synonym(*id, Vec::new(), synonyms, active, next_fresh); + return expand_synonym(*id, Vec::new(), synonyms, fuel, next_fresh); } TypeKind::Application(function, argument) => { - let function = expand_inner(function, synonyms, active, next_fresh)?; - let argument = expand_inner(argument, synonyms, active, next_fresh)?; + let function = expand_inner(function, synonyms, fuel, next_fresh)?; + let argument = expand_inner(argument, synonyms, fuel, next_fresh)?; let applied = hir::Type { kind: TypeKind::Application(Box::new(function), Box::new(argument)), span: ty.span, @@ -60,13 +82,13 @@ fn expand_inner( && let Some(synonym) = synonyms.get(&id) && synonym.parameters.len() == arguments.len() { - return expand_synonym(id, arguments, synonyms, active, next_fresh); + return expand_synonym(id, arguments, synonyms, fuel, next_fresh); } return Some(applied); } TypeKind::Function { parameter, result } => TypeKind::Function { - parameter: Box::new(expand_inner(parameter, synonyms, active, next_fresh)?), - result: Box::new(expand_inner(result, synonyms, active, next_fresh)?), + parameter: Box::new(expand_inner(parameter, synonyms, fuel, next_fresh)?), + result: Box::new(expand_inner(result, synonyms, fuel, next_fresh)?), }, TypeKind::Forall { variables, body } => TypeKind::Forall { variables: variables @@ -76,29 +98,29 @@ fn expand_inner( name: variable.name.clone(), name_span: variable.name_span, kind: match &variable.kind { - Some(kind) => Some(expand_inner(kind, synonyms, active, next_fresh)?), + Some(kind) => Some(expand_inner(kind, synonyms, fuel, next_fresh)?), None => None, }, }) }) .collect::>>()?, - body: Box::new(expand_inner(body, synonyms, active, next_fresh)?), + body: Box::new(expand_inner(body, synonyms, fuel, next_fresh)?), }, TypeKind::Constrained { constraint, body } => TypeKind::Constrained { - constraint: Box::new(expand_inner(constraint, synonyms, active, next_fresh)?), - body: Box::new(expand_inner(body, synonyms, active, next_fresh)?), + constraint: Box::new(expand_inner(constraint, synonyms, fuel, next_fresh)?), + body: Box::new(expand_inner(body, synonyms, fuel, next_fresh)?), }, TypeKind::Row { fields, tail } => TypeKind::Row { - fields: expand_fields(fields, synonyms, active, next_fresh)?, + fields: expand_fields(fields, synonyms, fuel, next_fresh)?, tail: match tail { - Some(tail) => Some(Box::new(expand_inner(tail, synonyms, active, next_fresh)?)), + Some(tail) => Some(Box::new(expand_inner(tail, synonyms, fuel, next_fresh)?)), None => None, }, }, TypeKind::Record { fields, tail } => TypeKind::Record { - fields: expand_fields(fields, synonyms, active, next_fresh)?, + fields: expand_fields(fields, synonyms, fuel, next_fresh)?, tail: match tail { - Some(tail) => Some(Box::new(expand_inner(tail, synonyms, active, next_fresh)?)), + Some(tail) => Some(Box::new(expand_inner(tail, synonyms, fuel, next_fresh)?)), None => None, }, }, @@ -113,7 +135,7 @@ fn expand_inner( fn expand_fields( fields: &[hir::TypeField], synonyms: &SynonymMap, - active: &mut HashSet, + fuel: &mut usize, next_fresh: &mut u32, ) -> Option> { fields @@ -122,7 +144,7 @@ fn expand_fields( Some(hir::TypeField { label: field.label.clone(), label_span: field.label_span, - ty: expand_inner(&field.ty, synonyms, active, next_fresh)?, + ty: expand_inner(&field.ty, synonyms, fuel, next_fresh)?, span: field.span, }) }) @@ -133,13 +155,14 @@ fn expand_synonym( id: TypeId, arguments: Vec, synonyms: &SynonymMap, - active: &mut HashSet, + fuel: &mut usize, next_fresh: &mut u32, ) -> Option { let synonym = synonyms.get(&id)?; - if synonym.parameters.len() != arguments.len() || !active.insert(id) { + if synonym.parameters.len() != arguments.len() || synonym.cyclic || *fuel == 0 { return None; } + *fuel -= 1; let mapping = synonym .parameters .iter() @@ -147,9 +170,7 @@ fn expand_synonym( .zip(arguments) .collect::>(); let instantiated = hir::substitute_type_variables(&synonym.body, &mapping, next_fresh); - let expanded = expand_inner(&instantiated, synonyms, active, next_fresh); - active.remove(&id); - expanded + expand_inner(&instantiated, synonyms, fuel, next_fresh) } fn flatten_spine(ty: &hir::Type) -> (&hir::Type, Vec) { diff --git a/crates/psrs-kind/src/kind.rs b/crates/psrs-kind/src/kind.rs index 5c7d0782..1c7fa098 100644 --- a/crates/psrs-kind/src/kind.rs +++ b/crates/psrs-kind/src/kind.rs @@ -117,9 +117,18 @@ pub struct CheckedKindEnv { /// The module that declares each entry, so a use in one module is checked /// against, and reported against, the module that declared the type. pub declaring_module: HashMap, + /// Checked synonym templates retained for expansion during module rechecks. + pub(crate) synonyms: crate::check::synonyms::SynonymMap, } impl CheckedKindEnv { + /// Expands saturated synonyms with capture-avoiding substitution before + /// consumers elaborate their arguments. Returns `None` for a declaration + /// cycle or when the bounded expansion cannot finish. + pub fn expand_synonyms(&self, ty: &psrs_hir::Type) -> Option { + crate::check::synonyms::expand(ty, &self.synonyms) + } + pub fn roles(&self, id: TypeId) -> Option<&[Role]> { self.roles.get(&id).map(Vec::as_slice) } diff --git a/crates/psrs-kind/src/tests/declarations.rs b/crates/psrs-kind/src/tests/declarations.rs index 2fa1d5d4..41c9a7ab 100644 --- a/crates/psrs-kind/src/tests/declarations.rs +++ b/crates/psrs-kind/src/tests/declarations.rs @@ -100,7 +100,15 @@ fn rejects_an_unsaturated_foreign_data_constructor() { #[test] fn reports_a_partially_applied_function_synonym() { - let errors = check("module Main where\nnewtype N = N ((~>) Array)\n"); + // `~>` aliases a type synonym; applying the bare operator to fewer + // arguments than the synonym's arity is `PartiallyAppliedSynonym`. The + // plain function arrow is a builtin constructor and is never this error. + let errors = check( + "module Main where\n\ + type Natural f g = forall a. f a -> g a\n\ + infixr 4 type Natural as ~>\n\ + newtype N = N ((~>) Array)\n", + ); assert!( codes(&errors).contains(&"PartiallyAppliedSynonym"), "{errors:?}" diff --git a/crates/psrs-kind/src/tests/foreign.rs b/crates/psrs-kind/src/tests/foreign.rs new file mode 100644 index 00000000..427bef11 --- /dev/null +++ b/crates/psrs-kind/src/tests/foreign.rs @@ -0,0 +1,43 @@ +use super::*; + +#[test] +fn foreign_value_annotations_are_checked_at_their_declaring_module() { + let modules = resolve_program(&[ + ( + "Main.purs", + "module Main where\nimport Lib (bad)\nmain = bad\n", + ), + ( + "Lib.purs", + "module Lib where\ndata Region\nforeign import data R :: Region\nforeign import bad :: R\n", + ), + ( + "Other.purs", + "module Other where\nimport Lib (bad)\nvalue = bad\n", + ), + ]); + let diagnostics = check_modules(&modules); + let conflicts = diagnostics + .iter() + .filter(|error| error.code == "KindsDoNotUnify") + .collect::>(); + assert_eq!(conflicts.len(), 1, "{diagnostics:?}"); + assert_eq!(conflicts[0].origin, ModuleId(1)); +} + +#[test] +fn foreign_annotations_preserve_parameter_kinds_and_forall_scope() { + for source in [ + "module Test where\ndata Region\ndata Box :: Region -> Type\ndata Box r = Box\nforeign import bad :: Box Int\n", + "module Test where\nforeign import bad :: a\n", + ] { + let diagnostics = check(source); + assert!( + diagnostics + .iter() + .any(|error| matches!(error.code, "KindsDoNotUnify" | "UndefinedTypeVariable")), + "{diagnostics:?}" + ); + } + assert!(check("module Test where\nforeign import good :: forall a. a -> a\n").is_empty()); +} diff --git a/crates/psrs-kind/src/tests/instances.rs b/crates/psrs-kind/src/tests/instances.rs index 4d062cb1..224a1a04 100644 --- a/crates/psrs-kind/src/tests/instances.rs +++ b/crates/psrs-kind/src/tests/instances.rs @@ -60,19 +60,16 @@ fn accepts_the_function_constructor_as_an_instance_argument() { } #[test] -fn still_reports_a_partially_applied_function_synonym_in_a_type() { - // The same shape as above, but standing alone as a newtype field, where it - // is not checked against a class kind. `purs` reports - // PartiallyAppliedSynonym for `failing/TypeSynonyms9.purs`. - let errors = check( - "module Main where\n\ - newtype A (a :: (Type -> Type) -> Type -> Type) = A String\n\ - newtype B = B (A ((->) Array))\n", - ); - assert!( - codes(&errors).contains(&"PartiallyAppliedSynonym"), - "{errors:?}" - ); +fn accepts_a_partially_applied_function_arrow() { + // `(->)` is a builtin type constructor, not a synonym, so a partial + // application read on its own is valid: `purs` accepts `type Reader = (->) r` + // and `Free ((->) Unit)`. Only a named synonym is `PartiallyAppliedSynonym`. + check_ok(concat!( + "module Main where\n", + "type Reader = (->) Int\n", + "type ReaderString = (->) Int String\n", + "newtype B = B ((->) Boolean Int)\n", + )); } #[test] diff --git a/crates/psrs-kind/src/tests/mod.rs b/crates/psrs-kind/src/tests/mod.rs index a99ffe37..bb434c53 100644 --- a/crates/psrs-kind/src/tests/mod.rs +++ b/crates/psrs-kind/src/tests/mod.rs @@ -6,9 +6,11 @@ use crate::kind::KindDiagnostic; use psrs_hir::ModuleId; mod declarations; +mod foreign; mod instances; mod program; mod roles; +mod synonyms; pub(super) fn check(source: &str) -> Vec { check_modules(&[resolve(source)]) diff --git a/crates/psrs-kind/src/tests/synonyms.rs b/crates/psrs-kind/src/tests/synonyms.rs new file mode 100644 index 00000000..2fbc5838 --- /dev/null +++ b/crates/psrs-kind/src/tests/synonyms.rs @@ -0,0 +1,61 @@ +use super::*; + +#[test] +fn saturated_aliases_apply_or_discard_their_partial_alias_arguments() { + for usage in ["Apply (Wrap Int) Boolean", "Ignore (Wrap Int)"] { + check_ok(&format!( + "module Main where\ntype Apply f x = f x\ntype Ignore f = Int\ntype Wrap a b = {{ first :: a, second :: b }}\nvalue :: {usage}\nvalue = value\n" + )); + } +} + +#[test] +fn applying_an_open_row_alias_normalizes_before_saturation() { + check_ok( + "module Main where\ntype Apply f x = f x\ntype STATE s r = (state :: Array s | r)\nvalue :: Record (Apply (STATE Int) ())\nvalue = value\n", + ); +} + +#[test] +fn repeated_higher_order_alias_application_is_not_a_declaration_cycle() { + check_ok( + "module Main where\ntype Apply f x = f x\ntype Wrap a b = { first :: a, second :: b }\nvalue :: Apply (Apply (Wrap Int)) Boolean\nvalue = value\n", + ); +} + +#[test] +fn a_remaining_partial_alias_under_a_data_constructor_is_rejected() { + let errors = check( + "module Main where\ndata Holder f = Holder (f Boolean)\ntype Wrap a b = { first :: a, second :: b }\nvalue :: Holder (Wrap Int)\nvalue = value\n", + ); + assert!( + codes(&errors).contains(&"PartiallyAppliedSynonym"), + "{errors:?}" + ); +} + +#[test] +fn module_rechecks_retain_imported_alias_templates_and_arity() { + let modules = resolve_program(&[ + ( + "Alias.purs", + "module Alias where\ntype Apply f x = f x\ntype Wrap a b = { first :: a, second :: b }\n", + ), + ( + "Main.purs", + "module Main where\nimport Alias (Apply, Wrap)\nvalue :: Apply (Wrap Int) Boolean\nvalue = value\n", + ), + ]); + let (environment, errors) = check_env(&modules); + assert!(errors.is_empty(), "{errors:?}"); + let errors = crate::check_module(&modules[1], &environment); + assert!(errors.is_empty(), "{errors:?}"); +} + +#[test] +fn discarded_alias_arguments_still_satisfy_declared_kinds() { + let errors = check( + "module Main where\ntype Ignore :: Type -> Type\ntype Ignore a = Int\nvalue :: Ignore Array\nvalue = value\n", + ); + assert!(codes(&errors).contains(&"KindsDoNotUnify"), "{errors:?}"); +} diff --git a/crates/psrs-linker/Cargo.toml b/crates/psrs-linker/Cargo.toml new file mode 100644 index 00000000..4728c8c7 --- /dev/null +++ b/crates/psrs-linker/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "psrs-linker" +version.workspace = true +edition.workspace = true +license.workspace = true + +[[test]] +name = "guest" +path = "tests/guest/mod.rs" + +[dependencies] +psrs-runtime = { workspace = true, default-features = false, features = ["catalog", "gc-storage"] } +sha2 = "=0.10.9" +wasm-encoder.workspace = true +wasmparser.workspace = true +wit-component = "=0.245.1" +wit-parser = "=0.245.1" +wasm-compose = "=0.245.1" + +[dev-dependencies] +wasmprinter.workspace = true +wat = "=1.245.1" diff --git a/crates/psrs-linker/src/application/imports.rs b/crates/psrs-linker/src/application/imports.rs new file mode 100644 index 00000000..77d7349d --- /dev/null +++ b/crates/psrs-linker/src/application/imports.rs @@ -0,0 +1,134 @@ +//! Normalize checked function aliases for the component's unique import names. +use std::collections::BTreeMap; +use wasm_encoder::reencode::{Error, Reencode}; +use wasmparser::{Parser, Payload, TypeRef}; + +pub(crate) fn normalize(bytes: &[u8]) -> Result, String> { + let mut names = BTreeMap::new(); + let mut indices = Vec::new(); + for payload in Parser::new(0).parse_all(bytes) { + if let Payload::ImportSection(section) = payload.map_err(|e| e.to_string())? { + for import in section.into_imports() { + let import = import.map_err(|e| e.to_string())?; + if !matches!(import.ty, TypeRef::Func(_)) { + return Err( + "application import normalization requires checked functions".into(), + ); + } + let next = names.len() as u32; + let index = *names + .entry((import.module.to_owned(), import.name.to_owned())) + .or_insert(next); + indices.push(index); + } + } + } + if indices.len() == names.len() { + return Ok(bytes.to_vec()); + } + let mut encoder = Aliases { + original: indices.len() as u32, + retained: names.len() as u32, + indices, + }; + let mut module = wasm_encoder::Module::new(); + encoder + .parse_core_module(&mut module, Parser::new(0), bytes) + .map_err(|e| e.to_string())?; + let bytes = module.finish(); + wasmparser::Validator::new() + .validate_all(&bytes) + .map_err(|e| e.to_string())?; + Ok(bytes) +} + +struct Aliases { + original: u32, + retained: u32, + indices: Vec, +} + +impl Reencode for Aliases { + type Error = String; + + fn function_index(&mut self, index: u32) -> Result> { + Ok(if index < self.original { + self.indices[index as usize] + } else { + index - self.original + self.retained + }) + } + + fn parse_import_section( + &mut self, + imports: &mut wasm_encoder::ImportSection, + section: wasmparser::ImportSectionReader<'_>, + ) -> Result<(), Error> { + let mut seen = std::collections::BTreeSet::new(); + for import in section.into_imports() { + let import = import?; + if seen.insert((import.module, import.name)) { + self.parse_import(imports, import)?; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn alias_normalization_remaps_calls_exports_and_element_references() { + let bytes = wat::parse_str( + r#"(module + (type $f (func (result i32))) + (import "provider" "read" (func $a (type $f))) + (import "provider" "read" (func $b (type $f))) + (table 2 funcref) + (elem (i32.const 0) func $b $run) + (func $run (export "run") (type $f) call $b) + (export "alias" (func $b)))"#, + ) + .unwrap(); + let normalized = normalize(&bytes).unwrap(); + let mut imports = 0; + let mut exports = BTreeMap::new(); + let mut elements = Vec::new(); + let mut calls = Vec::new(); + for payload in Parser::new(0).parse_all(&normalized) { + match payload.unwrap() { + Payload::ImportSection(section) => imports = section.count(), + Payload::ExportSection(section) => { + for export in section { + let export = export.unwrap(); + exports.insert(export.name.to_owned(), export.index); + } + } + Payload::ElementSection(section) => { + for element in section { + let wasmparser::ElementItems::Functions(functions) = element.unwrap().items + else { + panic!("expected function references"); + }; + elements.extend(functions.into_iter().map(Result::unwrap)); + } + } + Payload::CodeSectionEntry(body) => { + for operator in body.get_operators_reader().unwrap() { + if let wasmparser::Operator::Call { function_index } = operator.unwrap() { + calls.push(function_index); + } + } + } + _ => {} + } + } + assert_eq!(imports, 1); + assert_eq!(exports["run"], 1); + assert_eq!(exports["alias"], 0); + assert_eq!(elements, [0, 1]); + assert_eq!(calls, [0]); + } +} diff --git a/crates/psrs-linker/src/application/mod.rs b/crates/psrs-linker/src/application/mod.rs new file mode 100644 index 00000000..1a21cafe --- /dev/null +++ b/crates/psrs-linker/src/application/mod.rs @@ -0,0 +1,190 @@ +//! Verify encoded application contracts before executing a checked link plan. +use crate::{CheckedLinkPlan, CoreType, LinkErrors, LinkStage}; +use wasmparser::{Operator, Parser, Payload, TypeRef}; + +pub(crate) fn verify(plan: &CheckedLinkPlan, bytes: &[u8]) -> Result<(), LinkErrors> { + check(plan, bytes).map_err(|message| LinkErrors::plain(LinkStage::Compose, message)) +} + +fn check(plan: &CheckedLinkPlan, bytes: &[u8]) -> Result<(), String> { + wasmparser::Validator::new() + .validate_all(bytes) + .map_err(|error| error.to_string())?; + let mut types = crate::CoreTypes::default(); + let mut seen = std::collections::BTreeSet::new(); + let mut memory = None; + let mut data = Vec::new(); + let mut function_types = Vec::new(); + let mut function_bodies = Vec::new(); + let mut function_exports = Vec::new(); + let mut imported_functions = 0_u32; + for payload in Parser::new(0).parse_all(bytes) { + match payload.map_err(|error| error.to_string())? { + Payload::TypeSection(reader) => { + for group in reader { + types.push(group.map_err(|error| error.to_string())?)?; + } + } + Payload::ImportSection(reader) => { + for import in reader.into_imports() { + let import = import.map_err(|error| error.to_string())?; + let TypeRef::Func(index) = import.ty else { + return Err("application imports an unplanned non-function item".into()); + }; + imported_functions += 1; + let binding = plan + .bindings() + .iter() + .find(|binding| { + binding.module == import.module && binding.field == import.name + }) + .ok_or_else(|| { + format!( + "application has an unplanned import `{}.{}`", + import.module, import.name + ) + })?; + let signature = types.signature(index)?; + if signature != binding.signature { + return Err( + "application import signature disagrees with the checked plan".into(), + ); + } + // Distinct source declarations may alias one checked ABI + // export. Every occurrence must match its complete type. + seen.insert((binding.module.clone(), binding.field.clone())); + } + } + Payload::MemorySection(reader) => { + for ty in reader { + let ty = ty.map_err(|error| error.to_string())?; + if memory.replace(ty).is_some() || ty.memory64 || ty.shared { + return Err("application memory differs from the checked plan".into()); + } + } + } + Payload::DataSection(reader) => { + for segment in reader { + let segment = segment.map_err(|error| error.to_string())?; + if let wasmparser::DataKind::Active { + memory_index, + offset_expr, + } = segment.kind + { + let mut ops = offset_expr.get_operators_reader(); + let Operator::I32Const { value } = + ops.read().map_err(|error| error.to_string())? + else { + return Err("application data offset is not constant".into()); + }; + if memory_index != 0 + || !matches!( + ops.read().map_err(|error| error.to_string())?, + Operator::End + ) + { + return Err("application data has an unplanned initialization".into()); + } + data.push((value as u32, segment.data)); + } + } + } + Payload::FunctionSection(reader) => { + for ty in reader { + function_types.push(ty.map_err(|error| error.to_string())?); + } + } + Payload::CodeSectionEntry(body) => function_bodies.push(body), + Payload::ExportSection(reader) => { + for export in reader { + let export = export.map_err(|error| error.to_string())?; + if export.kind == wasmparser::ExternalKind::Func { + function_exports.push((export.name.to_string(), export.index)); + } + } + } + Payload::StartSection { .. } => { + return Err("application start precedes planned shim resolution".into()); + } + _ => {} + } + } + if let Some(boundary) = plan.memory().heap_getter.as_ref() { + let (_, index) = function_exports + .iter() + .find(|(name, _)| name == &boundary.field) + .ok_or("application does not export the heap getter")?; + let defined = index + .checked_sub(imported_functions) + .ok_or("application heap getter must be defined locally")? + as usize; + let type_index = *function_types + .get(defined) + .ok_or("application heap getter has no signature")?; + let ty = types.signature(type_index)?; + if !ty.parameters.is_empty() || ty.result != Some(CoreType::I32) { + return Err("application heap getter must have signature () -> i32".into()); + } + let body = function_bodies + .get(defined) + .ok_or("application heap getter has no body")?; + if body + .get_locals_reader() + .map_err(|error| error.to_string())? + .get_count() + != 0 + { + return Err("application heap getter must have no locals".into()); + } + let mut ops = body + .get_operators_reader() + .map_err(|error| error.to_string())?; + if !matches!(ops.read().map_err(|error| error.to_string())?, Operator::I32Const { value } + if value as u32 == plan.memory().heap_start) + || !matches!( + ops.read().map_err(|error| error.to_string())?, + Operator::End + ) + || !ops.eof() + { + return Err("application heap getter must return only the checked constant".into()); + } + } + + for binding in plan.bindings() { + if !seen.contains(&(binding.module.clone(), binding.field.clone())) { + return Err(format!( + "application omits planned import `{}.{}`", + binding.module, binding.field + )); + } + } + if memory.is_some_and(|memory| memory.initial < plan.memory().minimum_pages) + || (!plan.artifacts().is_empty() && memory.is_none()) + { + return Err("application memory does not cover the checked reservations".into()); + } + for (start, bytes) in data { + let end = start + .checked_add(u32::try_from(bytes.len()).map_err(|_| "data length overflows")?) + .ok_or("application data range overflows")?; + let scratch = plan.memory().canonical_scratch; + let allocator = plan.memory().allocator_state; + if start >= allocator.0 && end <= allocator.1 { + if start != allocator.0 + || bytes.len() != 8 + || bytes[..4] != [0; 4] + || bytes[4..] != plan.memory().heap_start.to_le_bytes() + { + return Err("application allocator state disagrees with the checked plan".into()); + } + } else if start < scratch.0 || end > scratch.1 { + return Err("application active data overlaps an unowned region".into()); + } + } + Ok(()) +} + +pub(crate) mod imports; +#[cfg(test)] +mod tests; diff --git a/crates/psrs-linker/src/application/tests.rs b/crates/psrs-linker/src/application/tests.rs new file mode 100644 index 00000000..4110afe6 --- /dev/null +++ b/crates/psrs-linker/src/application/tests.rs @@ -0,0 +1,78 @@ +use crate::{ + BindingRequirement, Boundary, MemoryDemand, Provider, RequirementId, TargetLinkInput, + TargetPolicy, +}; + +#[test] +fn gc_application_contracts_compare_closed_types_before_composition() { + let context = crate::resolve_default_definitions().unwrap(); + let units = crate::runtime::package_offers(&psrs_runtime::PSRS_RUNTIME).unwrap(); + let storage = units + .iter() + .find(|unit| unit.id == psrs_runtime::STORAGE_UNIT.id) + .unwrap(); + let read = storage + .provided + .iter() + .find(|op| op.export == "array_read") + .unwrap(); + let plan = crate::plan( + &context, + TargetLinkInput { + requirements: vec![BindingRequirement { + id: RequirementId(0), + origin: "storage read".into(), + boundary: Boundary::RawCore { + module: psrs_runtime::STORAGE_MODULE.into(), + field: read.export.clone(), + }, + expected: Some(read.signature.clone()), + provider: Provider::RuntimeOperation { + name: read.name.clone(), + version: read.version.clone(), + }, + }], + units, + policy: TargetPolicy::default(), + memory: MemoryDemand { + canonical_scratch: (0, 16), + allocator_state: (16, 24), + base_heap_start: 24, + heap_alignment: 8, + growth_owner: crate::GENERATED_GROWTH_OWNER.into(), + maximum_pages: None, + }, + }, + ) + .unwrap(); + let text = r#"(module + (type (struct (field i64))) + (type $a (array (mut eqref))) + (import "psrs:runtime-storage" "array_read" (func (param (ref $a) i32) (result eqref))) + (memory (export "memory") 2))"#; + super::verify(&plan, &wat::parse_str(text).unwrap()).unwrap(); + let declaration = "(import \"psrs:runtime-storage\" \"array_read\" (func (param (ref $a) i32) (result eqref)))"; + let aliases = text.replace(declaration, &format!("{declaration}\n{declaration}")); + let bytes = wat::parse_str(&aliases).unwrap(); + wasmparser::Validator::new().validate_all(&bytes).unwrap(); + super::verify(&plan, &bytes).unwrap(); + let wrong_alias = text.replace( + declaration, + &format!( + "{declaration}\n{}", + declaration.replace("(ref $a)", "(ref null $a)") + ), + ); + let error = super::verify(&plan, &wat::parse_str(wrong_alias).unwrap()).unwrap_err(); + assert!(error.to_string().contains("application import signature")); + for modified in [ + text.replace("(ref $a)", "(ref null $a)"), + text.replace("(mut eqref)", "eqref"), + ] { + let error = super::verify(&plan, &wat::parse_str(modified).unwrap()).unwrap_err(); + assert!( + error.to_string().contains("application import signature"), + "{error}" + ); + } +} diff --git a/crates/psrs-linker/src/closure.rs b/crates/psrs-linker/src/closure.rs new file mode 100644 index 00000000..927191ac --- /dev/null +++ b/crates/psrs-linker/src/closure.rs @@ -0,0 +1,115 @@ +//! Derive the component tooling's executable import closure from planned calls. +//! +//! A live resource alias can require its defining interface, while unused +//! functions in that interface do not introduce their own type dependencies. +//! The encoder owns this projection; walking all WIT types over-approximates it. +use crate::plan::ResolvedBinding; +use crate::{CoreType, LinkErrors, LinkStage, ResolvedWorldContext}; +use wasm_encoder::{ + CodeSection, EntityType, ExportKind, ExportSection, Function, FunctionSection, ImportSection, + Instruction, MemorySection, MemoryType, Module, TypeSection, ValType, +}; + +pub(crate) fn host_imports( + context: &ResolvedWorldContext, + bindings: &[ResolvedBinding], +) -> Result, LinkErrors> { + let hosts: Vec<_> = bindings + .iter() + .filter(|binding| context.imports_interface(&binding.module)) + .collect(); + let Some(world) = context.composition_world() else { + let mut imports: Vec<_> = hosts.iter().map(|binding| binding.module.clone()).collect(); + imports.sort(); + imports.dedup(); + return Ok(imports); + }; + if hosts.is_empty() { + return Ok(Vec::new()); + } + let mut module = Module::new(); + let mut types = TypeSection::new(); + let mut imports = ImportSection::new(); + let mut seen = std::collections::BTreeSet::new(); + let mut count = 0_u32; + for binding in hosts { + if !seen.insert((&binding.module, &binding.field)) { + continue; + } + types.ty().function( + binding + .signature + .parameters + .iter() + .map(scalar) + .collect::, _>>()?, + binding.signature.result.as_ref().map(scalar).transpose()?, + ); + imports.import(&binding.module, &binding.field, EntityType::Function(count)); + count += 1; + } + types.ty().function([ValType::I32; 4], [ValType::I32]); + module.section(&types); + module.section(&imports); + let mut functions = FunctionSection::new(); + functions.function(count); + module.section(&functions); + let mut memories = MemorySection::new(); + memories.memory(MemoryType { + minimum: 1, + maximum: None, + memory64: false, + shared: false, + page_size_log2: None, + }); + module.section(&memories); + let mut exports = ExportSection::new(); + exports.export("memory", ExportKind::Memory, 0); + exports.export("cabi_realloc", ExportKind::Func, count); + module.section(&exports); + let mut function = Function::new([]); + function.instruction(&Instruction::Unreachable); + function.instruction(&Instruction::End); + let mut code = CodeSection::new(); + code.function(&function); + module.section(&code); + // This module is a nonexecuted signature projection, not an implementation. + // Export requirements are irrelevant to the import-only projection. + let mut resolve = context.resolve().clone(); + resolve.worlds[world].exports.clear(); + let mut bytes = module.finish(); + wit_component::embed_component_metadata( + &mut bytes, + &resolve, + world, + wit_component::StringEncoding::UTF8, + ) + .map_err(error)?; + let component = wit_component::ComponentEncoder::default() + .module(&bytes) + .map_err(error)? + .validate(true) + .encode() + .map_err(error)?; + crate::compose::component_imports(&component) +} + +fn scalar(ty: &CoreType) -> Result { + Ok(match ty { + CoreType::I32 => ValType::I32, + CoreType::I64 => ValType::I64, + CoreType::F32 => ValType::F32, + CoreType::F64 => ValType::F64, + CoreType::V128 | CoreType::Ref(_) => { + return Err(error( + "a raw GC or SIMD contract cannot cross a WIT scalar boundary", + )); + } + }) +} +fn error(error: impl std::fmt::Display) -> LinkErrors { + LinkErrors::plain( + LinkStage::Requirements, + format!("planned WIT call projection is invalid: {error}"), + ) +} diff --git a/crates/psrs-linker/src/compose.rs b/crates/psrs-linker/src/compose.rs new file mode 100644 index 00000000..194da4bd --- /dev/null +++ b/crates/psrs-linker/src/compose.rs @@ -0,0 +1,162 @@ +//! Component composition and final import-closure verification. + +use crate::definitions::ResolvedWorldContext; +use crate::error::{LinkErrors, LinkStage}; +use crate::plan::CheckedLinkPlan; +use wasmparser::{Parser, Payload}; +use wit_component::{ComponentEncoder, LibraryInfo, StringEncoding, embed_component_metadata}; + +/// The final composed component and the host interfaces it still imports. +#[derive(Clone, Debug)] +pub struct LinkedArtifact { + pub bytes: Vec, + pub external_world: Vec, +} + +/// Composes an encoded application with the plan's verified libraries. +/// +/// The final component's unresolved imports must equal the plan's +/// external world; the private runtime import must be closed. +pub fn compose( + context: &ResolvedWorldContext, + plan: &CheckedLinkPlan, + application: &[u8], +) -> Result { + let stage = LinkStage::Compose; + if !plan.uses_context(context) { + return Err(LinkErrors::plain( + stage, + "WIT context differs from the checked plan", + )); + } + let world = context.composition_world().ok_or_else(|| { + LinkErrors::plain( + stage, + "definition-only context cannot compose an executable component", + ) + })?; + crate::application::verify(plan, application)?; + let references = |signature: &crate::CoreSignature| { + signature + .parameters + .iter() + .chain(signature.result.iter()) + .any(|ty| matches!(ty, crate::CoreType::Ref(_))) + }; + let direct = plan + .bindings() + .iter() + .any(|binding| references(&binding.signature)) + || crate::core_assembly::canonical_libraries(plan).len() != plan.artifacts().len(); + let output = if direct { + crate::world_boundary::encode(context, plan, application)? + } else { + let mut bytes = application.to_vec(); + embed_component_metadata(&mut bytes, context.resolve(), world, StringEncoding::UTF8) + .map_err(|error| { + LinkErrors::plain( + stage, + format!("failed to embed component metadata: {error}"), + ) + })?; + let mut encoder = ComponentEncoder::default() + .module(&bytes) + .map_err(|error| { + LinkErrors::plain(stage, format!("failed to read the core module: {error:#}")) + })?; + for artifact in plan.artifacts() { + encoder = encoder + .library( + &artifact.module_name, + &artifact.bytes, + LibraryInfo { + instantiate_after_shims: artifact.instantiate_after_shims, + arguments: Vec::new(), + }, + ) + .map_err(|error| { + LinkErrors::one( + stage, + &artifact.id, + format!("failed to attach artifact: {error:#}"), + ) + })?; + } + encoder.validate(true).encode().map_err(|error| { + LinkErrors::plain(stage, format!("failed to encode the component: {error:#}")) + })? + }; + wasmparser::Validator::new() + .validate_all(&output) + .map_err(|error| { + LinkErrors::plain( + stage, + format!("composed component failed validation: {error}"), + ) + })?; + + let imports = component_imports(&output)?; + for import in &imports { + if import == psrs_runtime::MODULE_NAME { + return Err(LinkErrors::plain( + stage, + "the private runtime import was not closed inside the component", + )); + } + // Interface imports are named by canonical id and must be permitted by + // the world; type and function imports belong to those interfaces. + if !plan.external_world().contains(import) { + return Err(LinkErrors::one( + stage, + import.clone(), + "component imports an interface outside the permitted external world", + )); + } + } + if imports != plan.external_world() { + return Err(LinkErrors::plain( + stage, + format!( + "component import closure differs from the checked plan: expected {:?}, actual {imports:?}", + plan.external_world() + ), + )); + } + Ok(LinkedArtifact { + bytes: output, + external_world: imports, + }) +} + +pub(crate) fn component_imports(bytes: &[u8]) -> Result, LinkErrors> { + let mut imports = Vec::new(); + let mut depth = 0_usize; + for payload in Parser::new(0).parse_all(bytes) { + match payload.map_err(|error| { + LinkErrors::plain( + LinkStage::Compose, + format!("failed to parse component: {error}"), + ) + })? { + Payload::ComponentSection { .. } | Payload::ModuleSection { .. } => depth += 1, + Payload::End(_) => depth = depth.saturating_sub(1), + Payload::ComponentImportSection(reader) if depth == 0 => { + // Only the outermost component's import section lists + // unresolved host imports; nested shims have their own. + for import in reader { + let import = import.map_err(|error| { + LinkErrors::plain( + LinkStage::Compose, + format!("failed to read component import: {error}"), + ) + })?; + imports.push(import.name.0.to_string()); + } + } + _ => {} + } + } + imports.sort(); + imports.dedup(); + Ok(imports) +} diff --git a/crates/psrs-linker/src/core_assembly.rs b/crates/psrs-linker/src/core_assembly.rs new file mode 100644 index 00000000..a8b6a3df --- /dev/null +++ b/crates/psrs-linker/src/core_assembly.rs @@ -0,0 +1,223 @@ +//! Compiler-owned raw Core instance assembly, before canonical WIT boundaries. +use crate::{CheckedLinkPlan, LinkErrors, LinkStage}; +use std::collections::{BTreeMap, BTreeSet}; +use wasm_encoder::{ComponentBuilder, ModuleArg}; +use wasmparser::{Parser, Payload}; + +/// A provisional instance graph connecting original modules directly. +/// Canonical lift/lower and target-world closure remain separate obligations. +pub struct CoreAssembly { + component: ComponentBuilder, + application: u32, + application_module: u32, +} + +impl CoreAssembly { + pub fn application_instance(&self) -> u32 { + self.application + } + + pub fn application_module(&self) -> u32 { + self.application_module + } + + /// Consume the assembly to add canonical boundaries. This returns an encoder, + /// not a checked target artifact; callers must validate their completed graph. + pub fn into_encoder(self) -> ComponentBuilder { + self.component + } +} + +/// Provider instances and the original application module, before host +/// canonical lowering supplies application imports. +pub(crate) struct PreparedCore { + pub component: ComponentBuilder, + pub application_module: u32, + pub providers: BTreeMap, +} + +pub(crate) fn prepare_core( + plan: &CheckedLinkPlan, + application: &[u8], + canonical_libraries: &BTreeSet, +) -> Result { + crate::application::verify(plan, application)?; + // Every occurrence has been checked against its complete provider type. + // Components require unique import names, unlike standalone Core modules. + let application = crate::application::imports::normalize(application) + .map_err(|message| LinkErrors::plain(LinkStage::Compose, message))?; + let error = |message| LinkErrors::plain(LinkStage::Compose, message); + let mut pending = plan + .artifacts() + .iter() + .filter(|artifact| !canonical_libraries.contains(&artifact.module_name)) + .map(|artifact| { + Ok(( + artifact.module_name.as_str(), + artifact.bytes.as_slice(), + imports(&artifact.bytes)?, + )) + }) + .collect::, String>>() + .map_err(error)?; + let mut component = ComponentBuilder::default(); + let mut providers = BTreeMap::new(); + while !pending.is_empty() { + let ready = pending + .iter() + .position(|(_, _, imports)| imports.iter().all(|name| providers.contains_key(name))); + let Some(ready) = ready else { + let dependencies = pending + .iter() + .map(|(name, _, imports)| { + let missing = imports + .iter() + .filter(|name| !providers.contains_key(*name)) + .cloned() + .collect::>() + .join(", "); + format!("{name} -> [{missing}]") + }) + .collect::>() + .join("; "); + return Err(error(format!( + "raw provider graph has unresolved imports or cyclic provisioning: {dependencies}" + ))); + }; + let (name, bytes, imports) = pending.remove(ready); + if name == "__main_module__" || providers.contains_key(name) { + return Err(error( + "raw provider graph has duplicate or reserved module namespaces".into(), + )); + } + let module = component.core_module_raw(Some(name), bytes); + let args = imports + .iter() + .map(|name| (name.as_str(), ModuleArg::Instance(providers[name]))) + .collect::>(); + let instance = component.core_instantiate(Some(name), module, args); + providers.insert(name.to_string(), instance); + } + let application_module = component.core_module_raw(Some("__main_module__"), &application); + Ok(PreparedCore { + component, + application_module, + providers, + }) +} + +/// Assemble a closed acyclic raw provider graph without scalar function shims. +/// Host canonical lowering uses `prepare_core` and supplies host instances +/// before application instantiation. Cyclic provisioning remains unsupported. +pub fn assemble_core( + plan: &CheckedLinkPlan, + application: &[u8], +) -> Result { + if !plan.external_world().is_empty() { + return Err(LinkErrors::plain( + LinkStage::Compose, + "raw instance assembly requires a closed provider graph", + )); + } + let mut prepared = prepare_core(plan, application, &BTreeSet::new())?; + let names = + imports(application).map_err(|message| LinkErrors::plain(LinkStage::Compose, message))?; + let args = names + .iter() + .map(|name| { + prepared + .providers + .get(name) + .map(|instance| (name.as_str(), ModuleArg::Instance(*instance))) + .ok_or_else(|| { + LinkErrors::plain( + LinkStage::Compose, + "raw application has an unresolved import", + ) + }) + }) + .collect::, _>>()?; + let instance = prepared.component.core_instantiate( + Some("__main_module__"), + prepared.application_module, + args, + ); + Ok(CoreAssembly { + component: prepared.component, + application: instance, + application_module: prepared.application_module, + }) +} + +/// Reference interfaces and their consumers remain in original raw Core +/// modules. Scalar libraries retain the canonical encoder's checked memory +/// and application-shim provisioning instead of being instantiated too early. +pub(crate) fn canonical_libraries(plan: &CheckedLinkPlan) -> BTreeSet { + let references = |signature: &crate::CoreSignature| { + signature + .parameters + .iter() + .chain(signature.result.iter()) + .any(|ty| matches!(ty, crate::CoreType::Ref(_))) + }; + let mut raw = plan + .artifacts() + .iter() + .filter(|artifact| { + artifact + .exports + .iter() + .filter_map(|export| export.signature.as_ref()) + .any(references) + || artifact + .imports + .iter() + .filter_map(|import| match &import.kind { + crate::ImportKind::Function(signature) => Some(signature), + _ => None, + }) + .any(references) + }) + .map(|artifact| artifact.module_name.clone()) + .collect::>(); + loop { + let consumers = plan + .artifacts() + .iter() + .filter(|artifact| { + artifact + .imports + .iter() + .any(|import| raw.contains(&import.module)) + }) + .map(|artifact| artifact.module_name.clone()) + .collect::>(); + let before = raw.len(); + raw.extend(consumers); + if raw.len() == before { + break; + } + } + plan.artifacts() + .iter() + .filter(|artifact| !raw.contains(&artifact.module_name)) + .map(|artifact| artifact.module_name.clone()) + .collect() +} + +fn imports(bytes: &[u8]) -> Result, String> { + let mut imports = BTreeSet::new(); + for payload in Parser::new(0).parse_all(bytes) { + if let Payload::ImportSection(section) = payload.map_err(|error| error.to_string())? { + for import in section.into_imports() { + imports.insert( + import + .map_err(|error| error.to_string())? + .module + .to_string(), + ); + } + } + } + Ok(imports) +} diff --git a/crates/psrs-linker/src/core_types/mod.rs b/crates/psrs-linker/src/core_types/mod.rs new file mode 100644 index 00000000..0d7a1eca --- /dev/null +++ b/crates/psrs-linker/src/core_types/mod.rs @@ -0,0 +1,223 @@ +//! Module-independent raw signatures retaining complete GC recursion groups. +use crate::{CoreSignature, CoreType}; +use std::sync::Arc; +use wasmparser::{ + AbstractHeapType, CompositeInnerType, HeapType, PackedIndex, RecGroup, StorageType, SubType, + ValType, +}; + +/// A closed reference contract. Construction resolves module-local indices; +/// consumers cannot fabricate an unresolved reference or erase its group. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CoreReference { + nullable: bool, + heap: Heap, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum Heap { + Abstract { + shared: bool, + kind: AbstractHeapType, + }, + Defined { + group: Arc<[Definition]>, + member: u32, + }, + Bound(u32), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct Definition { + final_type: bool, + supertype: Option, + composite: Composite, +} +#[derive(Clone, Debug, PartialEq, Eq)] +enum Composite { + Function { + parameters: Vec, + results: Vec, + }, + Struct(Vec), + Array(Field), +} +#[derive(Clone, Debug, PartialEq, Eq)] +struct Field { + mutable: bool, + storage: FieldStorage, +} +#[derive(Clone, Debug, PartialEq, Eq)] +enum FieldStorage { + I8, + I16, + Value(CoreType), +} + +/// The owner of conversion from a parsed module type space to raw contracts. +#[derive(Default)] +pub struct CoreTypes { + definitions: Vec, +} + +impl CoreTypes { + pub fn from_module(bytes: &[u8]) -> Result { + wasmparser::Validator::new() + .validate_all(bytes) + .map_err(|error| error.to_string())?; + let mut types = Self::default(); + for payload in wasmparser::Parser::new(0).parse_all(bytes) { + if let wasmparser::Payload::TypeSection(section) = + payload.map_err(|error| error.to_string())? + { + for group in section { + types.push(group.map_err(|error| error.to_string())?)?; + } + } + } + Ok(types) + } + + pub(crate) fn push(&mut self, group: RecGroup) -> Result<(), String> { + let start = self.definitions.len() as u32; + let raw = group.into_types().collect::>(); + if raw.is_empty() { + return Err("raw contract has an empty recursion group".into()); + } + let end = start + raw.len() as u32; + let definitions = raw + .iter() + .map(|ty| self.definition(ty, start, end)) + .collect::, _>>()?; + let group: Arc<[Definition]> = definitions.into(); + self.definitions + .extend((0..raw.len() as u32).map(|member| Heap::Defined { + group: Arc::clone(&group), + member, + })); + Ok(()) + } + + pub fn value(&self, value: ValType) -> Result { + self.convert_value(value, 0, 0) + } + + pub fn signature(&self, index: u32) -> Result { + let Some(Heap::Defined { group, member }) = self.definitions.get(index as usize) else { + return Err("raw function has no defined type".into()); + }; + let Composite::Function { + parameters, + results, + } = &group[*member as usize].composite + else { + return Err("raw function refers to a non-function type".into()); + }; + let close = |ty: &CoreType| match ty { + CoreType::Ref(reference) => CoreType::Ref(CoreReference { + nullable: reference.nullable, + heap: match &reference.heap { + Heap::Bound(member) => Heap::Defined { + group: Arc::clone(group), + member: *member, + }, + heap => heap.clone(), + }, + }), + ty => ty.clone(), + }; + let result = match results.as_slice() { + [] => None, + [result] => Some(close(result)), + _ => return Err("raw function has multiple results".into()), + }; + Ok(CoreSignature { + parameters: parameters.iter().map(close).collect(), + result, + }) + } + + fn convert_value(&self, value: ValType, start: u32, end: u32) -> Result { + Ok(match value { + ValType::I32 => CoreType::I32, + ValType::I64 => CoreType::I64, + ValType::F32 => CoreType::F32, + ValType::F64 => CoreType::F64, + ValType::V128 => CoreType::V128, + ValType::Ref(reference) => CoreType::Ref(CoreReference { + nullable: reference.is_nullable(), + heap: self.heap(reference.heap_type(), start, end)?, + }), + }) + } + + fn heap(&self, heap: HeapType, start: u32, end: u32) -> Result { + match heap { + HeapType::Exact(_) => Err("raw exact-reference contracts are unsupported".into()), + HeapType::Abstract { shared, ty } => Ok(Heap::Abstract { shared, kind: ty }), + HeapType::Concrete(index) => { + let index = index + .as_module_index() + .ok_or("raw reference has a non-module index")?; + if (start..end).contains(&index) { + return Ok(Heap::Bound(index - start)); + } + self.definitions + .get(index as usize) + .cloned() + .ok_or_else(|| "raw reference crosses an unresolved recursion group".into()) + } + } + } + + fn definition(&self, ty: &SubType, start: u32, end: u32) -> Result { + if ty.composite_type.shared + || ty.composite_type.descriptor_idx.is_some() + || ty.composite_type.describes_idx.is_some() + { + return Err("raw shared or descriptor composite contracts are unsupported".into()); + } + let index = |index: PackedIndex| self.heap(HeapType::Concrete(index.unpack()), start, end); + let field = |field: &wasmparser::FieldType| { + Ok(Field { + mutable: field.mutable, + storage: match field.element_type { + StorageType::I8 => FieldStorage::I8, + StorageType::I16 => FieldStorage::I16, + StorageType::Val(value) => { + FieldStorage::Value(self.convert_value(value, start, end)?) + } + }, + }) + }; + let composite = match &ty.composite_type.inner { + CompositeInnerType::Func(function) => Composite::Function { + parameters: function + .params() + .iter() + .map(|ty| self.convert_value(*ty, start, end)) + .collect::>()?, + results: function + .results() + .iter() + .map(|ty| self.convert_value(*ty, start, end)) + .collect::>()?, + }, + CompositeInnerType::Struct(ty) => { + Composite::Struct(ty.fields.iter().map(field).collect::>()?) + } + CompositeInnerType::Array(ty) => Composite::Array(field(&ty.0)?), + CompositeInnerType::Cont(_) => { + return Err("raw continuation contracts are unsupported".into()); + } + }; + Ok(Definition { + final_type: ty.is_final, + supertype: ty.supertype_idx.map(index).transpose()?, + composite, + }) + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/psrs-linker/src/core_types/tests.rs b/crates/psrs-linker/src/core_types/tests.rs new file mode 100644 index 00000000..6b179155 --- /dev/null +++ b/crates/psrs-linker/src/core_types/tests.rs @@ -0,0 +1,61 @@ +use super::*; + +fn signature(source: &str, index: u32) -> CoreSignature { + CoreTypes::from_module(&wat::parse_str(source).unwrap()) + .unwrap() + .signature(index) + .unwrap() +} + +#[test] +fn gc_contracts_ignore_module_index_offsets() { + let a = signature( + "(module (type (array (mut eqref))) (type (func (param (ref 0) i32) (result eqref))))", + 1, + ); + let b = signature( + "(module (type (struct (field i64))) (type (array (mut eqref))) (type (func (param (ref 1) i32) (result eqref))))", + 2, + ); + assert_eq!(a, b); +} + +#[test] +fn gc_contracts_preserve_mutability_nullability_and_group_identity() { + let base = signature( + "(module (type (array (mut eqref))) (type (func (param (ref 0)))))", + 1, + ); + for source in [ + "(module (type (array eqref)) (type (func (param (ref 0)))))", + "(module (type (array (mut eqref))) (type (func (param (ref null 0)))))", + "(module (rec (type (array (mut eqref))) (type (struct (field i64)))) (type (func (param (ref 0)))))", + ] { + assert_ne!( + base, + signature(source, if source.contains("(rec") { 2 } else { 1 }) + ); + } +} + +#[test] +fn recursive_contracts_close_group_binders_without_module_indices() { + let a = signature( + "(module (rec (type (struct (field (ref null 1)))) (type (array (mut (ref null 0))))) (type (func (param (ref 0)))))", + 2, + ); + let b = signature( + "(module (type (array i32)) (rec (type (struct (field (ref null 2)))) (type (array (mut (ref null 1))))) (type (func (param (ref 1)))))", + 3, + ); + assert_eq!(a, b); +} + +#[test] +fn non_function_and_multiple_results_are_explicit_errors() { + let bytes = + wat::parse_str("(module (type (array i32)) (type (func (result i32 i64))))").unwrap(); + let types = CoreTypes::from_module(&bytes).unwrap(); + assert!(types.signature(0).unwrap_err().contains("non-function")); + assert!(types.signature(1).unwrap_err().contains("multiple results")); +} diff --git a/crates/psrs-linker/src/definitions.rs b/crates/psrs-linker/src/definitions.rs new file mode 100644 index 00000000..9b6d64e4 --- /dev/null +++ b/crates/psrs-linker/src/definitions.rs @@ -0,0 +1,203 @@ +//! WIT definition loading and the immutable resolved-world context. +//! +//! One resolved context is shared by backend ABI lowering and linker provider +//! validation. It is held behind an `Arc` so the backend can borrow it without +//! parsing a second copy or regenerating interface identities. + +use crate::error::{LinkErrors, LinkStage}; +use psrs_runtime::{APP_WIT, DEFAULT_WORLD, WASI_WIT, WitSource, WorldIdentity}; +use std::sync::Arc; +use wit_parser::{Resolve, WorldId, WorldItem}; + +/// One immutable resolved world shared by ABI lowering and provider validation. +pub struct ResolvedWorldContext { + resolve: Arc, + world: Option, + imports: Vec, + exports: Vec, +} + +impl ResolvedWorldContext { + /// Wraps an already-resolved `Resolve`/`WorldId` pair. Used by isolated + /// target tests with custom WIT fixtures. + pub fn from_resolve(resolve: Resolve, world: WorldId) -> Self { + let (imports, exports) = collect_interfaces(&resolve, world); + Self { + resolve: Arc::new(resolve), + world: Some(world), + imports, + exports, + } + } + + /// A context that permits every interface the resolve defines. Isolated ABI + /// fixtures use it; it has no world and cannot be composed. + pub fn permissive(resolve: Arc) -> Self { + let mut imports = resolve + .interfaces + .iter() + .filter_map(|(id, _)| resolve.id_of(id)) + .collect::>(); + imports.sort(); + Self { + resolve, + world: None, + imports, + exports: Vec::new(), + } + } + + /// The resolved definitions. Backend canonical flattening reads through + /// this borrow; it never loads a second copy. + pub fn resolve(&self) -> &Resolve { + &self.resolve + } + + /// A shared handle to the same resolved definitions. The backend's ABI + /// registry keeps this handle instead of cloning the `Resolve`. + pub fn shared_resolve(&self) -> Arc { + Arc::clone(&self.resolve) + } + + pub fn world(&self) -> WorldId { + self.world.expect("a permissive context has no world") + } + pub(crate) fn composition_world(&self) -> Option { + self.world + } + + /// Canonical ids of interfaces the default world imports. + pub fn world_imports(&self) -> &[String] { + &self.imports + } + + /// Canonical ids of interfaces the default world exports. + pub fn world_exports(&self) -> &[String] { + &self.exports + } + + /// Whether the world imports the named canonical interface. + pub fn imports_interface(&self, canonical: &str) -> bool { + self.imports.iter().any(|id| id == canonical) + } +} + +/// Resolves the supplied WIT sources into one world context. +/// +/// The application package must declare the requested world; a mismatch is a +/// hard error rather than a silent fallback. +pub fn resolve_definitions( + wasi: &[WitSource], + app: WitSource, + world: WorldIdentity, +) -> Result { + let mut resolve = Resolve::default(); + for source in wasi { + resolve + .push_str(source.path, source.contents) + .map_err(|error| { + LinkErrors::one( + LinkStage::Definitions, + source.path, + format!("invalid WIT source: {error}"), + ) + })?; + } + let package = resolve.push_str(app.path, app.contents).map_err(|error| { + LinkErrors::one( + LinkStage::Definitions, + app.path, + format!("invalid application WIT: {error}"), + ) + })?; + let declared = &resolve.packages[package].name; + if declared.namespace != world.package_namespace || declared.name != world.package_name { + return Err(LinkErrors::one( + LinkStage::Definitions, + app.path, + format!( + "application WIT declares `{}:{}`, not `{}:{}`", + declared.namespace, declared.name, world.package_namespace, world.package_name + ), + )); + } + let world_id = resolve.packages[package] + .worlds + .get(world.world) + .copied() + .ok_or_else(|| { + LinkErrors::one( + LinkStage::Definitions, + format!("{}:{}", world.package_namespace, world.package_name), + format!("application WIT is missing the `{}` world", world.world), + ) + })?; + let (imports, exports) = collect_interfaces(&resolve, world_id); + Ok(ResolvedWorldContext { + resolve: Arc::new(resolve), + world: Some(world_id), + imports, + exports, + }) +} + +/// Resolves the runtime's pinned default world. +pub fn resolve_default_definitions() -> Result { + resolve_definitions(WASI_WIT, APP_WIT, DEFAULT_WORLD) +} + +fn collect_interfaces(resolve: &Resolve, world: WorldId) -> (Vec, Vec) { + let mut imports = Vec::new(); + let mut exports = Vec::new(); + for item in resolve.worlds[world].imports.values() { + if let WorldItem::Interface { id, .. } = item + && let Some(id) = resolve.id_of(*id) + { + imports.push(id); + } + } + for item in resolve.worlds[world].exports.values() { + if let WorldItem::Interface { id, .. } = item + && let Some(id) = resolve.id_of(*id) + { + exports.push(id); + } + } + imports.sort(); + exports.sort(); + (imports, exports) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_world_resolves_and_exports_the_command() { + let context = resolve_default_definitions().expect("the pinned WIT should resolve"); + assert!( + context + .world_imports() + .iter() + .any(|id| id == "wasi:cli/stdout@0.2.12") + ); + assert!( + context + .world_exports() + .iter() + .any(|id| id == "wasi:cli/run@0.2.12") + ); + assert!(context.imports_interface("wasi:io/streams@0.2.12")); + assert!(!context.imports_interface("wasi:http/outgoing-handler@0.2.12")); + } + + #[test] + fn a_world_identity_mismatch_is_rejected() { + let bogus = WorldIdentity { + package_namespace: "other", + package_name: "app", + world: "command", + }; + assert!(resolve_definitions(WASI_WIT, APP_WIT, bogus).is_err()); + } +} diff --git a/crates/psrs-linker/src/digest.rs b/crates/psrs-linker/src/digest.rs new file mode 100644 index 00000000..ba411cf6 --- /dev/null +++ b/crates/psrs-linker/src/digest.rs @@ -0,0 +1,14 @@ +//! SHA-256 digests for pinned artifact bytes. + +use sha2::{Digest, Sha256}; + +/// The lowercase hexadecimal SHA-256 of `bytes`. +pub fn sha256_hex(bytes: &[u8]) -> String { + let digest = Sha256::digest(bytes); + let mut out = String::with_capacity(64); + for byte in digest { + use std::fmt::Write; + let _ = write!(out, "{byte:02x}"); + } + out +} diff --git a/crates/psrs-linker/src/error.rs b/crates/psrs-linker/src/error.rs new file mode 100644 index 00000000..25b4dcdd --- /dev/null +++ b/crates/psrs-linker/src/error.rs @@ -0,0 +1,97 @@ +//! Structured link failures. The linker never publishes a partial plan or a +//! composed artifact when any check fails. + +use std::fmt; + +/// The linking stage that rejected an input. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LinkStage { + /// WIT definition loading and world resolution. + Definitions, + /// Requirement/provider closure. + Requirements, + /// Artifact contract validation. + Verify, + /// Memory reservation and instantiation planning. + Memory, + /// Component composition and final import closure. + Compose, +} + +impl LinkStage { + fn label(self) -> &'static str { + match self { + LinkStage::Definitions => "definitions", + LinkStage::Requirements => "requirements", + LinkStage::Verify => "verify", + LinkStage::Memory => "memory", + LinkStage::Compose => "compose", + } + } +} + +/// One structured link failure carrying the owning subject identity. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LinkError { + pub stage: LinkStage, + /// The requirement origin, artifact id, or interface the failure names. + pub subject: Option, + pub message: String, +} + +/// A nonempty collection of link failures. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LinkErrors(pub Vec); + +impl LinkErrors { + pub fn new(errors: Vec) -> Self { + debug_assert!(!errors.is_empty(), "a link failure has at least one cause"); + Self(errors) + } + + pub fn one(stage: LinkStage, subject: impl Into, message: impl Into) -> Self { + Self(vec![LinkError { + stage, + subject: Some(subject.into()), + message: message.into(), + }]) + } + + pub fn plain(stage: LinkStage, message: impl Into) -> Self { + Self(vec![LinkError { + stage, + subject: None, + message: message.into(), + }]) + } +} + +impl From for LinkErrors { + fn from(error: LinkError) -> Self { + Self(vec![error]) + } +} + +impl fmt::Display for LinkError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: ", self.stage.label())?; + if let Some(subject) = &self.subject { + write!(f, "{subject}: ")?; + } + write!(f, "{}", self.message) + } +} + +impl fmt::Display for LinkErrors { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (index, error) in self.0.iter().enumerate() { + if index > 0 { + writeln!(f)?; + } + write!(f, "{error}")?; + } + Ok(()) + } +} + +impl std::error::Error for LinkErrors {} diff --git a/crates/psrs-linker/src/graph/mod.rs b/crates/psrs-linker/src/graph/mod.rs new file mode 100644 index 00000000..f0c28226 --- /dev/null +++ b/crates/psrs-linker/src/graph/mod.rs @@ -0,0 +1,382 @@ +//! Resolve live requirements against offered runtime units. +//! +//! Selection is metadata-first. Byte verification runs only after every live +//! requirement has one compatible provider and the state, growth, and memory +//! identities agree. A failed route does not try another unit or artifact kind. + +mod select; + +use crate::definitions::ResolvedWorldContext; +use crate::error::{LinkErrors, LinkStage}; +use crate::plan::{InitializationStep, ProviderEdge, ResolvedBinding, SelectedUnit}; +use crate::target::{ + ArtifactReference, BindingRequirement, Boundary, CoreSignature, ExportKind, ImportKind, + Provider, RuntimeUnitOffer, TargetPolicy, +}; +use crate::verify::{VerifiedArtifact, verify_artifact}; +use std::collections::{BTreeMap, BTreeSet}; + +pub(crate) struct ClosedProviders { + pub bindings: Vec, + pub units: Vec, + pub edges: Vec, + pub initialization: Vec, + pub verified: BTreeMap, + pub artifacts: Vec, + pub growth_owner: String, + pub shared_import: Option<(String, String)>, +} + +pub(crate) fn close( + context: &ResolvedWorldContext, + requirements: &[BindingRequirement], + units: &[RuntimeUnitOffer], + policy: &TargetPolicy, + growth_owner: &str, +) -> Result { + let stage = LinkStage::Requirements; + if growth_owner.is_empty() { + return Err(LinkErrors::plain(stage, "missing growth owner")); + } + let mut requirement_ids = BTreeSet::new(); + for requirement in requirements { + if !requirement_ids.insert(requirement.id) { + return Err(LinkErrors::one( + stage, + &requirement.origin, + "duplicate requirement identity", + )); + } + } + index_units(units)?; + + let mut selected = BTreeSet::new(); + let mut order = Vec::new(); + let mut stack = Vec::new(); + let mut edges = Vec::new(); + let mut bindings = Vec::new(); + let mut claims = BTreeMap::<(String, String), (CoreSignature, String)>::new(); + + for requirement in requirements { + match &requirement.provider { + Provider::Generated => edges.push(ProviderEdge::Generated { + requirement: requirement.id, + origin: requirement.origin.clone(), + }), + Provider::HostInterface { interface } => select::bind_host( + context, + policy, + requirement, + interface, + &mut claims, + &mut bindings, + &mut edges, + )?, + Provider::RuntimeOperation { name, version } => { + let provider = select::resolve_operation( + units, + name, + version, + requirement.expected.as_ref(), + Some(&requirement.boundary), + &requirement.origin, + )?; + let unit = &units[provider]; + let operation = select::offered(unit, name, version).expect("resolved operation"); + let Boundary::RawCore { module, field } = &requirement.boundary else { + return Err(LinkErrors::one( + stage, + &requirement.origin, + "a runtime operation requires a raw-core boundary", + )); + }; + select::claim( + &mut claims, + (module.clone(), field.clone()), + operation.signature.clone(), + &unit.id, + )?; + edges.push(ProviderEdge::Runtime { + requirement: requirement.id, + origin: requirement.origin.clone(), + operation: name.clone(), + version: version.clone(), + unit: unit.id.clone(), + }); + bindings.push(ResolvedBinding { + requirement: requirement.id, + origin: requirement.origin.clone(), + module: module.clone(), + field: field.clone(), + signature: operation.signature.clone(), + }); + close_unit( + units, + provider, + &mut selected, + &mut order, + &mut stack, + &mut edges, + )?; + } + } + } + + let growth = growth_owner.to_string(); + check_state(units, &order, &growth)?; + let shared_import = shared_memory(units, &order)?; + let verified = verify_selected(units, &order)?; + let initialization = initialization(&verified.units); + Ok(ClosedProviders { + bindings, + units: verified.units, + edges, + initialization, + verified: verified.verified, + artifacts: verified.artifacts, + growth_owner: growth, + shared_import, + }) +} + +fn index_units(units: &[RuntimeUnitOffer]) -> Result<(), LinkErrors> { + let stage = LinkStage::Requirements; + let mut ids = BTreeSet::new(); + let mut artifacts = BTreeSet::new(); + for unit in units { + if unit.id.is_empty() || unit.semantic_contract_version.is_empty() { + return Err(LinkErrors::plain(stage, "runtime unit is missing identity")); + } + if !ids.insert(unit.id.clone()) { + return Err(LinkErrors::one(stage, &unit.id, "duplicate runtime unit")); + } + if unit.artifact.contract.id.is_empty() + || !artifacts.insert(unit.artifact.contract.id.clone()) + { + return Err(LinkErrors::one( + stage, + &unit.id, + "runtime unit is missing a unique artifact variant", + )); + } + let mut operations = BTreeSet::new(); + for operation in &unit.provided { + if operation.name.is_empty() + || operation.version.is_empty() + || operation.export.is_empty() + { + return Err(LinkErrors::one( + stage, + &unit.id, + "runtime operation is missing identity", + )); + } + if !operations.insert((operation.name.clone(), operation.version.clone())) { + return Err(LinkErrors::one( + stage, + &unit.id, + format!( + "unit advertises `{}@{}` more than once", + operation.name, operation.version + ), + )); + } + } + } + Ok(()) +} + +fn close_unit( + units: &[RuntimeUnitOffer], + index: usize, + selected: &mut BTreeSet, + order: &mut Vec, + stack: &mut Vec, + edges: &mut Vec, +) -> Result<(), LinkErrors> { + if selected.contains(&index) { + return Ok(()); + } + if stack.contains(&index) { + return Err(LinkErrors::one( + LinkStage::Requirements, + &units[index].id, + "unsupported provider cycle", + )); + } + agree_with_contract(&units[index])?; + stack.push(index); + let required = units[index].required.clone(); + let from_unit = units[index].id.clone(); + for operation in required { + let provider = select::resolve_operation( + units, + &operation.name, + &operation.version, + Some(&operation.signature), + None, + &from_unit, + )?; + edges.push(ProviderEdge::Dependency { + from_unit: from_unit.clone(), + operation: operation.name, + version: operation.version, + unit: units[provider].id.clone(), + }); + close_unit(units, provider, selected, order, stack, edges)?; + } + stack.pop(); + selected.insert(index); + order.push(index); + Ok(()) +} + +fn agree_with_contract(unit: &RuntimeUnitOffer) -> Result<(), LinkErrors> { + for operation in &unit.provided { + let matches = unit.artifact.contract.exports.iter().any(|export| { + export.kind == ExportKind::Func + && export.name == operation.export + && export.signature.as_ref() == Some(&operation.signature) + }); + if !matches { + return Err(LinkErrors::one( + LinkStage::Requirements, + &unit.id, + format!( + "operation `{}` disagrees with the artifact contract", + operation.export + ), + )); + } + } + Ok(()) +} + +fn check_state( + units: &[RuntimeUnitOffer], + order: &[usize], + growth_owner: &str, +) -> Result<(), LinkErrors> { + let stage = LinkStage::Requirements; + let mut owners = BTreeMap::new(); + let mut growth = Vec::new(); + for index in order { + let unit = &units[*index]; + if let Some(owner) = &unit.state_owner { + if owner.is_empty() { + return Err(LinkErrors::one(stage, &unit.id, "missing state owner")); + } + if owners.insert(owner.clone(), unit.id.clone()).is_some() { + return Err(LinkErrors::one( + stage, + owner.clone(), + "duplicate state owner", + )); + } + } + // The demand names one growth owner. That owner may be the generated + // allocator today or the allocator runtime unit that replaces it. + // Any other selected unit that grows the same memory conflicts. + if unit.grows_memory && unit.id != growth_owner { + growth.push(unit.id.clone()); + } + } + if !growth.is_empty() { + return Err(LinkErrors::one( + stage, + growth.join(","), + format!("conflicting growth owners; `{growth_owner}` already owns growth"), + )); + } + Ok(()) +} + +fn shared_memory( + units: &[RuntimeUnitOffer], + order: &[usize], +) -> Result, LinkErrors> { + let mut shared = None; + for index in order { + let unit = &units[*index]; + let memories: Vec<_> = unit + .artifact + .contract + .imports + .iter() + .filter(|import| matches!(import.kind, ImportKind::Memory { .. })) + .collect(); + if memories.len() > 1 { + return Err(LinkErrors::one( + LinkStage::Memory, + &unit.id, + "unsupported memory profile", + )); + } + if let Some(import) = memories.first() { + let name = (import.module.clone(), import.field.clone()); + if shared.get_or_insert_with(|| name.clone()) != &name { + return Err(LinkErrors::one( + LinkStage::Memory, + &unit.id, + "conflicting shared-memory imports", + )); + } + } + } + Ok(shared) +} + +struct VerifiedSelection { + verified: BTreeMap, + artifacts: Vec, + units: Vec, +} + +fn verify_selected( + units: &[RuntimeUnitOffer], + order: &[usize], +) -> Result { + let mut verified = BTreeMap::new(); + let mut artifacts = Vec::new(); + let mut selected = Vec::new(); + for index in order { + let unit = &units[*index]; + let id = unit.artifact.contract.id.clone(); + if !verified.contains_key(&id) { + verified.insert( + id.clone(), + verify_artifact(&unit.artifact.contract, &unit.artifact.bytes)?, + ); + } + selected.push(SelectedUnit { + id: unit.id.clone(), + artifact: id, + kind: unit.artifact.contract.kind, + state_owner: unit.state_owner.clone(), + grows_memory: unit.grows_memory, + instantiate_after_shims: unit.artifact.contract.instantiate_after_shims, + }); + artifacts.push(unit.artifact.clone()); + } + Ok(VerifiedSelection { + verified, + artifacts, + units: selected, + }) +} + +fn initialization(units: &[SelectedUnit]) -> Vec { + let mut steps = Vec::new(); + for unit in units.iter().filter(|unit| !unit.instantiate_after_shims) { + steps.push(InitializationStep::Instantiate { + unit: unit.id.clone(), + }); + } + steps.push(InitializationStep::ResolveShims); + for unit in units.iter().filter(|unit| unit.instantiate_after_shims) { + steps.push(InitializationStep::Instantiate { + unit: unit.id.clone(), + }); + } + steps +} diff --git a/crates/psrs-linker/src/graph/select.rs b/crates/psrs-linker/src/graph/select.rs new file mode 100644 index 00000000..8f05a178 --- /dev/null +++ b/crates/psrs-linker/src/graph/select.rs @@ -0,0 +1,183 @@ +//! Choose the one compatible provider for an operation or host import. + +use crate::definitions::ResolvedWorldContext; +use crate::error::{LinkErrors, LinkStage}; +use crate::plan::{ProviderEdge, ResolvedBinding}; +use crate::target::{ + ArtifactKind, BindingRequirement, Boundary, CoreSignature, RuntimeUnitOffer, TargetPolicy, +}; +use std::collections::BTreeMap; + +pub(super) fn resolve_operation( + units: &[RuntimeUnitOffer], + name: &str, + version: &str, + expected: Option<&CoreSignature>, + boundary: Option<&Boundary>, + origin: &str, +) -> Result { + let stage = LinkStage::Requirements; + let Some(expected) = expected else { + return Err(LinkErrors::one( + stage, + origin, + format!("runtime operation `{name}@{version}` has no checked signature"), + )); + }; + let mut compatible = Vec::new(); + let mut boundary_mismatch = false; + let mut kind_mismatch = false; + for (index, unit) in units.iter().enumerate() { + let Some(operation) = offered(unit, name, version) else { + continue; + }; + let signature_ok = operation.signature == *expected; + let kind_ok = unit.artifact.contract.kind == ArtifactKind::CoreModule; + let boundary_ok = match boundary { + None => true, + Some(Boundary::RawCore { module, field }) => { + module == &unit.artifact.contract.module_name && field == &operation.export + } + Some(Boundary::ResolvedWit { .. }) => false, + }; + if signature_ok && kind_ok && boundary_ok { + compatible.push(index); + } else if signature_ok && kind_ok { + boundary_mismatch = true; + } else if signature_ok { + kind_mismatch = true; + } + } + match compatible.len() { + 1 => Ok(compatible[0]), + 0 if !units + .iter() + .any(|unit| offered(unit, name, version).is_some()) => + { + Err(LinkErrors::one( + stage, + origin, + format!("no runtime unit provides `{name}@{version}`"), + )) + } + 0 if kind_mismatch && !boundary_mismatch => Err(LinkErrors::one( + stage, + origin, + format!("incompatible artifact kind for `{name}@{version}`"), + )), + 0 if boundary_mismatch => Err(LinkErrors::one( + stage, + origin, + "artifact boundary does not name the selected export", + )), + 0 => Err(LinkErrors::one( + stage, + origin, + "runtime operation signature disagrees with the checked requirement", + )), + _ => Err(LinkErrors::one( + stage, + origin, + format!("ambiguous providers for `{name}@{version}`"), + )), + } +} + +pub(super) fn offered<'a>( + unit: &'a RuntimeUnitOffer, + name: &str, + version: &str, +) -> Option<&'a crate::target::OfferedOperation> { + unit.provided + .iter() + .find(|operation| operation.name == name && operation.version == version) +} + +pub(super) fn bind_host( + context: &ResolvedWorldContext, + policy: &TargetPolicy, + requirement: &BindingRequirement, + interface: &str, + claims: &mut BTreeMap<(String, String), (CoreSignature, String)>, + bindings: &mut Vec, + edges: &mut Vec, +) -> Result<(), LinkErrors> { + let stage = LinkStage::Requirements; + if !context.imports_interface(interface) { + return Err(LinkErrors::one( + stage, + &requirement.origin, + format!("host interface `{interface}` is outside the resolved world"), + )); + } + if !policy + .permitted_host_interfaces + .iter() + .any(|permitted| permitted == interface) + { + return Err(LinkErrors::one( + stage, + &requirement.origin, + format!("host interface `{interface}` is not permitted by the selected target profile"), + )); + } + let Some(expected) = requirement.expected.clone() else { + return Err(LinkErrors::one( + stage, + &requirement.origin, + "a host binding requires a checked raw signature", + )); + }; + let (module, field) = match &requirement.boundary { + Boundary::ResolvedWit { + interface: name, + function, + } if name == interface => (name.clone(), function.clone()), + _ => { + return Err(LinkErrors::one( + stage, + &requirement.origin, + "host binding must cross the selected WIT interface", + )); + } + }; + claim( + claims, + (module.clone(), field.clone()), + expected.clone(), + interface, + )?; + edges.push(ProviderEdge::Host { + requirement: requirement.id, + origin: requirement.origin.clone(), + interface: interface.to_string(), + }); + bindings.push(ResolvedBinding { + requirement: requirement.id, + origin: requirement.origin.clone(), + module, + field, + signature: expected, + }); + Ok(()) +} + +pub(super) fn claim( + claims: &mut BTreeMap<(String, String), (CoreSignature, String)>, + key: (String, String), + signature: CoreSignature, + provider: &str, +) -> Result<(), LinkErrors> { + if let Some((existing, existing_provider)) = claims.get(&key) { + if existing != &signature || existing_provider != provider { + return Err(LinkErrors::one( + LinkStage::Requirements, + format!("{}.{}", key.0, key.1), + "ambiguous or conflicting providers for one import identity", + )); + } + return Ok(()); + } + claims.insert(key, (signature, provider.to_string())); + Ok(()) +} diff --git a/crates/psrs-linker/src/guest/graph.rs b/crates/psrs-linker/src/guest/graph.rs new file mode 100644 index 00000000..7fbe26af --- /dev/null +++ b/crates/psrs-linker/src/guest/graph.rs @@ -0,0 +1,188 @@ +use super::{CheckedComponentPlan, ComponentLinkInput, ComponentReference, ResolvedGuestBinding}; +use crate::{LinkErrors, LinkStage, LinkedArtifact}; +use std::collections::{BTreeMap, BTreeSet}; +use wasm_compose::graph::{Component, CompositionGraph, EncodeOptions}; +use wasmparser::{ComponentTypeRef, Validator}; + +/// Close executable component dependencies without files, automatic discovery, +/// or a host fallback for an explicitly selected guest provider. +pub fn plan_components(input: ComponentLinkInput) -> Result { + build(input).map_err(|message| LinkErrors::plain(LinkStage::Requirements, message)) +} + +fn build(input: ComponentLinkInput) -> Result { + let ComponentLinkInput { + application, + guests, + bindings, + policy, + features, + } = input; + let mut references = BTreeMap::new(); + for reference in std::iter::once(application.clone()).chain(guests) { + if references.insert(reference.id.clone(), reference).is_some() { + return Err("duplicate component artifact identity".into()); + } + } + let mut selections = BTreeMap::new(); + for binding in bindings { + if binding.export != binding.interface { + return Err("guest exports must match the pinned canonical interface identity; implicit version adaptation is unsupported".into()); + } + if selections + .insert(binding.interface.clone(), binding) + .is_some() + { + return Err("duplicate or conflicting guest interface provider".into()); + } + } + let mut graph = CompositionGraph::new(); + let mut validator = Validator::new_with_features(features); + let mut instances = BTreeMap::new(); + let mut pending = vec![application.id.clone()]; + let mut used = BTreeSet::new(); + let mut edges = Vec::new(); + let mut external = BTreeSet::new(); + while let Some(id) = pending.pop() { + if !used.insert(id.clone()) { + continue; + } + let reference = references + .get(&id) + .ok_or_else(|| format!("absent selected guest artifact `{id}`"))?; + verify_digest(reference)?; + if id != application.id { + // Provider initialization has no executable-start contract in v1. + for payload in wasmparser::Parser::new(0).parse_all(&reference.bytes) { + match payload.map_err(|error| error.to_string())? { + wasmparser::Payload::StartSection { .. } + | wasmparser::Payload::ComponentStartSection { .. } => { + return Err(format!( + "guest `{id}` has unsupported executable initialization" + )); + } + _ => {} + } + } + } + let component = Component::from_bytes(&mut validator, id.clone(), reference.bytes.clone()) + .map_err(|error| format!("component `{id}` is not executable: {error:#}"))?; + let imports = component + .imports() + .map(|(index, name, ty)| (index, name.to_owned(), ty)) + .collect::>(); + let component_id = graph + .add_component(component) + .map_err(|error| error.to_string())?; + let instance = graph + .instantiate(component_id) + .map_err(|error| error.to_string())?; + instances.insert(id.clone(), (component_id, instance)); + for (index, name, ty) in imports { + if !matches!(ty, ComponentTypeRef::Instance(_)) { + return Err(format!( + "component `{id}` imports unsupported non-interface `{name}`" + )); + } + if let Some(binding) = selections.get(&name) { + if binding.artifact == application.id { + return Err("the root application cannot provide a guest dependency".into()); + } + pending.push(binding.artifact.clone()); + edges.push(( + id.clone(), + index, + binding.artifact.clone(), + binding.export.clone(), + )); + } else if policy.permitted_host_interfaces.contains(&name) { + external.insert(name); + } else { + return Err(format!( + "unresolved or disallowed component interface `{name}` required by `{id}`" + )); + } + } + } + let mut selected_bindings = Vec::new(); + for (target, import, source, export) in edges { + selected_bindings.push(ResolvedGuestBinding { + consumer: target.clone(), + interface: export.clone(), + provider: source.clone(), + export: export.clone(), + }); + let (component_id, source_instance) = instances[&source]; + let target_instance = instances[&target].1; + let export_index = graph + .get_component(component_id) + .unwrap() + .export_by_name(&export) + .map(|(index, _, _)| index) + .ok_or_else(|| format!("guest `{source}` omits selected export `{export}`"))?; + graph + .connect(source_instance, Some(export_index), target_instance, import) + .map_err(|error| { + format!("incompatible guest binding `{source}.{export}`: {error:#}") + })?; + } + // Graph encoding validates resource remapping, dependency ordering and all + // canonical instance connections. Cycles are rejected, never made host imports. + let bytes = graph + .encode(EncodeOptions { + define_components: true, + export: Some(instances[&application.id].1), + validate: true, + }) + .map_err(|error| format!("component graph cannot be composed: {error:#}"))?; + let actual = crate::compose::component_imports(&bytes).map_err(|error| error.to_string())?; + let expected: Vec<_> = external.into_iter().collect(); + if actual != expected { + return Err(format!( + "component import closure differs from the plan: expected {expected:?}, actual {actual:?}" + )); + } + let artifacts: Vec<_> = used + .iter() + .map(|id| (id.clone(), references[id].sha256.clone())) + .collect(); + Validator::new_with_features(features) + .validate_all(&bytes) + .map_err(|error| error.to_string())?; + Ok(CheckedComponentPlan { + application_digest: application.sha256, + bytes, + artifacts, + external_world: expected, + bindings: selected_bindings, + features, + }) +} + +fn verify_digest(reference: &ComponentReference) -> Result<(), String> { + if crate::sha256_hex(&reference.bytes) != reference.sha256 { + return Err(format!( + "component `{}` digest does not match its pin", + reference.id + )); + } + Ok(()) +} + +/// Execute a component plan only for the exact application it checked. +pub fn compose_component( + plan: &CheckedComponentPlan, + application: &[u8], +) -> Result { + let component = plan; + if crate::sha256_hex(application) != component.application_digest { + return Err(LinkErrors::plain( + LinkStage::Compose, + "application bytes differ from the checked component plan", + )); + } + Ok(LinkedArtifact { + bytes: component.bytes.clone(), + external_world: plan.external_world().to_vec(), + }) +} diff --git a/crates/psrs-linker/src/guest/mod.rs b/crates/psrs-linker/src/guest/mod.rs new file mode 100644 index 00000000..db96ba40 --- /dev/null +++ b/crates/psrs-linker/src/guest/mod.rs @@ -0,0 +1,70 @@ +//! Explicit, in-memory composition of pinned guest WIT interface providers. +mod graph; +pub use graph::{compose_component, plan_components}; + +/// A pinned, executable component; definitions alone are not providers. +#[derive(Clone, Debug)] +pub struct ComponentReference { + pub id: String, + pub sha256: String, + pub bytes: Vec, +} + +/// Bind the whole imported interface to one export of one guest instance. +/// This preserves a resource's constructor/method/destructor identity. +#[derive(Clone, Debug)] +pub struct GuestBinding { + pub interface: String, + pub artifact: String, + pub export: String, +} + +/// Target-only entry point for an already typed application component. +/// Core-module callers still use `plan` and `compose` before this boundary. +#[derive(Clone, Debug)] +pub struct ComponentLinkInput { + pub application: ComponentReference, + pub guests: Vec, + pub bindings: Vec, + pub policy: crate::TargetPolicy, + /// The selected target's validator feature policy, applied to every input. + pub features: wasmparser::WasmFeatures, +} + +/// One live whole-interface edge in the executable graph. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ResolvedGuestBinding { + pub consumer: String, + pub interface: String, + pub provider: String, + pub export: String, +} + +/// A checked, closed component graph; construction is private to planning. +/// Component memories stay inside their owners and have no core memory plan. +#[derive(Clone, Debug)] +pub struct CheckedComponentPlan { + pub(crate) application_digest: String, + pub(crate) bytes: Vec, + pub(crate) artifacts: Vec<(String, String)>, + pub(crate) external_world: Vec, + pub(crate) bindings: Vec, + pub(crate) features: wasmparser::WasmFeatures, +} + +impl CheckedComponentPlan { + /// Selected component identities and pins, including the root application. + pub fn component_artifacts(&self) -> &[(String, String)] { + &self.artifacts + } + pub fn bindings(&self) -> &[ResolvedGuestBinding] { + &self.bindings + } + pub fn features(&self) -> wasmparser::WasmFeatures { + self.features + } + /// Exact residual host interface imports after graph closure. + pub fn external_world(&self) -> &[String] { + &self.external_world + } +} diff --git a/crates/psrs-linker/src/lib.rs b/crates/psrs-linker/src/lib.rs new file mode 100644 index 00000000..c9d205c8 --- /dev/null +++ b/crates/psrs-linker/src/lib.rs @@ -0,0 +1,51 @@ +//! Independent checked target linker. +//! +//! This crate owns WIT definition loading and the resolved-world identity, +//! provider closure and artifact contract validation, the immutable checked +//! link plan, and component composition. It consumes linker-owned target +//! records and the `psrs-runtime` catalog; it depends on no compiler IR crate. +//! +//! The conceptual API is: +//! +//! ```text +//! resolve_definitions(...) -> ResolvedWorldContext +//! plan(context, TargetLinkInput) -> CheckedLinkPlan +//! compose(context, plan, encoded_application) -> LinkedArtifact +//! ``` + +mod application; +mod closure; +mod compose; +mod core_assembly; +pub use core_assembly::{CoreAssembly, assemble_core}; +mod core_types; +pub use core_types::{CoreReference, CoreTypes}; +mod definitions; +mod digest; +mod error; +mod graph; +pub mod guest; +pub mod plan; +pub mod runtime; +mod stack; +mod target; +mod verify; +mod world_boundary; + +pub use compose::{LinkedArtifact, compose}; +pub use definitions::{ResolvedWorldContext, resolve_default_definitions, resolve_definitions}; +pub use digest::sha256_hex; +pub use error::{LinkError, LinkErrors, LinkStage}; +pub use plan::{ + CheckedLinkPlan, HeapBoundary, InitializationStep, LinkReport, MemoryPlan, ProviderEdge, + ResolvedBinding, SelectedUnit, plan, +}; +pub use stack::{StackBound, measure_stack_bound}; +pub use target::{ + APPLICATION_MEMORY, ArtifactContract, ArtifactKind, ArtifactReference, BindingRequirement, + Boundary, CoreSignature, CoreType, DeclaredElement, DeclaredExport, DeclaredGlobal, + DeclaredImport, DeclaredTable, ExportKind, GENERATED_GROWTH_OWNER, ImportKind, + InitializationContract, MemoryDemand, OfferedOperation, Provider, RequiredOperation, + RequirementId, RuntimeUnitOffer, StorageContract, StorageRegion, TargetLinkInput, TargetPolicy, +}; +pub use verify::{VerifiedArtifact, verify_artifact}; diff --git a/crates/psrs-linker/src/plan.rs b/crates/psrs-linker/src/plan.rs new file mode 100644 index 00000000..e14cfc9c --- /dev/null +++ b/crates/psrs-linker/src/plan.rs @@ -0,0 +1,454 @@ +//! Provider closure, artifact verification, and memory/instantiation planning. + +use crate::definitions::ResolvedWorldContext; +use crate::error::{LinkErrors, LinkStage}; +use crate::target::{ArtifactKind, CoreSignature, RequirementId, StorageRegion, TargetLinkInput}; +use crate::verify::VerifiedArtifact; + +/// A requirement bound to its verified provider. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ResolvedBinding { + pub requirement: RequirementId, + pub origin: String, + pub module: String, + pub field: String, + pub signature: CoreSignature, +} + +/// The application's exported heap boundary and the provider import it satisfies. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HeapBoundary { + /// Core-module name of the runtime unit that imports the boundary. + pub provider_module: String, + /// Import module, for example `__main_module__`. + pub import_module: String, + /// Import field and application export name. + pub field: String, +} + +/// The checked memory ownership and allocator boundary. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MemoryPlan { + /// Identity of this shared memory. One plan owns one memory. + pub memory_id: String, + pub heap_start: u32, + pub heap_alignment: u32, + pub minimum_pages: u64, + pub maximum_pages: u64, + /// The only owner allowed to grow this memory. + pub growth_owner: String, + /// The core import every selected memory-using unit shares, when any does. + pub shared_import: Option<(String, String)>, + /// The constant heap-boundary getter the selected growth owner imports. + /// + /// `provider_module` is the owner's core-module name. The application + /// exports `field`; composition aliases that export into `import_module`. + pub heap_getter: Option, + /// Every owned region, sorted by start address. + pub reservations: Vec, + pub(crate) canonical_scratch: (u32, u32), + pub(crate) allocator_state: (u32, u32), +} + +/// A runtime unit selected by provider closure. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SelectedUnit { + pub id: String, + pub artifact: String, + pub kind: crate::ArtifactKind, + pub state_owner: Option, + pub grows_memory: bool, + pub instantiate_after_shims: bool, +} + +/// One edge in the checked provider graph. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ProviderEdge { + Generated { + requirement: RequirementId, + origin: String, + }, + Runtime { + requirement: RequirementId, + origin: String, + operation: String, + version: String, + unit: String, + }, + /// A selected unit's required operation, closed to another unit. + Dependency { + from_unit: String, + operation: String, + version: String, + unit: String, + }, + Host { + requirement: RequirementId, + origin: String, + interface: String, + }, +} + +/// Checked instantiation order. Calls happen only after shim resolution. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum InitializationStep { + Instantiate { unit: String }, + ResolveShims, +} + +/// The published identities and digests of one successful plan. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LinkReport { + pub memory_id: String, + pub growth_owner: String, + pub units: Vec, + pub initialization: Vec, + pub digests: Vec, + pub external_world: Vec, +} + +/// One immutable checked target link plan. +/// +/// Construction is private to successful planning, so every live requirement +/// has exactly one verified provider and the memory and external world are +/// internally consistent. +#[derive(Clone, Debug)] +pub struct CheckedLinkPlan { + bindings: Vec, + artifacts: Vec, + units: Vec, + edges: Vec, + initialization: Vec, + memory: MemoryPlan, + external_world: Vec, + digests: Vec, + context: ( + std::sync::Arc, + Option, + ), +} + +impl CheckedLinkPlan { + pub(crate) fn uses_context(&self, context: &ResolvedWorldContext) -> bool { + std::sync::Arc::ptr_eq(&self.context.0, &context.shared_resolve()) + && self.context.1 == context.composition_world() + } + /// The binding for a requirement, when it contributes a core import. + pub fn import(&self, requirement: RequirementId) -> Option<&ResolvedBinding> { + self.bindings + .iter() + .find(|binding| binding.requirement == requirement) + } + + pub fn bindings(&self) -> &[ResolvedBinding] { + &self.bindings + } + + pub fn artifacts(&self) -> &[VerifiedArtifact] { + &self.artifacts + } + + pub fn memory(&self) -> &MemoryPlan { + &self.memory + } + + /// The permitted residual host interfaces, sorted. + pub fn external_world(&self) -> &[String] { + &self.external_world + } + + pub fn digests(&self) -> &[String] { + &self.digests + } + + pub fn units(&self) -> &[SelectedUnit] { + &self.units + } + + pub fn edges(&self) -> &[ProviderEdge] { + &self.edges + } + + pub fn initialization(&self) -> &[InitializationStep] { + &self.initialization + } + + pub fn growth_owner(&self) -> &str { + &self.memory.growth_owner + } + + /// Identities, initialization order, and digests for this plan. + pub fn report(&self) -> LinkReport { + LinkReport { + memory_id: self.memory.memory_id.clone(), + growth_owner: self.memory.growth_owner.clone(), + units: self.units.iter().map(|unit| unit.id.clone()).collect(), + initialization: self.initialization.clone(), + digests: self.digests.clone(), + external_world: self.external_world.clone(), + } + } +} + +/// Plans a checked link from a resolved world and target input. +pub fn plan( + context: &ResolvedWorldContext, + input: TargetLinkInput, +) -> Result { + let TargetLinkInput { + requirements, + units, + memory: memory_demand, + policy, + } = input; + let stage = LinkStage::Requirements; + let closed = crate::graph::close( + context, + &requirements, + &units, + &policy, + &memory_demand.growth_owner, + )?; + let memory = plan_memory(&closed, &memory_demand)?; + + let mut digests = closed + .verified + .values() + .map(|value| value.sha256.clone()) + .collect::>(); + digests.sort(); + digests.dedup(); + + let external = crate::closure::host_imports(context, &closed.bindings)?; + for interface in &external { + if !context.imports_interface(interface) + || !policy.permitted_host_interfaces.contains(interface) + { + return Err(LinkErrors::one( + stage, + interface, + "component resource dependency is outside the selected world or target profile", + )); + } + } + Ok(CheckedLinkPlan { + bindings: closed.bindings, + artifacts: closed.verified.into_values().collect(), + units: closed.units, + edges: closed.edges, + initialization: closed.initialization, + memory, + external_world: external, + digests, + context: (context.shared_resolve(), context.composition_world()), + }) +} + +fn plan_memory( + closed: &crate::graph::ClosedProviders, + demand: &crate::target::MemoryDemand, +) -> Result { + let stage = LinkStage::Memory; + let maximum = demand.maximum_pages.unwrap_or(65536); + if maximum == 0 || maximum > 65536 { + return Err(LinkErrors::plain(stage, "unsupported memory maximum")); + } + if demand.canonical_scratch.0 > demand.canonical_scratch.1 + || demand.allocator_state.0 > demand.allocator_state.1 + { + return Err(LinkErrors::plain( + stage, + "storage reservation has reversed bounds", + )); + } + let mut reservations = Vec::new(); + if demand.canonical_scratch.0 < demand.canonical_scratch.1 { + reservations.push(region("canonical-scratch", demand.canonical_scratch)); + } + if demand.allocator_state.0 < demand.allocator_state.1 { + reservations.push(region("allocator-state", demand.allocator_state)); + } + let mut heap_start = demand.base_heap_start; + let mut minimum_pages = u64::from(heap_start).div_ceil(0x1_0000) + 1; + + for reference in &closed.artifacts { + let id = &reference.contract.id; + let contract = &reference.contract; + let Some(storage) = &contract.storage else { + continue; + }; + if contract.kind == ArtifactKind::CoreModule && storage.stack.end <= storage.stack.start { + return Err(LinkErrors::one( + stage, + id, + "artifact stack reservation is empty", + )); + } + if storage.stack.end - storage.stack.start < storage.stack_bound_bytes { + return Err(LinkErrors::one( + stage, + id, + "artifact stack reservation is smaller than its reviewed bound", + )); + } + reservations.push(StorageRegion { + owner: id.clone(), + start: storage.static_data.start, + end: storage.static_data.end, + }); + reservations.push(StorageRegion { + owner: id.clone(), + start: storage.stack.start, + end: storage.stack.end, + }); + heap_start = heap_start.max(storage.heap_start); + minimum_pages = minimum_pages.max(storage.minimum_pages); + } + + reservations.sort_by_key(|region| (region.start, region.end)); + if reservations.iter().any(|region| region.start > region.end) { + return Err(LinkErrors::plain( + stage, + "storage reservation has reversed bounds", + )); + } + for pair in reservations.windows(2) { + if pair[1].start < pair[0].end { + return Err(LinkErrors::one( + stage, + pair[1].owner.clone(), + format!( + "reservation [{}, {}) overlaps `{}` at [{}, {})", + pair[1].start, pair[1].end, pair[0].owner, pair[0].start, pair[0].end + ), + )); + } + } + let alignment = demand.heap_alignment; + if !alignment.is_power_of_two() || !heap_start.is_multiple_of(alignment) { + return Err(LinkErrors::plain( + stage, + "allocator boundary is not aligned to its block granularity", + )); + } + if let Some(last) = reservations.last() + && last.end > heap_start + { + return Err(LinkErrors::one( + stage, + last.owner.clone(), + "a reservation extends past the allocator boundary", + )); + } + if minimum_pages > maximum { + return Err(LinkErrors::plain( + stage, + "memory plan exceeds its declared maximum", + )); + } + if u64::from(heap_start) > minimum_pages * 0x1_0000 { + return Err(LinkErrors::plain( + stage, + "declared minimum pages do not cover the allocator boundary", + )); + } + // The allocator grows memory on demand; reserve one initial page of heap + // beyond the boundary where every reserved region ends. + minimum_pages = minimum_pages.max(u64::from(heap_start).div_ceil(0x1_0000) + 1); + if minimum_pages > maximum { + return Err(LinkErrors::plain( + stage, + "memory plan exceeds its declared maximum", + )); + } + + Ok(MemoryPlan { + memory_id: crate::APPLICATION_MEMORY.to_string(), + heap_start, + heap_alignment: alignment, + minimum_pages, + maximum_pages: maximum, + growth_owner: closed.growth_owner.clone(), + shared_import: closed.shared_import.clone(), + heap_getter: heap_boundary(closed, demand)?, + reservations, + canonical_scratch: demand.canonical_scratch, + allocator_state: demand.allocator_state, + }) +} + +fn heap_boundary( + closed: &crate::graph::ClosedProviders, + demand: &crate::target::MemoryDemand, +) -> Result, LinkErrors> { + let Some(position) = closed + .units + .iter() + .position(|unit| unit.id == demand.growth_owner) + else { + if closed + .artifacts + .iter() + .any(|artifact| artifact.contract.imports.iter().any(is_heap_getter)) + { + return Err(LinkErrors::plain( + LinkStage::Memory, + "heap getter imports require a planned runtime growth owner", + )); + } + return Ok(None); + }; + let stage = LinkStage::Memory; + let unit = &closed.units[position]; + if !unit.grows_memory { + return Err(LinkErrors::one( + stage, + &unit.id, + "the named growth owner does not grow memory", + )); + } + let contract = &closed.artifacts[position].contract; + let functions: Vec<_> = contract + .imports + .iter() + .filter(|import| matches!(import.kind, crate::ImportKind::Function(_))) + .collect(); + let [import] = functions.as_slice() else { + return Err(LinkErrors::one( + stage, + &unit.id, + "the growth owner must import exactly one constant heap-boundary getter", + )); + }; + if !is_heap_getter(import) { + return Err(LinkErrors::one( + stage, + &unit.id, + "invalid application heap getter contract", + )); + } + Ok(Some(HeapBoundary { + provider_module: contract.module_name.clone(), + import_module: import.module.clone(), + field: import.field.clone(), + })) +} + +fn region(owner: &str, range: (u32, u32)) -> StorageRegion { + StorageRegion { + owner: owner.to_string(), + start: range.0, + end: range.1, + } +} + +/// The application-bound getter protocol; composition verifies its constant body. +pub(crate) fn is_heap_getter(import: &crate::DeclaredImport) -> bool { + import.module == psrs_runtime::APPLICATION_MODULE + && import.field == psrs_runtime::HEAP_BOUNDARY_IMPORT + && import.kind + == crate::ImportKind::Function(CoreSignature { + parameters: Vec::new(), + result: Some(crate::CoreType::I32), + }) +} diff --git a/crates/psrs-linker/src/runtime/encoded.rs b/crates/psrs-linker/src/runtime/encoded.rs new file mode 100644 index 00000000..fff9494c --- /dev/null +++ b/crates/psrs-linker/src/runtime/encoded.rs @@ -0,0 +1,159 @@ +//! Resolves declared encoded-unit schemas independently of executable bytes. +use crate::target::{ + ArtifactContract, ArtifactKind, ArtifactReference, CoreSignature, DeclaredExport, ExportKind, + InitializationContract, OfferedOperation, RuntimeUnitOffer, +}; +use crate::{CoreTypes, LinkErrors, LinkStage}; +use psrs_runtime::{EncodedHeap, EncodedRuntimeUnit, EncodedValue, RawType}; +use wasmparser::{AbstractHeapType, HeapType, RefType, ValType}; + +pub(super) fn offer(unit: &EncodedRuntimeUnit) -> Result { + let resolve = || -> Result { + let types = CoreTypes::from_module(&(unit.type_schema)())?; + let provided = (unit.operations)() + .into_iter() + .map(|operation| { + Ok(OfferedOperation { + name: operation.name, + version: operation.version.into(), + export: operation.export.into(), + signature: CoreSignature { + parameters: operation + .parameters + .into_iter() + .map(|ty| types.value(value(ty)?)) + .collect::>()?, + result: operation + .result + .map(|ty| types.value(value(ty)?)) + .transpose()?, + }, + }) + }) + .collect::, String>>()?; + let bytes = (unit.encode)(); + let contract = ArtifactContract { + id: unit.id.into(), + kind: ArtifactKind::CoreModule, + module_name: unit.module_name.into(), + sha256: crate::sha256_hex(&bytes), + provenance: unit.provenance.into(), + required_features: unit.required_features.iter().map(|s| (*s).into()).collect(), + imports: vec![], + exports: provided + .iter() + .map(|op| DeclaredExport { + name: op.export.clone(), + kind: ExportKind::Func, + signature: Some(op.signature.clone()), + }) + .collect(), + tables: vec![], + elements: vec![], + globals: vec![], + storage: None, + initialization: InitializationContract { + start_forbidden: true, + data_range: (0, 0), + }, + instantiate_after_shims: false, + }; + Ok(RuntimeUnitOffer { + id: unit.id.into(), + semantic_contract_version: unit.version.into(), + provided, + required: vec![], + artifact: ArtifactReference { contract, bytes }, + state_owner: None, + grows_memory: false, + }) + }; + resolve().map_err(|message| LinkErrors::one(LinkStage::Verify, unit.id, message)) +} + +fn value(value: EncodedValue) -> Result { + Ok(match value { + EncodedValue::Scalar(ty) => match ty { + RawType::I32 => ValType::I32, + RawType::I64 => ValType::I64, + RawType::F32 => ValType::F32, + RawType::F64 => ValType::F64, + }, + EncodedValue::Ref { nullable, heap } => { + let heap = match heap { + EncodedHeap::Defined(index) => { + HeapType::Concrete(wasmparser::UnpackedIndex::Module(index)) + } + heap => HeapType::Abstract { + shared: false, + ty: match heap { + EncodedHeap::Func => AbstractHeapType::Func, + EncodedHeap::Extern => AbstractHeapType::Extern, + EncodedHeap::Any => AbstractHeapType::Any, + EncodedHeap::Eq => AbstractHeapType::Eq, + EncodedHeap::I31 => AbstractHeapType::I31, + EncodedHeap::Struct => AbstractHeapType::Struct, + EncodedHeap::Array => AbstractHeapType::Array, + EncodedHeap::Defined(_) => unreachable!(), + }, + }, + }; + ValType::Ref( + RefType::new(nullable, heap).ok_or("declared reference index is out of range")?, + ) + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn storage_offer_verifies_against_independent_declared_schema() { + let units = crate::runtime::package_offers(&psrs_runtime::PSRS_RUNTIME).unwrap(); + let storage = units + .iter() + .find(|unit| unit.id == psrs_runtime::STORAGE_UNIT.id) + .unwrap(); + crate::verify_artifact(&storage.artifact.contract, &storage.artifact.bytes).unwrap(); + assert_eq!( + storage.provided.len(), + psrs_runtime::StorageOperation::ALL.len() + ); + assert!(storage.provided.iter().any(|op| { + op.signature + .parameters + .iter() + .any(|ty| matches!(ty, crate::CoreType::Ref(_))) + })); + } + + #[test] + fn valid_bytes_with_wrong_declared_reference_contract_are_rejected() { + let mut storage = offer(&psrs_runtime::STORAGE_UNIT).unwrap(); + let export = storage + .artifact + .contract + .exports + .iter_mut() + .find(|export| { + export.signature.as_ref().is_some_and(|sig| { + sig.parameters + .iter() + .any(|ty| matches!(ty, crate::CoreType::Ref(_))) + }) + }) + .unwrap(); + let signature = export.signature.as_mut().unwrap(); + let parameter = signature + .parameters + .iter_mut() + .find(|ty| matches!(ty, crate::CoreType::Ref(_))) + .unwrap(); + *parameter = crate::CoreType::I32; + let error = crate::verify_artifact(&storage.artifact.contract, &storage.artifact.bytes) + .unwrap_err(); + assert!(error.to_string().contains("signature"), "{error}"); + } +} diff --git a/crates/psrs-linker/src/runtime/mod.rs b/crates/psrs-linker/src/runtime/mod.rs new file mode 100644 index 00000000..7a8635a4 --- /dev/null +++ b/crates/psrs-linker/src/runtime/mod.rs @@ -0,0 +1,182 @@ +//! Converts runtime catalog data into linker-owned artifact contracts. +//! +//! The runtime package owns the raw declaration; the linker owns the typed +//! contract it verifies and plans against. + +mod encoded; + +use crate::target::{ + ArtifactContract, ArtifactKind, ArtifactReference, CoreSignature, CoreType, DeclaredElement, + DeclaredExport, DeclaredGlobal, DeclaredImport, DeclaredTable, ExportKind, ImportKind, + InitializationContract, OfferedOperation, RequiredOperation, RuntimeUnitOffer, StorageContract, + StorageRegion, +}; +use psrs_runtime::{RawType, RuntimeArtifact, RuntimePackage, RuntimeUnit}; + +/// Maps a catalog artifact into the contract its bytes must satisfy. +pub fn contract(artifact: &RuntimeArtifact) -> ArtifactContract { + ArtifactContract { + elements: artifact + .elements + .iter() + .map(|element| DeclaredElement { + table: element.table, + offset: element.offset, + functions: element.functions.to_vec(), + }) + .collect(), + id: artifact.id.to_string(), + kind: ArtifactKind::CoreModule, + module_name: artifact.module_name.to_string(), + sha256: artifact.provenance.sha256.to_string(), + provenance: format!( + "{} {} via {} ({} {}): {}", + artifact.provenance.dependency, + artifact.provenance.dependency_revision, + artifact.provenance.rust_toolchain, + artifact.provenance.target, + artifact.provenance.profile, + artifact.provenance.recipe, + ), + required_features: artifact + .required_features + .iter() + .map(|feature| (*feature).to_string()) + .collect(), + imports: artifact + .imports + .iter() + .map(|import| DeclaredImport { + module: import.module.to_string(), + field: import.field.to_string(), + kind: match import.kind { + psrs_runtime::RawImportKind::Memory { minimum, maximum } => { + ImportKind::Memory { minimum, maximum } + } + psrs_runtime::RawImportKind::Function { parameters, result } => { + ImportKind::Function(CoreSignature { + parameters: parameters.iter().copied().map(core_type).collect(), + result: result.map(core_type), + }) + } + }, + }) + .collect(), + exports: artifact + .function_exports + .iter() + .map(|export| DeclaredExport { + name: export.name.to_string(), + kind: ExportKind::Func, + signature: Some(CoreSignature { + parameters: export.parameters.iter().copied().map(core_type).collect(), + result: export.result.map(core_type), + }), + }) + .chain(artifact.global_exports.iter().map(|name| DeclaredExport { + name: (*name).to_string(), + kind: ExportKind::Global, + signature: None, + })) + .collect(), + tables: artifact + .tables + .iter() + .map(|table| DeclaredTable { + element: table.element.to_string(), + minimum: table.minimum, + maximum: table.maximum, + }) + .collect(), + globals: artifact + .globals + .iter() + .map(|(mutable, initial)| DeclaredGlobal { + mutable: *mutable, + initial: *initial, + }) + .collect(), + storage: Some(StorageContract { + static_data: StorageRegion { + owner: artifact.id.to_string(), + start: artifact.storage.static_data.0, + end: artifact.storage.static_data.1, + }, + stack: StorageRegion { + owner: artifact.id.to_string(), + start: artifact.storage.stack.0, + end: artifact.storage.stack.1, + }, + heap_start: artifact.storage.heap_start, + minimum_pages: artifact.storage.minimum_pages, + stack_pointer_global: artifact.storage.stack_pointer_global, + stack_bound_bytes: artifact.storage.stack_bound_bytes, + stack_bound_evidence: artifact.storage.stack_bound_evidence.to_string(), + }), + initialization: InitializationContract { + start_forbidden: artifact.start_forbidden, + data_range: artifact.data_range, + }, + instantiate_after_shims: artifact.instantiate_after_shims, + } +} + +/// Maps one catalog unit onto the offer planning resolves. +pub fn offer(unit: &RuntimeUnit) -> RuntimeUnitOffer { + RuntimeUnitOffer { + id: unit.id.to_string(), + semantic_contract_version: unit.semantic_contract_version.to_string(), + provided: unit + .provided + .iter() + .map(|operation| OfferedOperation { + name: operation.name.to_string(), + version: operation.version.to_string(), + export: operation.abi.export.to_string(), + signature: signature(operation.abi.parameters, operation.abi.result), + }) + .collect(), + required: unit + .required + .iter() + .map(|operation| RequiredOperation { + name: operation.name.to_string(), + version: operation.version.to_string(), + signature: signature(operation.parameters, operation.result), + }) + .collect(), + artifact: ArtifactReference { + contract: contract(unit.variant), + bytes: unit.variant.bytes.to_vec(), + }, + state_owner: unit.state.owner.map(str::to_string), + grows_memory: unit.state.grows_memory, + } +} + +/// Maps every unit of a catalog package. Unused units stay unselected. +pub fn package_offers( + package: &RuntimePackage, +) -> Result, crate::LinkErrors> { + let mut units = package.units.iter().copied().map(offer).collect::>(); + for unit in package.encoded_units { + units.push(encoded::offer(unit)?); + } + Ok(units) +} + +fn signature(parameters: &[RawType], result: Option) -> CoreSignature { + CoreSignature { + parameters: parameters.iter().copied().map(core_type).collect(), + result: result.map(core_type), + } +} + +fn core_type(ty: RawType) -> CoreType { + match ty { + RawType::I32 => CoreType::I32, + RawType::I64 => CoreType::I64, + RawType::F32 => CoreType::F32, + RawType::F64 => CoreType::F64, + } +} diff --git a/crates/psrs-linker/src/stack/mod.rs b/crates/psrs-linker/src/stack/mod.rs new file mode 100644 index 00000000..1062dadc --- /dev/null +++ b/crates/psrs-linker/src/stack/mod.rs @@ -0,0 +1,322 @@ +//! Static linear-stack bound analysis for a nonrecursive core artifact. +//! +//! Each function's prologue subtracts its frame size from the stack-pointer +//! global. The maximum bytes an artifact can use is the largest sum of frame +//! sizes along a path reachable from an exported function or start function. +//! Reachable cycles and indirect calls are rejected rather than bounded. + +use wasmparser::{Operator, Parser, Payload}; + +/// A measured upper bound on linear-stack bytes. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct StackBound { + pub bytes: u32, +} + +/// Measures the static stack bound of a core module. +/// +/// `stack_pointer_global` is the mutable global whose prologue adjustment +/// reserves a frame. Indirect calls and calls into imported functions make the +/// bound unknown, so they are errors rather than silent under-approximations. +/// Exported tables are also rejected: callers could otherwise enter a private +/// function through them. Modules with no entry points are analyzed in full. +pub fn measure_stack_bound(bytes: &[u8], stack_pointer_global: u32) -> Result { + measure_stack_bound_with_imports(bytes, stack_pointer_global, &[]) +} + +/// Imported function indices proven by composition to use no linear stack. +/// All other imported calls retain the conservative rejection behavior. +pub(crate) fn measure_stack_bound_with_imports( + bytes: &[u8], + stack_pointer_global: u32, + stackless_imports: &[u32], +) -> Result { + // Exception and suspension proposals require a separate frame-unwind + // proof. They cannot enter this normal-return-only analysis. + use wasmparser::WasmFeatures as F; + let features = F::MVP + | F::MUTABLE_GLOBAL + | F::SIGN_EXTENSION + | F::SATURATING_FLOAT_TO_INT + | F::MULTI_VALUE + | F::BULK_MEMORY + | F::REFERENCE_TYPES + | F::FUNCTION_REFERENCES + | F::GC + | F::TAIL_CALL; + wasmparser::Validator::new_with_features(features) + .validate_all(bytes) + .map_err(|error| error.to_string())?; + let mut bodies = Vec::new(); + let mut roots = Vec::new(); + let mut imported_functions = 0_usize; + + for payload in Parser::new(0).parse_all(bytes) { + match payload.map_err(|error| error.to_string())? { + Payload::ImportSection(reader) => { + for import in reader.into_imports() { + if matches!( + import.map_err(|error| error.to_string())?.ty, + wasmparser::TypeRef::Func(_) + ) { + imported_functions += 1; + } + } + } + Payload::CodeSectionEntry(body) => { + bodies.push(analyze_body(&body, stack_pointer_global)); + } + Payload::ExportSection(reader) => { + for export in reader { + let export = export.map_err(|error| error.to_string())?; + match export.kind { + wasmparser::ExternalKind::Func => roots.push(export.index), + wasmparser::ExternalKind::Table => { + return Err( + "an exported table makes runtime entry points unknown".into() + ); + } + _ => {} + } + } + } + Payload::StartSection { func, .. } => roots.push(func), + _ => {} + } + } + + let count = bodies.len(); + // A private function can only execute through an entry point's call graph. + // Unsupported indirect calls are still rejected on every reachable path. + // Definition-only test modules retain conservative whole-module analysis. + if roots.is_empty() { + roots.extend((0..count).map(|index| index as u32 + imported_functions as u32)); + } + let mut memo = vec![None::; count]; + let mut visiting = vec![false; count]; + let mut bound = 0_u32; + for root in roots { + let index = (root as usize) + .checked_sub(imported_functions) + .filter(|index| *index < count) + .ok_or("an imported or unknown entry point makes the stack bound unknown")?; + bound = bound.max(visit( + index, + &bodies, + &mut memo, + &mut visiting, + imported_functions, + stackless_imports, + )?); + } + Ok(StackBound { bytes: bound }) +} + +fn analyze_body( + body: &wasmparser::FunctionBody<'_>, + stack_pointer_global: u32, +) -> Result<(u32, Vec), String> { + let mut calls = Vec::new(); + let mut reader = body + .get_operators_reader() + .map_err(|error| error.to_string())?; + let mut operators = Vec::new(); + while !reader.eof() { + operators.push(reader.read().map_err(|error| error.to_string())?); + } + let (frame, frame_local, prologue_end) = match operators.as_slice() { + [ + Operator::GlobalGet { global_index }, + Operator::I32Const { value }, + Operator::I32Sub, + Operator::LocalTee { local_index }, + Operator::GlobalSet { + global_index: target, + }, + .., + ] if *global_index == stack_pointer_global + && *target == stack_pointer_global + && *value > 0 => + { + (*value as u32, Some(*local_index), 5) + } + [ + Operator::GlobalGet { global_index }, + Operator::I32Const { value }, + Operator::I32Sub, + Operator::GlobalSet { + global_index: target, + }, + .., + ] if *global_index == stack_pointer_global + && *target == stack_pointer_global + && *value > 0 => + { + (*value as u32, None, 4) + } + _ => (0, None, 0), + }; + let mut depth = 0_u32; + let mut restored = false; + let mut index = prologue_end; + while index < operators.len() { + match &operators[index] { + Operator::GlobalGet { global_index } if *global_index == stack_pointer_global => { + if frame_local.is_none() + && frame > 0 + && depth == 0 + && !restored + && matches!(operators.get(index + 1), Some(Operator::I32Const { value }) if *value as u32 == frame) + && matches!(operators.get(index + 2), Some(Operator::I32Add)) + && matches!(operators.get(index + 3), Some(Operator::GlobalSet { global_index }) if *global_index == stack_pointer_global) + { + restored = true; + index += 4; + continue; + } + return Err("unrecognized stack-pointer access makes the bound unknown".into()); + } + Operator::LocalGet { local_index } if Some(*local_index) == frame_local => { + let restores = !restored + && matches!(operators.get(index + 1), Some(Operator::I32Const { value }) if *value as u32 == frame) + && matches!(operators.get(index + 2), Some(Operator::I32Add)) + && matches!(operators.get(index + 3), Some(Operator::GlobalSet { global_index }) if *global_index == stack_pointer_global); + // LLVM leaves the epilogue inside the function's wrapper block, + // optionally reloads the return value, and returns immediately. + // That return leaves the function, so the restore covers it + // without blessing any later path. + let covers_return = match operators.get(index + 4) { + Some(Operator::Return) => Some(5), + Some(Operator::LocalGet { .. }) + if matches!(operators.get(index + 5), Some(Operator::Return)) => + { + Some(6) + } + _ => None, + }; + if restores && (depth == 0 || covers_return.is_some()) { + if depth == 0 { + restored = true; + index += 4; + } else { + index += covers_return.expect("the return is covered"); + } + continue; + } + } + Operator::GlobalSet { global_index } if *global_index == stack_pointer_global => { + return Err("unrecognized stack-pointer write makes the bound unknown".into()); + } + Operator::LocalSet { local_index } | Operator::LocalTee { local_index } + if Some(*local_index) == frame_local => + { + return Err("the saved stack frame is overwritten".into()); + } + Operator::Call { function_index } => { + calls.push(*function_index); + } + Operator::ReturnCall { function_index } => { + if frame > 0 && !restored { + return Err("return bypasses stack restoration".into()); + } + calls.push(*function_index); + } + Operator::CallIndirect { .. } + | Operator::ReturnCallIndirect { .. } + | Operator::CallRef { .. } + | Operator::ReturnCallRef { .. } => { + return Err("an indirect call makes the stack bound unknown".into()); + } + Operator::Block { .. } | Operator::Loop { .. } | Operator::If { .. } => { + if restored { + return Err("control flow after stack restoration is unsupported".into()); + } + depth += 1; + } + Operator::End if depth > 0 => depth -= 1, + Operator::Return if frame > 0 && !restored => { + return Err("return bypasses stack restoration".into()); + } + Operator::Br { relative_depth } + | Operator::BrIf { relative_depth } + | Operator::BrOnNull { relative_depth } + | Operator::BrOnNonNull { relative_depth } + | Operator::BrOnCast { relative_depth, .. } + | Operator::BrOnCastFail { relative_depth, .. } + if frame > 0 && !restored && *relative_depth >= depth => + { + return Err("branch bypasses stack restoration".into()); + } + Operator::BrTable { targets } + if frame > 0 + && !restored + && (targets.default() >= depth + || targets + .targets() + .any(|target| target.is_ok_and(|target| target >= depth))) => + { + return Err("branch bypasses stack restoration".into()); + } + _ => {} + } + index += 1; + } + if frame > 0 + && !restored + && !matches!( + operators.as_slice(), + [.., Operator::Unreachable, Operator::End] + ) + { + return Err("a returning function does not restore its stack frame".into()); + } + Ok((frame, calls)) +} + +fn visit( + index: usize, + bodies: &[Result<(u32, Vec), String>], + memo: &mut [Option], + visiting: &mut [bool], + imported_functions: usize, + stackless_imports: &[u32], +) -> Result { + if let Some(value) = memo[index] { + return Ok(value); + } + if visiting[index] { + return Err("a recursive call graph is not a supported runtime library".into()); + } + visiting[index] = true; + let (frame, callees) = bodies[index].as_ref().map_err(Clone::clone)?; + let mut deepest = 0_u32; + for callee in callees { + let callee = *callee as usize; + if callee < imported_functions { + if stackless_imports.contains(&(callee as u32)) { + continue; + } + return Err("a call into an imported function makes the stack bound unknown".into()); + } + let defined = callee - imported_functions; + if defined < bodies.len() { + deepest = deepest.max(visit( + defined, + bodies, + memo, + visiting, + imported_functions, + stackless_imports, + )?); + } else { + return Err("callee is outside the analyzed module".into()); + } + } + visiting[index] = false; + let total = frame.checked_add(deepest).ok_or("stack bound overflows")?; + memo[index] = Some(total); + Ok(total) +} + +#[cfg(test)] +mod tests; diff --git a/crates/psrs-linker/src/stack/tests.rs b/crates/psrs-linker/src/stack/tests.rs new file mode 100644 index 00000000..aac7b320 --- /dev/null +++ b/crates/psrs-linker/src/stack/tests.rs @@ -0,0 +1,233 @@ +use super::*; + +#[test] +fn bounds_cover_exported_entry_points_and_every_reachable_private_function() { + for (export, call_private, export_table, accepted) in [ + (0, false, false, true), + (1, false, false, false), + (0, true, false, false), + (0, false, true, false), + ] { + let bytes = entry_module(export, call_private, export_table); + assert_eq!(measure_stack_bound(&bytes, 0).is_ok(), accepted); + } +} + +fn entry_module(export: u32, call_private: bool, export_table: bool) -> Vec { + use wasm_encoder::*; + let mut module = Module::new(); + let mut types = TypeSection::new(); + types.ty().function([], []); + module.section(&types); + let mut functions = FunctionSection::new(); + functions.function(0); + functions.function(0); + module.section(&functions); + let mut tables = TableSection::new(); + tables.table(TableType { + element_type: RefType::FUNCREF, + minimum: 1, + maximum: Some(1), + table64: false, + shared: false, + }); + module.section(&tables); + let mut exports = ExportSection::new(); + exports.export("entry", ExportKind::Func, export); + if export_table { + exports.export("table", ExportKind::Table, 0); + } + module.section(&exports); + let mut code = CodeSection::new(); + let mut entry = Function::new([]); + if call_private { + entry.instruction(&Instruction::Call(1)); + } + entry.instruction(&Instruction::End); + code.function(&entry); + let mut private = Function::new([]); + private.instruction(&Instruction::I32Const(0)); + private.instruction(&Instruction::CallIndirect { + type_index: 0, + table_index: 0, + }); + private.instruction(&Instruction::End); + code.function(&private); + module.section(&code); + module.finish() +} + +#[test] +fn the_runtime_artifact_has_a_measured_static_bound() { + let bound = measure_stack_bound(psrs_runtime::NUMBER_RUNTIME.bytes, 0) + .expect("the numeric runtime is nonrecursive"); + assert_eq!( + bound.bytes, + psrs_runtime::NUMBER_RUNTIME.storage.stack_bound_bytes, + "the declared stack bound must match the static analysis" + ); +} + +#[test] +fn a_recursive_graph_is_rejected() { + // Two mutually recursive functions, each reserving a frame. + let bytes = recursive_module(); + assert!( + measure_stack_bound(&bytes, 0) + .unwrap_err() + .contains("recursive call graph") + ); +} + +#[test] +fn unrecognized_stack_writes_and_unbalanced_frames_are_rejected() { + use wasm_encoder::Instruction as I; + let prologue = [ + I::GlobalGet(0), + I::I32Const(32), + I::I32Sub, + I::LocalTee(0), + I::GlobalSet(0), + ]; + for suffix in [ + vec![I::I32Const(1), I::GlobalSet(0)], + vec![I::Return], + vec![I::Br(0)], + vec![I::I32Const(9), I::LocalSet(0)], + vec![I::GlobalGet(0), I::I32Const(32), I::I32Sub, I::GlobalSet(0)], + vec![], + ] { + let mut ops = prologue.to_vec(); + ops.extend(suffix); + assert!(measure_stack_bound(&single_function(&ops), 0).is_err()); + } + assert!(measure_stack_bound(&single_function(&[I::I32Const(5), I::GlobalSet(0)]), 0).is_err()); +} + +#[test] +fn a_reference_branch_cannot_bypass_an_otherwise_valid_restoration() { + use wasm_encoder::Instruction as I; + let ops = [ + I::GlobalGet(0), + I::I32Const(32), + I::I32Sub, + I::LocalTee(0), + I::GlobalSet(0), + I::RefNull(wasm_encoder::HeapType::FUNC), + I::BrOnNull(0), + I::Drop, + I::LocalGet(0), + I::I32Const(32), + I::I32Add, + I::GlobalSet(0), + ]; + let bytes = single_function(&ops); + wasmparser::Validator::new() + .validate_all(&bytes) + .expect("valid module"); + assert!( + measure_stack_bound(&bytes, 0) + .unwrap_err() + .contains("branch bypasses") + ); +} + +fn single_function(ops: &[wasm_encoder::Instruction<'_>]) -> Vec { + use wasm_encoder::*; + let mut module = Module::new(); + let mut types = TypeSection::new(); + types.ty().function([], []); + module.section(&types); + let mut functions = FunctionSection::new(); + functions.function(0); + module.section(&functions); + let mut globals = GlobalSection::new(); + globals.global( + GlobalType { + val_type: ValType::I32, + mutable: true, + shared: false, + }, + &ConstExpr::i32_const(1024), + ); + module.section(&globals); + let mut function = Function::new([(1, ValType::I32)]); + for op in ops { + function.instruction(op); + } + function.instruction(&Instruction::End); + let mut code = CodeSection::new(); + code.function(&function); + module.section(&code); + module.finish() +} + +fn recursive_module() -> Vec { + use wasm_encoder::{ + CodeSection, Function, FunctionSection, GlobalSection, GlobalType, Instruction, Module, + TypeSection, ValType, + }; + let mut module = Module::new(); + let mut types = TypeSection::new(); + types.ty().function([], []); + module.section(&types); + let mut functions = FunctionSection::new(); + functions.function(0); + functions.function(0); + module.section(&functions); + let mut globals = GlobalSection::new(); + globals.global( + GlobalType { + val_type: ValType::I32, + mutable: true, + shared: false, + }, + &wasm_encoder::ConstExpr::i32_const(1024), + ); + module.section(&globals); + let mut code = CodeSection::new(); + let mut first = Function::new([]); + first.instruction(&Instruction::GlobalGet(0)); + first.instruction(&Instruction::I32Const(32)); + first.instruction(&Instruction::I32Sub); + first.instruction(&Instruction::GlobalSet(0)); + first.instruction(&Instruction::Call(1)); + first.instruction(&Instruction::GlobalGet(0)); + first.instruction(&Instruction::I32Const(32)); + first.instruction(&Instruction::I32Add); + first.instruction(&Instruction::GlobalSet(0)); + first.instruction(&Instruction::End); + code.function(&first); + let mut second = Function::new([]); + second.instruction(&Instruction::GlobalGet(0)); + second.instruction(&Instruction::I32Const(32)); + second.instruction(&Instruction::I32Sub); + second.instruction(&Instruction::GlobalSet(0)); + second.instruction(&Instruction::Call(0)); + second.instruction(&Instruction::GlobalGet(0)); + second.instruction(&Instruction::I32Const(32)); + second.instruction(&Instruction::I32Add); + second.instruction(&Instruction::GlobalSet(0)); + second.instruction(&Instruction::End); + code.function(&second); + module.section(&code); + module.finish() +} + +#[test] +fn imported_calls_require_an_explicit_cross_module_stack_proof() { + let bytes = wat::parse_str( + r#"(module + (import "application" "constant" (func $get (result i32))) + (func (export "entry") call $get drop))"#, + ) + .unwrap(); + assert!(measure_stack_bound(&bytes, 0).is_err()); + assert_eq!( + measure_stack_bound_with_imports(&bytes, 0, &[0]) + .unwrap() + .bytes, + 0 + ); + assert!(measure_stack_bound_with_imports(&bytes, 0, &[1]).is_err()); +} diff --git a/crates/psrs-linker/src/target.rs b/crates/psrs-linker/src/target.rs new file mode 100644 index 00000000..ee56136f --- /dev/null +++ b/crates/psrs-linker/src/target.rs @@ -0,0 +1,279 @@ +//! Linker-owned target records. +//! +//! The backend converts checked IR requirements into these records. They carry +//! no compiler IR nodes, language `TypeId`s, or MIR bodies, so plan and +//! composition tests can construct artifacts without compiling PureScript. + +/// A requirement identity assigned by the producer. The backend keeps the map +/// from this identity to a source symbol and span for diagnostics. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RequirementId(pub u32); + +/// The checked core-Wasm value contract, independent of module-local indices. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum CoreType { + I32, + I64, + F32, + F64, + V128, + Ref(crate::core_types::CoreReference), +} + +/// A core-Wasm function signature. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CoreSignature { + pub parameters: Vec, + pub result: Option, +} + +/// The boundary a requirement crosses. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Boundary { + /// A private core-module import, closed inside the component. + RawCore { module: String, field: String }, + /// A WIT interface import resolved through the world. + ResolvedWit { interface: String, function: String }, +} + +/// The selected implementation for a requirement. +/// +/// A runtime operation names an identity and version. Planning selects the +/// unique compatible unit; a caller-chosen artifact is not a provider. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Provider { + /// A compiler-generated local operation or helper; no import results. + Generated, + /// Resolve the unique offered unit that provides this operation. + RuntimeOperation { name: String, version: String }, + /// A host interface retained in the component's external world. + HostInterface { interface: String }, +} + +/// One live executable requirement and its selected provider. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BindingRequirement { + pub id: RequirementId, + /// A stable origin label for diagnostics, for example `numberToString` or + /// `wasi:cli/stdout.get-stdout`. + pub origin: String, + pub boundary: Boundary, + /// The expected raw core signature, when the requirement crosses a raw or + /// WIT boundary. `None` for a generated helper that never leaves the module. + pub expected: Option, + pub provider: Provider, +} + +/// Whether an artifact is a core module or a component. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ArtifactKind { + CoreModule, + /// A typed guest component provider. Use `guest::plan_components`; a + /// component cannot satisfy a raw-core artifact contract. + Component, +} + +/// The kind of a declared artifact import. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ImportKind { + /// A shared memory import. Limits are part of the pinned contract. + Memory { + minimum: u64, + maximum: Option, + }, + /// An `i32` global import. + Global { + mutable: bool, + }, + Function(CoreSignature), +} + +/// An import the artifact's contract declares. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DeclaredImport { + pub module: String, + pub field: String, + pub kind: ImportKind, +} + +/// An export the artifact's contract declares. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DeclaredExport { + pub name: String, + pub kind: ExportKind, + /// The function signature when `kind` is `Func`. + pub signature: Option, +} + +/// The kind of a declared export. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ExportKind { + Func, + Global, +} + +/// A table the artifact's contract declares. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DeclaredTable { + pub element: String, + pub minimum: u32, + pub maximum: Option, +} + +/// A global the artifact's contract declares. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DeclaredGlobal { + pub mutable: bool, + /// The constant `i32` initial value. + pub initial: u32, +} + +/// An explicitly declared active function-table initializer. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DeclaredElement { + pub table: u32, + pub offset: u32, + pub functions: Vec, +} + +/// A declared private execution-storage region. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct StorageRegion { + pub owner: String, + pub start: u32, + pub end: u32, +} + +/// An artifact's private execution storage and allocator boundary. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct StorageContract { + pub static_data: StorageRegion, + pub stack: StorageRegion, + pub heap_start: u32, + pub minimum_pages: u64, + /// The index of the mutable stack-pointer global the artifact uses. + pub stack_pointer_global: u32, + /// A reviewed upper bound on stack bytes for the supported calling pattern. + pub stack_bound_bytes: u32, + /// Where the stack bound comes from; a measured static bound names the + /// analysis, while a reviewed build assumption names its pending evidence. + pub stack_bound_evidence: String, +} + +/// Constraints on an artifact's initialization. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct InitializationContract { + pub start_forbidden: bool, + /// Every active data segment must lie in this half-open range. + pub data_range: (u32, u32), +} + +/// The declared contract an executable artifact must satisfy. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ArtifactContract { + pub id: String, + pub kind: ArtifactKind, + /// The private core-module import identity the artifact uses. + pub module_name: String, + /// SHA-256 of the artifact bytes. + pub sha256: String, + /// Human-readable pinned provenance (dependency, toolchain, recipe). + pub provenance: String, + pub required_features: Vec, + pub imports: Vec, + pub exports: Vec, + pub tables: Vec, + pub elements: Vec, + pub globals: Vec, + pub storage: Option, + pub initialization: InitializationContract, + /// Whether the artifact must be instantiated after the call shims. + pub instantiate_after_shims: bool, +} + +/// A pinned artifact and the contract it must satisfy. +#[derive(Clone, Debug)] +pub struct ArtifactReference { + pub contract: ArtifactContract, + pub bytes: Vec, +} + +/// The target policy the backend converts from its capability profile. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct TargetPolicy { + /// Canonical ids of the host interfaces the selected target profile permits. + /// The backend derives these from the resolved world and the capability + /// families, so the linker never parses backend capability types. + pub permitted_host_interfaces: Vec, +} + +/// Growth owner while the backend still synthesizes `cabi_realloc`. +/// +/// The canonical realloc adapter replaces this name with one allocator runtime +/// unit. Planning accepts that unit when the demand names its identity. +pub const GENERATED_GROWTH_OWNER: &str = "psrs:allocator/generated"; + +/// Identity of the one shared application memory. +pub const APPLICATION_MEMORY: &str = "application"; + +/// One operation an offered runtime unit provides. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct OfferedOperation { + pub name: String, + pub version: String, + pub export: String, + pub signature: CoreSignature, +} + +/// An operation an offered unit requires from another provider. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RequiredOperation { + pub name: String, + pub version: String, + pub signature: CoreSignature, +} + +/// A candidate runtime unit and the one variant it advertises. +#[derive(Clone, Debug)] +pub struct RuntimeUnitOffer { + pub id: String, + pub semantic_contract_version: String, + pub provided: Vec, + pub required: Vec, + pub artifact: ArtifactReference, + /// Private state owner. `None` means the unit owns no private state. + pub state_owner: Option, + /// The unit grows the shared memory. This conflicts with any other owner. + pub grows_memory: bool, +} + +/// The canonical ABI memory layout demanded by the application. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MemoryDemand { + /// The reserved canonical scratch region `[start, end)`. + pub canonical_scratch: (u32, u32), + /// The allocator-state region `[start, end)`. + pub allocator_state: (u32, u32), + /// The application's allocator start before any artifact reservation. + pub base_heap_start: u32, + /// The allocator block granularity the heap start must satisfy. + pub heap_alignment: u32, + /// The one owner allowed to grow the shared memory. + /// + /// The backend currently supplies [`GENERATED_GROWTH_OWNER`]. A selected + /// runtime unit may take this role when the demand names that unit. + pub growth_owner: String, + /// Declared maximum page count. `None` means the wasm32 limit of 65536. + pub maximum_pages: Option, +} + +/// The complete checked input to [`crate::plan::plan`]. +#[derive(Clone, Debug)] +pub struct TargetLinkInput { + pub requirements: Vec, + /// Candidate runtime units. Planning selects only the units a live + /// requirement or a selected unit's dependency reaches. + pub units: Vec, + pub policy: TargetPolicy, + pub memory: MemoryDemand, +} diff --git a/crates/psrs-linker/src/verify/mod.rs b/crates/psrs-linker/src/verify/mod.rs new file mode 100644 index 00000000..3c9ef363 --- /dev/null +++ b/crates/psrs-linker/src/verify/mod.rs @@ -0,0 +1,135 @@ +//! Artifact contract validation over the actual bytes. +//! +//! The contract declares what a pinned artifact must contain. Verification +//! parses the real module and rejects any stale digest, unexplained export, +//! import, table, global, feature, data range, or initialization path. + +use crate::error::{LinkErrors, LinkStage}; +use crate::target::{ArtifactContract, ArtifactKind}; +use wasmparser::WasmFeatures; + +mod parse; +#[cfg(test)] +mod tests; + +use parse::{check_contract, parse_core_module}; + +/// A verified core artifact ready for planning and composition. +#[derive(Clone, Debug)] +pub struct VerifiedArtifact { + pub id: String, + pub module_name: String, + pub sha256: String, + pub bytes: Vec, + /// Complete raw interfaces retained after executable contract verification. + pub imports: Vec, + pub exports: Vec, + pub instantiate_after_shims: bool, + /// The measured static stack bound, when the artifact declares storage. + pub stack_bound_bytes: Option, +} + +/// Validates `bytes` against `contract`, returning the verified artifact. +pub fn verify_artifact( + contract: &ArtifactContract, + bytes: &[u8], +) -> Result { + let stage = LinkStage::Verify; + let id = &contract.id; + match contract.kind { + ArtifactKind::CoreModule => {} + ArtifactKind::Component => { + return Err(LinkErrors::one( + stage, + id, + "component artifacts require typed component planning, not a raw-core contract; there is no silent host fallback", + )); + } + } + let digest = crate::sha256_hex(bytes); + if digest != contract.sha256 { + return Err(LinkErrors::one( + stage, + id, + format!( + "artifact digest mismatch: declared {}, actual {digest}", + contract.sha256 + ), + )); + } + let parsed = parse_core_module(bytes).map_err(|message| LinkErrors::one(stage, id, message))?; + let features = wasm_features(&contract.required_features) + .map_err(|message| LinkErrors::one(stage, id, message))?; + wasmparser::Validator::new_with_features(features) + .validate_all(bytes) + .map_err(|error| { + LinkErrors::one(stage, id, format!("artifact is not valid Wasm: {error}")) + })?; + check_contract(contract, &parsed)?; + let stack_bound_bytes = match &contract.storage { + Some(storage) => { + let function_imports: Vec<_> = contract + .imports + .iter() + .filter(|import| matches!(import.kind, crate::ImportKind::Function(_))) + .collect(); + let stackless_imports = if function_imports.len() == 1 + && crate::plan::is_heap_getter(function_imports[0]) + { + vec![0] + } else { + Vec::new() + }; + // Application verification discharges this cross-module obligation by + // requiring the getter to contain only i32.const and end. + let measured = crate::stack::measure_stack_bound_with_imports( + bytes, + storage.stack_pointer_global, + &stackless_imports, + ) + .map_err(|message| { + LinkErrors::one(stage, id, format!("stack analysis failed: {message}")) + })?; + if measured.bytes > storage.stack_bound_bytes { + return Err(LinkErrors::one( + stage, + id, + format!( + "artifact stack use {} exceeds its declared {} bytes", + measured.bytes, storage.stack_bound_bytes + ), + )); + } + Some(measured.bytes) + } + None => None, + }; + Ok(VerifiedArtifact { + id: contract.id.clone(), + module_name: contract.module_name.clone(), + sha256: digest, + bytes: bytes.to_vec(), + imports: contract.imports.clone(), + exports: contract.exports.clone(), + instantiate_after_shims: contract.instantiate_after_shims, + stack_bound_bytes, + }) +} + +fn wasm_features(names: &[String]) -> Result { + let mut features = WasmFeatures::MVP; + for name in names { + features |= match name.as_str() { + "mutable-globals" => WasmFeatures::MUTABLE_GLOBAL, + "sign-extension" => WasmFeatures::SIGN_EXTENSION, + "saturating-float-to-int" => WasmFeatures::SATURATING_FLOAT_TO_INT, + "multi-value" => WasmFeatures::MULTI_VALUE, + "bulk-memory" => WasmFeatures::BULK_MEMORY, + "reference-types" => WasmFeatures::REFERENCE_TYPES, + "function-references" => WasmFeatures::FUNCTION_REFERENCES, + "gc" => WasmFeatures::GC, + other => return Err(format!("unknown required Wasm feature `{other}`")), + }; + } + Ok(features) +} diff --git a/crates/psrs-linker/src/verify/parse.rs b/crates/psrs-linker/src/verify/parse.rs new file mode 100644 index 00000000..05e8a170 --- /dev/null +++ b/crates/psrs-linker/src/verify/parse.rs @@ -0,0 +1,409 @@ +//! Parsing a core module and comparing it with a declared contract. + +use crate::error::{LinkErrors, LinkStage}; +use crate::target::{ArtifactContract, CoreSignature, DeclaredExport, ExportKind, ImportKind}; +use wasmparser::{ExternalKind, Operator, Parser, Payload, TypeRef, ValType}; + +pub(super) fn check_contract( + contract: &ArtifactContract, + parsed: &ParsedCore, +) -> Result<(), LinkErrors> { + let stage = LinkStage::Verify; + let id = &contract.id; + + if parsed.imports.len() != contract.imports.len() + || parsed + .imports + .iter() + .zip(&contract.imports) + .any(|(a, b)| a.module != b.module || a.field != b.field || a.kind != b.kind) + { + return Err(LinkErrors::one( + stage, + id, + "artifact imports do not match the declared contract", + )); + } + + let mut declared_exports: Vec<&DeclaredExport> = contract.exports.iter().collect(); + for export in &parsed.exports { + let Some(index) = declared_exports + .iter() + .position(|declared| declared.name == export.name && declared.kind == export.kind) + else { + return Err(LinkErrors::one( + stage, + id, + format!("artifact has an undeclared export `{}`", export.name), + )); + }; + let declared = declared_exports.swap_remove(index); + if declared.kind == ExportKind::Func && declared.signature != export.signature { + return Err(LinkErrors::one( + stage, + id, + format!("export `{}` has an unexpected signature", export.name), + )); + } + } + if let Some(missing) = declared_exports.first() { + return Err(LinkErrors::one( + stage, + id, + format!("artifact is missing declared export `{}`", missing.name), + )); + } + + if tables_differ(parsed, contract) { + return Err(LinkErrors::one( + stage, + id, + "artifact tables do not match the declared contract", + )); + } + if globals_differ(parsed, contract) { + return Err(LinkErrors::one( + stage, + id, + "artifact globals do not match the declared contract", + )); + } + if let Some(storage) = &contract.storage { + let absolute = storage.stack_pointer_global as usize; + let defined = absolute + .checked_sub(parsed.imported_globals) + .ok_or_else(|| LinkErrors::one(stage, id, "stack pointer global is an import"))?; + let pointer = parsed + .globals + .get(defined) + .ok_or_else(|| LinkErrors::one(stage, id, "stack pointer global is absent"))?; + if !pointer.mutable + || pointer.initial != storage.stack.end + || storage.stack.start >= storage.stack.end + || storage.static_data.start > storage.static_data.end + || contract.initialization.data_range.0 < storage.static_data.start + || contract.initialization.data_range.1 > storage.static_data.end + || storage.stack_bound_bytes > storage.stack.end - storage.stack.start + { + return Err(LinkErrors::one( + stage, + id, + "execution storage disagrees with the artifact stack pointer or initialization", + )); + } + } + if parsed.elements != contract.elements { + return Err(LinkErrors::one( + stage, + id, + "artifact element segments do not match the declared contract", + )); + } + for (start, length) in &parsed.data_ranges { + let end = start + .checked_add(*length) + .ok_or_else(|| LinkErrors::one(stage, id, "artifact data range overflows"))?; + let (low, high) = contract.initialization.data_range; + if *start < low || end > high { + return Err(LinkErrors::one( + stage, + id, + "artifact data outside its declared initialization range", + )); + } + } + if contract.initialization.start_forbidden && parsed.has_start { + return Err(LinkErrors::one( + stage, + id, + "artifact defines a start function, which its contract forbids", + )); + } + Ok(()) +} + +#[derive(Debug)] +struct ParsedImport { + module: String, + field: String, + kind: ImportKind, +} + +struct ParsedExport { + name: String, + kind: ExportKind, + signature: Option, +} + +struct ParsedTable { + element: String, + minimum: u32, + maximum: Option, +} + +struct ParsedGlobal { + mutable: bool, + initial: u32, +} + +pub(super) struct ParsedCore { + imports: Vec, + exports: Vec, + tables: Vec, + globals: Vec, + data_ranges: Vec<(u32, u32)>, + has_start: bool, + elements: Vec, + imported_globals: usize, +} + +impl ParsedTable { + fn matches(&self, declared: &crate::target::DeclaredTable) -> bool { + self.element == declared.element + && self.minimum == declared.minimum + && self.maximum == declared.maximum + } +} + +fn tables_differ(parsed: &ParsedCore, contract: &ArtifactContract) -> bool { + parsed.tables.len() != contract.tables.len() + || parsed + .tables + .iter() + .zip(&contract.tables) + .any(|(a, b)| !a.matches(b)) +} + +fn globals_differ(parsed: &ParsedCore, contract: &ArtifactContract) -> bool { + parsed.globals.len() != contract.globals.len() + || parsed + .globals + .iter() + .zip(&contract.globals) + .any(|(a, b)| a.mutable != b.mutable || a.initial != b.initial) +} + +pub(super) fn parse_core_module(bytes: &[u8]) -> Result { + let mut types = crate::CoreTypes::default(); + let mut imported_functions = Vec::new(); + let mut defined_functions = Vec::new(); + let mut imports = Vec::new(); + let mut exports = Vec::new(); + let mut tables = Vec::new(); + let mut globals = Vec::new(); + let mut data_ranges = Vec::new(); + let mut memories = 0_u32; + let mut has_start = false; + let mut elements = Vec::new(); + let mut imported_globals = 0_usize; + + for payload in Parser::new(0).parse_all(bytes) { + match payload.map_err(|error| error.to_string())? { + Payload::TypeSection(reader) => { + for group in reader { + types.push(group.map_err(|error| error.to_string())?)?; + } + } + Payload::ImportSection(reader) => { + for import in reader.into_imports() { + let import = import.map_err(|error| error.to_string())?; + match import.ty { + TypeRef::Memory(memory) => { + if memory.memory64 || memory.shared { + return Err("artifact imports an unsupported memory".into()); + } + imports.push(ParsedImport { + module: import.module.to_string(), + field: import.name.to_string(), + kind: ImportKind::Memory { + minimum: memory.initial, + maximum: memory.maximum, + }, + }); + } + TypeRef::Global(global) => { + if global.content_type != ValType::I32 || global.shared { + return Err("artifact imports an unsupported global".into()); + } + imported_globals += 1; + imports.push(ParsedImport { + module: import.module.to_string(), + field: import.name.to_string(), + kind: ImportKind::Global { + mutable: global.mutable, + }, + }); + } + TypeRef::Func(index) => { + imported_functions.push(index); + imports.push(ParsedImport { + module: import.module.to_string(), + field: import.name.to_string(), + kind: ImportKind::Function(function_signature( + imported_functions.len() as u32 - 1, + &imported_functions, + &[], + &types, + )?), + }); + } + _ => return Err("artifact imports an unsupported item".into()), + } + } + } + Payload::FunctionSection(reader) => { + for ty in reader { + defined_functions.push(ty.map_err(|error| error.to_string())?); + } + } + Payload::TableSection(reader) => { + for table in reader { + let table = table.map_err(|error| error.to_string())?; + let element = if table.ty.element_type == wasmparser::RefType::FUNCREF { + "funcref" + } else if table.ty.element_type == wasmparser::RefType::EXTERNREF { + "externref" + } else { + return Err("artifact defines an unsupported table element type".into()); + }; + tables.push(ParsedTable { + element: element.into(), + minimum: table.ty.initial as u32, + maximum: table.ty.maximum.map(|value| value as u32), + }); + } + } + Payload::GlobalSection(reader) => { + for global in reader { + let global = global.map_err(|error| error.to_string())?; + if global.ty.content_type != ValType::I32 || global.ty.shared { + return Err("artifact defines an unsupported global".into()); + } + let initial = constant_i32(&global.init_expr)?; + globals.push(ParsedGlobal { + mutable: global.ty.mutable, + initial, + }); + } + } + Payload::ExportSection(reader) => { + for export in reader { + let export = export.map_err(|error| error.to_string())?; + match export.kind { + ExternalKind::Func => { + let signature = function_signature( + export.index, + &imported_functions, + &defined_functions, + &types, + )?; + exports.push(ParsedExport { + name: export.name.to_string(), + kind: ExportKind::Func, + signature: Some(signature), + }); + } + ExternalKind::Global => exports.push(ParsedExport { + name: export.name.to_string(), + kind: ExportKind::Global, + signature: None, + }), + _ => { + return Err(format!( + "artifact export `{}` has an undeclared kind", + export.name + )); + } + } + } + } + Payload::DataSection(reader) => { + for data in reader { + let data = data.map_err(|error| error.to_string())?; + let wasmparser::DataKind::Active { + memory_index: 0, + offset_expr, + } = data.kind + else { + return Err("artifact has unsupported or passive data storage".into()); + }; + let offset = constant_i32(&offset_expr)?; + data_ranges.push((offset, data.data.len() as u32)); + } + } + Payload::MemorySection(_) => memories += 1, + Payload::StartSection { .. } => has_start = true, + Payload::ElementSection(reader) => { + for element in reader { + let element = element.map_err(|error| error.to_string())?; + let wasmparser::ElementKind::Active { + table_index, + offset_expr, + } = element.kind + else { + return Err( + "artifact has unsupported passive or declarative elements".into() + ); + }; + let wasmparser::ElementItems::Functions(functions) = element.items else { + return Err("artifact has unsupported element expressions".into()); + }; + elements.push(crate::DeclaredElement { + table: table_index.unwrap_or(0), + offset: constant_i32(&offset_expr)?, + functions: functions + .into_iter() + .collect::, _>>() + .map_err(|error| error.to_string())?, + }); + } + } + _ => {} + } + } + if memories > 0 { + return Err("artifact defines its own memory instead of importing one".into()); + } + Ok(ParsedCore { + imports, + exports, + tables, + globals, + data_ranges, + has_start, + elements, + imported_globals, + }) +} + +fn constant_i32(expr: &wasmparser::ConstExpr<'_>) -> Result { + let mut ops = expr.get_operators_reader(); + let value = match ops.read().map_err(|error| error.to_string())? { + Operator::I32Const { value } => value as u32, + _ => return Err("artifact constant is not an i32".into()), + }; + if !matches!( + ops.read().map_err(|error| error.to_string())?, + Operator::End + ) { + return Err("artifact constant has extra operators".into()); + } + Ok(value) +} + +fn function_signature( + index: u32, + imported_functions: &[u32], + defined_functions: &[u32], + types: &crate::CoreTypes, +) -> Result { + let type_index = if (index as usize) < imported_functions.len() { + imported_functions[index as usize] + } else { + let defined = index as usize - imported_functions.len(); + *defined_functions + .get(defined) + .ok_or("artifact export refers to an unknown function")? + }; + types.signature(type_index) +} diff --git a/crates/psrs-linker/src/verify/tests.rs b/crates/psrs-linker/src/verify/tests.rs new file mode 100644 index 00000000..c556ac11 --- /dev/null +++ b/crates/psrs-linker/src/verify/tests.rs @@ -0,0 +1,380 @@ +//! Contract-verification tests. + +use super::*; +use crate::target::{ + ArtifactContract, ArtifactKind, CoreSignature, CoreType, DeclaredExport, DeclaredGlobal, + DeclaredImport, DeclaredTable, ExportKind, ImportKind, InitializationContract, StorageContract, + StorageRegion, +}; + +fn number_format_contract() -> ArtifactContract { + ArtifactContract { + elements: vec![crate::DeclaredElement { + table: 0, + offset: 1, + functions: vec![54], + }], + id: "psrs:runtime-number".into(), + kind: ArtifactKind::CoreModule, + module_name: psrs_runtime::MODULE_NAME.into(), + sha256: psrs_runtime::NUMBER_RUNTIME.provenance.sha256.into(), + provenance: "test".into(), + required_features: psrs_runtime::NUMBER_RUNTIME + .required_features + .iter() + .map(|feature| (*feature).to_string()) + .collect(), + imports: vec![DeclaredImport { + module: psrs_runtime::MEMORY_MODULE.into(), + field: psrs_runtime::MEMORY_FIELD.into(), + kind: ImportKind::Memory { + minimum: 3, + maximum: None, + }, + }], + exports: vec![ + DeclaredExport { + name: psrs_runtime::NUMBER_FORMAT.export.into(), + kind: ExportKind::Func, + signature: Some(CoreSignature { + parameters: vec![CoreType::F64, CoreType::I32, CoreType::I32], + result: Some(CoreType::I32), + }), + }, + DeclaredExport { + name: psrs_runtime::HEAP_BASE_EXPORT.into(), + kind: ExportKind::Global, + signature: None, + }, + DeclaredExport { + name: psrs_runtime::DECIMAL_EXPORT.into(), + kind: ExportKind::Func, + signature: Some(CoreSignature { + parameters: vec![CoreType::I32, CoreType::I32], + result: Some(CoreType::F64), + }), + }, + DeclaredExport { + name: psrs_runtime::ACOS_EXPORT.into(), + kind: ExportKind::Func, + signature: Some(CoreSignature { + parameters: vec![CoreType::F64], + result: Some(CoreType::F64), + }), + }, + DeclaredExport { + name: psrs_runtime::ASIN_EXPORT.into(), + kind: ExportKind::Func, + signature: Some(CoreSignature { + parameters: vec![CoreType::F64], + result: Some(CoreType::F64), + }), + }, + DeclaredExport { + name: psrs_runtime::ATAN_EXPORT.into(), + kind: ExportKind::Func, + signature: Some(CoreSignature { + parameters: vec![CoreType::F64], + result: Some(CoreType::F64), + }), + }, + DeclaredExport { + name: psrs_runtime::ATAN2_EXPORT.into(), + kind: ExportKind::Func, + signature: Some(CoreSignature { + parameters: vec![CoreType::F64, CoreType::F64], + result: Some(CoreType::F64), + }), + }, + DeclaredExport { + name: psrs_runtime::SIN_EXPORT.into(), + kind: ExportKind::Func, + signature: Some(CoreSignature { + parameters: vec![CoreType::F64], + result: Some(CoreType::F64), + }), + }, + DeclaredExport { + name: psrs_runtime::COS_EXPORT.into(), + kind: ExportKind::Func, + signature: Some(CoreSignature { + parameters: vec![CoreType::F64], + result: Some(CoreType::F64), + }), + }, + DeclaredExport { + name: psrs_runtime::TAN_EXPORT.into(), + kind: ExportKind::Func, + signature: Some(CoreSignature { + parameters: vec![CoreType::F64], + result: Some(CoreType::F64), + }), + }, + DeclaredExport { + name: psrs_runtime::EXP_EXPORT.into(), + kind: ExportKind::Func, + signature: Some(CoreSignature { + parameters: vec![CoreType::F64], + result: Some(CoreType::F64), + }), + }, + DeclaredExport { + name: psrs_runtime::LOG_EXPORT.into(), + kind: ExportKind::Func, + signature: Some(CoreSignature { + parameters: vec![CoreType::F64], + result: Some(CoreType::F64), + }), + }, + DeclaredExport { + name: psrs_runtime::POW_EXPORT.into(), + kind: ExportKind::Func, + signature: Some(CoreSignature { + parameters: vec![CoreType::F64, CoreType::F64], + result: Some(CoreType::F64), + }), + }, + DeclaredExport { + name: psrs_runtime::MIN_EXPORT.into(), + kind: ExportKind::Func, + signature: Some(CoreSignature { + parameters: vec![CoreType::F64, CoreType::F64], + result: Some(CoreType::F64), + }), + }, + DeclaredExport { + name: psrs_runtime::MAX_EXPORT.into(), + kind: ExportKind::Func, + signature: Some(CoreSignature { + parameters: vec![CoreType::F64, CoreType::F64], + result: Some(CoreType::F64), + }), + }, + DeclaredExport { + name: psrs_runtime::SIGN_EXPORT.into(), + kind: ExportKind::Func, + signature: Some(CoreSignature { + parameters: vec![CoreType::F64], + result: Some(CoreType::F64), + }), + }, + DeclaredExport { + name: psrs_runtime::REMAINDER_EXPORT.into(), + kind: ExportKind::Func, + signature: Some(CoreSignature { + parameters: vec![CoreType::F64, CoreType::F64], + result: Some(CoreType::F64), + }), + }, + DeclaredExport { + name: psrs_runtime::IS_NAN_EXPORT.into(), + kind: ExportKind::Func, + signature: Some(CoreSignature { + parameters: vec![CoreType::F64], + result: Some(CoreType::I32), + }), + }, + DeclaredExport { + name: psrs_runtime::NAN_EXPORT.into(), + kind: ExportKind::Func, + signature: Some(CoreSignature { + parameters: vec![], + result: Some(CoreType::F64), + }), + }, + DeclaredExport { + name: psrs_runtime::INFINITY_EXPORT.into(), + kind: ExportKind::Func, + signature: Some(CoreSignature { + parameters: vec![], + result: Some(CoreType::F64), + }), + }, + ], + tables: vec![DeclaredTable { + element: "funcref".into(), + minimum: 2, + maximum: Some(2), + }], + globals: vec![ + DeclaredGlobal { + mutable: true, + initial: psrs_runtime::HEAP_START, + }, + DeclaredGlobal { + mutable: false, + initial: psrs_runtime::HEAP_START, + }, + ], + storage: Some(StorageContract { + static_data: StorageRegion { + owner: "number-format".into(), + start: psrs_runtime::RESERVED_START, + end: psrs_runtime::STACK_BOTTOM, + }, + stack: StorageRegion { + owner: "number-format".into(), + start: psrs_runtime::STACK_BOTTOM, + end: psrs_runtime::HEAP_START, + }, + heap_start: psrs_runtime::HEAP_START, + minimum_pages: 3, + stack_pointer_global: 0, + stack_bound_bytes: psrs_runtime::NUMBER_RUNTIME.storage.stack_bound_bytes, + stack_bound_evidence: psrs_runtime::NUMBER_RUNTIME + .storage + .stack_bound_evidence + .into(), + }), + initialization: InitializationContract { + start_forbidden: true, + data_range: (psrs_runtime::RESERVED_START, psrs_runtime::STACK_BOTTOM), + }, + instantiate_after_shims: true, + } +} + +#[test] +fn embedded_artifact_satisfies_its_contract() { + let contract = number_format_contract(); + verify_artifact(&contract, psrs_runtime::NUMBER_RUNTIME.bytes) + .expect("the pinned artifact should verify"); +} + +#[test] +fn element_initializers_must_match_the_complete_declared_contract() { + for change in 0..4 { + let mut contract = number_format_contract(); + match change { + 0 => contract.elements.clear(), + 1 => contract.elements[0].offset += 1, + 2 => contract.elements[0].table += 1, + _ => contract.elements[0].functions[0] += 1, + } + assert!( + verify_artifact(&contract, psrs_runtime::NUMBER_RUNTIME.bytes) + .unwrap_err() + .to_string() + .contains("element segments") + ); + } +} + +#[test] +fn a_component_contract_cannot_use_raw_core_verification() { + let mut contract = number_format_contract(); + contract.kind = ArtifactKind::Component; + let error = verify_artifact(&contract, psrs_runtime::NUMBER_RUNTIME.bytes) + .expect_err("component bytes cannot satisfy a raw-core contract"); + assert!( + error.to_string().contains("no silent host fallback"), + "{error}" + ); +} + +#[test] +fn a_stale_digest_is_rejected() { + let mut contract = number_format_contract(); + contract.sha256 = "0".repeat(64); + assert!(verify_artifact(&contract, psrs_runtime::NUMBER_RUNTIME.bytes).is_err()); +} + +#[test] +fn an_undeclared_export_is_rejected() { + let mut contract = number_format_contract(); + contract.exports.remove(1); + assert!(verify_artifact(&contract, psrs_runtime::NUMBER_RUNTIME.bytes).is_err()); +} + +#[test] +fn an_undeclared_import_is_rejected() { + let mut contract = number_format_contract(); + contract.imports.clear(); + assert!(verify_artifact(&contract, psrs_runtime::NUMBER_RUNTIME.bytes).is_err()); +} + +#[test] +fn an_overlapping_data_range_is_rejected() { + let mut contract = number_format_contract(); + contract.initialization.data_range = (0, 16); + assert!(verify_artifact(&contract, psrs_runtime::NUMBER_RUNTIME.bytes).is_err()); +} + +#[test] +fn storage_cannot_name_an_absent_immutable_or_displaced_stack_pointer() { + for index in [1, 99] { + let mut contract = number_format_contract(); + contract.storage.as_mut().unwrap().stack_pointer_global = index; + assert!(verify_artifact(&contract, psrs_runtime::NUMBER_RUNTIME.bytes).is_err()); + } + let mut contract = number_format_contract(); + contract.storage.as_mut().unwrap().stack.end += 8; + assert!(verify_artifact(&contract, psrs_runtime::NUMBER_RUNTIME.bytes).is_err()); +} + +#[test] +fn independent_export_table_and_feature_contract_drift_is_rejected() { + let mut contract = number_format_contract(); + contract.exports[0].signature.as_mut().unwrap().parameters[0] = CoreType::F32; + assert!( + verify_artifact(&contract, psrs_runtime::NUMBER_RUNTIME.bytes) + .unwrap_err() + .to_string() + .contains("signature") + ); + let mut contract = number_format_contract(); + let mut missing = contract.exports[0].clone(); + missing.name = "absent-export".into(); + contract.exports.push(missing); + assert!( + verify_artifact(&contract, psrs_runtime::NUMBER_RUNTIME.bytes) + .unwrap_err() + .to_string() + .contains("missing declared export") + ); + let mut contract = number_format_contract(); + contract.tables[0].minimum = 0; + assert!( + verify_artifact(&contract, psrs_runtime::NUMBER_RUNTIME.bytes) + .unwrap_err() + .to_string() + .contains("tables") + ); + let mut contract = number_format_contract(); + contract.required_features.clear(); + assert!( + verify_artifact(&contract, psrs_runtime::NUMBER_RUNTIME.bytes) + .unwrap_err() + .to_string() + .contains("not valid Wasm") + ); +} + +#[test] +fn a_valid_but_undeclared_eager_initializer_is_rejected() { + let text = wasmprinter::print_bytes(psrs_runtime::NUMBER_RUNTIME.bytes).unwrap(); + let prefix = text.trim_end().strip_suffix(')').unwrap(); + let bytes = wat::parse_str(format!("{prefix}(func $eager) (start $eager))")).unwrap(); + // The pinned numeric artifact uses saturating truncation. The default + // validator leaves that proposal off, so this check uses the same feature + // set as static stack analysis. + use wasmparser::WasmFeatures as F; + let features = F::MVP + | F::MUTABLE_GLOBAL + | F::SIGN_EXTENSION + | F::SATURATING_FLOAT_TO_INT + | F::MULTI_VALUE + | F::BULK_MEMORY + | F::REFERENCE_TYPES; + wasmparser::Validator::new_with_features(features) + .validate_all(&bytes) + .expect("valid initializer module"); + let mut contract = number_format_contract(); + contract.sha256 = crate::sha256_hex(&bytes); + assert!( + verify_artifact(&contract, &bytes) + .unwrap_err() + .to_string() + .contains("start function") + ); +} diff --git a/crates/psrs-linker/src/world_boundary/facade.rs b/crates/psrs-linker/src/world_boundary/facade.rs new file mode 100644 index 00000000..6fccc476 --- /dev/null +++ b/crates/psrs-linker/src/world_boundary/facade.rs @@ -0,0 +1,148 @@ +//! Interface-only WIT input; original application instructions are never replaced. +use super::*; +use wasm_encoder::{ElementSection, ExportSection, GlobalSection, ImportSection}; + +struct Indices(Vec); +impl Reencode for Indices { + type Error = String; + fn function_index(&mut self, index: u32) -> Result> { + Ok(self.0.get(index as usize).copied().unwrap_or(index)) + } +} + +/// Preserve permitted host and registered canonical library imports while +/// replacing raw provider imports with local declarations. Import permutation +/// is reflected in all function references. +pub(super) fn boundary_module(bytes: &[u8], hosts: &[String]) -> Result, String> { + let mut imports = ImportSection::new(); + let mut private = Vec::new(); + let mut functions = FunctionSection::new(); + let mut permutation = Vec::new(); + let mut host_count = 0; + let mut has_imports = false; + for payload in Parser::new(0).parse_all(bytes) { + match payload.map_err(|e| e.to_string())? { + Payload::ImportSection(section) => { + has_imports = true; + for import in section.into_imports() { + let import = import.map_err(|e| e.to_string())?; + let TypeRef::Func(index) = import.ty else { + return Err( + "canonical boundary projection requires application-owned storage" + .into(), + ); + }; + if hosts.iter().any(|host| host == import.module) { + imports.import( + import.module, + import.name, + wasm_encoder::EntityType::Function(index), + ); + permutation.push((true, host_count)); + host_count += 1; + } else { + permutation.push((false, private.len() as u32)); + private.push(index); + } + } + for index in &private { + functions.function(*index); + } + } + Payload::FunctionSection(section) => { + for index in section { + functions.function(index.map_err(|e| e.to_string())?); + } + } + Payload::CustomSection(section) if section.name() == MARKER => { + return Err("source module uses a reserved canonical boundary marker".into()); + } + Payload::StartSection { .. } => { + return Err( + "canonical boundary cannot project an application start function".into(), + ); + } + _ => {} + } + } + let mut indices = Indices( + permutation + .into_iter() + .map(|(host, index)| if host { index } else { host_count + index }) + .collect(), + ); + let mut module = Module::new(); + let mut code = CodeSection::new(); + for _ in 0..functions.len() { + let mut body = Function::new([]); + body.instruction(&Instruction::Unreachable); + body.instruction(&Instruction::End); + code.function(&body); + } + let mut code_emitted = false; + for payload in Parser::new(0).parse_all(bytes) { + match payload.map_err(|e| e.to_string())? { + Payload::ImportSection(_) => { + if !imports.is_empty() { + module.section(&imports); + } + if !functions.is_empty() { + module.section(&functions); + } + } + Payload::FunctionSection(_) + | Payload::CodeSectionEntry(_) + | Payload::Version { .. } => {} + Payload::CodeSectionStart { .. } => { + module.section(&code); + code_emitted = true; + } + Payload::ExportSection(section) => { + let mut exports = ExportSection::new(); + indices + .parse_export_section(&mut exports, section) + .map_err(|e| e.to_string())?; + module.section(&exports); + } + Payload::GlobalSection(section) => { + let mut globals = GlobalSection::new(); + indices + .parse_global_section(&mut globals, section) + .map_err(|e| e.to_string())?; + module.section(&globals); + } + Payload::ElementSection(section) => { + let mut elements = ElementSection::new(); + indices + .parse_element_section(&mut elements, section) + .map_err(|e| e.to_string())?; + module.section(&elements); + } + Payload::End(_) => { + if !code_emitted && !functions.is_empty() { + module.section(&code); + } + } + payload => { + if let Some((id, range)) = payload.as_section() { + module.section(&RawSection { + id, + data: &bytes[range], + }); + if id == 1 && !has_imports && !functions.is_empty() { + module.section(&functions); + } + } + } + } + } + module.section(&CustomSection { + name: Cow::Borrowed(MARKER), + data: Cow::Borrowed(b"1"), + }); + let bytes = module.finish(); + wasmparser::Validator::new() + .validate_all(&bytes) + .map_err(|e| e.to_string())?; + Ok(bytes) +} diff --git a/crates/psrs-linker/src/world_boundary/mod.rs b/crates/psrs-linker/src/world_boundary/mod.rs new file mode 100644 index 00000000..817c0610 --- /dev/null +++ b/crates/psrs-linker/src/world_boundary/mod.rs @@ -0,0 +1,237 @@ +//! WIT boundary encoding kept separate from private raw Core instance assembly. +use crate::{CheckedLinkPlan, LinkErrors, LinkStage, ResolvedWorldContext}; +use std::borrow::Cow; +use wasm_encoder::reencode::{Error, Reencode, ReencodeComponent}; +use wasm_encoder::{ + CodeSection, Component, CustomSection, Function, FunctionSection, Instruction, Module, + RawSection, +}; +use wasmparser::{Parser, Payload, TypeRef}; + +const MARKER: &str = "psrs-linker:canonical-boundary"; + +pub(super) fn encode( + context: &ResolvedWorldContext, + plan: &CheckedLinkPlan, + application: &[u8], +) -> Result, LinkErrors> { + fn error(message: impl Into) -> LinkErrors { + LinkErrors::plain(LinkStage::Compose, message) + } + let libraries = crate::core_assembly::canonical_libraries(plan); + let prepared = crate::core_assembly::prepare_core(plan, application, &libraries)?; + let application_module = prepared.application_module; + let providers = prepared.providers; + let builder = prepared.component; + let modules = builder.core_module_count(); + let instances = builder.core_instance_count(); + let prefix = builder.finish(); + let mut visible = plan.external_world().to_vec(); + visible.extend(libraries.iter().cloned()); + let mut facade = boundary_module(application, &visible).map_err(error)?; + let world = context + .composition_world() + .ok_or_else(|| error("canonical boundary requires a resolved world"))?; + wit_component::embed_component_metadata( + &mut facade, + context.resolve(), + world, + wit_component::StringEncoding::UTF8, + ) + .map_err(|e| error(format!("{e:#}")))?; + let mut encoder = wit_component::ComponentEncoder::default() + .module(&facade) + .map_err(|e| error(format!("{e:#}")))?; + for artifact in plan + .artifacts() + .iter() + .filter(|artifact| libraries.contains(&artifact.module_name)) + { + encoder = encoder + .library( + &artifact.module_name, + &artifact.bytes, + wit_component::LibraryInfo { + instantiate_after_shims: artifact.instantiate_after_shims, + arguments: Vec::new(), + }, + ) + .map_err(|e| error(format!("{e:#}")))?; + } + let boundary = encoder + .validate(true) + .encode() + .map_err(|e| error(format!("{e:#}")))?; + let mut component = Component::new(); + CopyModules + .parse_component(&mut component, Parser::new(0), &prefix) + .map_err(|e| error(format!("{e:#}")))?; + let mut attach = Attach { + modules, + instances, + providers, + application_module, + depth: 0, + module_seen: false, + instance_seen: false, + source_instances: 0, + source_application: None, + }; + attach + .parse_component(&mut component, Parser::new(0), &boundary) + .map_err(|e| error(format!("{e:#}")))?; + if !attach.module_seen || !attach.instance_seen { + return Err(error( + "canonical boundary did not identify its application instance", + )); + } + Ok(component.finish()) +} + +mod facade; +use facade::boundary_module; + +struct CopyModules; +impl Reencode for CopyModules { + type Error = String; +} +impl ReencodeComponent for CopyModules { + fn parse_component_submodule( + &mut self, + component: &mut Component, + _: Parser, + bytes: &[u8], + ) -> Result<(), Error> { + component.section(&RawSection { id: 1, data: bytes }); + Ok(()) + } +} + +struct Attach { + modules: u32, + instances: u32, + providers: std::collections::BTreeMap, + application_module: u32, + depth: u32, + source_instances: u32, + source_application: Option, + module_seen: bool, + instance_seen: bool, +} +impl Reencode for Attach { + type Error = String; +} +impl ReencodeComponent for Attach { + fn push_depth(&mut self) { + self.depth += 1; + } + fn pop_depth(&mut self) { + self.depth -= 1; + } + fn module_index(&mut self, index: u32) -> u32 { + if self.depth == 0 { + self.root_module(index) + } else { + index + } + } + fn outer_module_index(&mut self, count: u32, index: u32) -> u32 { + if count == self.depth { + self.root_module(index) + } else { + index + } + } + fn instance_index(&mut self, index: u32) -> u32 { + if self.depth != 0 { + return index; + } + index + self.instances + } + fn parse_component_submodule( + &mut self, + component: &mut Component, + parser: Parser, + bytes: &[u8], + ) -> Result<(), Error> { + let marker = Parser::new(0).parse_all(bytes).any(|payload| matches!(payload, Ok(Payload::CustomSection(section)) if section.name() == MARKER && section.data() == b"1")); + if self.depth == 0 && !self.module_seen { + if !marker { + return Err(Error::UserError( + "canonical application module lacks its identity marker".into(), + )); + } + self.module_seen = true; + return Ok(()); + } + if marker { + return Err(Error::UserError( + "canonical application module identity is repeated".into(), + )); + } + wasm_encoder::reencode::component_utils::parse_component_submodule( + self, component, parser, bytes, + ) + } + fn parse_instance( + &mut self, + instances: &mut wasm_encoder::InstanceSection, + instance: wasmparser::Instance<'_>, + ) -> Result<(), Error> { + if self.depth == 0 { + let index = self.source_instances; + self.source_instances += 1; + if matches!( + instance, + wasmparser::Instance::Instantiate { + module_index: 0, + .. + } + ) { + if self.instance_seen { + return Err(Error::UserError( + "canonical application instance is repeated".into(), + )); + } + let wasmparser::Instance::Instantiate { args, .. } = instance else { + unreachable!() + }; + let mut actual = args + .iter() + .map(|arg| { + if self.providers.contains_key(arg.name) { + return Err(Error::UserError( + "canonical host and raw provider namespaces conflict".into(), + )); + } + Ok(( + arg.name, + wasm_encoder::ModuleArg::Instance(self.instance_index(arg.index)), + )) + }) + .collect::, Error>>()?; + actual.extend(self.providers.iter().map(|(name, index)| { + (name.as_str(), wasm_encoder::ModuleArg::Instance(*index)) + })); + instances.instantiate(self.application_module, actual); + self.instance_seen = true; + self.source_application = Some(index); + return Ok(()); + } + } + wasm_encoder::reencode::component_utils::parse_instance(self, instances, instance) + } +} + +impl Attach { + fn root_module(&self, index: u32) -> u32 { + if index == 0 { + self.application_module + } else { + index + self.modules - 1 + } + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/psrs-linker/src/world_boundary/tests.rs b/crates/psrs-linker/src/world_boundary/tests.rs new file mode 100644 index 00000000..0195ea60 --- /dev/null +++ b/crates/psrs-linker/src/world_boundary/tests.rs @@ -0,0 +1,253 @@ +use super::*; + +#[test] +fn facade_preserves_function_indices_and_complete_public_gc_signatures() { + let source = wat::parse_str( + r#"(module + (type $a (array (mut eqref))) + (import "private" "read" (func $read (param (ref $a) i32) (result eqref))) + (memory (export "memory") 1) + (func (export "run") (result i32) i32.const 7) + (export "read" (func $read)))"#, + ) + .unwrap(); + let facade = boundary_module(&source, &[]).unwrap(); + let original = crate::CoreTypes::from_module(&source).unwrap(); + let projected = crate::CoreTypes::from_module(&facade).unwrap(); + assert_eq!( + original.signature(1).unwrap(), + projected.signature(1).unwrap() + ); + assert_eq!( + original.signature(2).unwrap(), + projected.signature(2).unwrap() + ); + assert!( + !Parser::new(0) + .parse_all(&facade) + .any(|payload| matches!(payload, Ok(Payload::ImportSection(_)))) + ); + let mut exports = Vec::new(); + for payload in Parser::new(0).parse_all(&facade) { + if let Payload::ExportSection(section) = payload.unwrap() { + for export in section { + let export = export.unwrap(); + if export.kind == wasmparser::ExternalKind::Func { + exports.push((export.name.to_string(), export.index)); + } + } + } + } + assert!(exports.contains(&("read".into(), 0))); + assert!(exports.contains(&("run".into(), 1))); +} + +#[test] +fn an_exported_import_only_module_gets_a_valid_boundary_declaration() { + let source = wat::parse_str( + r#"(module + (import "private" "run" (func (result i32))) + (memory (export "memory") 1) + (export "run" (func 0)))"#, + ) + .unwrap(); + let projected = boundary_module(&source, &[]).unwrap(); + wasmparser::Validator::new() + .validate_all(&projected) + .unwrap(); +} + +#[test] +fn facade_retains_registered_scalar_libraries_and_relocates_raw_reference_functions() { + let source = wat::parse_str( + r#"(module + (type $array (array (mut eqref))) + (import "raw" "read" (func $read (param (ref $array) i32) (result eqref))) + (import "scalar" "allocate" (func $allocate (param i32) (result i32))) + (memory (export "memory") 1) + (export "read" (func $read)) + (export "allocate" (func $allocate)))"#, + ) + .unwrap(); + let facade = boundary_module(&source, &["scalar".into()]).unwrap(); + wasmparser::Validator::new().validate_all(&facade).unwrap(); + let mut imports = Vec::new(); + let mut exports = Vec::new(); + for payload in Parser::new(0).parse_all(&facade) { + match payload.unwrap() { + Payload::ImportSection(section) => { + for import in section.into_imports() { + imports.push(import.unwrap().module.to_owned()); + } + } + Payload::ExportSection(section) => { + for export in section { + let export = export.unwrap(); + if export.kind == wasmparser::ExternalKind::Func { + exports.push((export.name.to_owned(), export.index)); + } + } + } + _ => {} + } + } + assert_eq!(imports, ["scalar"]); + assert!(exports.contains(&("allocate".into(), 0))); + assert!(exports.contains(&("read".into(), 1))); + let original = crate::CoreTypes::from_module(&source).unwrap(); + let projected = crate::CoreTypes::from_module(&facade).unwrap(); + assert_eq!( + original.signature(1).unwrap(), + projected.signature(1).unwrap() + ); + assert_eq!( + original.signature(2).unwrap(), + projected.signature(2).unwrap() + ); +} + +#[test] +fn reserved_boundary_identity_and_storage_imports_are_rejected() { + let mut source = Module::new(); + source.section(&CustomSection { + name: Cow::Borrowed(MARKER), + data: Cow::Borrowed(b"1"), + }); + assert!( + boundary_module(&source.finish(), &[]) + .unwrap_err() + .contains("reserved") + ); + let source = wat::parse_str(r#"(module (import "env" "memory" (memory 1)))"#).unwrap(); + assert!( + boundary_module(&source, &[]) + .unwrap_err() + .contains("application-owned storage") + ); +} + +#[test] +fn index_mapping_tracks_application_identity_and_component_scopes() { + let mut attach = Attach { + modules: 4, + instances: 4, + providers: Default::default(), + application_module: 2, + depth: 0, + module_seen: true, + instance_seen: true, + source_instances: 3, + source_application: Some(1), + }; + assert_eq!(attach.module_index(0), 2); + assert_eq!(attach.module_index(1), 4); + assert_eq!(attach.instance_index(0), 4); + assert_eq!(attach.instance_index(1), 5); + assert_eq!(attach.instance_index(2), 6); + attach.push_depth(); + assert_eq!(attach.module_index(0), 0); + assert_eq!(attach.instance_index(1), 1); + assert_eq!(attach.outer_module_index(1, 0), 2); + attach.pop_depth(); +} + +#[test] +fn malformed_boundary_module_and_instance_identities_are_rejected() { + fn attach() -> Attach { + Attach { + modules: 2, + instances: 2, + providers: Default::default(), + application_module: 1, + depth: 0, + module_seen: false, + instance_seen: false, + source_instances: 0, + source_application: None, + } + } + let missing = wat::parse_str("(component (core module))").unwrap(); + let error = attach() + .parse_component(&mut Component::new(), Parser::new(0), &missing) + .unwrap_err(); + assert!(error.to_string().contains("identity marker")); + + let mut module = Module::new(); + module.section(&CustomSection { + name: Cow::Borrowed(MARKER), + data: Cow::Borrowed(b"1"), + }); + let bytes = module.finish(); + let mut component = wasm_encoder::ComponentBuilder::default(); + component.core_module_raw(None, &bytes); + component.core_module_raw(None, &bytes); + let repeated = component.finish(); + let error = attach() + .parse_component(&mut Component::new(), Parser::new(0), &repeated) + .unwrap_err(); + assert!(error.to_string().contains("identity is repeated")); + + let mut component = wasm_encoder::ComponentBuilder::default(); + let module = component.core_module_raw(None, &bytes); + component.core_instantiate(None, module, []); + component.core_instantiate(None, module, []); + let repeated = component.finish(); + let error = attach() + .parse_component(&mut Component::new(), Parser::new(0), &repeated) + .unwrap_err(); + assert!(error.to_string().contains("instance is repeated")); +} + +#[test] +fn facade_retains_host_imports_and_remaps_private_function_references() { + let source = wat::parse_str( + r#"(module + (type $a (array (mut eqref))) + (import "private" "read" (func $read (param (ref $a) i32) (result eqref))) + (import "host" "write" (func $host (param i32))) + (table 2 funcref) + (elem (i32.const 0) func $read $host) + (global (ref func) ref.func $read) + (memory (export "memory") 1) + (func (export "run") (result i32) i32.const 7) + (export "read" (func $read)) + (export "host" (func $host)))"#, + ) + .unwrap(); + let projected = boundary_module(&source, &["host".into()]).unwrap(); + let mut exports = Vec::new(); + let mut imports = Vec::new(); + let mut functions = Vec::new(); + for payload in Parser::new(0).parse_all(&projected) { + match payload.unwrap() { + Payload::ImportSection(section) => { + for import in section.into_imports() { + let import = import.unwrap(); + imports.push((import.module.to_string(), import.name.to_string())); + } + } + Payload::ExportSection(section) => { + for export in section { + let export = export.unwrap(); + if export.kind == wasmparser::ExternalKind::Func { + exports.push((export.name.to_string(), export.index)); + } + } + } + Payload::ElementSection(section) => { + for element in section { + let wasmparser::ElementItems::Functions(items) = element.unwrap().items else { + panic!("function element expected") + }; + functions.extend(items.into_iter().map(Result::unwrap)); + } + } + _ => {} + } + } + assert_eq!(imports, [("host".into(), "write".into())]); + assert_eq!(functions, [1, 0]); + assert!(exports.contains(&("host".into(), 0))); + assert!(exports.contains(&("read".into(), 1))); + assert!(exports.contains(&("run".into(), 2))); +} diff --git a/crates/psrs-linker/tests/compose.rs b/crates/psrs-linker/tests/compose.rs new file mode 100644 index 00000000..f6eab4e3 --- /dev/null +++ b/crates/psrs-linker/tests/compose.rs @@ -0,0 +1,210 @@ +//! Target-only composition tests: the core module is built with `wasm-encoder` +//! and no compiler IR participates. + +use psrs_linker::{ + BindingRequirement, Boundary, CoreSignature, CoreType, MemoryDemand, Provider, RequirementId, + TargetLinkInput, TargetPolicy, plan, resolve_default_definitions, +}; +use wasm_encoder::{ + CodeSection, EntityType, ExportKind, ExportSection, Function, FunctionSection, ImportSection, + Instruction, MemorySection, MemoryType, Module, TypeSection, ValType, +}; + +fn core_module() -> Vec { + let mut module = Module::new(); + let mut types = TypeSection::new(); + types.ty().function([], [ValType::I32]); + types + .ty() + .function([ValType::F64, ValType::I32, ValType::I32], [ValType::I32]); + module.section(&types); + let mut imports = ImportSection::new(); + imports.import( + psrs_runtime::MODULE_NAME, + psrs_runtime::NUMBER_EXPORT, + EntityType::Function(1), + ); + module.section(&imports); + let mut functions = FunctionSection::new(); + functions.function(0); + module.section(&functions); + let mut memories = MemorySection::new(); + memories.memory(MemoryType { + minimum: 4, + maximum: None, + memory64: false, + shared: false, + page_size_log2: None, + }); + module.section(&memories); + let mut exports = ExportSection::new(); + exports.export("memory", ExportKind::Memory, 0); + exports.export("wasi:cli/run@0.2.12#run", ExportKind::Func, 1); + module.section(&exports); + let mut code = CodeSection::new(); + let mut body = Function::new([]); + body.instruction(&Instruction::I32Const(0)); + body.instruction(&Instruction::End); + code.function(&body); + module.section(&code); + module.finish() +} + +fn input() -> TargetLinkInput { + TargetLinkInput { + requirements: vec![BindingRequirement { + id: RequirementId(0), + origin: "NumberToString".into(), + boundary: Boundary::RawCore { + module: psrs_runtime::MODULE_NAME.into(), + field: psrs_runtime::NUMBER_EXPORT.into(), + }, + expected: Some(CoreSignature { + parameters: vec![CoreType::F64, CoreType::I32, CoreType::I32], + result: Some(CoreType::I32), + }), + provider: Provider::RuntimeOperation { + name: psrs_runtime::NUMBER_TO_STRING_OP.name.into(), + version: psrs_runtime::NUMBER_TO_STRING_OP.version.into(), + }, + }], + units: vec![psrs_linker::runtime::offer(&psrs_runtime::NUMBER_UNIT)], + policy: TargetPolicy::default(), + memory: MemoryDemand { + canonical_scratch: (0, 16), + allocator_state: (16, 24), + base_heap_start: 24, + heap_alignment: 8, + growth_owner: psrs_linker::GENERATED_GROWTH_OWNER.into(), + maximum_pages: None, + }, + } +} + +#[test] +fn composition_attaches_the_library_and_closes_the_private_import() { + let context = resolve_default_definitions().unwrap(); + let link = plan(&context, input()).expect("the plan should be valid"); + let core = core_module(); + let composed = + psrs_linker::compose(&context, &link, &core).expect("composition should succeed"); + wasmparser::Validator::new() + .validate_all(&composed.bytes) + .expect("the composed component should validate"); + assert!( + !composed + .external_world + .iter() + .any(|id| id == psrs_runtime::MODULE_NAME), + "the private runtime import must be closed: {:?}", + composed.external_world + ); +} + +#[test] +fn composition_without_the_artifact_fails_closed() { + let context = resolve_default_definitions().unwrap(); + // A plan with no provider cannot close the core module's runtime import. + let link = plan( + &context, + TargetLinkInput { + requirements: Vec::new(), + units: Vec::new(), + policy: TargetPolicy::default(), + memory: MemoryDemand { + canonical_scratch: (0, 16), + allocator_state: (16, 24), + base_heap_start: 24, + heap_alignment: 8, + growth_owner: psrs_linker::GENERATED_GROWTH_OWNER.into(), + maximum_pages: None, + }, + }, + ) + .unwrap(); + assert!(psrs_linker::compose(&context, &link, &core_module()).is_err()); +} + +#[test] +fn composition_rejects_a_different_resolved_context() { + let context = resolve_default_definitions().unwrap(); + let link = plan(&context, input()).unwrap(); + let other = resolve_default_definitions().unwrap(); + let error = psrs_linker::compose(&other, &link, &core_module()).unwrap_err(); + assert!(error.to_string().contains("context differs")); +} + +#[test] +fn composition_rejects_memory_and_signature_drift_from_the_plan() { + let context = resolve_default_definitions().unwrap(); + let link = plan(&context, input()).unwrap(); + let core = core_module(); + // Structural rewrites preserve module validity while violating the plan. + let text = wasmprinter::print_bytes(&core).unwrap(); + let small = wat::parse_str(text.replace("(memory (;0;) 4)", "(memory (;0;) 1)")).unwrap(); + assert_ne!(small, core); + assert!( + psrs_linker::compose(&context, &link, &small) + .unwrap_err() + .to_string() + .contains("memory") + ); + let changed = + wat::parse_str(text.replace("(param f64 i32 i32)", "(param f32 i32 i32)")).unwrap(); + assert_ne!(changed, core); + assert!( + psrs_linker::compose(&context, &link, &changed) + .unwrap_err() + .to_string() + .contains("signature") + ); +} + +#[test] +fn a_host_resource_return_uses_the_executable_interface_closure() { + let context = resolve_default_definitions().unwrap(); + let mut input = input(); + input.units.clear(); + input.requirements = vec![BindingRequirement { + id: RequirementId(0), + origin: "get-stdout".into(), + boundary: Boundary::ResolvedWit { + interface: "wasi:cli/stdout@0.2.12".into(), + function: "get-stdout".into(), + }, + expected: Some(CoreSignature { + parameters: vec![], + result: Some(CoreType::I32), + }), + provider: Provider::HostInterface { + interface: "wasi:cli/stdout@0.2.12".into(), + }, + }]; + input.policy.permitted_host_interfaces = context.world_imports().to_vec(); + let link = plan(&context, input).unwrap(); + let core = wat::parse_str( + r#"(module + (import "wasi:cli/stdout@0.2.12" "get-stdout" (func (result i32))) + (memory (export "memory") 2) + (func (export "wasi:cli/run@0.2.12#run") (result i32) call 0 drop i32.const 0))"#, + ) + .unwrap(); + let linked = psrs_linker::compose(&context, &link, &core).unwrap(); + assert_eq!(linked.external_world, link.external_world()); +} + +#[test] +fn composition_rejects_unowned_data_and_a_wrong_allocator_boundary() { + let context = resolve_default_definitions().unwrap(); + let link = plan(&context, input()).unwrap(); + let text = wasmprinter::print_bytes(core_module()).unwrap(); + let prefix = text.trim_end().strip_suffix(')').unwrap(); + for segment in [ + r#"(data (i32.const 131072) "x")"#, + r#"(data (i32.const 16) "\00\00\00\00\00\00\00\00")"#, + ] { + let bytes = wat::parse_str(format!("{prefix}{segment})")).unwrap(); + let error = psrs_linker::compose(&context, &link, &bytes).unwrap_err(); + assert!(error.to_string().contains("application"), "{error}"); + } +} diff --git a/crates/psrs-linker/tests/gc_storage.rs b/crates/psrs-linker/tests/gc_storage.rs new file mode 100644 index 00000000..e6d82470 --- /dev/null +++ b/crates/psrs-linker/tests/gc_storage.rs @@ -0,0 +1,294 @@ +//! Checked GC provider selection and application composition, without compiler IR. +use psrs_linker::{ + BindingRequirement, Boundary, CoreTypes, Provider, RequirementId, TargetLinkInput, TargetPolicy, +}; + +fn application(nullable: bool) -> Vec { + application_with_expected(nullable, 78) +} + +fn application_with_expected(nullable: bool, expected: i32) -> Vec { + let array = if nullable { + "(ref null $a)" + } else { + "(ref $a)" + }; + wat::parse_str(format!(r#"(module + (type (struct (field i64))) + (type $a (array (mut eqref))) + (import "psrs:runtime-storage" "array_fill" (func $fill (param i32 eqref) (result (ref $a)))) + (import "psrs:runtime-storage" "array_write" (func $write (param {array} i32 eqref))) + (import "psrs:runtime-storage" "array_read" (func $read (param (ref $a) i32) (result eqref))) + (memory (export "memory") 2) + (func (export "wasi:cli/run@0.2.12#run") (result i32) (local $a (ref null $a)) (local $alias (ref null $a)) + i32.const 1 i32.const 7 ref.i31 call $fill local.tee $a local.set $alias + local.get $a ref.as_non_null i32.const 0 i32.const 78 ref.i31 call $write + local.get $alias ref.as_non_null i32.const 0 call $read + ref.cast (ref i31) i31.get_s i32.const {expected} i32.ne + local.get $a local.get $alias ref.eq i32.eqz i32.or))"#)).unwrap() +} + +fn setup() -> ( + psrs_linker::ResolvedWorldContext, + psrs_linker::CheckedLinkPlan, +) { + setup_in_context(psrs_linker::resolve_default_definitions().unwrap()) +} + +fn setup_in_context( + context: psrs_linker::ResolvedWorldContext, +) -> ( + psrs_linker::ResolvedWorldContext, + psrs_linker::CheckedLinkPlan, +) { + let units = psrs_linker::runtime::package_offers(&psrs_runtime::PSRS_RUNTIME).unwrap(); + let storage = units + .iter() + .find(|unit| unit.id == psrs_runtime::STORAGE_UNIT.id) + .unwrap(); + let requirements = storage + .provided + .iter() + .filter(|op| op.export != "trap") + .enumerate() + .map(|(index, op)| BindingRequirement { + id: RequirementId(index as u32), + origin: op.name.clone(), + boundary: Boundary::RawCore { + module: psrs_runtime::STORAGE_MODULE.into(), + field: op.export.clone(), + }, + expected: Some(op.signature.clone()), + provider: Provider::RuntimeOperation { + name: op.name.clone(), + version: op.version.clone(), + }, + }) + .collect(); + let plan = psrs_linker::plan( + &context, + TargetLinkInput { + requirements, + units, + policy: TargetPolicy::default(), + memory: psrs_linker::MemoryDemand { + canonical_scratch: (0, 16), + allocator_state: (16, 24), + base_heap_start: 24, + heap_alignment: 8, + growth_owner: psrs_linker::GENERATED_GROWTH_OWNER.into(), + maximum_pages: None, + }, + }, + ) + .unwrap(); + (context, plan) +} + +#[test] +fn checked_provider_selection_matches_gc_contracts_and_rejects_application_drift() { + let (context, plan) = setup(); + assert_eq!(plan.artifacts().len(), 1); + let types = CoreTypes::from_module(&application(false)).unwrap(); + let units = psrs_linker::runtime::package_offers(&psrs_runtime::PSRS_RUNTIME).unwrap(); + let storage = units + .iter() + .find(|unit| unit.id == psrs_runtime::STORAGE_UNIT.id) + .unwrap(); + for (index, export) in [(2, "array_fill"), (3, "array_write"), (4, "array_read")] { + let operation = storage + .provided + .iter() + .find(|op| op.export == export) + .unwrap(); + assert_eq!(types.signature(index).unwrap(), operation.signature); + } + let error = psrs_linker::compose(&context, &plan, &application(true)).unwrap_err(); + assert!( + error.to_string().contains("application import signature"), + "{error}" + ); + let error = match psrs_linker::assemble_core(&plan, &application(true)) { + Ok(_) => panic!("nullable array drift must fail before direct assembly"), + Err(error) => error, + }; + assert!( + error.to_string().contains("application import signature"), + "{error}" + ); +} + +#[test] +fn checked_storage_provider_closes_gc_imports() { + let (context, plan) = setup(); + let app = application(false); + let linked = psrs_linker::compose(&context, &plan, &app).unwrap(); + let units = psrs_linker::runtime::package_offers(&psrs_runtime::PSRS_RUNTIME).unwrap(); + let provider = &units + .iter() + .find(|unit| unit.id == psrs_runtime::STORAGE_UNIT.id) + .unwrap() + .artifact + .bytes; + let mut originals = 0; + for payload in wasmparser::Parser::new(0).parse_all(&linked.bytes) { + if let wasmparser::Payload::ModuleSection { + unchecked_range, .. + } = payload.unwrap() + { + let bytes = &linked.bytes[unchecked_range]; + if bytes == app || bytes == provider { + originals += 1; + } + } + } + assert_eq!( + originals, 2, + "the component must retain both original core modules" + ); + assert!( + !linked + .external_world + .iter() + .any(|name| name == psrs_runtime::STORAGE_MODULE) + ); + let wrong = + psrs_linker::compose(&context, &plan, &application_with_expected(false, 79)).unwrap(); + execute_commands("world", [(linked.bytes, true), (wrong.bytes, false)]); +} + +fn direct_command(plan: &psrs_linker::CheckedLinkPlan, app: &[u8]) -> Vec { + use wasm_encoder::{ComponentExportKind, ComponentValType, ExportKind}; + let assembled = psrs_linker::assemble_core(plan, app).unwrap(); + let application = assembled.application_instance(); + let mut component = assembled.into_encoder(); + let run = component.core_alias_export( + None, + application, + "wasi:cli/run@0.2.12#run", + ExportKind::Func, + ); + let (result, ty) = component.type_defined(None); + ty.result(None, None); + let (signature, mut ty) = component.type_function(None); + ty.params(std::iter::empty::<(&str, ComponentValType)>()); + ty.result(Some(ComponentValType::Type(result))); + let run = component.lift_func(None, run, signature, []); + let instance = component.instance_count(); + let mut bytes = component.finish(); + use wasm_encoder::ComponentSection; + let mut instances = wasm_encoder::ComponentInstanceSection::new(); + instances.export_items([("run", ComponentExportKind::Func, run)]); + instances.append_to_component(&mut bytes); + let mut exports = wasm_encoder::ComponentExportSection::new(); + exports.export( + "wasi:cli/run@0.2.12", + ComponentExportKind::Instance, + instance, + None, + ); + exports.append_to_component(&mut bytes); + wasmparser::Validator::new().validate_all(&bytes).unwrap(); + bytes +} + +#[test] +fn direct_core_instance_assembly_executes_gc_storage_inside_a_component() { + let (_, plan) = setup(); + execute_commands( + "direct", + [ + (direct_command(&plan, &application(false)), true), + ( + direct_command(&plan, &application_with_expected(false, 79)), + false, + ), + ], + ); +} + +fn execute_commands(label: &str, cases: [(Vec, bool); 2]) { + let version = std::process::Command::new("wasmtime") + .arg("--version") + .output(); + if !version.is_ok_and(|output| output.status.success()) { + assert_ne!( + std::env::var("PSRS_REQUIRE_WASMTIME").as_deref(), + Ok("1"), + "required Wasmtime unavailable" + ); + eprintln!("skipping GC component execution: Wasmtime unavailable"); + return; + } + let path = std::env::temp_dir().join(format!( + "psrs-gc-component-{label}-{}.wasm", + std::process::id() + )); + for (bytes, success) in cases { + std::fs::write(&path, bytes).unwrap(); + let output = std::process::Command::new("wasmtime") + .arg("run") + .arg(&path) + .output() + .unwrap(); + assert_eq!( + output.status.success(), + success, + "expected success={success}: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + std::fs::remove_file(&path).unwrap(); +} + +#[test] +fn canonical_boundary_uses_the_selected_world_and_public_parameters() { + let mut resolve = wit_parser::Resolve::default(); + let package = resolve + .push_str( + "counter.wit", + "package test:counter; world counter { export check: func(offset: u32) -> u32; }", + ) + .unwrap(); + let world = resolve.packages[package].worlds["counter"]; + let context = psrs_linker::ResolvedWorldContext::from_resolve(resolve, world); + let (context, plan) = setup_in_context(context); + let text = r#"(module + (type $a (array (mut eqref))) + (import "psrs:runtime-storage" "array_fill" (func $fill (param i32 eqref) (result (ref $a)))) + (import "psrs:runtime-storage" "array_write" (func $write (param (ref $a) i32 eqref))) + (import "psrs:runtime-storage" "array_read" (func $read (param (ref $a) i32) (result eqref))) + (memory (export "memory") 2) + (func (export "check") (param $offset i32) (result i32) (local $a (ref null $a)) + i32.const 1 ref.null eq call $fill local.set $a + local.get $a ref.as_non_null i32.const 0 local.get $offset i32.const 1 i32.add ref.i31 call $write + local.get $a ref.as_non_null i32.const 0 call $read ref.cast (ref i31) i31.get_s))"#; + let app = wat::parse_str(text).unwrap(); + let linked = psrs_linker::compose(&context, &plan, &app).unwrap(); + assert!(linked.external_world.is_empty()); + let decoded = wit_component::decode(&linked.bytes).unwrap(); + let wit_component::DecodedWasm::Component(resolve, world) = decoded else { + panic!("expected executable component") + }; + let wit_parser::WorldItem::Function(function) = + &resolve.worlds[world].exports[&wit_parser::WorldKey::Name("check".into())] + else { + panic!("expected check function") + }; + assert_eq!( + function + .params + .iter() + .map(|parameter| (parameter.name.as_str(), parameter.ty)) + .collect::>(), + vec![("offset", wit_parser::Type::U32)] + ); + assert_eq!(function.result, Some(wit_parser::Type::U32)); + let wrong = text + .replace("(param $offset i32)", "(param $offset i64)") + .replace("local.get $offset", "local.get $offset i32.wrap_i64"); + let wrong = wat::parse_str(wrong).unwrap(); + wasmparser::Validator::new().validate_all(&wrong).unwrap(); + let error = psrs_linker::compose(&context, &plan, &wrong).unwrap_err(); + assert!(error.to_string().contains("type mismatch"), "{error}"); +} diff --git a/crates/psrs-linker/tests/graph.rs b/crates/psrs-linker/tests/graph.rs new file mode 100644 index 00000000..c05c08b5 --- /dev/null +++ b/crates/psrs-linker/tests/graph.rs @@ -0,0 +1,355 @@ +//! Provider-graph closure. These tests build target records only. + +use psrs_linker::{ + ArtifactContract, ArtifactKind, ArtifactReference, BindingRequirement, Boundary, CoreSignature, + CoreType, DeclaredExport, ExportKind, InitializationContract, InitializationStep, MemoryDemand, + Provider, ProviderEdge, RequiredOperation, RequirementId, RuntimeUnitOffer, TargetLinkInput, + TargetPolicy, plan, resolve_default_definitions, +}; +use wasm_encoder::{ + CodeSection, ExportKind as EncodedExport, ExportSection, Function, FunctionSection, + Instruction, Module, TypeSection, ValType, +}; + +fn memory() -> MemoryDemand { + MemoryDemand { + canonical_scratch: (0, 16), + allocator_state: (16, 24), + base_heap_start: 24, + heap_alignment: 8, + growth_owner: psrs_linker::GENERATED_GROWTH_OWNER.into(), + maximum_pages: None, + } +} + +fn input(requirements: Vec, units: Vec) -> TargetLinkInput { + let permitted = resolve_default_definitions() + .map(|context| context.world_imports().to_vec()) + .unwrap_or_default(); + TargetLinkInput { + requirements, + units, + policy: TargetPolicy { + permitted_host_interfaces: permitted, + }, + memory: memory(), + } +} + +fn signature() -> CoreSignature { + CoreSignature { + parameters: Vec::new(), + result: Some(CoreType::I32), + } +} + +fn dependency_bytes() -> Vec { + let mut module = Module::new(); + let mut types = TypeSection::new(); + types.ty().function([], [ValType::I32]); + module.section(&types); + let mut functions = FunctionSection::new(); + functions.function(0); + module.section(&functions); + let mut exports = ExportSection::new(); + exports.export("dep", EncodedExport::Func, 0); + module.section(&exports); + let mut code = CodeSection::new(); + let mut body = Function::new([]); + body.instruction(&Instruction::I32Const(0)); + body.instruction(&Instruction::End); + code.function(&body); + module.section(&code); + module.finish() +} + +fn dependency_unit(id: &str, operation: &str) -> RuntimeUnitOffer { + let bytes = dependency_bytes(); + let digest = psrs_linker::sha256_hex(&bytes); + RuntimeUnitOffer { + id: id.into(), + semantic_contract_version: "1".into(), + provided: vec![psrs_linker::OfferedOperation { + name: operation.into(), + version: "1".into(), + export: "dep".into(), + signature: signature(), + }], + required: Vec::new(), + artifact: ArtifactReference { + contract: ArtifactContract { + id: format!("{id}-artifact"), + kind: ArtifactKind::CoreModule, + module_name: id.into(), + sha256: digest, + provenance: "test".into(), + required_features: Vec::new(), + imports: Vec::new(), + exports: vec![DeclaredExport { + name: "dep".into(), + kind: ExportKind::Func, + signature: Some(signature()), + }], + tables: Vec::new(), + elements: Vec::new(), + globals: Vec::new(), + storage: None, + initialization: InitializationContract { + start_forbidden: true, + data_range: (0, 0), + }, + instantiate_after_shims: false, + }, + bytes, + }, + state_owner: Some(id.into()), + grows_memory: false, + } +} + +fn number_unit() -> RuntimeUnitOffer { + psrs_linker::runtime::offer(&psrs_runtime::NUMBER_UNIT) +} + +fn formatter() -> BindingRequirement { + BindingRequirement { + id: RequirementId(0), + origin: "NumberToString".into(), + boundary: Boundary::RawCore { + module: psrs_runtime::MODULE_NAME.into(), + field: psrs_runtime::NUMBER_EXPORT.into(), + }, + expected: Some(CoreSignature { + parameters: vec![CoreType::F64, CoreType::I32, CoreType::I32], + result: Some(CoreType::I32), + }), + provider: Provider::RuntimeOperation { + name: psrs_runtime::NUMBER_TO_STRING_OP.name.into(), + version: psrs_runtime::NUMBER_TO_STRING_OP.version.into(), + }, + } +} + +fn require(unit: &mut RuntimeUnitOffer, operation: &str, expected: CoreSignature) { + unit.required.push(RequiredOperation { + name: operation.into(), + version: "1".into(), + signature: expected, + }); +} + +#[test] +fn a_required_operation_selects_its_provider_before_the_root_unit() { + let context = resolve_default_definitions().unwrap(); + let mut root = number_unit(); + require(&mut root, "psrs:dep/value", signature()); + let link = plan( + &context, + input( + vec![formatter()], + vec![root, dependency_unit("psrs:dep", "psrs:dep/value")], + ), + ) + .expect("the dependency should close"); + assert_eq!( + link.units() + .iter() + .map(|unit| unit.id.as_str()) + .collect::>(), + ["psrs:dep", "psrs:runtime/number"] + ); + assert!(link.edges().iter().any(|edge| matches!( + edge, + ProviderEdge::Dependency { unit, .. } if unit == "psrs:dep" + ))); + assert_eq!( + link.initialization(), + &[ + InitializationStep::Instantiate { + unit: "psrs:dep".into() + }, + InitializationStep::Instantiate { + unit: "psrs:runtime/number".into() + }, + InitializationStep::ResolveShims, + ] + ); + let report = link.report(); + assert_eq!(report.memory_id, psrs_linker::APPLICATION_MEMORY); + assert_eq!(report.growth_owner, psrs_linker::GENERATED_GROWTH_OWNER); + assert_eq!(report.digests.len(), 2); +} + +#[test] +fn an_unreached_unit_stays_out_of_the_plan() { + let context = resolve_default_definitions().unwrap(); + let link = plan( + &context, + input( + vec![formatter()], + vec![number_unit(), dependency_unit("psrs:dep", "psrs:dep/value")], + ), + ) + .unwrap(); + assert_eq!(link.units().len(), 1); + assert_eq!(link.artifacts().len(), 1); +} + +#[test] +fn a_missing_provider_is_rejected() { + let context = resolve_default_definitions().unwrap(); + let error = plan(&context, input(vec![formatter()], Vec::new())) + .unwrap_err() + .to_string(); + assert!(error.contains("no runtime unit provides"), "{error}"); + assert!(!error.contains("digest"), "{error}"); +} + +#[test] +fn a_missing_dependency_is_rejected() { + let context = resolve_default_definitions().unwrap(); + let mut root = number_unit(); + require(&mut root, "psrs:missing/op", signature()); + let error = plan(&context, input(vec![formatter()], vec![root])) + .unwrap_err() + .to_string(); + assert!( + error.contains("no runtime unit provides `psrs:missing/op@1`"), + "{error}" + ); +} + +#[test] +fn ambiguous_providers_are_not_resolved_by_trying_one() { + let context = resolve_default_definitions().unwrap(); + let mut first = dependency_unit("psrs:left", "psrs:dep/value"); + let mut second = dependency_unit("psrs:right", "psrs:dep/value"); + first.artifact.contract.module_name = "psrs:dep".into(); + second.artifact.contract.module_name = "psrs:dep".into(); + let requirement = BindingRequirement { + id: RequirementId(0), + origin: "dep".into(), + boundary: Boundary::RawCore { + module: "psrs:dep".into(), + field: "dep".into(), + }, + expected: Some(signature()), + provider: Provider::RuntimeOperation { + name: "psrs:dep/value".into(), + version: "1".into(), + }, + }; + let error = plan(&context, input(vec![requirement], vec![first, second])) + .unwrap_err() + .to_string(); + assert!(error.contains("ambiguous providers"), "{error}"); + assert!(!error.contains("digest"), "{error}"); +} + +#[test] +fn a_component_variant_is_not_substituted_for_a_raw_operation() { + let context = resolve_default_definitions().unwrap(); + let mut unit = number_unit(); + unit.artifact.contract.kind = ArtifactKind::Component; + let error = plan(&context, input(vec![formatter()], vec![unit])) + .unwrap_err() + .to_string(); + assert!(error.contains("incompatible artifact kind"), "{error}"); + assert!(!error.contains("digest"), "{error}"); +} + +#[test] +fn the_core_provider_remains_selected_beside_a_component_advertisement() { + let context = resolve_default_definitions().unwrap(); + let mut component = number_unit(); + component.id = "psrs:runtime/number-component".into(); + component.artifact.contract.id = "psrs:runtime-number-component".into(); + component.artifact.contract.kind = ArtifactKind::Component; + component.state_owner = Some("psrs:runtime/number-component".into()); + let link = plan( + &context, + input(vec![formatter()], vec![number_unit(), component]), + ) + .unwrap(); + assert_eq!(link.units().len(), 1); + assert_eq!(link.units()[0].kind, ArtifactKind::CoreModule); +} + +#[test] +fn a_provider_cycle_is_rejected() { + let context = resolve_default_definitions().unwrap(); + let mut left = dependency_unit("psrs:left", "psrs:left/value"); + let mut right = dependency_unit("psrs:right", "psrs:right/value"); + require(&mut left, "psrs:right/value", signature()); + require(&mut right, "psrs:left/value", signature()); + let requirement = BindingRequirement { + id: RequirementId(0), + origin: "left".into(), + boundary: Boundary::RawCore { + module: "psrs:left".into(), + field: "dep".into(), + }, + expected: Some(signature()), + provider: Provider::RuntimeOperation { + name: "psrs:left/value".into(), + version: "1".into(), + }, + }; + let error = plan(&context, input(vec![requirement], vec![left, right])) + .unwrap_err() + .to_string(); + assert!(error.contains("unsupported provider cycle"), "{error}"); +} + +#[test] +fn duplicate_state_owners_are_rejected() { + let context = resolve_default_definitions().unwrap(); + let mut root = number_unit(); + require(&mut root, "psrs:dep/value", signature()); + let mut dependency = dependency_unit("psrs:dep", "psrs:dep/value"); + dependency.state_owner = root.state_owner.clone(); + let error = plan(&context, input(vec![formatter()], vec![root, dependency])) + .unwrap_err() + .to_string(); + assert!(error.contains("duplicate state owner"), "{error}"); +} + +#[test] +fn the_demand_may_name_a_runtime_unit_as_the_growth_owner() { + let context = resolve_default_definitions().unwrap(); + let allocator = psrs_linker::runtime::offer(&psrs_runtime::ALLOCATOR_UNIT); + let requirement = BindingRequirement { + id: RequirementId(0), + origin: "allocator".into(), + boundary: Boundary::RawCore { + module: psrs_runtime::ALLOCATOR_MODULE.into(), + field: psrs_runtime::REALLOC_EXPORT.into(), + }, + expected: Some(CoreSignature { + parameters: vec![CoreType::I32; 4], + result: Some(CoreType::I32), + }), + provider: Provider::RuntimeOperation { + name: psrs_runtime::ALLOCATOR_REALLOC_OP.name.into(), + version: psrs_runtime::ALLOCATOR_REALLOC_OP.version.into(), + }, + }; + let mut input = input(vec![requirement], vec![allocator]); + input.memory.growth_owner = psrs_runtime::ALLOCATOR_UNIT.id.into(); + let link = plan(&context, input).expect("the named unit should own growth"); + assert_eq!(link.growth_owner(), psrs_runtime::ALLOCATOR_UNIT.id); + assert!(link.memory().heap_getter.is_some()); +} + +#[test] +fn a_second_growth_owner_is_rejected() { + let context = resolve_default_definitions().unwrap(); + let mut root = number_unit(); + require(&mut root, "psrs:dep/value", signature()); + let mut dependency = dependency_unit("psrs:dep", "psrs:dep/value"); + dependency.grows_memory = true; + let error = plan(&context, input(vec![formatter()], vec![root, dependency])) + .unwrap_err() + .to_string(); + assert!(error.contains("conflicting growth owners"), "{error}"); +} diff --git a/crates/psrs-linker/tests/guest/fixtures.rs b/crates/psrs-linker/tests/guest/fixtures.rs new file mode 100644 index 00000000..d186cb79 --- /dev/null +++ b/crates/psrs-linker/tests/guest/fixtures.rs @@ -0,0 +1,173 @@ +use wit_parser::Resolve; +pub const INTERFACE: &str = "test:lib/api@1.0.0"; + +const WIT: &str = r#" +package test:lib@1.0.0; +interface api { + echo: func(value: string) -> string; + bytes: func() -> result, u32>; + posts: func() -> u32; + drops: func() -> u32; + resource token { + constructor(value: u32); + get: func() -> u32; + } +} +world provider { export api; } +world application { import api; export wasi:cli/run@0.2.12; } +"#; + +fn encode(wit: &str, world: &str, wat: &str) -> Vec { + let mut resolve = Resolve::default(); + for source in psrs_runtime::WASI_WIT { + resolve.push_str(source.path, source.contents).unwrap(); + } + let package = resolve.push_str("fixture.wit", wit).unwrap(); + let world = resolve.packages[package].worlds[world]; + let mut bytes = wat::parse_str(wat).unwrap(); + wit_component::embed_component_metadata( + &mut bytes, + &resolve, + world, + wit_component::StringEncoding::UTF8, + ) + .unwrap(); + wit_component::ComponentEncoder::default() + .module(&bytes) + .unwrap() + .validate(true) + .encode() + .unwrap() +} + +pub fn components() -> (Vec, Vec) { + ( + encode(WIT, "application", APPLICATION), + encode(WIT, "provider", PROVIDER), + ) +} + +pub fn incompatible_provider() -> Vec { + // A valid provider with the same name but a different checked echo shape. + let wit = WIT.replace( + "echo: func(value: string) -> string;", + "echo: func(value: u32) -> u32;", + ); + let wat = PROVIDER.replace("(param $p i32) (param $n i32) (result i32)\n i32.const 0 local.get $p i32.store\n i32.const 4 local.get $n i32.store\n i32.const 0", "(param $p i32) (result i32) local.get $p"); + let wat = wat.replace( + "(func (export \"cabi_post_test:lib/api@1.0.0#echo\") (param i32) call $post)", + "", + ); + encode(&wit, "provider", &wat) +} + +const PROVIDER: &str = r#" +(module + (import "[export]test:lib/api@1.0.0" "[resource-new]token" (func $new (param i32) (result i32))) + (memory (export "memory") 2) + (global $posts (mut i32) (i32.const 0)) + (global $drops (mut i32) (i32.const 0)) + (func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) i32.const 4096) + (func (export "test:lib/api@1.0.0#echo") (param $p i32) (param $n i32) (result i32) + i32.const 0 local.get $p i32.store + i32.const 4 local.get $n i32.store + i32.const 0) + (func (export "test:lib/api@1.0.0#bytes") (result i32) + i32.const 256 i32.const 104 i32.store8 + i32.const 257 i32.const 105 i32.store8 + i32.const 258 i32.const 33 i32.store8 + i32.const 0 i32.const 0 i32.store + i32.const 4 i32.const 256 i32.store + i32.const 8 i32.const 3 i32.store + i32.const 0) + (func $post + global.get $posts i32.const 1 i32.add global.set $posts + i32.const 4096 i32.const 0 i32.const 32 memory.fill + i32.const 256 i32.const 0 i32.const 3 memory.fill) + (func (export "cabi_post_test:lib/api@1.0.0#echo") (param i32) call $post) + (func (export "cabi_post_test:lib/api@1.0.0#bytes") (param i32) call $post) + (func (export "test:lib/api@1.0.0#posts") (result i32) global.get $posts) + (func (export "test:lib/api@1.0.0#drops") (result i32) global.get $drops) + (func (export "test:lib/api@1.0.0#[constructor]token") (param i32) (result i32) local.get 0 call $new) + (func (export "test:lib/api@1.0.0#[method]token.get") (param i32) (result i32) local.get 0) + (func (export "test:lib/api@1.0.0#[dtor]token") (param i32) + local.get 0 i32.const 39 i32.ne if unreachable end + global.get $drops i32.const 1 i32.add global.set $drops)) +"#; + +const APPLICATION: &str = r#" +(module + (import "test:lib/api@1.0.0" "echo" (func $echo (param i32 i32 i32))) + (import "test:lib/api@1.0.0" "bytes" (func $bytes (param i32))) + (import "test:lib/api@1.0.0" "posts" (func $posts (result i32))) + (import "test:lib/api@1.0.0" "drops" (func $drops (result i32))) + (import "test:lib/api@1.0.0" "[constructor]token" (func $new (param i32) (result i32))) + (import "test:lib/api@1.0.0" "[method]token.get" (func $get (param i32) (result i32))) + (import "test:lib/api@1.0.0" "[resource-drop]token" (func $drop (param i32))) + (memory (export "memory") 2) + (data (i32.const 1024) "firsté") + (data (i32.const 2048) "second🙂") + (global $bump (mut i32) (i32.const 4096)) + (func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) (local $p i32) + global.get $bump local.tee $p local.get 3 i32.add global.set $bump local.get $p) + (func $eq (param $a i32) (param $b i32) (param $n i32) (local $i i32) + block loop + local.get $i local.get $n i32.eq br_if 1 + local.get $a local.get $i i32.add i32.load8_u + local.get $b local.get $i i32.add i32.load8_u i32.ne if unreachable end + local.get $i i32.const 1 i32.add local.set $i br 0 + end end) + (func (export "wasi:cli/run@0.2.12#run") (result i32) (local $token i32) + i32.const 1024 i32.const 7 i32.const 64 call $echo + i32.const 2048 i32.const 10 i32.const 80 call $echo + i32.const 68 i32.load i32.const 7 i32.ne if unreachable end + i32.const 84 i32.load i32.const 10 i32.ne if unreachable end + i32.const 64 i32.load i32.const 1024 i32.const 7 call $eq + i32.const 80 i32.load i32.const 2048 i32.const 10 call $eq + i32.const 128 call $bytes + i32.const 128 i32.load if unreachable end + i32.const 136 i32.load i32.const 3 i32.ne if unreachable end + i32.const 132 i32.load i32.load8_u i32.const 104 i32.ne if unreachable end + i32.const 132 i32.load i32.load8_u offset=2 i32.const 33 i32.ne if unreachable end + call $posts i32.const 3 i32.ne if unreachable end + i32.const 39 call $new local.tee $token call $get i32.const 39 i32.ne if unreachable end + local.get $token call $drop + call $drops i32.const 1 i32.ne if unreachable end + i32.const 0)) +"#; + +pub fn forwarding_component(import: &str, export: &str) -> Vec { + wat::parse_str(format!( + r#"(component + (type $api (instance (export "value" (func (result u32))))) + (import "{import}" (instance $value (type $api))) + (export "{export}" (instance $value)))"# + )) + .unwrap() +} + +pub fn constant_component(export: &str) -> Vec { + wat::parse_str(format!( + r#"(component + (core module $m (func (export "value") (result i32) i32.const 42)) + (core instance $m (instantiate $m)) + (func $value (result u32) (canon lift (core func $m "value"))) + (instance $api (export "value" (func $value))) + (export "{export}" (instance $api)))"# + )) + .unwrap() +} + +pub fn definition_only_package() -> Vec { + let mut resolve = Resolve::default(); + let package = resolve + .push_str( + "definitions.wit", + r#" + package test:lib@1.0.0; + interface api { value: func() -> u32; } + "#, + ) + .unwrap(); + wit_component::encode(&resolve, package).unwrap() +} diff --git a/crates/psrs-linker/tests/guest/mod.rs b/crates/psrs-linker/tests/guest/mod.rs new file mode 100644 index 00000000..4602006f --- /dev/null +++ b/crates/psrs-linker/tests/guest/mod.rs @@ -0,0 +1,210 @@ +mod fixtures; +use psrs_linker::guest::{ + ComponentLinkInput, ComponentReference, GuestBinding, compose_component, plan_components, +}; +use psrs_linker::{TargetPolicy, sha256_hex}; + +fn reference(id: &str, bytes: Vec) -> ComponentReference { + ComponentReference { + id: id.into(), + sha256: sha256_hex(&bytes), + bytes, + } +} + +fn fixture_input() -> ComponentLinkInput { + let (application, provider) = fixtures::components(); + ComponentLinkInput { + application: reference("application", application), + guests: vec![reference("provider", provider)], + bindings: vec![GuestBinding { + interface: fixtures::INTERFACE.into(), + artifact: "provider".into(), + export: fixtures::INTERFACE.into(), + }], + policy: TargetPolicy::default(), + features: wasmparser::WasmFeatures::default(), + } +} + +#[test] +fn strings_lists_results_post_return_and_resource_lifetimes_execute() { + let input = fixture_input(); + let application = input.application.bytes.clone(); + let plan = plan_components(input).expect("typed guest interfaces should connect"); + assert!(plan.external_world().is_empty()); + let linked = compose_component(&plan, &application).unwrap(); + let path = std::env::temp_dir().join(format!("psrs-guest-{}.wasm", std::process::id())); + std::fs::write(&path, &linked.bytes).unwrap(); + let output = std::process::Command::new("wasmtime") + .arg("run") + .arg(&path) + .output() + .expect("Wasmtime is required for guest composition acceptance"); + std::fs::remove_file(&path).unwrap(); + assert!(output.status.success(), "{output:?}"); + assert!(output.stdout.is_empty()); + assert!(output.stderr.is_empty(), "{output:?}"); +} + +#[test] +fn stale_guest_and_changed_application_are_rejected() { + let mut input = fixture_input(); + input.guests[0].sha256 = "0".repeat(64); + assert!( + plan_components(input) + .unwrap_err() + .to_string() + .contains("digest") + ); + let input = fixture_input(); + let plan = plan_components(input).unwrap(); + assert!(compose_component(&plan, b"other application").is_err()); +} + +#[test] +fn missing_guest_and_version_drift_never_fall_back_to_the_host() { + let mut input = fixture_input(); + input.guests.clear(); + input + .policy + .permitted_host_interfaces + .push(fixtures::INTERFACE.into()); + assert!(plan_components(input).is_err()); + let mut input = fixture_input(); + input.bindings[0].export = "test:lib/api@2.0.0".into(); + assert!(plan_components(input).is_err()); +} + +#[test] +fn an_incompatible_guest_interface_is_rejected() { + let mut input = fixture_input(); + let bytes = fixtures::incompatible_provider(); + input.guests[0] = reference("provider", bytes); + assert!(plan_components(input).is_err()); +} + +const API: &str = "test:graph/api@1.0.0"; +const DEP: &str = "test:graph/dependency@1.0.0"; +const HOST: &str = "test:graph/host@1.0.0"; + +fn graph_input() -> ComponentLinkInput { + ComponentLinkInput { + application: reference("application", fixtures::forwarding_component(API, API)), + guests: vec![reference( + "provider", + fixtures::forwarding_component(HOST, API), + )], + bindings: vec![GuestBinding { + interface: API.into(), + artifact: "provider".into(), + export: API.into(), + }], + policy: TargetPolicy { + permitted_host_interfaces: vec![HOST.into()], + }, + features: wasmparser::WasmFeatures::default(), + } +} + +#[test] +fn only_used_guest_dependencies_contribute_exact_residual_host_imports() { + let mut input = graph_input(); + // Unreachable artifacts are neither validated nor included in the closure. + input + .guests + .push(reference("unused", b"not an executable component".to_vec())); + let plan = plan_components(input).unwrap(); + assert_eq!(plan.external_world(), &[HOST]); + assert_eq!(plan.component_artifacts().len(), 2); + let mut input = graph_input(); + input.policy.permitted_host_interfaces.clear(); + assert!( + plan_components(input) + .unwrap_err() + .to_string() + .contains(HOST) + ); +} + +#[test] +fn transitive_guest_dependencies_close_without_host_fallback() { + let mut input = graph_input(); + input.guests[0] = reference("provider", fixtures::forwarding_component(DEP, API)); + input + .guests + .push(reference("dependency", fixtures::constant_component(DEP))); + input.bindings.push(GuestBinding { + interface: DEP.into(), + artifact: "dependency".into(), + export: DEP.into(), + }); + let plan = plan_components(input).unwrap(); + assert!(plan.external_world().is_empty()); + assert_eq!(plan.component_artifacts().len(), 3); +} + +#[test] +fn provider_conflicts_missing_exports_and_dependency_cycles_fail_closed() { + let mut input = graph_input(); + input.bindings.push(input.bindings[0].clone()); + assert!( + plan_components(input) + .unwrap_err() + .to_string() + .contains("conflicting") + ); + let mut input = graph_input(); + input.guests[0] = reference("provider", fixtures::constant_component(DEP)); + assert!( + plan_components(input) + .unwrap_err() + .to_string() + .contains("omits selected export") + ); + let mut input = graph_input(); + input.guests[0] = reference("provider", fixtures::forwarding_component(DEP, API)); + input.guests.push(reference( + "dependency", + fixtures::forwarding_component(API, DEP), + )); + input.bindings.push(GuestBinding { + interface: DEP.into(), + artifact: "dependency".into(), + export: DEP.into(), + }); + assert!(plan_components(input).is_err()); +} + +#[test] +fn component_features_are_validated_against_the_selected_profile() { + let mut input = graph_input(); + input + .features + .remove(wasmparser::WasmFeatures::COMPONENT_MODEL); + assert!(plan_components(input).is_err()); +} + +#[test] +fn definition_bytes_cannot_supply_an_executable_interface() { + let mut input = fixture_input(); + input.guests[0] = reference("provider", fixtures::definition_only_package()); + assert!(plan_components(input).is_err()); +} + +#[test] +fn guest_executable_initialization_requires_an_explicit_contract() { + let mut input = fixture_input(); + let provider = wat::parse_str(format!( + r#"(component + (core module $m (func $start) (start $start)) + (core instance $m (instantiate $m)) + (instance $api) + (export "{}" (instance $api)))"#, + fixtures::INTERFACE + )) + .unwrap(); + input.guests = vec![reference("provider", provider)]; + let error = plan_components(input).unwrap_err().to_string(); + assert!(error.contains("initialization"), "{error}"); +} diff --git a/crates/psrs-linker/tests/heap_getter.rs b/crates/psrs-linker/tests/heap_getter.rs new file mode 100644 index 00000000..1cd55d40 --- /dev/null +++ b/crates/psrs-linker/tests/heap_getter.rs @@ -0,0 +1,170 @@ +//! Cross-module heap configuration and rejection of executable getter bodies. +use psrs_linker::{ + BindingRequirement, Boundary, CoreSignature, CoreType, MemoryDemand, Provider, RequirementId, + TargetLinkInput, TargetPolicy, plan, resolve_default_definitions, +}; + +fn input(heap_start: u32) -> TargetLinkInput { + TargetLinkInput { + requirements: vec![BindingRequirement { + id: RequirementId(0), + origin: "allocator".into(), + boundary: Boundary::RawCore { + module: psrs_runtime::ALLOCATOR_MODULE.into(), + field: psrs_runtime::REALLOC_EXPORT.into(), + }, + expected: Some(CoreSignature { + parameters: vec![CoreType::I32; 4], + result: Some(CoreType::I32), + }), + provider: Provider::RuntimeOperation { + name: psrs_runtime::ALLOCATOR_REALLOC_OP.name.into(), + version: psrs_runtime::ALLOCATOR_REALLOC_OP.version.into(), + }, + }], + units: vec![psrs_linker::runtime::offer(&psrs_runtime::ALLOCATOR_UNIT)], + policy: TargetPolicy::default(), + memory: MemoryDemand { + canonical_scratch: (0, 16384), + allocator_state: (16384, 16384), + base_heap_start: heap_start, + heap_alignment: 8, + growth_owner: psrs_runtime::ALLOCATOR_UNIT.id.into(), + maximum_pages: None, + }, + } +} + +fn application(pages: u64, getter: &str, getter_export: &str) -> Vec { + wat::parse_str(format!( + r#"(module + (import "psrs:allocator" "cabi_realloc" + (func $alloc (param i32 i32 i32 i32) (result i32))) + (memory (export "memory") {pages}) + (func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) + local.get 0 local.get 1 local.get 2 local.get 3 call $alloc) + (func (export "wasi:cli/run@0.2.12#run") (result i32) i32.const 0) + (func (export "{getter_export}") {getter}))"# + )) + .unwrap() +} + +#[test] +fn one_pinned_allocator_accepts_different_checked_heap_boundaries() { + let context = resolve_default_definitions().unwrap(); + for heap in [65536, 262144] { + let link = plan(&context, input(heap)).unwrap(); + assert_eq!(link.memory().heap_start, heap); + let core = application( + link.memory().minimum_pages, + &format!("(result i32) i32.const {heap}"), + psrs_runtime::HEAP_BOUNDARY_IMPORT, + ); + let component = psrs_linker::compose(&context, &link, &core).unwrap(); + assert!(component.external_world.is_empty()); + } +} + +#[test] +fn getter_must_be_a_local_constant_with_the_checked_signature_and_value() { + let context = resolve_default_definitions().unwrap(); + let link = plan(&context, input(65536)).unwrap(); + for getter in [ + "(result i32) i32.const 65544", + "(param i32) (result i32) i32.const 65536", + "(result i64) i64.const 65536", + "(result i32) (local i32) i32.const 65536", + "(result i32) memory.size drop i32.const 65536", + "(result i32) i32.const 1 i32.const 2 i32.store i32.const 65536", + "(result i32) i32.const 0 i32.const 0 i32.const 8 i32.const 8 call $alloc drop i32.const 65536", + ] { + let core = application( + link.memory().minimum_pages, + getter, + psrs_runtime::HEAP_BOUNDARY_IMPORT, + ); + let error = psrs_linker::compose(&context, &link, &core) + .unwrap_err() + .to_string(); + assert!(error.contains("heap getter"), "{getter}: {error}"); + } + let core = application( + link.memory().minimum_pages, + "(result i32) i32.const 65536", + "other", + ); + assert!(psrs_linker::compose(&context, &link, &core).is_err()); +} + +#[test] +fn allocator_import_signature_drift_is_rejected() { + let mut contract = psrs_linker::runtime::contract(&psrs_runtime::ALLOCATOR_RUNTIME); + let import = contract + .imports + .iter_mut() + .find(|import| matches!(import.kind, psrs_linker::ImportKind::Function(_))) + .unwrap(); + import.kind = psrs_linker::ImportKind::Function(CoreSignature { + parameters: vec![CoreType::I32], + result: Some(CoreType::I32), + }); + assert!( + psrs_linker::verify_artifact(&contract, psrs_runtime::ALLOCATOR_RUNTIME.bytes).is_err() + ); +} + +#[test] +fn allocator_executes_with_each_application_supplied_boundary() { + use std::process::Command; + if Command::new("wasmtime").arg("--version").output().is_err() { + assert_ne!(std::env::var("PSRS_REQUIRE_WASMTIME").as_deref(), Ok("1")); + eprintln!("skipping heap getter execution: wasmtime is not installed"); + return; + } + let context = resolve_default_definitions().unwrap(); + // The last boundary exercises real high-bit pointers through allocation, + // content-preserving resize and free, without a large payload allocation. + for heap in [65536_u32, 262144, 0x8000_0000] { + let link = plan(&context, input(heap)).unwrap(); + // Allocate beyond the initial page, verify bounds and scratch survival, + // resize with content preservation, then free through the same instance. + let run = format!( + r#" + (func (export "wasi:cli/run@0.2.12#run") (result i32) (local $p i32) + i32.const 32 i32.const 1234 i32.store + i32.const 0 i32.const 0 i32.const 8 i32.const 131072 call 0 local.set $p + local.get $p i32.const {heap} i32.lt_u if unreachable end + local.get $p i32.const 42 i32.store + i32.const 32 i32.load i32.const 1234 i32.ne if unreachable end + local.get $p i32.const 131072 i32.const 8 i32.const 262144 call 0 local.set $p + local.get $p i32.load i32.const 42 i32.ne if unreachable end + local.get $p i32.const 262144 i32.const 8 i32.const 0 call 0 drop + i32.const 0) + "# + ); + let core = wat::parse_str(format!( + r#"(module + (import "psrs:allocator" "cabi_realloc" (func (param i32 i32 i32 i32) (result i32))) + (memory (export "memory") {}) + (func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) + local.get 0 local.get 1 local.get 2 local.get 3 call 0) + (func (export "get_heap_base") (result i32) i32.const {heap}) + {run})"#, + link.memory().minimum_pages + )) + .unwrap(); + let component = psrs_linker::compose(&context, &link, &core).unwrap(); + let path = std::env::temp_dir().join(format!( + "psrs-heap-getter-{}-{heap}.wasm", + std::process::id() + )); + std::fs::write(&path, component.bytes).unwrap(); + let output = Command::new("wasmtime") + .arg("run") + .arg(&path) + .output() + .unwrap(); + let _ = std::fs::remove_file(path); + assert!(output.status.success(), "heap {heap}: {output:?}"); + } +} diff --git a/crates/psrs-linker/tests/plan.rs b/crates/psrs-linker/tests/plan.rs new file mode 100644 index 00000000..cb531f12 --- /dev/null +++ b/crates/psrs-linker/tests/plan.rs @@ -0,0 +1,293 @@ +//! Target-only linker tests: no compiler IR is constructed. + +use psrs_linker::{ + BindingRequirement, Boundary, CoreSignature, CoreType, MemoryDemand, Provider, RequirementId, + RuntimeUnitOffer, TargetLinkInput, TargetPolicy, plan, resolve_default_definitions, +}; + +fn memory() -> MemoryDemand { + MemoryDemand { + canonical_scratch: (0, 16), + allocator_state: (16, 24), + base_heap_start: 24, + heap_alignment: 8, + growth_owner: psrs_linker::GENERATED_GROWTH_OWNER.into(), + maximum_pages: None, + } +} + +fn input(requirements: Vec, units: Vec) -> TargetLinkInput { + let permitted = resolve_default_definitions() + .map(|context| context.world_imports().to_vec()) + .unwrap_or_default(); + TargetLinkInput { + requirements, + units, + policy: TargetPolicy { + permitted_host_interfaces: permitted, + }, + memory: memory(), + } +} + +fn number_unit() -> RuntimeUnitOffer { + psrs_linker::runtime::offer(&psrs_runtime::NUMBER_UNIT) +} + +fn formatter_requirement() -> BindingRequirement { + BindingRequirement { + id: RequirementId(0), + origin: "NumberToString".into(), + boundary: Boundary::RawCore { + module: psrs_runtime::MODULE_NAME.into(), + field: psrs_runtime::NUMBER_EXPORT.into(), + }, + expected: Some(CoreSignature { + parameters: vec![CoreType::F64, CoreType::I32, CoreType::I32], + result: Some(CoreType::I32), + }), + provider: Provider::RuntimeOperation { + name: psrs_runtime::NUMBER_TO_STRING_OP.name.into(), + version: psrs_runtime::NUMBER_TO_STRING_OP.version.into(), + }, + } +} + +fn stdout_requirement(id: u32) -> BindingRequirement { + BindingRequirement { + id: RequirementId(id), + origin: "wasi:cli/stdout.get-stdout".into(), + boundary: Boundary::ResolvedWit { + interface: "wasi:cli/stdout@0.2.12".into(), + function: "get-stdout".into(), + }, + expected: Some(CoreSignature { + parameters: Vec::new(), + result: Some(CoreType::I32), + }), + provider: Provider::HostInterface { + interface: "wasi:cli/stdout@0.2.12".into(), + }, + } +} + +#[test] +fn a_live_formatter_requirement_reserves_storage_and_closes_the_private_import() { + let context = resolve_default_definitions().unwrap(); + let link = plan( + &context, + input( + vec![formatter_requirement(), stdout_requirement(1)], + vec![number_unit()], + ), + ) + .expect("the formatter plan should be valid"); + + assert_eq!(link.artifacts().len(), 1); + assert_eq!(link.memory().heap_start, psrs_runtime::HEAP_START); + assert!(link.memory().minimum_pages >= 4); + assert_eq!( + link.import(RequirementId(0)).unwrap().module, + psrs_runtime::MODULE_NAME + ); + // The encoder retains only the live resource owner, not unused methods' + // dependencies (`error` and `poll` belong to other streams operations). + assert_eq!( + link.external_world(), + &["wasi:cli/stdout@0.2.12", "wasi:io/streams@0.2.12"] + ); + assert_eq!(link.digests().len(), 1); + assert_eq!( + link.digests()[0], + psrs_runtime::NUMBER_RUNTIME.provenance.sha256 + ); +} + +#[test] +fn an_unused_implementation_contributes_neither_bytes_nor_storage() { + let context = resolve_default_definitions().unwrap(); + let link = plan(&context, input(Vec::new(), Vec::new())).expect("an empty plan is valid"); + assert!(link.artifacts().is_empty()); + assert!(link.memory().heap_start < psrs_runtime::HEAP_START); +} + +#[test] +fn a_missing_artifact_provider_is_rejected() { + let context = resolve_default_definitions().unwrap(); + let result = plan(&context, input(vec![formatter_requirement()], Vec::new())); + assert!(result.is_err(), "an absent artifact must not resolve"); +} + +#[test] +fn a_host_interface_outside_the_world_is_rejected() { + let context = resolve_default_definitions().unwrap(); + let requirement = BindingRequirement { + id: RequirementId(0), + origin: "wasi:http/outgoing-handler.handle".into(), + boundary: Boundary::ResolvedWit { + interface: "wasi:http/outgoing-handler@0.2.12".into(), + function: "handle".into(), + }, + expected: Some(CoreSignature { + parameters: Vec::new(), + result: Some(CoreType::I32), + }), + provider: Provider::HostInterface { + interface: "wasi:http/outgoing-handler@0.2.12".into(), + }, + }; + assert!( + plan(&context, input(vec![requirement], Vec::new())).is_err(), + "a definition outside the world must not satisfy a live import" + ); +} + +#[test] +fn a_world_interface_disabled_by_the_target_profile_is_rejected() { + let context = resolve_default_definitions().unwrap(); + // `wasi:cli/stdout` is in the world but not in this restricted policy. + let result = plan( + &context, + TargetLinkInput { + requirements: vec![stdout_requirement(0)], + units: Vec::new(), + policy: TargetPolicy { + permitted_host_interfaces: vec!["wasi:io/streams@0.2.12".into()], + }, + memory: memory(), + }, + ); + assert!( + result.is_err(), + "the target capability profile must gate a world interface" + ); +} + +#[test] +fn a_different_world_version_does_not_satisfy_a_pinned_import() { + let context = resolve_default_definitions().unwrap(); + let requirement = BindingRequirement { + id: RequirementId(0), + origin: "wasi:cli/stdout@0.2.11.get-stdout".into(), + boundary: Boundary::ResolvedWit { + interface: "wasi:cli/stdout@0.2.11".into(), + function: "get-stdout".into(), + }, + expected: Some(CoreSignature { + parameters: Vec::new(), + result: Some(CoreType::I32), + }), + provider: Provider::HostInterface { + interface: "wasi:cli/stdout@0.2.11".into(), + }, + }; + assert!( + plan(&context, input(vec![requirement], Vec::new())).is_err(), + "a pinned import version must match the resolved world exactly" + ); +} + +#[test] +fn a_reservation_overlapping_canonical_state_is_rejected() { + let context = resolve_default_definitions().unwrap(); + let mut unit = number_unit(); + let storage = unit.artifact.contract.storage.as_mut().unwrap(); + storage.static_data.start = 16; + let result = plan(&context, input(vec![formatter_requirement()], vec![unit])); + assert!(result.is_err(), "overlapping reservations must be rejected"); +} + +#[test] +fn conflicting_providers_for_one_import_identity_are_rejected() { + let context = resolve_default_definitions().unwrap(); + let mut other = formatter_requirement(); + other.id = RequirementId(1); + other.origin = "NumberToStringAgain".into(); + other.expected = Some(CoreSignature { + // A deliberately different signature for the same import identity. + parameters: vec![CoreType::F64, CoreType::I32], + result: Some(CoreType::I32), + }); + assert!( + plan( + &context, + input(vec![formatter_requirement(), other], vec![number_unit()]) + ) + .is_err(), + "two signatures for one import identity must be rejected" + ); +} + +/// A definition-only contract is not an executable provider; verification of a +/// mismatched contract must fail before any plan is published. +#[test] +fn a_definition_contract_does_not_satisfy_execution() { + let mut unit = number_unit(); + unit.artifact.contract.exports.clear(); + let artifact = unit; + let context = resolve_default_definitions().unwrap(); + assert!( + plan( + &context, + input(vec![formatter_requirement()], vec![artifact]) + ) + .is_err() + ); +} + +#[test] +fn the_policy_cannot_introduce_an_interface_absent_from_the_world() { + let context = resolve_default_definitions().unwrap(); + let mut requirement = stdout_requirement(0); + let interface = "wasi:cli/stdout@99.0.0"; + requirement.boundary = Boundary::ResolvedWit { + interface: interface.into(), + function: "get-stdout".into(), + }; + requirement.provider = Provider::HostInterface { + interface: interface.into(), + }; + let mut input = input(vec![requirement], Vec::new()); + input + .policy + .permitted_host_interfaces + .push(interface.into()); + assert!( + plan(&context, input) + .unwrap_err() + .to_string() + .contains("outside the resolved world") + ); +} + +#[test] +fn impossible_memory_demands_are_rejected_before_arithmetic() { + let context = resolve_default_definitions().unwrap(); + for (range, alignment) in [((16, 0), 8), ((0, 16), 3), ((0, 16), 0)] { + let mut input = input(Vec::new(), Vec::new()); + input.memory.canonical_scratch = range; + input.memory.heap_alignment = alignment; + assert!(plan(&context, input).is_err()); + } + let mut unit = number_unit(); + unit.artifact + .contract + .storage + .as_mut() + .unwrap() + .minimum_pages = u64::MAX; + assert!(plan(&context, input(vec![formatter_requirement()], vec![unit])).is_err()); +} + +#[test] +fn a_resource_owner_disabled_by_policy_cannot_hide_behind_a_live_method() { + let context = resolve_default_definitions().unwrap(); + let mut input = input(vec![stdout_requirement(0)], Vec::new()); + input.policy.permitted_host_interfaces = vec!["wasi:cli/stdout@0.2.12".into()]; + assert!( + plan(&context, input) + .unwrap_err() + .to_string() + .contains("resource dependency") + ); +} diff --git a/crates/psrs-resolve/src/lib.rs b/crates/psrs-resolve/src/lib.rs index d5bca052..b523b5ae 100644 --- a/crates/psrs-resolve/src/lib.rs +++ b/crates/psrs-resolve/src/lib.rs @@ -2,6 +2,6 @@ mod resolver; pub use resolver::{ ProgramError, ResolveError, ResolveErrorKind, ResolveOptions, bootstrap_externals, - resolve_module, resolve_module_with_externals, resolve_program, resolve_program_partial, - resolve_program_with_options, + compiler_provided_module, resolve_module, resolve_module_with_externals, resolve_program, + resolve_program_partial, resolve_program_with_options, }; diff --git a/crates/psrs-resolve/src/resolver/bootstrap.rs b/crates/psrs-resolve/src/resolver/bootstrap.rs index 1c31ad5b..aac83c50 100644 --- a/crates/psrs-resolve/src/resolver/bootstrap.rs +++ b/crates/psrs-resolve/src/resolver/bootstrap.rs @@ -4,14 +4,15 @@ use psrs_hir::{ExternalKind, ExternalSymbol, Intrinsic}; /// /// The name, arity, category, and type of each intrinsic come from its /// descriptor in `psrs-hir`, so this table is not a second source of truth. -/// `Intrinsic::ALL` is kept exact by a compile-time assertion, so every variant -/// is bootstrapped. +/// `Intrinsic::ALL` is kept exact by a compile-time assertion. Binding-only +/// operations are resolved from explicit primitive bindings, not exposed here. pub fn bootstrap_externals() -> Vec { Intrinsic::ALL .into_iter() .map(|intrinsic| { let descriptor = intrinsic.descriptor(); ExternalSymbol { + source_module: None, symbol: intrinsic.symbol(), name: descriptor.name.to_owned(), kind: ExternalKind::Intrinsic(intrinsic), diff --git a/crates/psrs-resolve/src/resolver/exports/import_scope.rs b/crates/psrs-resolve/src/resolver/exports/import_scope.rs new file mode 100644 index 00000000..a095ec66 --- /dev/null +++ b/crates/psrs-resolve/src/resolver/exports/import_scope.rs @@ -0,0 +1,26 @@ +use super::{Resolver, ast, hir}; + +impl Resolver { + /// A pseudo-module combines import scopes, so ambiguity is checked by + /// member identity in its namespace, as it is for a qualified reference. + pub(super) fn check_reexport_scope( + &mut self, + name: &ast::Name, + imports: &[hir::Import], + ) -> bool { + let mut valid = true; + for import in imports { + for symbol in &import.symbols { + let qualified = format!("{}.{}", name.text, symbol.external_name); + valid &= self.lookup_global(&qualified, name.span).is_some(); + } + for imported in &import.types { + let qualified = format!("{}.{}", name.text, imported.name); + valid &= self + .lookup_qualified_type(&qualified, &name.text, &imported.name, name.span) + .is_some(); + } + } + valid + } +} diff --git a/crates/psrs-resolve/src/resolver/exports/mod.rs b/crates/psrs-resolve/src/resolver/exports/mod.rs index 47ab9bcd..74517ecb 100644 --- a/crates/psrs-resolve/src/resolver/exports/mod.rs +++ b/crates/psrs-resolve/src/resolver/exports/mod.rs @@ -1,5 +1,6 @@ use super::ResolveErrorKind; use super::names::Resolver; +use super::names::builtin_type; use psrs_ast as ast; use psrs_hir::{ self as hir, ExportedInstance, ExportedOperator, ExportedSymbol, ExportedType, @@ -8,6 +9,7 @@ use psrs_hir::{ use psrs_span::TextRange; use std::collections::{HashMap, HashSet}; +mod import_scope; mod instance_visibility; mod transitive; use instance_visibility::instance_is_public; @@ -284,83 +286,92 @@ impl Resolver { }) .cloned() .collect(); - let [import] = matches.as_slice() else { - if matches.is_empty() { - self.report( - ResolveErrorKind::UnknownExport, - name.text.clone(), - name.span, - ); - } else { - // More than one import provides the qualifier. - self.report_conflict(name.text.clone(), name.span); - } - return; - }; - let pseudo = import.alias.is_some(); - for symbol in &import.symbols { - if import.fixities.iter().any(|fixity| { - fixity.namespace == hir::FixityNamespace::Value - && fixity.operator == symbol.external_name - }) { - continue; - } - // A real (unaliased) import re-exports names from unqualified - // scope, so an ambiguous name is a scope conflict. - if !pseudo - && self - .unqualified - .get(&symbol.external_name) - .is_some_and(|symbols| symbols.iter().any(|other| *other != symbol.symbol)) - { - self.report_conflict(symbol.external_name.clone(), name.span); - continue; - } - self.add_value( - &mut exports.values, - &mut exports.value_sources, - symbol.symbol, - symbol.external_name.clone(), + if matches.is_empty() { + self.report( + ResolveErrorKind::UnknownExport, + name.text.clone(), name.span, ); + return; } - for fixity in &import.fixities { - match fixity.target { - hir::FixityTarget::Value(symbol) => self.add_operator( - &mut exports.operators, + // A shared import alias (for example the standard library's + // `as Exports`) denotes the union of those modules' exports. + if matches.len() > 1 && !self.check_reexport_scope(name, &matches) { + return; + } + for import in &matches { + let pseudo = import.alias.is_some(); + for symbol in &import.symbols { + if import.fixities.iter().any(|fixity| { + fixity.namespace == hir::FixityNamespace::Value + && fixity.operator == symbol.external_name + }) { + continue; + } + // A real (unaliased) import re-exports names from unqualified + // scope, so an ambiguous name is a scope conflict. + if !pseudo + && self + .unqualified + .get(&symbol.external_name) + .is_some_and(|symbols| symbols.iter().any(|other| *other != symbol.symbol)) + { + self.report_conflict(symbol.external_name.clone(), name.span); + continue; + } + self.add_value( + &mut exports.values, &mut exports.value_sources, - symbol, - fixity.operator.clone(), - fixity.target_name.clone(), - name.span, - ), - hir::FixityTarget::Type(reference) => self.add_type_operator( - &mut exports.type_operators, - &mut exports.type_sources, - reference, - fixity.operator.clone(), - fixity.target_name.clone(), + symbol.symbol, + symbol.external_name.clone(), name.span, - ), + ); } - } - for imported in &import.types { - if import.fixities.iter().any(|fixity| { - fixity.namespace == hir::FixityNamespace::Type && fixity.operator == imported.name - }) { - continue; + for fixity in &import.fixities { + match fixity.target { + hir::FixityTarget::Value(symbol) => self.add_operator( + &mut exports.operators, + &mut exports.value_sources, + symbol, + fixity.operator.clone(), + fixity.target_name.clone(), + name.span, + ), + hir::FixityTarget::Type(reference) => self.add_type_operator( + &mut exports.type_operators, + &mut exports.type_sources, + reference, + fixity.operator.clone(), + fixity.target_name.clone(), + name.span, + ), + } + } + for imported in &import.types { + if import.fixities.iter().any(|fixity| { + fixity.namespace == hir::FixityNamespace::Type + && fixity.operator == imported.name + }) { + continue; + } + self.add_type( + exports, + TypeExportRequest { + reference: imported.reference, + name: imported.name.clone(), + name_span: name.span, + is_class: false, + constructors: (!imported.constructors.is_empty()).then(|| { + imported + .constructors + .iter() + .map(|(_, symbol)| *symbol) + .collect() + }), + opaque: imported.opaque, + }, + ); } - self.add_type( - exports, - TypeExportRequest { - reference: imported.reference, - name: imported.name.clone(), - name_span: name.span, - is_class: false, - constructors: None, - opaque: imported.opaque, - }, - ); } } @@ -381,6 +392,10 @@ impl Resolver { .collect(), ); } + // `Format()` exports the type and none of its constructors. + if members.names.is_empty() { + return Some(Vec::new()); + } let mut symbols = Vec::new(); for member in &members.names { match declaration @@ -436,8 +451,15 @@ impl Resolver { if let Some(id) = self.type_names.get(name) { return Some(TypeReference::Named(*id)); } - self.imported_types + if let Some(reference) = self + .imported_types .get(name) .and_then(|references| references.first().copied()) + { + return Some(reference); + } + // `Unit` is a compiler builtin with no local declaration. `Data.Unit` + // re-exports that builtin so `import Data.Unit (Unit)` is the same type. + builtin_type(name).map(TypeReference::Builtin) } } diff --git a/crates/psrs-resolve/src/resolver/exports/transitive.rs b/crates/psrs-resolve/src/resolver/exports/transitive.rs index 3001787f..eb8328a9 100644 --- a/crates/psrs-resolve/src/resolver/exports/transitive.rs +++ b/crates/psrs-resolve/src/resolver/exports/transitive.rs @@ -118,9 +118,16 @@ impl Resolver { continue; }; let mut referenced = Vec::new(); - for constructor in &declaration.constructors { - for field in &constructor.fields { - collect_named_types(field, &mut referenced); + // Field types matter only for constructors the export actually + // lists. `T` and `T()` keep those constructors hidden. + if let Some(exported_constructors) = &exported.constructors { + for constructor in &declaration.constructors { + if !exported_constructors.contains(&constructor.symbol) { + continue; + } + for field in &constructor.fields { + collect_named_types(field, &mut referenced); + } } } if let Some(body) = &declaration.body { @@ -192,7 +199,9 @@ impl Resolver { if let Some(declaration) = own_declarations.get(&value.symbol) && let Some(signature) = &declaration.signature { - collect_named_types(signature, &mut required); + // A value type is stored after synonym expansion, so a local + // synonym in the explicit signature is not itself a dependency. + collect_named_types(&expand_value_synonyms(signature, &own), &mut required); } // Explicit signatures are checked at P3, where their named HIR // references are already resolved. Inferred public value types are @@ -266,3 +275,166 @@ fn collect_named_types(ty: &hir::Type, out: &mut Vec) { | hir::TypeKind::String(_) => {} } } + +/// Expands fully applied local type synonyms in a value signature. +/// +/// Exported declaration bodies keep the names written in source. Value types +/// do not: a saturated local synonym is replaced by its body, with its +/// parameters substituted, before the export dependency walk. An unsaturated +/// or recursive synonym stays in place; kind checking owns that error. +fn expand_value_synonyms( + ty: &hir::Type, + own: &HashMap, +) -> hir::Type { + expand_value_type(ty, own, &HashMap::new(), &[], &mut Vec::new()) +} + +fn expand_value_type( + ty: &hir::Type, + own: &HashMap, + subst: &HashMap, + bound: &[String], + stack: &mut Vec, +) -> hir::Type { + if let Some(expanded) = expand_saturated_synonym(ty, own, subst, bound, stack) { + return expanded; + } + let kind = match &ty.kind { + hir::TypeKind::Wildcard => hir::TypeKind::Wildcard, + hir::TypeKind::Variable(name) => { + if bound.iter().any(|binder| binder == name) { + hir::TypeKind::Variable(name.clone()) + } else if let Some(replacement) = subst.get(name) { + return replacement.clone(); + } else { + hir::TypeKind::Variable(name.clone()) + } + } + hir::TypeKind::Constructor(builtin) => hir::TypeKind::Constructor(*builtin), + hir::TypeKind::Named(id) => hir::TypeKind::Named(*id), + hir::TypeKind::Opaque(id) => hir::TypeKind::Opaque(*id), + hir::TypeKind::Application(function, argument) => hir::TypeKind::Application( + Box::new(expand_value_type(function, own, subst, bound, stack)), + Box::new(expand_value_type(argument, own, subst, bound, stack)), + ), + hir::TypeKind::OperatorChain { + operands, + operators, + } => hir::TypeKind::OperatorChain { + operands: operands + .iter() + .map(|operand| expand_value_type(operand, own, subst, bound, stack)) + .collect(), + operators: operators.clone(), + }, + hir::TypeKind::Function { parameter, result } => hir::TypeKind::Function { + parameter: Box::new(expand_value_type(parameter, own, subst, bound, stack)), + result: Box::new(expand_value_type(result, own, subst, bound, stack)), + }, + hir::TypeKind::Forall { variables, body } => { + let mut inner = bound.to_vec(); + let variables = variables + .iter() + .map(|variable| { + let kind = variable + .kind + .as_ref() + .map(|kind| expand_value_type(kind, own, subst, &inner, stack)); + inner.push(variable.name.clone()); + hir::TypeParameter { + name: variable.name.clone(), + name_span: variable.name_span, + kind, + } + }) + .collect(); + hir::TypeKind::Forall { + variables, + body: Box::new(expand_value_type(body, own, subst, &inner, stack)), + } + } + hir::TypeKind::Constrained { constraint, body } => hir::TypeKind::Constrained { + constraint: Box::new(expand_value_type(constraint, own, subst, bound, stack)), + body: Box::new(expand_value_type(body, own, subst, bound, stack)), + }, + hir::TypeKind::Row { fields, tail } => hir::TypeKind::Row { + fields: expand_fields(fields, own, subst, bound, stack), + tail: tail + .as_ref() + .map(|tail| Box::new(expand_value_type(tail, own, subst, bound, stack))), + }, + hir::TypeKind::Record { fields, tail } => hir::TypeKind::Record { + fields: expand_fields(fields, own, subst, bound, stack), + tail: tail + .as_ref() + .map(|tail| Box::new(expand_value_type(tail, own, subst, bound, stack))), + }, + hir::TypeKind::Integer(value) => hir::TypeKind::Integer(value.clone()), + hir::TypeKind::String(value) => hir::TypeKind::String(value.clone()), + }; + hir::Type { + kind, + span: ty.span, + } +} + +fn expand_fields( + fields: &[hir::TypeField], + own: &HashMap, + subst: &HashMap, + bound: &[String], + stack: &mut Vec, +) -> Vec { + fields + .iter() + .map(|field| hir::TypeField { + label: field.label.clone(), + label_span: field.label_span, + ty: expand_value_type(&field.ty, own, subst, bound, stack), + span: field.span, + }) + .collect() +} + +fn expand_saturated_synonym( + ty: &hir::Type, + own: &HashMap, + subst: &HashMap, + bound: &[String], + stack: &mut Vec, +) -> Option { + let (head, arguments) = applied_spine(ty); + let hir::TypeKind::Named(id) = head.kind else { + return None; + }; + let declaration = own.get(&id)?; + if declaration.kind != TypeDeclarationKind::TypeSynonym { + return None; + } + let body = declaration.body.as_ref()?; + if arguments.len() != declaration.parameters.len() || stack.contains(&id) { + return None; + } + stack.push(id); + let mut inner = HashMap::new(); + for (parameter, argument) in declaration.parameters.iter().zip(arguments) { + inner.insert( + parameter.name.clone(), + expand_value_type(argument, own, subst, bound, stack), + ); + } + let expanded = expand_value_type(body, own, &inner, &[], stack); + stack.pop(); + Some(expanded) +} + +fn applied_spine(ty: &hir::Type) -> (&hir::Type, Vec<&hir::Type>) { + let mut arguments = Vec::new(); + let mut head = ty; + while let hir::TypeKind::Application(function, argument) = &head.kind { + arguments.push(argument.as_ref()); + head = function.as_ref(); + } + arguments.reverse(); + (head, arguments) +} diff --git a/crates/psrs-resolve/src/resolver/mod.rs b/crates/psrs-resolve/src/resolver/mod.rs index 5f852a05..f9719491 100644 --- a/crates/psrs-resolve/src/resolver/mod.rs +++ b/crates/psrs-resolve/src/resolver/mod.rs @@ -8,8 +8,8 @@ use std::collections::{HashMap, HashSet}; mod program; pub use program::{ - ProgramError, ResolveOptions, resolve_program, resolve_program_partial, - resolve_program_with_options, + ProgramError, ResolveOptions, compiler_provided_module, resolve_program, + resolve_program_partial, resolve_program_with_options, }; mod bootstrap; diff --git a/crates/psrs-resolve/src/resolver/module_resolution.rs b/crates/psrs-resolve/src/resolver/module_resolution.rs index 1aa3b093..da59a743 100644 --- a/crates/psrs-resolve/src/resolver/module_resolution.rs +++ b/crates/psrs-resolve/src/resolver/module_resolution.rs @@ -30,6 +30,20 @@ pub(crate) fn resolve_ast_module( )); } } + // Foreign values share the ordinary value namespace and may be named by a + // fixity declaration before their signatures are resolved. + for (index, foreign) in module.foreign_imports.iter().enumerate() { + if globals + .insert(foreign.name.text.clone(), foreign_symbol(module_id, index)) + .is_some() + { + errors.push(ResolveError::named( + ResolveErrorKind::DuplicateDeclaration, + foreign.name.text.clone(), + foreign.name.span, + )); + } + } let mut type_declarations = module.type_declarations; let role_declarations: HashMap = module @@ -47,12 +61,14 @@ pub(crate) fn resolve_ast_module( let mut external_globals = HashMap::new(); for external in &inputs.externals { - // `coerce` and `undefined` are reached through their virtual module - // interfaces, not as free names, so their bootstrap spellings stay out - // of the value namespace. + // `coerce`, `unsafeCoerce`, and `undefined` are reached through their + // virtual module interfaces, not as free names, so their bootstrap + // spellings stay out of the value namespace. if matches!( &external.kind, - ExternalKind::Intrinsic(hir::Intrinsic::Coerce | hir::Intrinsic::Undefined) + ExternalKind::Intrinsic( + hir::Intrinsic::Coerce | hir::Intrinsic::UnsafeCoerce | hir::Intrinsic::Undefined + ) ) { continue; } @@ -82,24 +98,50 @@ pub(crate) fn resolve_ast_module( resolver.opaque_types.extend(opaque_types); resolver.note_imported_opaque_types(); - // A `foreign import` declares an external value whose type and WIT binding + // A `foreign import` declares an external value whose type and target binding // come from source. Resolve its annotation first so expressions can refer // to it by name. for (index, foreign) in module.foreign_imports.iter().enumerate() { - let binding = foreign - .binding - .split_once('#') - .filter(|(interface, function)| !interface.is_empty() && !function.is_empty()); - let Some((interface, function)) = binding else { - resolver.errors.push(ResolveError { - kind: ResolveErrorKind::InvalidHir, - span: foreign.span, - message: format!( - "`{}` is not a `\"#\"` WIT binding", - foreign.binding - ), - }); - continue; + let kind = match &foreign.binding { + None => ExternalKind::Library { + module: module.name.text.clone(), + }, + Some(binding) => { + let Some((interface, function)) = binding + .split_once('#') + .filter(|(interface, function)| !interface.is_empty() && !function.is_empty()) + else { + resolver.errors.push(ResolveError { + kind: ResolveErrorKind::InvalidHir, + span: foreign.span, + message: format!( + "`{binding}` is not a `\"#\"` WIT binding" + ), + }); + continue; + }; + if interface == "psrs:intrinsic" { + let Some(intrinsic) = hir::Intrinsic::from_binding(function) else { + resolver.errors.push(ResolveError { + kind: ResolveErrorKind::InvalidHir, + span: foreign.span, + message: format!("unknown primitive binding `{function}`"), + }); + continue; + }; + ExternalKind::Primitive(intrinsic) + } else if interface.starts_with("psrs:runtime-") { + ExternalKind::Runtime { + module: interface.into(), + function: function.into(), + } + } else { + ExternalKind::Wit { + interface: interface.into(), + function: function.into(), + } + } + } }; let Some(signature) = resolver.resolve_type(foreign.annotation.clone()) else { continue; @@ -109,12 +151,10 @@ pub(crate) fn resolve_ast_module( foreign.name.text.clone(), symbol, ExternalSymbol { + source_module: Some(module_id), symbol, name: foreign.name.text.clone(), - kind: ExternalKind::Wit { - interface: interface.to_string(), - function: function.to_string(), - }, + kind, signature: Some(signature), }, foreign.name.span, @@ -169,7 +209,21 @@ pub(crate) fn resolve_ast_module( .zip(plans) .filter_map(|(declaration, plan)| { let role = role_declarations.get(&declaration.name().text).cloned(); - resolver.resolve_type_declaration(plan, declaration, role) + let mut declaration = resolver.resolve_type_declaration(plan, declaration, role)?; + // Bind canonical interface exports to the declaration's ordinary + // resolved identity. Type checking validates the interface contract. + declaration.compiler_class = hir::compiler_interface(&module.name.text) + .filter(|interface| { + interface.implementation == hir::InterfaceImplementation::Source + }) + .and_then(|interface| { + interface + .classes + .iter() + .find(|(name, _)| *name == declaration.name) + .map(|(_, identity)| *identity) + }); + Some(declaration) }) .collect(); let mut instance_names = HashSet::new(); diff --git a/crates/psrs-resolve/src/resolver/names/mod.rs b/crates/psrs-resolve/src/resolver/names/mod.rs index d23bdc74..6170e63f 100644 --- a/crates/psrs-resolve/src/resolver/names/mod.rs +++ b/crates/psrs-resolve/src/resolver/names/mod.rs @@ -2,7 +2,7 @@ use super::{ResolveError, ResolveErrorKind}; use psrs_ast::{self as ast, ExprKind as AstExprKind}; use psrs_hir::{ self as hir, CaseBranchCoverage, Expr, ExprKind, ExternalSymbol, LocalBinder, LocalBinding, - LocalId, ModuleId, SymbolId, TypeId, TypeReference, + LocalId, SymbolId, TypeId, TypeReference, }; use psrs_span::TextRange; use std::collections::{HashMap, HashSet}; @@ -10,12 +10,10 @@ use std::collections::{HashMap, HashSet}; mod patterns; struct QualifiedImport { - module: ModuleId, values: HashMap, } pub(super) struct QualifiedTypeImport { - pub(super) module: ModuleId, pub(super) types: HashMap, } @@ -86,10 +84,7 @@ impl Resolver { qualified .entry(qualifier.clone()) .or_default() - .push(QualifiedImport { - module: import.module, - values, - }); + .push(QualifiedImport { values }); let types = import .types .iter() @@ -98,10 +93,7 @@ impl Resolver { qualified_types .entry(qualifier) .or_default() - .push(QualifiedTypeImport { - module: import.module, - types, - }); + .push(QualifiedTypeImport { types }); } if !imports.iter().any(|import| import.module_name == "Prim") { for &(name, builtin) in &util::PRIM_TYPES { @@ -142,7 +134,11 @@ impl Resolver { external: ExternalSymbol, span: TextRange, ) { - if self.external_globals.insert(name.clone(), symbol).is_some() { + if let Some(previous) = self.external_globals.insert(name.clone(), symbol) + && self.externals.iter().any(|external| { + external.symbol == previous && external.kind.requires_checked_signature() + }) + { self.errors.push(ResolveError::named( ResolveErrorKind::DuplicateExternal, name, @@ -153,6 +149,10 @@ impl Resolver { } pub(super) fn resolve_expr(&mut self, expression: ast::Expr) -> Option { + psrs_span::with_sufficient_stack(|| self.resolve_expr_inner(expression)) + } + + fn resolve_expr_inner(&mut self, expression: ast::Expr) -> Option { let span = expression.span; let kind = match expression.kind { AstExprKind::Name(name) => { @@ -178,6 +178,12 @@ impl Resolver { .map(|(label, value)| Some((label, self.resolve_expr(value)?))) .collect::>>()?, ), + AstExprKind::MatchProduct(fields) => ExprKind::MatchProduct( + fields + .into_iter() + .map(|(label, value)| Some((label, self.resolve_expr(value)?))) + .collect::>>()?, + ), AstExprKind::RecordUpdate { expression, fields } => { self.resolve_record_update(*expression, fields, span)? } @@ -300,7 +306,7 @@ impl Resolver { fn resolve_record_update( &mut self, expression: ast::Expr, - fields: Vec<(String, ast::Expr)>, + fields: Vec, span: TextRange, ) -> Option { let record = self.resolve_expr(expression)?; @@ -330,19 +336,17 @@ impl Resolver { fn resolve_record_update_fields( &mut self, base: &Expr, - fields: Vec<(String, ast::Expr)>, + fields: Vec, ) -> Option> { fields .into_iter() - .map(|(label, value)| { - let value_span = value.span; - let value = match value.kind { - AstExprKind::RecordUpdate { expression, fields } - if matches!( - expression.kind, - AstExprKind::Name(ref name) if name.text == label - ) => - { + .map(|field| { + let label = field.label; + let value = match field.value { + ast::RecordUpdateValue::Nested { + fields, + span: value_span, + } => { let nested_record = Expr { kind: ExprKind::FieldAccess { expression: Box::new(base.clone()), @@ -360,17 +364,14 @@ impl Resolver { span: value_span, } } - kind => self.resolve_expr(ast::Expr { - kind, - span: value_span, - })?, + ast::RecordUpdateValue::Expression(value) => self.resolve_expr(value)?, }; Some((label, value)) }) .collect() } - fn lookup_local(&self, name: &str) -> Option<&LocalBinder> { + pub(super) fn lookup_local(&self, name: &str) -> Option<&LocalBinder> { self.scopes.iter().rev().find_map(|scope| scope.get(name)) } @@ -381,9 +382,6 @@ impl Resolver { if let Some(symbol) = self.globals.get(text) { return Some(*symbol); } - if let Some(symbol) = self.external_globals.get(text) { - return Some(*symbol); - } if let Some(symbols) = self.unqualified.get(text) { let first = symbols[0]; if symbols.iter().all(|symbol| *symbol == first) { @@ -392,6 +390,9 @@ impl Resolver { self.report_conflict(text.to_string(), span); return None; } + if let Some(symbol) = self.external_globals.get(text) { + return Some(*symbol); + } self.report(ResolveErrorKind::UnknownName, text.to_string(), span); None } @@ -407,15 +408,15 @@ impl Resolver { self.report(ResolveErrorKind::UnknownName, text.to_string(), span); return None; }; - let mut found: Option<(ModuleId, SymbolId)> = None; + let mut found: Option = None; let mut conflict = false; for candidate in candidates { let Some(symbol) = candidate.values.get(member) else { continue; }; match found { - None => found = Some((candidate.module, *symbol)), - Some((module, existing)) if module != candidate.module || existing != *symbol => { + None => found = Some(*symbol), + Some(existing) if existing != *symbol => { conflict = true; } _ => {} @@ -425,7 +426,7 @@ impl Resolver { self.report_conflict(text.to_string(), span); return None; } - if let Some((_, symbol)) = found { + if let Some(symbol) = found { return Some(symbol); } self.report(ResolveErrorKind::UnknownName, text.to_string(), span); @@ -465,4 +466,6 @@ fn ast_expr_is_guarded(expression: &ast::Expr) -> bool { } } -pub(super) use util::{PRIM_TYPES, builtin_type, is_uppercase, prim_type, split_qualified}; +pub(super) use util::{ + PRIM_TYPES, builtin_type, implicit_prim_class, is_uppercase, prim_type, split_qualified, +}; diff --git a/crates/psrs-resolve/src/resolver/names/util.rs b/crates/psrs-resolve/src/resolver/names/util.rs index f680d2bd..1ec30627 100644 --- a/crates/psrs-resolve/src/resolver/names/util.rs +++ b/crates/psrs-resolve/src/resolver/names/util.rs @@ -1,4 +1,4 @@ -use psrs_hir::BuiltinType; +use psrs_hir::{BuiltinType, TypeReference}; pub(in crate::resolver) const PRIM_TYPES: [(&str, BuiltinType); 12] = [ ("Array", BuiltinType::Array), @@ -19,10 +19,32 @@ pub(in crate::resolver) fn split_qualified(text: &str) -> Option<(&str, &str)> { if let Some(index) = text.rfind(".(") && text.ends_with(')') { - return Some((&text[..index], &text[index + 2..text.len() - 1])); + let qualifier = &text[..index]; + return is_module_qualifier(qualifier) + .then_some((qualifier, &text[index + 2..text.len() - 1])); + } + // A symbolic member can itself contain dots. Walk module components + // from the left rather than mistaking the last operator dot for a separator. + for (index, _) in text.match_indices('.') { + let qualifier = &text[..index]; + let member = &text[index + 1..]; + if is_module_qualifier(qualifier) + && !member.is_empty() + && !member.chars().next().is_some_and(char::is_uppercase) + { + return Some((qualifier, member)); + } } let index = text.rfind('.')?; - Some((&text[..index], &text[index + 1..])) + (is_module_qualifier(&text[..index]) && index + 1 < text.len()) + .then_some((&text[..index], &text[index + 1..])) +} + +fn is_module_qualifier(qualifier: &str) -> bool { + !qualifier.is_empty() + && qualifier + .split('.') + .all(|part| part.chars().next().is_some_and(char::is_uppercase)) } pub(in crate::resolver) fn is_uppercase(name: &str) -> bool { @@ -45,13 +67,44 @@ pub(in crate::resolver) fn builtin_type(name: &str) -> Option { "Row" => BuiltinType::Row, "Record" => BuiltinType::Record, "Array" => BuiltinType::Array, - "Function" | "->" | "~>" => BuiltinType::Function, + "Function" | "->" => BuiltinType::Function, _ => return None, }) } +/// `Partial` lives in `Prim` and is in scope in every module, the same way +/// `Int` is. Source does not import it. +pub(in crate::resolver) fn implicit_prim_class(name: &str) -> Option { + psrs_hir::primitive_type_declarations() + .into_iter() + .find(|(owner, declaration)| *owner == "Prim" && declaration.name == name) + .map(|(_, declaration)| TypeReference::Named(declaration.id)) +} + pub(in crate::resolver) fn prim_type(name: &str) -> Option { PRIM_TYPES .iter() .find_map(|(prim_name, builtin)| (*prim_name == name).then_some(*builtin)) } + +#[cfg(test)] +mod tests { + use super::split_qualified; + + #[test] + fn symbolic_operator_names_are_not_split_as_qualified_names() { + assert_eq!(split_qualified(".&."), None); + assert_eq!( + split_qualified("Data.Int.Bits.and"), + Some(("Data.Int.Bits", "and")) + ); + assert_eq!( + split_qualified("Data.Int.Bits.(.&.)"), + Some(("Data.Int.Bits", ".&.")) + ); + assert_eq!(split_qualified("A..."), Some(("A", ".."))); + assert_eq!(split_qualified("Foo.Bar..&."), Some(("Foo.Bar", ".&."))); + assert_eq!(split_qualified("Data.Array"), Some(("Data", "Array"))); + assert_eq!(split_qualified("A."), None); + } +} diff --git a/crates/psrs-resolve/src/resolver/operators.rs b/crates/psrs-resolve/src/resolver/operators.rs index 701aebcc..646dd733 100644 --- a/crates/psrs-resolve/src/resolver/operators.rs +++ b/crates/psrs-resolve/src/resolver/operators.rs @@ -1,6 +1,8 @@ use super::names::Resolver; use psrs_ast as ast; -use psrs_hir::{self as hir, Associativity, ExprKind, ResolvedOperator}; +use psrs_hir::{ + self as hir, Associativity, ExprKind, LocalId, ModuleId, ResolvedOperator, SymbolId, +}; use std::collections::HashMap; pub(super) fn merge_fixities( @@ -30,6 +32,7 @@ pub(super) fn merge_fixities( hir::FixityNamespace::Value => &mut value_fixities, hir::FixityNamespace::Type => &mut type_fixities, }; + destination.insert(format!("{qualifier}.{}", fixity.operator), fixity.clone()); destination.insert(format!("{qualifier}.({})", fixity.operator), fixity.clone()); } } @@ -48,7 +51,7 @@ impl Resolver { .collect::>>()?; let operators = operators .into_iter() - .map(|operator| self.resolve_operator(&operator.name.text, operator.span)) + .map(|operator| self.resolve_chain_operator(&operator.name.text, operator.span)) .collect::>>()?; Some(ExprKind::OperatorChain { operands, @@ -63,7 +66,7 @@ impl Resolver { side: ast::SectionSide, span: psrs_span::TextRange, ) -> Option { - let operator = self.resolve_operator(&operator.text, operator.span)?; + let operator = self.resolve_chain_operator(&operator.text, operator.span)?; let operand = self.resolve_expr(operand)?; let binder = self.new_local(format!("__psrs_section_{}", span.start), span); Some(ExprKind::OperatorSection { @@ -83,17 +86,51 @@ impl Resolver { span: psrs_span::TextRange, ) -> Option { let symbol = self.lookup_global(name, span)?; + Some(self.global_operator(name, symbol, span)) + } + + /// A backticked identifier is the value itself, not a fixity declaration. + /// A name in scope wins. Otherwise the operator is the global of that name. + fn resolve_chain_operator( + &mut self, + name: &str, + span: psrs_span::TextRange, + ) -> Option { + if let Some(local) = self.lookup_local(name).map(|binder| binder.id) { + return Some(local_operator(local, span)); + } + self.resolve_operator(name, span) + } + + fn global_operator( + &self, + name: &str, + symbol: SymbolId, + span: psrs_span::TextRange, + ) -> ResolvedOperator { let (associativity, precedence) = self .fixities .get(name) .map(|fixity| (fixity.associativity, fixity.precedence)) .unwrap_or_else(|| default_fixity(name)); - Some(ResolvedOperator { + ResolvedOperator { symbol, + local: None, operator_span: span, associativity, precedence, - }) + } + } +} + +/// Backticked values sit above the symbolic operator table, left-associative. +fn local_operator(local: LocalId, span: psrs_span::TextRange) -> ResolvedOperator { + ResolvedOperator { + symbol: SymbolId::new(ModuleId::INTRINSICS, u32::MAX), + local: Some(local), + operator_span: span, + associativity: Associativity::Left, + precedence: 10, } } diff --git a/crates/psrs-resolve/src/resolver/program/interface.rs b/crates/psrs-resolve/src/resolver/program/interface.rs index f8bcf093..f42cc7c3 100644 --- a/crates/psrs-resolve/src/resolver/program/interface.rs +++ b/crates/psrs-resolve/src/resolver/program/interface.rs @@ -28,6 +28,18 @@ impl Interface { class_members: HashMap::new(), opaque: HashSet::new(), }; + let registered = hir::compiler_interface(name) + .filter(|entry| entry.implementation == hir::InterfaceImplementation::Compiler); + if let Some(registered) = registered { + for &(name, intrinsic) in registered.values { + interface.values.insert(name.into(), intrinsic.symbol()); + } + for &(name, id) in registered.types { + interface + .types + .insert(name.into(), TypeReference::Named(id)); + } + } match name { "Prim" => { for &(member, builtin) in &PRIM_TYPES { @@ -42,16 +54,8 @@ impl Interface { .values .insert("undefined".to_owned(), Intrinsic::Undefined.symbol()); } - "Safe.Coerce" => { - interface - .values - .insert("coerce".to_owned(), Intrinsic::Coerce.symbol()); - interface.types.insert( - "Coercible".to_owned(), - TypeReference::Named(TypeId::COERCIBLE), - ); - } - _ if name != "Prim.Coerce" + _ if registered.is_none() + && name != "Prim.Coerce" && !hir::primitive_type_declarations() .iter() .any(|(owner, _)| *owner == name) => @@ -143,6 +147,28 @@ impl Interface { class_members.insert(exported.name.clone(), members); } } + for exported in &exports.types { + let TypeReference::Named(id) = exported.reference else { + continue; + }; + if declarations.contains_key(&id) { + continue; + } + let Some(symbols) = &exported.constructors else { + continue; + }; + let mut members = Vec::new(); + for symbol in symbols { + let Some(value) = + exports.values.iter().find(|value| value.symbol == *symbol) + else { + continue; + }; + values.entry(value.name.clone()).or_insert(*symbol); + members.push((value.name.clone(), *symbol)); + } + constructors.insert(exported.name.clone(), members); + } for operator in &exports.type_operators { types.insert(operator.name.clone(), operator.reference); if let Some(fixity) = find_fixity(module, &operator.name) { @@ -155,7 +181,7 @@ impl Interface { values.insert(declaration.name.clone(), declaration.symbol); } for external in &module.externals { - if matches!(external.kind, hir::ExternalKind::Wit { .. }) { + if external.kind.requires_checked_signature() { values.insert(external.name.clone(), external.symbol); } } diff --git a/crates/psrs-resolve/src/resolver/program/mod.rs b/crates/psrs-resolve/src/resolver/program/mod.rs index 3f855302..1740cea8 100644 --- a/crates/psrs-resolve/src/resolver/program/mod.rs +++ b/crates/psrs-resolve/src/resolver/program/mod.rs @@ -17,6 +17,12 @@ mod interface; use interface::Interface; +/// Whether the compiler provides `name` as a primitive interface rather than +/// from source. A library file with the same name must not shadow it. +pub fn compiler_provided_module(name: &str) -> bool { + Interface::primitive_module(name).is_some() +} + /// A resolution error tied to one module of a program. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ProgramError { @@ -254,12 +260,18 @@ fn build_import( symbols.push(imported(*symbol, name, name, import.span)); } for (name, reference) in &interface.types { - types.push(imported_type( + let mut imported = imported_type( *reference, name, import.span, reference_is_opaque(interface, *reference), - )); + ); + imported.constructors = interface + .constructors + .get(name) + .cloned() + .unwrap_or_default(); + types.push(imported); } } Some(list) if list.hiding => { @@ -287,12 +299,18 @@ fn build_import( } for (name, reference) in &interface.types { if !hidden.contains(name) { - types.push(imported_type( + let mut imported = imported_type( *reference, name, import.span, reference_is_opaque(interface, *reference), - )); + ); + imported.constructors = interface + .constructors + .get(name) + .cloned() + .unwrap_or_default(); + types.push(imported); } } } @@ -311,18 +329,21 @@ fn build_import( unknown_import(module_index, name, errors); continue; }; - types.push(imported_type( + let mut imported_ty = imported_type( reference, &name.text, name.span, reference_is_opaque(interface, reference), - )); + ); match members { None => {} Some(members) if members.all => { - for (member, symbol) in - interface.constructors.get(&name.text).into_iter().flatten() - { + imported_ty.constructors = interface + .constructors + .get(&name.text) + .cloned() + .unwrap_or_default(); + for (member, symbol) in &imported_ty.constructors { symbols.push(imported(*symbol, member, member, name.span)); } } @@ -335,12 +356,17 @@ fn build_import( .find(|(name, _)| *name == member.text) }); match found { - Some((name, symbol)) => symbols.push(imported( - *symbol, - name, - name, - member.span, - )), + Some((constructor_name, symbol)) => { + imported_ty + .constructors + .push((constructor_name.clone(), *symbol)); + symbols.push(imported( + *symbol, + constructor_name, + constructor_name, + member.span, + )); + } None => errors.push(ProgramError { module: module_index, error: ResolveError::named( @@ -353,6 +379,7 @@ fn build_import( } } } + types.push(imported_ty); } ast::ImportRef::Class(name) => { match interface.types.get(&name.text).copied() { @@ -454,6 +481,7 @@ fn imported_type( name: name.to_string(), span, opaque, + constructors: Vec::new(), } } diff --git a/crates/psrs-resolve/src/resolver/program/tests.rs b/crates/psrs-resolve/src/resolver/program/tests.rs index d9af0691..631ada7d 100644 --- a/crates/psrs-resolve/src/resolver/program/tests.rs +++ b/crates/psrs-resolve/src/resolver/program/tests.rs @@ -60,6 +60,15 @@ fn import(module_name: &str) -> ast::Import { } } +fn import_as(module_name: &str, alias: &str) -> ast::Import { + ast::Import { + module: name(module_name), + alias: Some(name(alias)), + list: None, + span: TextRange::new(0, 10), + } +} + fn import_list(module_name: &str, items: Vec, hiding: bool) -> ast::Import { let span = TextRange::new(0, 20); ast::Import { @@ -407,6 +416,37 @@ fn a_module_can_reexport_an_imported_operator_alias_without_its_target() { ); } +#[test] +fn a_module_reexports_all_imports_sharing_one_alias() { + let left = module("Left", Vec::new(), None, vec![value("left", integer("1"))]); + let right = module( + "Right", + Vec::new(), + None, + vec![value("right", integer("2"))], + ); + let facade = module( + "Facade", + vec![import_as("Left", "Exports"), import_as("Right", "Exports")], + Some(ast::ExportList { + items: vec![ast::ExportRef::Module(name("Exports"))], + span: TextRange::new(0, 20), + }), + Vec::new(), + ); + + let resolved = resolve_program(vec![left, right, facade]).unwrap(); + let names = resolved[2] + .exports + .as_ref() + .unwrap() + .values + .iter() + .map(|value| value.name.as_str()) + .collect::>(); + assert_eq!(names, std::collections::HashSet::from(["left", "right"])); +} + #[test] fn an_exported_operator_alias_must_export_a_local_target() { // The same shape, but the target is declared in the exporting module and is diff --git a/crates/psrs-resolve/src/resolver/tests.rs b/crates/psrs-resolve/src/resolver/tests.rs index b318da85..55e06f4f 100644 --- a/crates/psrs-resolve/src/resolver/tests.rs +++ b/crates/psrs-resolve/src/resolver/tests.rs @@ -115,7 +115,7 @@ fn resolves_bootstrap_integer_add_to_intrinsic_id() { resolve_module_with_externals(module, ModuleId(0), &bootstrap_externals()).unwrap(); assert!(matches!( resolved.declarations[0].value.kind, - ExprKind::Operator { operator, .. } if operator == Intrinsic::I32Add.symbol() + ExprKind::Operator { operator, .. } if operator == Intrinsic::IntAdd.symbol() )); resolved.verify().unwrap(); } diff --git a/crates/psrs-resolve/src/resolver/type_resolution.rs b/crates/psrs-resolve/src/resolver/type_resolution/mod.rs similarity index 89% rename from crates/psrs-resolve/src/resolver/type_resolution.rs rename to crates/psrs-resolve/src/resolver/type_resolution/mod.rs index 190bfbf5..c67849c7 100644 --- a/crates/psrs-resolve/src/resolver/type_resolution.rs +++ b/crates/psrs-resolve/src/resolver/type_resolution/mod.rs @@ -1,12 +1,17 @@ -use super::names::{Resolver, builtin_type, is_uppercase, prim_type, split_qualified}; +use super::names::{ + Resolver, builtin_type, implicit_prim_class, is_uppercase, prim_type, split_qualified, +}; use super::{PlannedType, ResolveErrorKind}; use psrs_ast as ast; use psrs_hir::{ - self as hir, ModuleId, Type as HirType, TypeDeclarationKind, TypeId, TypeKind as HirTypeKind, + self as hir, Type as HirType, TypeDeclarationKind, TypeId, TypeKind as HirTypeKind, TypeReference, }; use psrs_span::TextRange; +mod roles; +use roles::lower_role_declaration; + impl Resolver { pub(super) fn resolve_type(&mut self, expression: ast::Type) -> Option { let span = expression.span; @@ -17,14 +22,24 @@ impl Resolver { let reference = self.lookup_qualified_type(&name.text, qualifier, member, name.span)?; self.type_reference_kind(reference) + } else if name.text == "->" { + // The function arrow is a type operator, not a name imported + // from `Prim`. Keep its builtin identity independent of the + // source-level implicit import rules for `Function`. + HirTypeKind::Constructor(builtin_type("->")?) + } else if name + .text + .chars() + .next() + .is_some_and(|first| !first.is_alphabetic() && first != '_') + { + // Symbolic names are type operators. Bare and infix uses + // share alias lookup; an absent alias is a resolution error. + let reference = self.lookup_type_operator_reference(&name.text, name.span)?; + self.type_reference_kind(reference) } else if is_uppercase(&name.text) { let reference = self.lookup_type_name(&name.text, name.span)?; self.type_reference_kind(reference) - } else if matches!(name.text.as_str(), "->" | "~>") { - // Function arrows are type operators, not names imported - // from `Prim`. Keep their builtin identity independent of - // the source-level implicit import rules for `Function`. - HirTypeKind::Constructor(builtin_type(&name.text)?) } else { HirTypeKind::Variable(name.text) } @@ -86,16 +101,7 @@ impl Resolver { name: &str, span: TextRange, ) -> Option { - let reference = if let Some(fixity) = self.type_fixities.get(name) { - match fixity.target { - hir::FixityTarget::Type(reference) => reference, - hir::FixityTarget::Value(_) => unreachable!("type fixities target types"), - } - } else if let Some((qualifier, member)) = split_qualified(name) { - self.lookup_qualified_type(name, qualifier, member, span)? - } else { - self.lookup_type_name(name, span)? - }; + let reference = self.lookup_type_operator_reference(name, span)?; let (associativity, precedence) = self .type_fixities .get(name) @@ -109,6 +115,23 @@ impl Resolver { }) } + fn lookup_type_operator_reference( + &mut self, + name: &str, + span: TextRange, + ) -> Option { + if let Some(fixity) = self.type_fixities.get(name) { + match fixity.target { + hir::FixityTarget::Type(reference) => Some(reference), + hir::FixityTarget::Value(_) => unreachable!("type fixities target types"), + } + } else if let Some((qualifier, member)) = split_qualified(name) { + self.lookup_qualified_type(name, qualifier, member, span) + } else { + self.lookup_type_name(name, span) + } + } + fn resolve_type_parameter( &mut self, parameter: ast::TypeParameter, @@ -168,8 +191,11 @@ impl Resolver { self.report(ResolveErrorKind::UnknownTypeName, text.to_owned(), span); None } - (None, None) => match builtin_type(text) { - Some(builtin) => Some(TypeReference::Builtin(builtin)), + (None, None) => match builtin_type(text) + .map(TypeReference::Builtin) + .or_else(|| implicit_prim_class(text)) + { + Some(reference) => Some(reference), None => { self.report(ResolveErrorKind::UnknownTypeName, text.to_owned(), span); None @@ -190,8 +216,7 @@ impl Resolver { .iter() .any(|import| import.module_name == "Prim" && import.alias.as_deref() == Some("Prim")); let default_prim = if qualifier == "Prim" && !has_explicit_prim_qualifier { - prim_type(member) - .map(|builtin| (ModuleId::COMPILER_PRELUDE, TypeReference::Builtin(builtin))) + prim_type(member).map(TypeReference::Builtin) } else { None }; @@ -200,17 +225,15 @@ impl Resolver { self.report(ResolveErrorKind::UnknownTypeName, text.to_owned(), span); return None; } - let mut found: Option<(ModuleId, TypeReference)> = default_prim; + let mut found: Option = default_prim; let mut conflict = false; for candidate in candidates.into_iter().flatten() { let Some(reference) = candidate.types.get(member) else { continue; }; match found { - None => found = Some((candidate.module, *reference)), - Some((module, existing)) - if module != candidate.module || existing != *reference => - { + None => found = Some(*reference), + Some(existing) if existing != *reference => { conflict = true; } _ => {} @@ -220,7 +243,7 @@ impl Resolver { self.report_conflict(text.to_owned(), span); return None; } - if let Some((_, reference)) = found { + if let Some(reference) = found { return Some(reference); } self.report(ResolveErrorKind::UnknownTypeName, text.to_owned(), span); @@ -448,6 +471,7 @@ impl Resolver { name: name.text, name_span: name.span, kind, + compiler_class: None, parameters, constructors, members, @@ -460,23 +484,3 @@ impl Resolver { }) } } - -fn lower_role_declaration( - declaration: Option, -) -> Option { - declaration.map(|declaration| hir::RoleDeclaration { - roles: declaration - .roles - .into_iter() - .map(|annotation| { - let role = match annotation.role { - ast::TypeRole::Nominal => hir::Role::Nominal, - ast::TypeRole::Representational => hir::Role::Representational, - ast::TypeRole::Phantom => hir::Role::Phantom, - }; - (role, annotation.span) - }) - .collect(), - span: declaration.span, - }) -} diff --git a/crates/psrs-resolve/src/resolver/type_resolution/roles.rs b/crates/psrs-resolve/src/resolver/type_resolution/roles.rs new file mode 100644 index 00000000..ccf95326 --- /dev/null +++ b/crates/psrs-resolve/src/resolver/type_resolution/roles.rs @@ -0,0 +1,22 @@ +use psrs_ast as ast; +use psrs_hir as hir; + +pub(super) fn lower_role_declaration( + declaration: Option, +) -> Option { + declaration.map(|declaration| hir::RoleDeclaration { + roles: declaration + .roles + .into_iter() + .map(|annotation| { + let role = match annotation.role { + ast::TypeRole::Nominal => hir::Role::Nominal, + ast::TypeRole::Representational => hir::Role::Representational, + ast::TypeRole::Phantom => hir::Role::Phantom, + }; + (role, annotation.span) + }) + .collect(), + span: declaration.span, + }) +} diff --git a/crates/psrs-runtime/Cargo.toml b/crates/psrs-runtime/Cargo.toml new file mode 100644 index 00000000..d6042c20 --- /dev/null +++ b/crates/psrs-runtime/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "psrs-runtime" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[[bin]] +name = "psrs-runtime-package" +path = "tools/package.rs" +required-features = ["artifact-tool"] + +[features] +default = ["catalog"] +# Immutable host-consumed catalog data: WIT sources, world identity, artifact +# bytes and provenance. Compiles no executable target code. +catalog = [] +# The executable number module: formatting, parsing, and scalar math. +number = ["dep:ryu-js", "dep:libm"] +# The executable canonical realloc provider, built separately from number. +allocator = ["dep:dlmalloc"] +# Host-side encoding of the executable GC storage provider. Its target code +# uses Wasm GC directly, without a Rust heap or linear-memory handles. +gc-storage = ["dep:wasm-encoder"] +# Host-only build tool for reserving the numeric artifact's private stack. +artifact-tool = ["dep:wasm-encoder", "wasm-encoder/wasmparser", "dep:wasmparser"] + +[dependencies] +ryu-js = { version = "=1.0.2", default-features = false, optional = true } +libm = { version = "=0.2.15", default-features = false, optional = true } +dlmalloc = { version = "=0.2.14", default-features = false, optional = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +wasm-encoder = { workspace = true, optional = true } +wasmparser = { workspace = true, optional = true } + +[dev-dependencies] +wasm-encoder = { workspace = true, features = ["wasmparser"] } +wasmprinter.workspace = true +wasmparser.workspace = true diff --git a/crates/psrs-runtime/artifact/psrs_allocator.wasm b/crates/psrs-runtime/artifact/psrs_allocator.wasm new file mode 100644 index 00000000..84e04de5 Binary files /dev/null and b/crates/psrs-runtime/artifact/psrs_allocator.wasm differ diff --git a/crates/psrs-runtime/artifact/psrs_runtime.wasm b/crates/psrs-runtime/artifact/psrs_runtime.wasm new file mode 100755 index 00000000..168cab9e Binary files /dev/null and b/crates/psrs-runtime/artifact/psrs_runtime.wasm differ diff --git a/crates/psrs-runtime/src/abi/allocator.rs b/crates/psrs-runtime/src/abi/allocator.rs new file mode 100644 index 00000000..869489fd --- /dev/null +++ b/crates/psrs-runtime/src/abi/allocator.rs @@ -0,0 +1,27 @@ +//! Canonical allocator bindings, raw signature, and private storage. + +use super::{RawCallProtocol, RawFunctionAbi, RawType}; + +/// Private core-module import identity of the canonical allocator. +pub const ALLOCATOR_MODULE: &str = "psrs:allocator"; +/// Exported canonical realloc function. +pub const REALLOC_EXPORT: &str = "cabi_realloc"; +/// Heap-boundary getter imported from the application. +pub const HEAP_BOUNDARY_IMPORT: &str = "get_heap_base"; +/// First byte of the allocator artifact's static data. +pub const ALLOCATOR_STATIC_START: u32 = 32768; +/// Lowest permitted allocator stack address; checked frames stay above scratch. +pub const ALLOCATOR_STACK_BOTTOM: u32 = 16384; +/// Initial stack pointer and end of the allocator stack reservation. +pub const ALLOCATOR_STACK_TOP: u32 = 32768; +/// Exclusive end of the allocator static data reservation. +pub const ALLOCATOR_STATIC_END: u32 = 65536; +/// Contract heap boundary when no later reservation extends it. +pub const ALLOCATOR_HEAP_START: u32 = 65536; + +pub const ALLOCATOR_REALLOC: RawFunctionAbi = RawFunctionAbi { + export: REALLOC_EXPORT, + parameters: &[RawType::I32, RawType::I32, RawType::I32, RawType::I32], + result: Some(RawType::I32), + protocol: RawCallProtocol::Scalars, +}; diff --git a/crates/psrs-runtime/src/abi/mod.rs b/crates/psrs-runtime/src/abi/mod.rs new file mode 100644 index 00000000..3fe6bd1f --- /dev/null +++ b/crates/psrs-runtime/src/abi/mod.rs @@ -0,0 +1,45 @@ +//! Raw ABI contracts shared by target implementations and the host catalog. +//! This module contains data only and embeds no artifact or WIT bytes. + +mod allocator; +mod number; +mod storage; + +pub use allocator::*; +pub use number::*; +pub use storage::*; + +/// The runtime imports the application's canonical memory. +pub const MEMORY_MODULE: &str = "env"; +/// The runtime's memory import field. +pub const MEMORY_FIELD: &str = "memory"; +/// Core import namespace used by composition for application exports. +pub const APPLICATION_MODULE: &str = "__main_module__"; + +/// Raw scalar types in the runtime's core-Wasm function ABI. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RawType { + I32, + I64, + F32, + F64, +} + +/// Language-value transport and normal-return ownership for a raw export. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RawCallProtocol { + /// Scalar arguments/results only; Unit results use a void export. + Scalars, + /// Borrowed canonical UTF-8 bytes; caller copies, then releases on return. + Utf8Input, + /// Caller-owned bounded UTF-8 output; caller recovers and releases on return. + Utf8Output { capacity: usize }, +} + +/// An artifact export's raw signature and language-value transport protocol. +pub struct RawFunctionAbi { + pub export: &'static str, + pub parameters: &'static [RawType], + pub result: Option, + pub protocol: RawCallProtocol, +} diff --git a/crates/psrs-runtime/src/abi/number.rs b/crates/psrs-runtime/src/abi/number.rs new file mode 100644 index 00000000..264af8b3 --- /dev/null +++ b/crates/psrs-runtime/src/abi/number.rs @@ -0,0 +1,189 @@ +//! Numeric export identities, transport contracts, and private storage. + +use super::{RawCallProtocol, RawFunctionAbi, RawType}; + +/// Maximum capacity required by an ECMAScript binary64 token. +pub const NUMBER_CAPACITY: usize = 32; +/// Private core-module import identity used by the compiler. +pub const MODULE_NAME: &str = "psrs:runtime"; +/// Exported raw formatting function. +pub const NUMBER_EXPORT: &str = "number_to_string"; +/// Exported raw complete-decimal conversion function. +pub const DECIMAL_EXPORT: &str = "number_from_decimal"; +/// Exported raw inverse-cosine function. +pub const ACOS_EXPORT: &str = "number_acos"; +/// Exported raw inverse-sine function. +pub const ASIN_EXPORT: &str = "number_asin"; +/// Exported raw inverse-tangent function. +pub const ATAN_EXPORT: &str = "number_atan"; +/// Exported raw four-quadrant inverse-tangent function. +pub const ATAN2_EXPORT: &str = "number_atan2"; +/// Exported raw sine function. +pub const SIN_EXPORT: &str = "number_sin"; +/// Exported raw cosine function. +pub const COS_EXPORT: &str = "number_cos"; +/// Exported raw tangent function. +pub const TAN_EXPORT: &str = "number_tan"; +/// Exported raw base-e exponential function. +pub const EXP_EXPORT: &str = "number_exp"; +/// Exported raw natural-logarithm function. +pub const LOG_EXPORT: &str = "number_log"; +/// Exported raw exponentiation function. +pub const POW_EXPORT: &str = "number_pow"; +/// Exported raw minimum function. +pub const MIN_EXPORT: &str = "number_min"; +/// Exported raw maximum function. +pub const MAX_EXPORT: &str = "number_max"; +/// Exported raw sign function. +pub const SIGN_EXPORT: &str = "number_sign"; +/// Exported raw JavaScript remainder function. +pub const REMAINDER_EXPORT: &str = "number_remainder"; +/// Exported raw NaN predicate. The result is an `i32` 0 or 1. +pub const IS_NAN_EXPORT: &str = "number_is_nan"; +/// Exported raw canonical NaN. +pub const NAN_EXPORT: &str = "number_nan"; +/// Exported raw positive infinity. +pub const INFINITY_EXPORT: &str = "number_infinity"; +/// Lower addresses remain owned by the application's canonical ABI. +pub const RESERVED_START: u32 = 65536; +/// Static data must end before the separately reserved 64 KiB stack. +pub const STACK_BOTTOM: u32 = 131072; +/// The caller's allocator begins after the private stack. +pub const HEAP_START: u32 = 196608; +/// The runtime exports its heap boundary for the preparer to relocate. +pub const HEAP_BASE_EXPORT: &str = "__heap_base"; + +pub const NUMBER_FORMAT: RawFunctionAbi = RawFunctionAbi { + export: NUMBER_EXPORT, + parameters: &[RawType::F64, RawType::I32, RawType::I32], + result: Some(RawType::I32), + protocol: RawCallProtocol::Utf8Output { + capacity: NUMBER_CAPACITY, + }, +}; + +pub const NUMBER_PARSE: RawFunctionAbi = RawFunctionAbi { + export: DECIMAL_EXPORT, + parameters: &[RawType::I32, RawType::I32], + result: Some(RawType::F64), + protocol: RawCallProtocol::Utf8Input, +}; + +pub const NUMBER_ACOS: RawFunctionAbi = RawFunctionAbi { + export: ACOS_EXPORT, + parameters: &[RawType::F64], + result: Some(RawType::F64), + protocol: RawCallProtocol::Scalars, +}; + +pub const NUMBER_ASIN: RawFunctionAbi = RawFunctionAbi { + export: ASIN_EXPORT, + parameters: &[RawType::F64], + result: Some(RawType::F64), + protocol: RawCallProtocol::Scalars, +}; + +pub const NUMBER_ATAN: RawFunctionAbi = RawFunctionAbi { + export: ATAN_EXPORT, + parameters: &[RawType::F64], + result: Some(RawType::F64), + protocol: RawCallProtocol::Scalars, +}; + +pub const NUMBER_ATAN2: RawFunctionAbi = RawFunctionAbi { + export: ATAN2_EXPORT, + parameters: &[RawType::F64, RawType::F64], + result: Some(RawType::F64), + protocol: RawCallProtocol::Scalars, +}; + +pub const NUMBER_SIN: RawFunctionAbi = RawFunctionAbi { + export: SIN_EXPORT, + parameters: &[RawType::F64], + result: Some(RawType::F64), + protocol: RawCallProtocol::Scalars, +}; + +pub const NUMBER_COS: RawFunctionAbi = RawFunctionAbi { + export: COS_EXPORT, + parameters: &[RawType::F64], + result: Some(RawType::F64), + protocol: RawCallProtocol::Scalars, +}; + +pub const NUMBER_TAN: RawFunctionAbi = RawFunctionAbi { + export: TAN_EXPORT, + parameters: &[RawType::F64], + result: Some(RawType::F64), + protocol: RawCallProtocol::Scalars, +}; + +pub const NUMBER_EXP: RawFunctionAbi = RawFunctionAbi { + export: EXP_EXPORT, + parameters: &[RawType::F64], + result: Some(RawType::F64), + protocol: RawCallProtocol::Scalars, +}; + +pub const NUMBER_LOG: RawFunctionAbi = RawFunctionAbi { + export: LOG_EXPORT, + parameters: &[RawType::F64], + result: Some(RawType::F64), + protocol: RawCallProtocol::Scalars, +}; + +pub const NUMBER_POW: RawFunctionAbi = RawFunctionAbi { + export: POW_EXPORT, + parameters: &[RawType::F64, RawType::F64], + result: Some(RawType::F64), + protocol: RawCallProtocol::Scalars, +}; + +pub const NUMBER_MIN: RawFunctionAbi = RawFunctionAbi { + export: MIN_EXPORT, + parameters: &[RawType::F64, RawType::F64], + result: Some(RawType::F64), + protocol: RawCallProtocol::Scalars, +}; + +pub const NUMBER_MAX: RawFunctionAbi = RawFunctionAbi { + export: MAX_EXPORT, + parameters: &[RawType::F64, RawType::F64], + result: Some(RawType::F64), + protocol: RawCallProtocol::Scalars, +}; + +pub const NUMBER_SIGN: RawFunctionAbi = RawFunctionAbi { + export: SIGN_EXPORT, + parameters: &[RawType::F64], + result: Some(RawType::F64), + protocol: RawCallProtocol::Scalars, +}; + +pub const NUMBER_REMAINDER: RawFunctionAbi = RawFunctionAbi { + export: REMAINDER_EXPORT, + parameters: &[RawType::F64, RawType::F64], + result: Some(RawType::F64), + protocol: RawCallProtocol::Scalars, +}; + +pub const NUMBER_IS_NAN: RawFunctionAbi = RawFunctionAbi { + export: IS_NAN_EXPORT, + parameters: &[RawType::F64], + result: Some(RawType::I32), + protocol: RawCallProtocol::Scalars, +}; + +pub const NUMBER_NAN: RawFunctionAbi = RawFunctionAbi { + export: NAN_EXPORT, + parameters: &[], + result: Some(RawType::F64), + protocol: RawCallProtocol::Scalars, +}; + +pub const NUMBER_INFINITY: RawFunctionAbi = RawFunctionAbi { + export: INFINITY_EXPORT, + parameters: &[], + result: Some(RawType::F64), + protocol: RawCallProtocol::Scalars, +}; diff --git a/crates/psrs-runtime/src/abi/storage/mod.rs b/crates/psrs-runtime/src/abi/storage/mod.rs new file mode 100644 index 00000000..affe6e24 --- /dev/null +++ b/crates/psrs-runtime/src/abi/storage/mod.rs @@ -0,0 +1,113 @@ +//! GC storage ABI, separate from scalar/linear-memory artifact contracts. +//! Operands and results describe only storage values, without execution tokens. + +mod projection; +pub use projection::*; + +pub const STORAGE_MODULE: &str = "psrs:runtime-storage"; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum StorageType { + I32, + /// A non-null mutable Wasm GC array of nullable eqref values. + Array, + /// A nullable eqref; checked callers own boxing and payload validation. + Value, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum StorageOperation { + Fill, + Read, + Write, + Trap, +} + +/// Checked source-value relations before GC boxing and ABI conversion. +/// Element occurrences in one binding must denote the same source type. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum StorageValue { + Int, + Unit, + Element, + Array, + /// Only a non-returning operation may have an arbitrary payload type. + Any, +} + +pub struct StorageSourceContract { + /// Complete source operands corresponding to the raw ABI parameters. + pub parameters: &'static [StorageValue], + pub payload: StorageValue, +} + +pub struct StorageAbi { + pub export: &'static str, + pub parameters: &'static [StorageType], + pub result: Option, + /// A trap operation has no normal return, despite its physical void type. + pub returns_normally: bool, +} + +impl StorageOperation { + pub const ALL: [Self; 4] = [Self::Fill, Self::Read, Self::Write, Self::Trap]; + + pub fn from_export(export: &str) -> Option { + Self::ALL + .into_iter() + .find(|operation| operation.abi().export == export) + } + + /// Source element occurrences must share the same checked type. + pub const fn source_contract(self) -> StorageSourceContract { + use StorageValue::*; + match self { + Self::Fill => StorageSourceContract { + parameters: &[Int, Element], + payload: Array, + }, + Self::Read => StorageSourceContract { + parameters: &[Array, Int], + payload: Element, + }, + Self::Write => StorageSourceContract { + parameters: &[Array, Int, Element], + payload: Unit, + }, + Self::Trap => StorageSourceContract { + parameters: &[], + payload: Any, + }, + } + } + + pub const fn abi(self) -> StorageAbi { + use StorageType::*; + match self { + Self::Fill => StorageAbi { + export: "array_fill", + parameters: &[I32, Value], + result: Some(Array), + returns_normally: true, + }, + Self::Read => StorageAbi { + export: "array_read", + parameters: &[Array, I32], + result: Some(Value), + returns_normally: true, + }, + Self::Write => StorageAbi { + export: "array_write", + parameters: &[Array, I32, Value], + result: None, + returns_normally: true, + }, + Self::Trap => StorageAbi { + export: "trap", + parameters: &[], + result: None, + returns_normally: false, + }, + } + } +} diff --git a/crates/psrs-runtime/src/abi/storage/projection.rs b/crates/psrs-runtime/src/abi/storage/projection.rs new file mode 100644 index 00000000..b1d3c04c --- /dev/null +++ b/crates/psrs-runtime/src/abi/storage/projection.rs @@ -0,0 +1,147 @@ +//! Checked source-to-raw storage call contracts, with no compiler IR dependency. +use super::{StorageAbi, StorageOperation, StorageSourceContract, StorageType, StorageValue}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum StorageResultProjection { + /// The caller recovers a checked payload from the declared raw result. + Value(StorageType), + /// The export returns normally without a value. The caller produces Unit. + Unit, + /// The export cannot return normally; no result is produced. + Never, +} + +/// Validated correspondence between source operands and raw ABI operands. +/// Consumers still own checked element-type correspondence, boxing and recovery, +/// and mutable array identity. The raw ABI alone does not prove those facts. +pub struct StorageCallProjection { + source: StorageSourceContract, + abi: StorageAbi, + result: StorageResultProjection, +} + +impl StorageCallProjection { + pub fn checked(source: StorageSourceContract, abi: StorageAbi) -> Result { + if source.parameters.len() != abi.parameters.len() { + return Err("runtime source and physical argument counts disagree"); + } + for (source, physical) in source.parameters.iter().zip(abi.parameters) { + if !matches!( + (source, physical), + (StorageValue::Int, StorageType::I32) + | (StorageValue::Array, StorageType::Array) + | (StorageValue::Element, StorageType::Value) + ) { + return Err("runtime source operand has no declared raw storage projection"); + } + } + let result = if !abi.returns_normally { + if abi.result.is_some() || source.payload != StorageValue::Any { + return Err( + "non-returning storage export must have no raw result and an arbitrary source payload", + ); + } + StorageResultProjection::Never + } else { + match (source.payload, abi.result) { + (StorageValue::Int, Some(StorageType::I32)) => { + StorageResultProjection::Value(StorageType::I32) + } + (StorageValue::Array, Some(StorageType::Array)) => { + StorageResultProjection::Value(StorageType::Array) + } + (StorageValue::Element, Some(StorageType::Value)) => { + StorageResultProjection::Value(StorageType::Value) + } + (StorageValue::Unit, None) => StorageResultProjection::Unit, + _ => return Err("runtime source payload has no declared raw result projection"), + } + }; + Ok(Self { + source, + abi, + result, + }) + } + + pub fn source(&self) -> &StorageSourceContract { + &self.source + } + pub fn abi(&self) -> &StorageAbi { + &self.abi + } + pub fn result(&self) -> StorageResultProjection { + self.result + } + pub fn operands(&self) -> impl ExactSizeIterator + '_ { + self.source + .parameters + .iter() + .copied() + .zip(self.abi.parameters.iter().copied()) + } +} + +impl StorageOperation { + pub fn projection(self) -> Result { + StorageCallProjection::checked(self.source_contract(), self.abi()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_storage_export_has_an_explicit_result_projection() { + for (operation, result) in [ + ( + StorageOperation::Fill, + StorageResultProjection::Value(StorageType::Array), + ), + ( + StorageOperation::Read, + StorageResultProjection::Value(StorageType::Value), + ), + (StorageOperation::Write, StorageResultProjection::Unit), + (StorageOperation::Trap, StorageResultProjection::Never), + ] { + let plan = operation.projection().unwrap(); + assert_eq!(plan.result(), result); + assert_eq!( + plan.operands().len(), + operation.source_contract().parameters.len() + ); + assert_eq!(plan.abi().export, operation.abi().export); + } + } + + #[test] + fn rejects_operand_and_payload_contract_drift() { + let mut abi = StorageOperation::Fill.abi(); + abi.parameters = &[StorageType::Value, StorageType::I32]; + assert!( + StorageCallProjection::checked(StorageOperation::Fill.source_contract(), abi).is_err() + ); + let mut abi = StorageOperation::Write.abi(); + abi.result = Some(StorageType::I32); + assert!( + StorageCallProjection::checked(StorageOperation::Write.source_contract(), abi).is_err() + ); + let mut abi = StorageOperation::Trap.abi(); + abi.returns_normally = true; + assert!( + StorageCallProjection::checked(StorageOperation::Trap.source_contract(), abi).is_err() + ); + let mut abi = StorageOperation::Trap.abi(); + abi.result = Some(StorageType::Value); + assert!( + StorageCallProjection::checked(StorageOperation::Trap.source_contract(), abi).is_err() + ); + let mut abi = StorageOperation::Read.abi(); + abi.parameters = &[StorageType::Array]; + assert!( + StorageCallProjection::checked(StorageOperation::Read.source_contract(), abi).is_err() + ); + } +} diff --git a/crates/psrs-runtime/src/allocator/algorithm.rs b/crates/psrs-runtime/src/allocator/algorithm.rs new file mode 100644 index 00000000..9ffe1fc0 --- /dev/null +++ b/crates/psrs-runtime/src/allocator/algorithm.rs @@ -0,0 +1,298 @@ +//! Canonical `cabi_realloc` steps over a caller-supplied backing store. +//! +//! Live records sit in a prefix of each backing allocation. Lookup walks those +//! trusted records. It does not form a header address from the caller's pointer. + +const RECORD: u32 = 20; + +pub trait Store { + fn allocate(&mut self, size: u32, align: u32) -> Option; + fn release(&mut self, pointer: u32, size: u32, align: u32); + fn read(&self, address: u32) -> u32; + fn write(&mut self, address: u32, value: u32); + fn copy(&mut self, from: u32, to: u32, len: u32); +} + +#[derive(Clone, Copy)] +struct Live { + backing: u32, + next: u32, + user: u32, + requested: u32, + size: u32, + align: u32, +} + +/// Runs one canonical call with unsigned wasm32 addresses and byte lengths. +/// `Err` is a trap. `Ok` is the returned pointer, including addresses above 2 GiB. +pub fn realloc( + store: &mut impl Store, + head: &mut u32, + old_ptr: u32, + old_len: u32, + align: u32, + new_len: u32, +) -> Result { + let align = canonical_align(align)?; + if new_len == 0 { + if old_ptr != 0 { + let live = find(store, *head, old_ptr)?; + if live.requested != old_len { + return Err(()); + } + unlink(store, head, live.backing); + store.release(live.backing, live.size, live.align); + } + return Ok(0); + } + if old_ptr == 0 { + if old_len != 0 { + return Err(()); + } + return insert(store, head, align, new_len); + } + let live = find(store, *head, old_ptr)?; + if live.requested != old_len { + return Err(()); + } + let user = insert(store, head, align, new_len)?; + store.copy(live.user, user, old_len.min(new_len)); + unlink(store, head, live.backing); + store.release(live.backing, live.size, live.align); + Ok(user) +} + +fn insert(store: &mut impl Store, head: &mut u32, align: u32, len: u32) -> Result { + let backing_align = align.max(4); + let user_offset = provision::align_u32(RECORD, align)?; + let size = user_offset.checked_add(len).ok_or(())?; + let backing = store.allocate(size, backing_align).ok_or(())?; + let user = backing.checked_add(user_offset).ok_or(())?; + if user == 0 { + store.release(backing, size, backing_align); + return Err(()); + } + store.write(backing, *head); + store.write(backing + 4, user); + store.write(backing + 8, len); + store.write(backing + 12, size); + store.write(backing + 16, backing_align); + *head = backing; + Ok(user) +} + +fn find(store: &impl Store, head: u32, old_ptr: u32) -> Result { + if old_ptr == 0 { + return Err(()); + } + let mut backing = head; + while backing != 0 { + let live = read_live(store, backing)?; + if live.user == old_ptr { + return Ok(live); + } + backing = live.next; + } + Err(()) +} + +fn read_live(store: &impl Store, backing: u32) -> Result { + Ok(Live { + backing, + next: store.read(backing), + user: store.read(backing + 4), + requested: store.read(backing + 8), + size: store.read(backing + 12), + align: store.read(backing + 16), + }) +} + +fn unlink(store: &mut impl Store, head: &mut u32, backing: u32) { + if *head == backing { + *head = store.read(backing); + return; + } + let mut previous = *head; + while previous != 0 { + let next = store.read(previous); + if next == backing { + store.write(previous, store.read(backing)); + return; + } + previous = next; + } +} + +fn canonical_align(align: u32) -> Result { + if align.is_power_of_two() { + Ok(align) + } else { + Err(()) + } +} + +pub mod provision { + pub fn align_u32(value: u32, align: u32) -> Result { + if align == 0 || !align.is_power_of_two() { + return Err(()); + } + let padded = value.checked_add(align - 1).ok_or(())?; + Ok(padded & !(align - 1)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct Heap { + bytes: Vec, + cursor: u32, + fail_after: u32, + allocs: u32, + } + + impl Heap { + fn new() -> Self { + Self { + bytes: vec![0; 64], + cursor: 64, + fail_after: u32::MAX, + allocs: 0, + } + } + } + + impl Store for Heap { + fn allocate(&mut self, size: u32, align: u32) -> Option { + self.allocs += 1; + if self.allocs > self.fail_after { + return None; + } + let start = provision::align_u32(self.cursor, align).ok()?; + let end = start.checked_add(size)?; + self.bytes.resize(end as usize, 0); + self.cursor = end; + Some(start) + } + + fn release(&mut self, _: u32, _: u32, _: u32) {} + + fn read(&self, address: u32) -> u32 { + let offset = address as usize; + u32::from_le_bytes(self.bytes[offset..offset + 4].try_into().unwrap()) + } + + fn write(&mut self, address: u32, value: u32) { + let offset = address as usize; + self.bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); + } + + fn copy(&mut self, from: u32, to: u32, len: u32) { + for index in 0..len { + let byte = self.bytes[(from + index) as usize]; + self.bytes[(to + index) as usize] = byte; + } + } + } + + #[test] + fn alignment_resize_copy_and_rejection_share_one_registry() { + let mut heap = Heap::new(); + let mut head = 0; + let first = realloc(&mut heap, &mut head, 0, 0, 8, 4).unwrap(); + heap.write(first, u32::from_le_bytes(*b"abcd")); + assert_eq!(first & 7, 0); + let resized = realloc(&mut heap, &mut head, first, 4, 32, 4).unwrap(); + assert_eq!(resized & 31, 0); + assert_eq!(&heap.bytes[resized as usize..resized as usize + 4], b"abcd"); + assert!(realloc(&mut heap, &mut head, 0, 0, 3, 4).is_err()); + assert!(realloc(&mut heap, &mut head, 0, 1, 8, 4).is_err()); + assert!(realloc(&mut heap, &mut head, 32, 4, 8, 4).is_err()); + assert!(realloc(&mut heap, &mut head, resized, 3, 8, 4).is_err()); + assert_eq!(realloc(&mut heap, &mut head, resized, 4, 8, 0).unwrap(), 0); + let again = realloc(&mut heap, &mut head, 0, 0, 8, 4).unwrap(); + assert_ne!(again, 0); + } + + #[test] + fn a_failed_replacement_keeps_the_old_allocation() { + let mut heap = Heap::new(); + heap.fail_after = 1; + let mut head = 0; + let first = realloc(&mut heap, &mut head, 0, 0, 8, 4).unwrap(); + heap.bytes[first as usize] = 9; + assert!(realloc(&mut heap, &mut head, first, 4, 8, 8).is_err()); + assert_eq!(heap.bytes[first as usize], 9); + assert_eq!(head, first - provision::align_u32(RECORD, 8).unwrap()); + } + + // Sparse bookkeeping exercises unsigned addresses/lengths without reserving + // gigabytes of host memory. Copies are recorded rather than materialized. + struct SparseHeap { + cursor: u32, + words: std::collections::BTreeMap, + copies: Vec<(u32, u32, u32)>, + released: Vec, + } + + impl SparseHeap { + fn at(cursor: u32) -> Self { + Self { + cursor, + words: Default::default(), + copies: Vec::new(), + released: Vec::new(), + } + } + } + + impl Store for SparseHeap { + fn allocate(&mut self, size: u32, align: u32) -> Option { + let address = provision::align_u32(self.cursor, align).ok()?; + self.cursor = address.checked_add(size)?; + Some(address) + } + fn release(&mut self, pointer: u32, _: u32, _: u32) { + self.released.push(pointer); + } + fn read(&self, address: u32) -> u32 { + self.words[&address] + } + fn write(&mut self, address: u32, value: u32) { + self.words.insert(address, value); + } + fn copy(&mut self, from: u32, to: u32, len: u32) { + self.copies.push((from, to, len)); + } + } + + #[test] + fn high_bit_addresses_can_be_resized_and_freed() { + let mut heap = SparseHeap::at(0x8000_0000); + let mut head = 0; + let first = realloc(&mut heap, &mut head, 0, 0, 4, 8).unwrap(); + assert_eq!(first, 0x8000_0014); + assert!(realloc(&mut heap, &mut head, first + 1, 8, 4, 0).is_err()); + let resized = realloc(&mut heap, &mut head, first, 8, 8, 16).unwrap(); + assert_eq!(heap.copies, vec![(first, resized, 8)]); + assert_eq!(heap.released, vec![0x8000_0000]); + assert_eq!(realloc(&mut heap, &mut head, resized, 16, 8, 0), Ok(0)); + assert_eq!(head, 0); + assert_eq!(heap.released.len(), 2); + assert!(realloc(&mut heap, &mut head, resized, 16, 8, 0).is_err()); + } + + #[test] + fn unsigned_lengths_are_preserved_and_overflow_is_rejected() { + let mut heap = SparseHeap::at(64); + let mut head = 0; + let first = realloc(&mut heap, &mut head, 0, 0, 4, 0x8000_0000).unwrap(); + let resized = realloc(&mut heap, &mut head, first, 0x8000_0000, 4, 1).unwrap(); + assert_eq!(heap.copies, vec![(first, resized, 1)]); + assert_eq!(realloc(&mut heap, &mut head, resized, 1, 4, 0), Ok(0)); + assert_eq!(realloc(&mut heap, &mut head, 0, 0, 0x8000_0000, 0), Ok(0)); + assert!(realloc(&mut heap, &mut head, 0, 0, 4, u32::MAX).is_err()); + assert_eq!(head, 0); + } +} diff --git a/crates/psrs-runtime/src/allocator/mod.rs b/crates/psrs-runtime/src/allocator/mod.rs new file mode 100644 index 00000000..0bba987d --- /dev/null +++ b/crates/psrs-runtime/src/allocator/mod.rs @@ -0,0 +1,8 @@ +//! Canonical allocation: portable validation and bookkeeping, plus a Wasm adapter. + +mod algorithm; +mod phase; +mod provision; + +#[cfg(target_arch = "wasm32")] +mod wasm; diff --git a/crates/psrs-runtime/src/allocator/phase.rs b/crates/psrs-runtime/src/allocator/phase.rs new file mode 100644 index 00000000..47467375 --- /dev/null +++ b/crates/psrs-runtime/src/allocator/phase.rs @@ -0,0 +1,40 @@ +//! Lazy provider phases. Reentry while initializing or dispatching traps. + +pub const UNINITIALIZED: u8 = 0; +pub const BUSY: u8 = 1; +pub const READY: u8 = 2; + +/// Moves `Uninitialized` to `Busy`. A second entry while busy is reentry. +pub fn enter(phase: &mut u8) -> Result { + match *phase { + UNINITIALIZED => { + *phase = BUSY; + Ok(true) + } + READY => { + *phase = BUSY; + Ok(false) + } + _ => Err(()), + } +} + +pub fn ready(phase: &mut u8) { + *phase = READY; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_first_call_initializes_and_reentry_traps() { + let mut phase = UNINITIALIZED; + assert_eq!(enter(&mut phase), Ok(true)); + assert_eq!(enter(&mut phase), Err(())); + ready(&mut phase); + assert_eq!(enter(&mut phase), Ok(false)); + ready(&mut phase); + assert_eq!(phase, READY); + } +} diff --git a/crates/psrs-runtime/src/allocator/provision.rs b/crates/psrs-runtime/src/allocator/provision.rs new file mode 100644 index 00000000..9f8df9e2 --- /dev/null +++ b/crates/psrs-runtime/src/allocator/provision.rs @@ -0,0 +1,60 @@ +//! Checked acquisition of fresh segments up to `memory.size`, then `memory.grow`. +//! +//! The cursor advances only after a segment is published. A failed grow leaves +//! the cursor where it was. Returned pages are never shrunk. + +pub const PAGE: usize = 65536; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Segment { + pub start: usize, + pub bytes: usize, + pub end: usize, + pub pages: usize, +} + +/// Plans a page-aligned segment at or above `cursor`. +/// +/// `pages` is zero when the current memory already covers the segment. +pub fn plan(cursor: usize, request: usize, mem_size: usize) -> Result { + if request == 0 || !PAGE.is_power_of_two() { + return Err(()); + } + let start = align_up(cursor, PAGE)?; + let bytes = align_up(request, PAGE)?; + let end = start.checked_add(bytes).ok_or(())?; + let pages = if end > mem_size { + end.checked_sub(mem_size).ok_or(())?.div_ceil(PAGE) + } else { + 0 + }; + Ok(Segment { + start, + bytes, + end, + pages, + }) +} + +pub fn align_up(value: usize, align: usize) -> Result { + if align == 0 || !align.is_power_of_two() { + return Err(()); + } + let padded = value.checked_add(align - 1).ok_or(())?; + Ok(padded & !(align - 1)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn an_in_range_segment_does_not_grow_or_move_the_failed_cursor() { + let segment = plan(196_608, 100, 262_144).unwrap(); + assert_eq!(segment.pages, 0); + assert_eq!(segment.start, 196_608); + assert_eq!(segment.bytes, PAGE); + assert!(plan(196_608, PAGE, 196_608).unwrap().pages >= 1); + assert!(plan(usize::MAX - 8, PAGE, usize::MAX).is_err()); + } +} diff --git a/crates/psrs-runtime/src/allocator/wasm.rs b/crates/psrs-runtime/src/allocator/wasm.rs new file mode 100644 index 00000000..f66c7993 --- /dev/null +++ b/crates/psrs-runtime/src/allocator/wasm.rs @@ -0,0 +1,171 @@ +//! Canonical realloc adapter over one `dlmalloc` 0.2.14 instance. +//! +//! The instance is created on the first call, after the heap-boundary import +//! can be read. No start function allocates. Reentry while the instance is +//! borrowed traps. + +use super::{algorithm, phase, provision}; + +use core::cell::Cell; +use dlmalloc::{Allocator, Dlmalloc}; + +const PAGE: usize = provision::PAGE; + +struct Provision { + cursor: Cell, +} + +unsafe impl Send for Provision {} + +unsafe impl Allocator for Provision { + fn alloc(&self, size: usize) -> (*mut u8, usize, u32) { + let cursor = self.cursor.get(); + let Ok(segment) = provision::plan(cursor, size, memory_bytes()) else { + return (core::ptr::null_mut(), 0, 0); + }; + if segment.pages > 0 { + let grown = core::arch::wasm32::memory_grow(0, segment.pages); + if grown == usize::MAX || segment.end > memory_bytes() { + return (core::ptr::null_mut(), 0, 0); + } + } + self.cursor.set(segment.end); + (segment.start as *mut u8, segment.bytes, 0) + } + + fn remap(&self, _: *mut u8, _: usize, _: usize, _: bool) -> *mut u8 { + core::ptr::null_mut() + } + + fn free_part(&self, _: *mut u8, _: usize, _: usize) -> bool { + false + } + + fn free(&self, _: *mut u8, _: usize) -> bool { + false + } + + fn can_release_part(&self, _: u32) -> bool { + false + } + + fn allocates_zeros(&self) -> bool { + true + } + + fn page_size(&self) -> usize { + PAGE + } +} + +struct WasmStore<'a> { + alloc: &'a mut Dlmalloc, +} + +impl algorithm::Store for WasmStore<'_> { + fn allocate(&mut self, size: u32, align: u32) -> Option { + // Rust allocations cannot exceed isize::MAX even though the canonical + // ABI accepts unsigned lengths. Reject such requests as allocation + // failures, without treating the high bit of an address as a sign. + if size > isize::MAX as u32 || align > isize::MAX as u32 { + return None; + } + let pointer = unsafe { self.alloc.malloc(size as usize, align as usize) }; + if pointer.is_null() { + None + } else { + Some(pointer as u32) + } + } + + fn release(&mut self, pointer: u32, size: u32, align: u32) { + unsafe { + self.alloc + .free(pointer as *mut u8, size as usize, align as usize); + } + } + + fn read(&self, address: u32) -> u32 { + unsafe { core::ptr::read_unaligned(address as *const u32) } + } + + fn write(&mut self, address: u32, value: u32) { + unsafe { core::ptr::write_unaligned(address as *mut u32, value) } + } + + fn copy(&mut self, from: u32, to: u32, len: u32) { + if len == 0 || from == to { + return; + } + unsafe { + core::ptr::copy(from as *const u8, to as *mut u8, len as usize); + } + } +} + +// The application supplies the final planned boundary through a real Wasm import. +#[link(wasm_import_module = "__main_module__")] +unsafe extern "C" { + fn get_heap_base() -> u32; +} + +// Keep one initialized storage object: its active data segment initializes +// every field without an executable BSS-zeroing start function. +struct State { + phase: u8, + head: u32, + alloc: Dlmalloc, +} + +static mut STATE: State = State { + phase: phase::UNINITIALIZED, + head: 0, + alloc: Dlmalloc::new_with_allocator(Provision { + cursor: Cell::new(0), + }), +}; + +fn memory_bytes() -> usize { + // A full wasm32 memory is 4 GiB, whose byte count exceeds usize::MAX. + // Saturation preserves coverage checks; segment planning still rejects + // ranges whose exclusive end cannot be represented. + core::arch::wasm32::memory_size(0).saturating_mul(PAGE) +} + +fn initialize(alloc: &mut Dlmalloc) -> Result<(), ()> { + let heap = unsafe { get_heap_base() } as usize; + if heap == 0 || !heap.is_multiple_of(8) || heap > memory_bytes() { + return Err(()); + } + alloc.allocator_mut().cursor.set(heap); + Ok(()) +} + +/// Canonical realloc export. A trap leaves no published replacement pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn cabi_realloc(old_ptr: u32, old_len: u32, align: u32, new_len: u32) -> u32 { + unsafe { + let state = &mut *core::ptr::addr_of_mut!(STATE); + let initializing = phase::enter(&mut state.phase).unwrap_or_else(|_| { + core::arch::wasm32::unreachable(); + }); + if initializing { + if initialize(&mut state.alloc).is_err() { + core::arch::wasm32::unreachable(); + } + } + let mut store = WasmStore { + alloc: &mut state.alloc, + }; + let result = algorithm::realloc( + &mut store, + &mut state.head, + old_ptr, + old_len, + align, + new_len, + ); + phase::ready(&mut state.phase); + result.unwrap_or_else(|_| core::arch::wasm32::unreachable()) + } +} diff --git a/crates/psrs-runtime/src/catalog/allocator.rs b/crates/psrs-runtime/src/catalog/allocator.rs new file mode 100644 index 00000000..ed6dffcb --- /dev/null +++ b/crates/psrs-runtime/src/catalog/allocator.rs @@ -0,0 +1,107 @@ +//! Pinned canonical allocator artifact and its state ownership. + +use super::model::*; +use super::{VERSION, op}; +use crate::abi::RawType; + +/// Canonical realloc, produced by `tools/build-allocator.sh`. +/// +/// Static data and the stack occupy the hole below the numeric runtime. The +/// imported `get_heap_base` function carries the planned shared heap boundary, +/// so this artifact stays pinned when that boundary moves. +pub const ALLOCATOR_RUNTIME: RuntimeArtifact = RuntimeArtifact { + id: "psrs:runtime-allocator", + module_name: crate::abi::ALLOCATOR_MODULE, + bytes: include_bytes!("../../artifact/psrs_allocator.wasm"), + provenance: ArtifactProvenance { + dependency: "dlmalloc", + dependency_revision: "0.2.14 (ad5208a115eaba24916f7456929832e310a81518c641f93fee4f89aa93aa3675)", + rust_toolchain: "1.99.0 (b940084d7 2026-09-28)", + target: "wasm32-unknown-unknown", + profile: "target-runtime", + recipe: "tools/build-allocator.sh: remap-path-prefix, no-redzone, --import-memory \ + --stack-first -zstack-size=32768 --global-base=32768; direct Rust output, \ + imports __main_module__.get_heap_base, no executable start", + sha256: "4eca097da7112522a23cd790d7562d643090ac7d5f63aa493e853b23925dfb23", + }, + required_features: &[ + "mutable-globals", + "sign-extension", + "multi-value", + "bulk-memory", + "reference-types", + ], + imports: &[ + RawImport { + module: crate::abi::MEMORY_MODULE, + field: crate::abi::MEMORY_FIELD, + kind: RawImportKind::Memory { + minimum: 1, + maximum: None, + }, + }, + RawImport { + module: crate::abi::APPLICATION_MODULE, + field: crate::abi::HEAP_BOUNDARY_IMPORT, + kind: RawImportKind::Function { + parameters: &[], + result: Some(RawType::I32), + }, + }, + ], + function_exports: &[RawExport { + name: crate::abi::REALLOC_EXPORT, + parameters: &[RawType::I32, RawType::I32, RawType::I32, RawType::I32], + result: Some(RawType::I32), + }], + global_exports: &[], + tables: &[RawTable { + element: "funcref", + minimum: 1, + maximum: Some(1), + }], + elements: &[], + globals: &[(true, crate::abi::ALLOCATOR_STACK_TOP)], + storage: RawStorage { + static_data: ( + crate::abi::ALLOCATOR_STATIC_START, + crate::abi::ALLOCATOR_STATIC_END, + ), + stack: ( + crate::abi::ALLOCATOR_STACK_BOTTOM, + crate::abi::ALLOCATOR_STACK_TOP, + ), + heap_start: crate::abi::ALLOCATOR_HEAP_START, + minimum_pages: 1, + stack_pointer_global: 0, + stack_bound_bytes: 80, + stack_bound_evidence: "static call-graph frame analysis of the pinned artifact (psrs-linker::measure_stack_bound)", + }, + start_forbidden: true, + data_range: ( + crate::abi::ALLOCATOR_STATIC_START, + crate::abi::ALLOCATOR_STATIC_END, + ), + instantiate_after_shims: false, +}; +/// Canonical `cabi_realloc`. +pub const ALLOCATOR_REALLOC_OP: ProvidedOperation = + op("psrs:allocator/realloc", &crate::abi::ALLOCATOR_REALLOC); + +const ALLOCATOR_OPERATIONS: &[ProvidedOperation] = &[ALLOCATOR_REALLOC_OP]; + +/// The exclusive shared-memory growth owner. +/// +/// It imports the application memory and the planned constant heap getter. +/// Instantiation does not call the canonical export; the first call initializes it. +pub const ALLOCATOR_UNIT: RuntimeUnit = RuntimeUnit { + id: "psrs:runtime/allocator", + semantic_contract_version: VERSION, + provided: ALLOCATOR_OPERATIONS, + required: &[], + variant: &ALLOCATOR_RUNTIME, + state: StateOwnership { + owner: Some("psrs:runtime/allocator"), + grows_memory: true, + }, +}; diff --git a/crates/psrs-runtime/src/catalog/encoded.rs b/crates/psrs-runtime/src/catalog/encoded.rs new file mode 100644 index 00000000..a6e581c6 --- /dev/null +++ b/crates/psrs-runtime/src/catalog/encoded.rs @@ -0,0 +1,80 @@ +//! Host-encoded independent units with separately declared type schemas. +use crate::RawType; + +#[derive(Clone, Copy)] +pub enum EncodedHeap { + Func, + Extern, + Any, + Eq, + I31, + Struct, + Array, + Defined(u32), +} +#[derive(Clone, Copy)] +pub enum EncodedValue { + Scalar(RawType), + Ref { nullable: bool, heap: EncodedHeap }, +} +#[derive(Clone)] +pub struct EncodedOperation { + pub name: String, + pub version: &'static str, + pub export: &'static str, + pub parameters: Vec, + pub result: Option, +} + +pub struct EncodedRuntimeUnit { + pub id: &'static str, + pub version: &'static str, + pub module_name: &'static str, + pub provenance: &'static str, + pub required_features: &'static [&'static str], + /// A types-only module defining the declaration's reference index space. + pub type_schema: fn() -> Vec, + pub operations: fn() -> Vec, + pub encode: fn() -> Vec, +} + +#[cfg(feature = "gc-storage")] +pub const STORAGE_UNIT: EncodedRuntimeUnit = EncodedRuntimeUnit { + id: "psrs:runtime-storage", + version: "1", + module_name: crate::STORAGE_MODULE, + provenance: "deterministic host encoder; storage ABI 1; pinned workspace wasm-encoder", + required_features: &["gc", "reference-types"], + type_schema: crate::storage::encode_type_schema, + operations: storage_operations, + encode: crate::storage::encode_module, +}; + +#[cfg(feature = "gc-storage")] +fn storage_operations() -> Vec { + use crate::{StorageOperation, StorageType}; + let value = |ty| match ty { + StorageType::I32 => EncodedValue::Scalar(RawType::I32), + StorageType::Array => EncodedValue::Ref { + nullable: false, + heap: EncodedHeap::Defined(0), + }, + StorageType::Value => EncodedValue::Ref { + nullable: true, + heap: EncodedHeap::Eq, + }, + }; + StorageOperation::ALL + .into_iter() + .map(|operation| { + let abi = operation.abi(); + EncodedOperation { + name: format!("storage.{}", abi.export), + version: STORAGE_UNIT.version, + export: abi.export, + parameters: abi.parameters.iter().copied().map(value).collect(), + result: abi.result.map(value), + } + }) + .collect() +} diff --git a/crates/psrs-runtime/src/catalog/mod.rs b/crates/psrs-runtime/src/catalog/mod.rs new file mode 100644 index 00000000..673b0511 --- /dev/null +++ b/crates/psrs-runtime/src/catalog/mod.rs @@ -0,0 +1,27 @@ +//! Immutable host-consumed runtime catalog. No executable target code is built here. +//! ABI contracts live in `abi`; each unit declares its artifact and operations +//! together, while WIT sources and the package aggregate have separate owners. + +mod allocator; +mod encoded; +pub use encoded::*; +mod model; +mod number; +mod package; +mod wit; + +pub use allocator::*; +pub use model::*; +pub use number::*; +pub use package::*; +pub use wit::*; + +const VERSION: &str = "1"; + +const fn op(name: &'static str, abi: &'static crate::abi::RawFunctionAbi) -> ProvidedOperation { + ProvidedOperation { + name, + version: VERSION, + abi, + } +} diff --git a/crates/psrs-runtime/src/catalog/model.rs b/crates/psrs-runtime/src/catalog/model.rs new file mode 100644 index 00000000..9038c8f8 --- /dev/null +++ b/crates/psrs-runtime/src/catalog/model.rs @@ -0,0 +1,153 @@ +//! Plain metadata records consumed by the linker and backend. + +use crate::abi::{RawFunctionAbi, RawType}; + +/// Reviewed provenance for a pinned compiler-owned runtime artifact. +#[derive(Clone, Copy, Debug)] +pub struct ArtifactProvenance { + /// The pinned dependency whose behavior the artifact implements. + pub dependency: &'static str, + /// The exact dependency revision. + pub dependency_revision: &'static str, + /// The Rust toolchain that produced the artifact. + pub rust_toolchain: &'static str, + /// The Rust target triple. + pub target: &'static str, + /// The Cargo profile. + pub profile: &'static str, + /// The linker flags and preparation recipe. + pub recipe: &'static str, + /// SHA-256 of the prepared artifact bytes. + pub sha256: &'static str, +} + +/// A function export the artifact declares. +#[derive(Clone, Copy, Debug)] +pub struct RawExport { + pub name: &'static str, + pub parameters: &'static [RawType], + pub result: Option, +} + +/// The kind of a raw artifact import. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RawImportKind { + /// A memory import and its declared limits. + Memory { minimum: u64, maximum: Option }, + /// A scalar function import. + Function { + parameters: &'static [RawType], + result: Option, + }, +} + +/// An import the artifact declares. +#[derive(Clone, Copy, Debug)] +pub struct RawImport { + pub module: &'static str, + pub field: &'static str, + pub kind: RawImportKind, +} + +/// A table the artifact declares. +#[derive(Clone, Copy, Debug)] +pub struct RawTable { + pub element: &'static str, + pub minimum: u32, + pub maximum: Option, +} + +/// An active initializer for a private function table. +#[derive(Clone, Copy, Debug)] +pub struct RawElement { + pub table: u32, + pub offset: u32, + pub functions: &'static [u32], +} + +/// Private execution storage and the allocator boundary an artifact requires. +#[derive(Clone, Copy, Debug)] +pub struct RawStorage { + pub static_data: (u32, u32), + pub stack: (u32, u32), + pub heap_start: u32, + pub minimum_pages: u64, + /// Absolute index of the mutable stack-pointer global, counting imports. + pub stack_pointer_global: u32, + /// A reviewed upper bound on stack bytes for supported calling behavior. + pub stack_bound_bytes: u32, + /// Where the bound comes from, including pending evidence. + pub stack_bound_evidence: &'static str, +} + +/// A pinned core-Wasm artifact and its declared raw contract. +#[derive(Clone, Copy, Debug)] +pub struct RuntimeArtifact { + pub id: &'static str, + /// The private core-module import identity the artifact uses. + pub module_name: &'static str, + /// The artifact's core-Wasm bytes. + pub bytes: &'static [u8], + pub provenance: ArtifactProvenance, + pub required_features: &'static [&'static str], + pub imports: &'static [RawImport], + pub function_exports: &'static [RawExport], + pub global_exports: &'static [&'static str], + pub tables: &'static [RawTable], + pub elements: &'static [RawElement], + /// Declared globals as `(mutable, constant i32 initial)`. + pub globals: &'static [(bool, u32)], + pub storage: RawStorage, + pub start_forbidden: bool, + pub data_range: (u32, u32), + pub instantiate_after_shims: bool, +} + +/// An operation a unit provides, with the raw export that implements it. +#[derive(Clone, Copy)] +pub struct ProvidedOperation { + pub name: &'static str, + pub version: &'static str, + pub abi: &'static RawFunctionAbi, +} + +/// An operation a unit requires from exactly one other provider. +#[derive(Clone, Copy)] +pub struct RequiredOperation { + pub name: &'static str, + pub version: &'static str, + pub parameters: &'static [crate::RawType], + pub result: Option, +} + +/// Who owns a unit's private state and whether it may grow shared memory. +#[derive(Clone, Copy)] +pub struct StateOwnership { + /// Stable owner identity. `None` means the unit has no private state. + pub owner: Option<&'static str>, + /// Exclusive growth of the planned shared memory. + pub grows_memory: bool, +} + +/// One runtime implementation unit and the single variant it advertises. +#[derive(Clone, Copy)] +pub struct RuntimeUnit { + pub id: &'static str, + pub semantic_contract_version: &'static str, + pub provided: &'static [ProvidedOperation], + pub required: &'static [RequiredOperation], + /// The produced core-module variant. A second variant needs an explicit + /// selection rule before it is added here. + pub variant: &'static RuntimeArtifact, + pub state: StateOwnership, +} + +/// A runtime package identity and the units it publishes. +#[derive(Clone, Copy)] +pub struct RuntimePackage { + pub id: &'static str, + pub version: &'static str, + pub provenance: &'static str, + pub units: &'static [&'static RuntimeUnit], + pub encoded_units: &'static [&'static super::EncodedRuntimeUnit], +} diff --git a/crates/psrs-runtime/src/catalog/number.rs b/crates/psrs-runtime/src/catalog/number.rs new file mode 100644 index 00000000..182d2e51 --- /dev/null +++ b/crates/psrs-runtime/src/catalog/number.rs @@ -0,0 +1,244 @@ +//! Pinned numeric artifact and the operations it provides. + +use super::model::*; +use super::{VERSION, op}; +use crate::abi::RawType; + +/// Numeric formatting and decimal conversion, produced by `tools/build.sh`. +pub const NUMBER_RUNTIME: RuntimeArtifact = RuntimeArtifact { + id: "psrs:runtime-number", + module_name: crate::abi::MODULE_NAME, + bytes: include_bytes!("../../artifact/psrs_runtime.wasm"), + provenance: ArtifactProvenance { + dependency: "ryu-js, libm, and Rust core::num::dec2flt", + dependency_revision: "ryu-js 1.0.2; libm 0.2.15; Rust 1.99.0", + rust_toolchain: "1.99.0 (b940084d7 2026-09-28)", + target: "wasm32-unknown-unknown", + profile: "target-runtime", + recipe: "tools/build.sh: RUSTFLAGS=-C no-redzone=yes; --import-memory --global-base=65536 \ + -zstack-size=65536 --export=__heap_base, then psrs-runtime-package", + sha256: "458a7df7eb27038c4d6b22aa52fc8ea283c6dca41120b0c0bf31f6a851aa0260", + }, + required_features: &[ + "mutable-globals", + "sign-extension", + "saturating-float-to-int", + "multi-value", + "bulk-memory", + "reference-types", + ], + imports: &[RawImport { + module: crate::abi::MEMORY_MODULE, + field: crate::abi::MEMORY_FIELD, + kind: RawImportKind::Memory { + minimum: 3, + maximum: None, + }, + }], + function_exports: &[ + RawExport { + name: crate::abi::NUMBER_EXPORT, + parameters: &[RawType::F64, RawType::I32, RawType::I32], + result: Some(RawType::I32), + }, + RawExport { + name: crate::abi::DECIMAL_EXPORT, + parameters: &[RawType::I32, RawType::I32], + result: Some(RawType::F64), + }, + RawExport { + name: crate::abi::ACOS_EXPORT, + parameters: &[RawType::F64], + result: Some(RawType::F64), + }, + RawExport { + name: crate::abi::ASIN_EXPORT, + parameters: &[RawType::F64], + result: Some(RawType::F64), + }, + RawExport { + name: crate::abi::ATAN_EXPORT, + parameters: &[RawType::F64], + result: Some(RawType::F64), + }, + RawExport { + name: crate::abi::ATAN2_EXPORT, + parameters: &[RawType::F64, RawType::F64], + result: Some(RawType::F64), + }, + RawExport { + name: crate::abi::SIN_EXPORT, + parameters: &[RawType::F64], + result: Some(RawType::F64), + }, + RawExport { + name: crate::abi::COS_EXPORT, + parameters: &[RawType::F64], + result: Some(RawType::F64), + }, + RawExport { + name: crate::abi::TAN_EXPORT, + parameters: &[RawType::F64], + result: Some(RawType::F64), + }, + RawExport { + name: crate::abi::EXP_EXPORT, + parameters: &[RawType::F64], + result: Some(RawType::F64), + }, + RawExport { + name: crate::abi::LOG_EXPORT, + parameters: &[RawType::F64], + result: Some(RawType::F64), + }, + RawExport { + name: crate::abi::POW_EXPORT, + parameters: &[RawType::F64, RawType::F64], + result: Some(RawType::F64), + }, + RawExport { + name: crate::abi::MIN_EXPORT, + parameters: &[RawType::F64, RawType::F64], + result: Some(RawType::F64), + }, + RawExport { + name: crate::abi::MAX_EXPORT, + parameters: &[RawType::F64, RawType::F64], + result: Some(RawType::F64), + }, + RawExport { + name: crate::abi::SIGN_EXPORT, + parameters: &[RawType::F64], + result: Some(RawType::F64), + }, + RawExport { + name: crate::abi::REMAINDER_EXPORT, + parameters: &[RawType::F64, RawType::F64], + result: Some(RawType::F64), + }, + RawExport { + name: crate::abi::IS_NAN_EXPORT, + parameters: &[RawType::F64], + result: Some(RawType::I32), + }, + RawExport { + name: crate::abi::NAN_EXPORT, + parameters: &[], + result: Some(RawType::F64), + }, + RawExport { + name: crate::abi::INFINITY_EXPORT, + parameters: &[], + result: Some(RawType::F64), + }, + ], + global_exports: &[crate::abi::HEAP_BASE_EXPORT], + tables: &[RawTable { + element: "funcref", + minimum: 2, + maximum: Some(2), + }], + elements: &[RawElement { + table: 0, + offset: 1, + functions: &[54], + }], + globals: &[ + (true, crate::abi::HEAP_START), + (false, crate::abi::HEAP_START), + ], + storage: RawStorage { + static_data: (crate::abi::RESERVED_START, crate::abi::STACK_BOTTOM), + stack: (crate::abi::STACK_BOTTOM, crate::abi::HEAP_START), + heap_start: crate::abi::HEAP_START, + minimum_pages: 3, + stack_pointer_global: 0, + stack_bound_bytes: 1680, + stack_bound_evidence: "static call-graph frame analysis of the pinned artifact (psrs-linker::measure_stack_bound)", + }, + start_forbidden: true, + data_range: (crate::abi::RESERVED_START, crate::abi::STACK_BOTTOM), + instantiate_after_shims: false, +}; + +/// `number_to_string`. +pub const NUMBER_TO_STRING_OP: ProvidedOperation = + op("psrs:number/to-string", &crate::abi::NUMBER_FORMAT); +/// `number_from_decimal`. +pub const NUMBER_FROM_DECIMAL_OP: ProvidedOperation = + op("psrs:number/from-decimal", &crate::abi::NUMBER_PARSE); +/// `number_acos`. +pub const NUMBER_ACOS_OP: ProvidedOperation = op("psrs:number/acos", &crate::abi::NUMBER_ACOS); +/// `number_asin`. +pub const NUMBER_ASIN_OP: ProvidedOperation = op("psrs:number/asin", &crate::abi::NUMBER_ASIN); +/// `number_atan`. +pub const NUMBER_ATAN_OP: ProvidedOperation = op("psrs:number/atan", &crate::abi::NUMBER_ATAN); +/// `number_atan2`. +pub const NUMBER_ATAN2_OP: ProvidedOperation = op("psrs:number/atan2", &crate::abi::NUMBER_ATAN2); +/// `number_sin`. +pub const NUMBER_SIN_OP: ProvidedOperation = op("psrs:number/sin", &crate::abi::NUMBER_SIN); +/// `number_cos`. +pub const NUMBER_COS_OP: ProvidedOperation = op("psrs:number/cos", &crate::abi::NUMBER_COS); +/// `number_tan`. +pub const NUMBER_TAN_OP: ProvidedOperation = op("psrs:number/tan", &crate::abi::NUMBER_TAN); +/// `number_exp`. +pub const NUMBER_EXP_OP: ProvidedOperation = op("psrs:number/exp", &crate::abi::NUMBER_EXP); +/// `number_log`. +pub const NUMBER_LOG_OP: ProvidedOperation = op("psrs:number/log", &crate::abi::NUMBER_LOG); +/// `number_pow`. +pub const NUMBER_POW_OP: ProvidedOperation = op("psrs:number/pow", &crate::abi::NUMBER_POW); +/// `number_min`. +pub const NUMBER_MIN_OP: ProvidedOperation = op("psrs:number/min", &crate::abi::NUMBER_MIN); +/// `number_max`. +pub const NUMBER_MAX_OP: ProvidedOperation = op("psrs:number/max", &crate::abi::NUMBER_MAX); +/// `number_sign`. +pub const NUMBER_SIGN_OP: ProvidedOperation = op("psrs:number/sign", &crate::abi::NUMBER_SIGN); +/// `number_remainder`. +pub const NUMBER_REMAINDER_OP: ProvidedOperation = + op("psrs:number/remainder", &crate::abi::NUMBER_REMAINDER); +/// `number_is_nan`. +pub const NUMBER_IS_NAN_OP: ProvidedOperation = + op("psrs:number/is-nan", &crate::abi::NUMBER_IS_NAN); +/// `number_nan`. +pub const NUMBER_NAN_OP: ProvidedOperation = op("psrs:number/nan", &crate::abi::NUMBER_NAN); +/// `number_infinity`. +pub const NUMBER_INFINITY_OP: ProvidedOperation = + op("psrs:number/infinity", &crate::abi::NUMBER_INFINITY); + +const NUMBER_OPERATIONS: &[ProvidedOperation] = &[ + NUMBER_TO_STRING_OP, + NUMBER_FROM_DECIMAL_OP, + NUMBER_ACOS_OP, + NUMBER_ASIN_OP, + NUMBER_ATAN_OP, + NUMBER_ATAN2_OP, + NUMBER_SIN_OP, + NUMBER_COS_OP, + NUMBER_TAN_OP, + NUMBER_EXP_OP, + NUMBER_LOG_OP, + NUMBER_POW_OP, + NUMBER_MIN_OP, + NUMBER_MAX_OP, + NUMBER_SIGN_OP, + NUMBER_REMAINDER_OP, + NUMBER_IS_NAN_OP, + NUMBER_NAN_OP, + NUMBER_INFINITY_OP, +]; + +/// Numeric formatting, parsing, and elementary operations. +/// +/// This unit owns its static data and stack. It imports the application memory +/// and does not grow it. The demand names the allocator that owns growth. +pub const NUMBER_UNIT: RuntimeUnit = RuntimeUnit { + id: "psrs:runtime/number", + semantic_contract_version: VERSION, + provided: NUMBER_OPERATIONS, + required: &[], + variant: &NUMBER_RUNTIME, + state: StateOwnership { + owner: Some("psrs:runtime/number"), + grows_memory: false, + }, +}; diff --git a/crates/psrs-runtime/src/catalog/package.rs b/crates/psrs-runtime/src/catalog/package.rs new file mode 100644 index 00000000..91486b6e --- /dev/null +++ b/crates/psrs-runtime/src/catalog/package.rs @@ -0,0 +1,46 @@ +//! The produced runtime package and package-level consistency checks. + +use super::{ALLOCATOR_UNIT, NUMBER_UNIT, RuntimePackage, VERSION}; + +/// The compiler-owned runtime package. +pub const PSRS_RUNTIME: RuntimePackage = RuntimePackage { + id: "psrs:runtime", + version: VERSION, + provenance: "compiler-owned catalog; each unit pins its own variant bytes and recipe", + units: &[&NUMBER_UNIT, &ALLOCATOR_UNIT], + #[cfg(feature = "gc-storage")] + encoded_units: &[&super::STORAGE_UNIT], + #[cfg(not(feature = "gc-storage"))] + encoded_units: &[], +}; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_number_unit_advertises_each_pinned_export_once() { + let exports: Vec<_> = NUMBER_UNIT + .variant + .function_exports + .iter() + .map(|export| export.name) + .collect(); + let provided: Vec<_> = NUMBER_UNIT + .provided + .iter() + .map(|operation| operation.abi.export) + .collect(); + assert_eq!(exports, provided); + const { assert!(NUMBER_UNIT.required.is_empty()) }; + assert_eq!(NUMBER_UNIT.state.owner, Some(NUMBER_UNIT.id)); + const { assert!(!NUMBER_UNIT.state.grows_memory) }; + assert_eq!(PSRS_RUNTIME.units.len(), 2); + assert_eq!(PSRS_RUNTIME.units[0].id, NUMBER_UNIT.id); + assert_eq!(PSRS_RUNTIME.units[1].id, ALLOCATOR_UNIT.id); + const { assert!(ALLOCATOR_UNIT.state.grows_memory) }; + assert_eq!(ALLOCATOR_UNIT.state.owner, Some(ALLOCATOR_UNIT.id)); + assert_eq!(ALLOCATOR_UNIT.provided.len(), 1); + assert!(ALLOCATOR_UNIT.required.is_empty()); + } +} diff --git a/crates/psrs-runtime/src/catalog/wit.rs b/crates/psrs-runtime/src/catalog/wit.rs new file mode 100644 index 00000000..df00fb48 --- /dev/null +++ b/crates/psrs-runtime/src/catalog/wit.rs @@ -0,0 +1,57 @@ +//! Pinned interface sources and the default application world. + +/// One pinned WIT source file: its catalog path and exact contents. +#[derive(Clone, Copy, Debug)] +pub struct WitSource { + pub path: &'static str, + pub contents: &'static str, +} + +/// The default world the command artifact implements. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct WorldIdentity { + pub package_namespace: &'static str, + pub package_name: &'static str, + pub world: &'static str, +} + +/// The `psrs:app/command` default world. +pub const DEFAULT_WORLD: WorldIdentity = WorldIdentity { + package_namespace: "psrs", + package_name: "app", + world: "command", +}; + +/// Vendored WASI 0.2.12 WIT in dependency order. +pub const WASI_WIT: &[WitSource] = &[ + WitSource { + path: "wasi/io.wit", + contents: include_str!("../../wit/deps/io.wit"), + }, + WitSource { + path: "wasi/clocks.wit", + contents: include_str!("../../wit/deps/clocks.wit"), + }, + WitSource { + path: "wasi/random.wit", + contents: include_str!("../../wit/deps/random.wit"), + }, + WitSource { + path: "wasi/filesystem.wit", + contents: include_str!("../../wit/deps/filesystem.wit"), + }, + WitSource { + path: "wasi/sockets.wit", + contents: include_str!("../../wit/deps/sockets.wit"), + }, + WitSource { + path: "wasi/cli.wit", + contents: include_str!("../../wit/deps/cli.wit"), + }, +]; + +/// The application world source. +pub const APP_WIT: WitSource = WitSource { + path: "psrs-app.wit", + contents: include_str!("../../wit/psrs-app.wit"), +}; diff --git a/crates/psrs-runtime/src/lib.rs b/crates/psrs-runtime/src/lib.rs new file mode 100644 index 00000000..71830510 --- /dev/null +++ b/crates/psrs-runtime/src/lib.rs @@ -0,0 +1,34 @@ +//! One runtime package with separate ABI, catalog, and executable module owners. +//! +//! `abi` defines data-only raw contracts for every build. Feature `catalog` +//! exposes pinned WIT, artifact metadata, and package units to native compiler +//! consumers and is the only default feature. Feature `number` builds the numeric implementation; +//! feature `allocator` builds the canonical allocator. Features are additive; +//! each pinned Wasm artifact selects exactly one executable feature. +//! Feature `gc-storage` exposes a host encoder for an independent GC storage +//! module; its target operations allocate no Rust or linear-memory heap. +//! Feature `artifact-tool` builds the host-only `psrs-runtime-package` binary. + +#![cfg_attr(target_arch = "wasm32", no_std)] + +pub mod abi; +pub use abi::*; + +#[cfg(all(feature = "gc-storage", not(target_arch = "wasm32")))] +pub mod storage; + +#[cfg(all(feature = "catalog", not(target_arch = "wasm32")))] +pub mod catalog; +#[cfg(all(feature = "catalog", not(target_arch = "wasm32")))] +pub use catalog::*; + +#[cfg(feature = "number")] +mod number; +#[cfg(feature = "number")] +pub use number::*; + +#[cfg(all(feature = "allocator", any(test, target_arch = "wasm32")))] +mod allocator; + +#[cfg(all(target_arch = "wasm32", not(test)))] +mod panic; diff --git a/crates/psrs-runtime/src/number/atan2.rs b/crates/psrs-runtime/src/number/atan2.rs new file mode 100644 index 00000000..faa176c3 --- /dev/null +++ b/crates/psrs-runtime/src/number/atan2.rs @@ -0,0 +1,146 @@ +//! Four-quadrant inverse tangent of two binary64 values. +//! +//! Moderate ratios call upstream `libm` 0.2.15 inverse tangent. An +//! exponent gap above 60 returns a signed half-pi, and a negative `x` with a +//! gap below -60 contributes zero before the pi adjustment. That cutoff +//! matches official `Math.atan2`. The argument order is `(y, x)`. Either NaN +//! produces NaN. The operation does not trap. NaN payloads are not part of +//! the public contract. + +/// Returns the angle from the positive x axis to `(x, y)`, in radians. +#[allow(clippy::excessive_precision, clippy::approx_constant)] +fn atan2(y: f64, x: f64) -> f64 { + const PI: f64 = 3.1415926535897931160E+00; + const PI_LO: f64 = 1.2246467991473531772E-16; + const PI_OVER_2: f64 = 1.5707963267948965580E+00; + + if x.is_nan() || y.is_nan() { + return x + y; + } + let mut ix = (x.to_bits() >> 32) as u32; + let lx = x.to_bits() as u32; + let mut iy = (y.to_bits() >> 32) as u32; + let ly = y.to_bits() as u32; + if (ix.wrapping_sub(0x3ff0_0000) | lx) == 0 { + return libm::atan(y); + } + let mut quadrant = ((iy >> 31) & 1) | ((ix >> 30) & 2); + ix &= 0x7fff_ffff; + iy &= 0x7fff_ffff; + + if (iy | ly) == 0 { + return match quadrant { + 0 | 1 => y, + 2 => PI, + _ => -PI, + }; + } + if (ix | lx) == 0 { + return if quadrant & 1 != 0 { + -PI / 2.0 + } else { + PI / 2.0 + }; + } + if ix == 0x7ff0_0000 { + if iy == 0x7ff0_0000 { + return match quadrant { + 0 => PI / 4.0, + 1 => -PI / 4.0, + 2 => 3.0 * PI / 4.0, + _ => -3.0 * PI / 4.0, + }; + } + return match quadrant { + 0 => 0.0, + 1 => -0.0, + 2 => PI, + _ => -PI, + }; + } + if iy == 0x7ff0_0000 { + return if quadrant & 1 != 0 { + -PI / 2.0 + } else { + PI / 2.0 + }; + } + let exponent_gap = (iy as i32).wrapping_sub(ix as i32) >> 20; + let reduced = if exponent_gap > 60 { + quadrant &= 1; + PI_OVER_2 + 0.5 * PI_LO + } else if quadrant & 2 != 0 && exponent_gap < -60 { + 0.0 + } else { + libm::atan((y / x).abs()) + }; + match quadrant { + 0 => reduced, + 1 => -reduced, + 2 => PI - (reduced - PI_LO), + _ => (reduced - PI_LO) - PI, + } +} + +/// C ABI export of [`atan2`]. +/// +/// # Safety +/// Every binary64 bit pattern is a valid argument. The function reads no +/// caller memory and retains no pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn number_atan2(y: f64, x: f64) -> f64 { + atan2(y, x) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn matches_quadrant_zeros_and_infinities() { + for (y, x, bits) in [ + (0.0, 1.0, 0), + (-0.0, 1.0, 0x8000_0000_0000_0000), + (0.0, -1.0, 0x4009_21fb_5444_2d18), + (-0.0, -1.0, 0xc009_21fb_5444_2d18), + (1.0, 0.0, 0x3ff9_21fb_5444_2d18), + (-1.0, 0.0, 0xbff9_21fb_5444_2d18), + (1.0, -0.0, 0x3ff9_21fb_5444_2d18), + (-1.0, -0.0, 0xbff9_21fb_5444_2d18), + (0.0, 0.0, 0), + (-0.0, 0.0, 0x8000_0000_0000_0000), + (0.0, -0.0, 0x4009_21fb_5444_2d18), + (-0.0, -0.0, 0xc009_21fb_5444_2d18), + (f64::INFINITY, f64::INFINITY, 0x3fe9_21fb_5444_2d18), + (f64::INFINITY, f64::NEG_INFINITY, 0x4002_d97c_7f33_21d2), + (f64::NEG_INFINITY, f64::INFINITY, 0xbfe9_21fb_5444_2d18), + (f64::NEG_INFINITY, f64::NEG_INFINITY, 0xc002_d97c_7f33_21d2), + (1.0, f64::INFINITY, 0), + (-1.0, f64::INFINITY, 0x8000_0000_0000_0000), + (1.0, f64::NEG_INFINITY, 0x4009_21fb_5444_2d18), + (-1.0, f64::NEG_INFINITY, 0xc009_21fb_5444_2d18), + (f64::INFINITY, 1.0, 0x3ff9_21fb_5444_2d18), + (f64::NEG_INFINITY, -1.0, 0xbff9_21fb_5444_2d18), + (1.0, 1.0, 0x3fe9_21fb_5444_2d18), + (1.0, -1.0, 0x4002_d97c_7f33_21d2), + (-1.0, 1.0, 0xbfe9_21fb_5444_2d18), + (-1.0, -1.0, 0xc002_d97c_7f33_21d2), + ] { + assert_eq!(atan2(y, x).to_bits(), bits, "{y}, {x}"); + } + assert!(atan2(f64::NAN, 1.0).is_nan()); + assert!(atan2(1.0, f64::NAN).is_nan()); + } + + #[test] + fn large_negative_x_ratios_return_a_signed_half_pi() { + for (y, x, bits) in [ + (0.1, -1e-20, 0x3ff9_21fb_5444_2d18), + (-0.1, -1e-20, 0xbff9_21fb_5444_2d18), + (1.0, -1e-20, 0x3ff9_21fb_5444_2d18), + (f64::MIN_POSITIVE, -1.0, 0x4009_21fb_5444_2d18), + ] { + assert_eq!(atan2(y, x).to_bits(), bits, "{y}, {x}"); + } + } +} diff --git a/crates/psrs-runtime/src/number/decimal.rs b/crates/psrs-runtime/src/number/decimal.rs new file mode 100644 index 00000000..a9e0fae5 --- /dev/null +++ b/crates/psrs-runtime/src/number/decimal.rs @@ -0,0 +1,67 @@ +//! Allocation-free conversion of a complete ASCII decimal token to binary64. + +/// Converts a complete signed decimal token, with an optional decimal exponent. +/// Invalid tokens return NaN. Prefix recognition and callback behavior belong +/// to the target library. The pinned Rust parser rounds to the nearest binary64. +/// +/// # Safety +/// `input` must identify `length` readable bytes in the caller's memory for the +/// duration of the call. No pointer is retained and no allocation occurs. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn number_from_decimal(input: *const u8, length: usize) -> f64 { + // SAFETY: the caller provides a readable buffer for this call. + let bytes = unsafe { core::slice::from_raw_parts(input, length) }; + if bytes + .iter() + .any(|byte| !matches!(byte, b'0'..=b'9' | b'+' | b'-' | b'.' | b'e' | b'E')) + { + return f64::NAN; + } + // SAFETY: every accepted byte is ASCII. + let token = unsafe { core::str::from_utf8_unchecked(bytes) }; + token.parse().unwrap_or(f64::NAN) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse(token: &str) -> f64 { + // SAFETY: the string owns the readable input for the call. + unsafe { number_from_decimal(token.as_ptr(), token.len()) } + } + + #[test] + fn rounds_decimal_boundaries_and_preserves_negative_zero() { + for (token, expected) in [ + ("42.5", 42.5), + ("-0", -0.0), + ("-1e-9999", -0.0), + ("1e309", f64::INFINITY), + ("5e-324", f64::from_bits(1)), + ("2.4703282292062327e-324", 0.0), + ("2.4703282292062328e-324", f64::from_bits(1)), + ("9007199254740993", 9007199254740992.0), + ( + "1.00000000000000011102230246251565404236316680908203125", + 1.0, + ), + ( + "1.000000000000000111022302462515654042363166809082031251", + f64::from_bits(1.0_f64.to_bits() + 1), + ), + ] { + assert_eq!(parse(token).to_bits(), expected.to_bits(), "{token}"); + } + } + + #[test] + fn rejects_partial_nondecimal_and_non_ascii_tokens() { + for token in [ + "", "+", ".", "1e", "1e+", "42.5tail", " 42", "42 ", "0x2a", "inf", "Infinity", "NaN", + "1", + ] { + assert!(parse(token).is_nan(), "{token}"); + } + } +} diff --git a/crates/psrs-runtime/src/number/format.rs b/crates/psrs-runtime/src/number/format.rs new file mode 100644 index 00000000..3eed1a99 --- /dev/null +++ b/crates/psrs-runtime/src/number/format.rs @@ -0,0 +1,53 @@ +//! The executable ECMAScript binary64 formatter built for the Wasm target. + +/// Formats a binary64 value using ECMAScript Number::toString semantics. +/// +/// # Safety +/// `output` must point to `capacity` writable bytes in the caller's memory, +/// disjoint from this library's static data and stack. The returned length +/// identifies initialized ASCII bytes; no allocation or retained pointer occurs. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn number_to_string(value: f64, output: *mut u8, capacity: usize) -> usize { + assert!(capacity >= crate::abi::NUMBER_CAPACITY); + if value.is_finite() { + // SAFETY: format64 requires 25 writable bytes and a finite input. + return unsafe { ryu_js::raw::format64(value, output) }; + } + let text = if value.is_nan() { + "NaN" + } else if value.is_sign_negative() { + "-Infinity" + } else { + "Infinity" + }; + // SAFETY: the caller owns the destination and the checked capacity fits it. + unsafe { core::ptr::copy_nonoverlapping(text.as_ptr(), output, text.len()) }; + text.len() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn formats_special_values_and_notation_boundaries() { + for (value, expected) in [ + (-0.0, "0"), + (f64::NAN, "NaN"), + (f64::INFINITY, "Infinity"), + (f64::NEG_INFINITY, "-Infinity"), + (1e-6, "0.000001"), + (1e-7, "1e-7"), + (1e20, "100000000000000000000"), + (1e21, "1e+21"), + (f64::from_bits(1), "5e-324"), + (f64::MAX, "1.7976931348623157e+308"), + ] { + let mut output = [0xff; crate::abi::NUMBER_CAPACITY]; + // SAFETY: the buffer owns all 32 bytes for the duration of the call. + let length = unsafe { number_to_string(value, output.as_mut_ptr(), output.len()) }; + assert_eq!(&output[..length], expected.as_bytes()); + assert!(output[length..].iter().all(|byte| *byte == 0xff)); + } + } +} diff --git a/crates/psrs-runtime/src/number/math.rs b/crates/psrs-runtime/src/number/math.rs new file mode 100644 index 00000000..a8e40af5 --- /dev/null +++ b/crates/psrs-runtime/src/number/math.rs @@ -0,0 +1,351 @@ +//! Scalar math exports and JavaScript numeric semantics. +//! +//! Elementary operations call `libm` 0.2.15. Minimum, maximum, sign, +//! remainder, NaN, infinity, and `isNaN` follow the official JavaScript +//! `Math` and `%` contracts. Remainder calls `libm::fmod`. None of these +//! operations trap. NaN payloads are not part of the public contract, except +//! that `isNaN` is true for every NaN. + +/// Returns the smaller value. Either NaN produces NaN. If either zero is +/// negative, a zero result is negative zero. +fn minimum(left: f64, right: f64) -> f64 { + if left.is_nan() || right.is_nan() { + return f64::NAN; + } + if left == 0.0 && right == 0.0 { + return f64::from_bits(left.to_bits() | right.to_bits()); + } + if left < right { left } else { right } +} + +/// Returns the larger value. Either NaN produces NaN. A zero result is +/// negative zero only when both zeros are negative. +fn maximum(left: f64, right: f64) -> f64 { + if left.is_nan() || right.is_nan() { + return f64::NAN; + } + if left == 0.0 && right == 0.0 { + return f64::from_bits(left.to_bits() & right.to_bits()); + } + if left > right { left } else { right } +} + +/// JavaScript `%`: the exact remainder, with a zero result taking the +/// dividend's sign. Division by zero or an infinite dividend produces NaN. +fn remainder(dividend: f64, divisor: f64) -> f64 { + libm::fmod(dividend, divisor) +} + +/// JavaScript `Math.pow`. An exponent of zero yields 1, including a NaN base. +/// A NaN exponent, and ±1 raised to an infinity, yield NaN. The C `pow` +/// convention returns 1 for those two shapes. +fn power(base: f64, exponent: f64) -> f64 { + if exponent.is_nan() || (base.abs() == 1.0 && exponent.is_infinite()) { + return f64::NAN; + } + libm::pow(base, exponent) +} + +/// `Math.sign`: NaN and both zeros are unchanged. Infinities become ±1. +fn sign(value: f64) -> f64 { + if value.is_nan() || value == 0.0 { + value + } else if value > 0.0 { + 1.0 + } else { + -1.0 + } +} + +/// C ABI export of inverse cosine, in radians. +/// +/// # Safety +/// Every binary64 bit pattern is a valid argument. The function reads no +/// caller memory and retains no pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn number_acos(value: f64) -> f64 { + libm::acos(value) +} + +/// C ABI export of inverse sine, in radians. +/// +/// # Safety +/// Every binary64 bit pattern is a valid argument. The function reads no +/// caller memory and retains no pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn number_asin(value: f64) -> f64 { + libm::asin(value) +} + +/// C ABI export of inverse tangent, in radians. +/// +/// # Safety +/// Every binary64 bit pattern is a valid argument. The function reads no +/// caller memory and retains no pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn number_atan(value: f64) -> f64 { + libm::atan(value) +} + +/// C ABI export of sine. +/// +/// # Safety +/// Every binary64 bit pattern is a valid argument. The function reads no +/// caller memory and retains no pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn number_sin(value: f64) -> f64 { + libm::sin(value) +} + +/// C ABI export of cosine. +/// +/// # Safety +/// Every binary64 bit pattern is a valid argument. The function reads no +/// caller memory and retains no pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn number_cos(value: f64) -> f64 { + libm::cos(value) +} + +/// C ABI export of tangent. +/// +/// # Safety +/// Every binary64 bit pattern is a valid argument. The function reads no +/// caller memory and retains no pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn number_tan(value: f64) -> f64 { + libm::tan(value) +} + +/// C ABI export of the base-e exponential. +/// +/// # Safety +/// Every binary64 bit pattern is a valid argument. The function reads no +/// caller memory and retains no pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn number_exp(value: f64) -> f64 { + libm::exp(value) +} + +/// C ABI export of the natural logarithm. +/// +/// # Safety +/// Every binary64 bit pattern is a valid argument. The function reads no +/// caller memory and retains no pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn number_log(value: f64) -> f64 { + libm::log(value) +} + +/// C ABI export of exponentiation. +/// +/// # Safety +/// Every binary64 bit pattern is a valid argument. The function reads no +/// caller memory and retains no pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn number_pow(base: f64, exponent: f64) -> f64 { + power(base, exponent) +} + +/// C ABI export of [`minimum`]. +/// +/// # Safety +/// Every binary64 bit pattern is a valid argument. The function reads no +/// caller memory and retains no pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn number_min(left: f64, right: f64) -> f64 { + minimum(left, right) +} + +/// C ABI export of [`maximum`]. +/// +/// # Safety +/// Every binary64 bit pattern is a valid argument. The function reads no +/// caller memory and retains no pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn number_max(left: f64, right: f64) -> f64 { + maximum(left, right) +} + +/// C ABI export of [`sign`]. +/// +/// # Safety +/// Every binary64 bit pattern is a valid argument. The function reads no +/// caller memory and retains no pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn number_sign(value: f64) -> f64 { + sign(value) +} + +/// C ABI export of [`remainder`]. +/// +/// # Safety +/// Every binary64 bit pattern is a valid argument. The function reads no +/// caller memory and retains no pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn number_remainder(dividend: f64, divisor: f64) -> f64 { + remainder(dividend, divisor) +} + +/// C ABI export of the NaN predicate. The result is `1` or `0`. +/// +/// # Safety +/// Every binary64 bit pattern is a valid argument. The function reads no +/// caller memory and retains no pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn number_is_nan(value: f64) -> i32 { + i32::from(value.is_nan()) +} + +/// C ABI export of canonical NaN. +/// +/// # Safety +/// The function reads no caller memory and retains no pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn number_nan() -> f64 { + f64::NAN +} + +/// C ABI export of positive infinity. +/// +/// # Safety +/// The function reads no caller memory and retains no pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn number_infinity() -> f64 { + f64::INFINITY +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn acos_matches_domain_boundaries_and_rejects_values_outside_the_interval() { + for (input, bits) in [ + (1.0, 0), + (-1.0, 0x400921fb54442d18), + (0.0, 0x3ff921fb54442d18), + (-0.0, 0x3ff921fb54442d18), + (0.5, 0x3ff0c152382d7366), + (-0.5, 0x4000c152382d7366), + ] { + assert_eq!(unsafe { number_acos(input) }.to_bits(), bits, "{input}"); + } + for input in [2.0, -2.0, f64::INFINITY, f64::NEG_INFINITY, f64::NAN] { + assert!(unsafe { number_acos(input) }.is_nan(), "{input}"); + } + } + + #[test] + fn asin_matches_domain_boundaries_and_rejects_values_outside_the_interval() { + for (input, bits) in [ + (0.0, 0), + (-0.0, 0x8000000000000000), + (1.0, 0x3ff921fb54442d18), + (-1.0, 0xbff921fb54442d18), + (0.5, 0x3fe0c152382d7366), + (-0.5, 0xbfe0c152382d7366), + ] { + assert_eq!(unsafe { number_asin(input) }.to_bits(), bits, "{input}"); + } + for input in [2.0, -2.0, f64::INFINITY, f64::NEG_INFINITY, f64::NAN] { + assert!(unsafe { number_asin(input) }.is_nan(), "{input}"); + } + } + + #[test] + fn atan_matches_zero_signs_unit_slopes_and_infinities() { + for (input, bits) in [ + (0.0, 0), + (-0.0, 0x8000000000000000), + (f64::from_bits(1), 1), + (f64::from_bits(0x8000_0000_0000_0001), 0x8000_0000_0000_0001), + (f64::MIN_POSITIVE, f64::MIN_POSITIVE.to_bits()), + (1.0, 0x3fe921fb54442d18), + (-1.0, 0xbfe921fb54442d18), + (f64::INFINITY, 0x3ff921fb54442d18), + (f64::NEG_INFINITY, 0xbff921fb54442d18), + ] { + assert_eq!(unsafe { number_atan(input) }.to_bits(), bits, "{input}"); + } + assert!(unsafe { number_atan(f64::NAN) }.is_nan()); + } + + #[test] + fn minimum_and_maximum_propagate_nan_and_select_zero_signs() { + let negative_zero = -0.0; + assert_eq!(minimum(1.0, 2.0).to_bits(), 1.0_f64.to_bits()); + assert_eq!(maximum(1.0, 2.0).to_bits(), 2.0_f64.to_bits()); + assert_eq!( + minimum(negative_zero, 0.0).to_bits(), + negative_zero.to_bits() + ); + assert_eq!( + minimum(0.0, negative_zero).to_bits(), + negative_zero.to_bits() + ); + assert_eq!(maximum(negative_zero, 0.0).to_bits(), 0.0_f64.to_bits()); + assert_eq!( + maximum(negative_zero, negative_zero).to_bits(), + negative_zero.to_bits() + ); + assert!(minimum(f64::NAN, 1.0).is_nan()); + assert!(maximum(1.0, f64::NAN).is_nan()); + assert_eq!( + minimum(f64::NEG_INFINITY, 1.0).to_bits(), + f64::NEG_INFINITY.to_bits() + ); + assert_eq!( + maximum(f64::INFINITY, 1.0).to_bits(), + f64::INFINITY.to_bits() + ); + } + + #[test] + fn remainder_follows_the_dividend_and_rejects_zero_divisors() { + let negative_zero = -0.0_f64; + assert_eq!(remainder(5.3, 2.0).to_bits(), 0x3ff4_cccc_cccc_cccc); + assert_eq!(remainder(-5.3, 2.0).to_bits(), 0xbff4_cccc_cccc_cccc); + assert_eq!(remainder(5.3, -2.0).to_bits(), 0x3ff4_cccc_cccc_cccc); + assert_eq!(remainder(-1.0, 1.0).to_bits(), negative_zero.to_bits()); + assert_eq!(remainder(1.0, 0.1).to_bits(), 0x3fb9_9999_9999_9996); + assert_eq!(remainder(1.0, f64::INFINITY).to_bits(), 1.0_f64.to_bits()); + assert_eq!( + remainder(negative_zero, 1.0).to_bits(), + negative_zero.to_bits() + ); + assert!(remainder(f64::INFINITY, 1.0).is_nan()); + assert!(remainder(1.0, 0.0).is_nan()); + assert!(remainder(1.0, negative_zero).is_nan()); + assert!(remainder(f64::NAN, 1.0).is_nan()); + } + + #[test] + fn sign_preserves_zeros_and_nan_and_collapses_infinities() { + let negative_zero = -0.0; + assert_eq!(sign(0.0).to_bits(), 0.0_f64.to_bits()); + assert_eq!(sign(negative_zero).to_bits(), negative_zero.to_bits()); + assert!(sign(f64::NAN).is_nan()); + assert_eq!(sign(42.0).to_bits(), 1.0_f64.to_bits()); + assert_eq!(sign(-42.0).to_bits(), (-1.0_f64).to_bits()); + assert_eq!(sign(f64::INFINITY).to_bits(), 1.0_f64.to_bits()); + assert_eq!(sign(f64::NEG_INFINITY).to_bits(), (-1.0_f64).to_bits()); + } + + #[test] + fn constants_power_and_nan_predicate_match_javascript() { + assert_eq!(unsafe { number_nan() }.to_bits(), 0x7ff8_0000_0000_0000); + assert_eq!( + unsafe { number_infinity() }.to_bits(), + 0x7ff0_0000_0000_0000 + ); + assert_eq!(unsafe { number_is_nan(f64::NAN) }, 1); + assert_eq!(unsafe { number_is_nan(f64::INFINITY) }, 0); + assert_eq!(unsafe { number_is_nan(0.0) }, 0); + assert!(power(1.0, f64::NAN).is_nan()); + assert!(power(1.0, f64::INFINITY).is_nan()); + assert!(power(-1.0, f64::NEG_INFINITY).is_nan()); + assert_eq!(power(f64::NAN, 0.0).to_bits(), 1.0_f64.to_bits()); + assert_eq!(power(2.0, 3.0).to_bits(), 8.0_f64.to_bits()); + } +} diff --git a/crates/psrs-runtime/src/number/mod.rs b/crates/psrs-runtime/src/number/mod.rs new file mode 100644 index 00000000..ce0005f0 --- /dev/null +++ b/crates/psrs-runtime/src/number/mod.rs @@ -0,0 +1,15 @@ +//! Executable numeric runtime: formatting, parsing, and scalar math. + +mod atan2; +mod decimal; +mod format; +mod math; + +pub use atan2::number_atan2; +pub use decimal::number_from_decimal; +pub use format::number_to_string; +pub use math::{ + number_acos, number_asin, number_atan, number_cos, number_exp, number_infinity, number_is_nan, + number_log, number_max, number_min, number_nan, number_pow, number_remainder, number_sign, + number_sin, number_tan, +}; diff --git a/crates/psrs-runtime/src/panic.rs b/crates/psrs-runtime/src/panic.rs new file mode 100644 index 00000000..52340369 --- /dev/null +++ b/crates/psrs-runtime/src/panic.rs @@ -0,0 +1,6 @@ +//! Trap-based panic behavior shared by both executable Wasm artifacts. + +#[panic_handler] +fn panic(_: &core::panic::PanicInfo<'_>) -> ! { + core::arch::wasm32::unreachable() +} diff --git a/crates/psrs-runtime/src/storage/mod.rs b/crates/psrs-runtime/src/storage/mod.rs new file mode 100644 index 00000000..ea395fe4 --- /dev/null +++ b/crates/psrs-runtime/src/storage/mod.rs @@ -0,0 +1,106 @@ +//! Executable GC storage provider owned by the runtime package. +//! +//! This host encoder produces an independent core module with its own index +//! space. The application must use the structurally matching mutable eqref +//! array type. It must validate/box payloads before erasure and preserve array +//! identity: copying a typed array to this representation is not a write ABI. +//! Compiler/linker integration requires a checked GC reference call protocol; +//! its encoded unit and separate type schema are advertised by the catalog. + +use crate::{StorageOperation, StorageType}; +use wasm_encoder::{ + BlockType, CodeSection, ExportKind, ExportSection, Function, FunctionSection, HeapType, + Instruction, Module, RefType, StorageType as FieldStorage, TypeSection, ValType, +}; + +/// Shared structural type contract; no nominal source region is stored here. +pub const ARRAY_TYPE_INDEX: u32 = 0; + +fn value_type(ty: StorageType) -> ValType { + match ty { + StorageType::I32 => ValType::I32, + StorageType::Array => ValType::Ref(RefType { + nullable: false, + heap_type: HeapType::Concrete(ARRAY_TYPE_INDEX), + }), + StorageType::Value => ValType::Ref(RefType::EQREF), + } +} + +/// The data-only declaration schema has an independent module index space. +/// Function contracts resolve their references against this schema, rather +/// than deriving declarations from executable export signatures. +pub fn encode_type_schema() -> Vec { + let mut module = Module::new(); + let mut types = TypeSection::new(); + types + .ty() + .array(&FieldStorage::Val(ValType::Ref(RefType::EQREF)), true); + module.section(&types); + module.finish() +} + +/// Encode the provider deterministically; no memory, imports, globals or start. +pub fn encode_module() -> Vec { + let mut module = Module::new(); + let mut types = TypeSection::new(); + types + .ty() + .array(&FieldStorage::Val(ValType::Ref(RefType::EQREF)), true); + let mut functions = FunctionSection::new(); + let mut exports = ExportSection::new(); + let mut code = CodeSection::new(); + for (index, operation) in StorageOperation::ALL.into_iter().enumerate() { + let abi = operation.abi(); + types.ty().function( + abi.parameters.iter().copied().map(value_type), + abi.result.into_iter().map(value_type), + ); + functions.function(index as u32 + 1); + exports.export(abi.export, ExportKind::Func, index as u32); + code.function(&body(operation)); + } + module.section(&types); + module.section(&functions); + module.section(&exports); + module.section(&code); + module.finish() +} + +fn body(operation: StorageOperation) -> Function { + let mut f = Function::new([]); + match operation { + StorageOperation::Fill => { + // Source lengths are signed Ints; a negative length must trap + // before array.new interprets it as an enormous unsigned count. + f.instruction(&Instruction::LocalGet(0)); + f.instruction(&Instruction::I32Const(0)); + f.instruction(&Instruction::I32LtS); + f.instruction(&Instruction::If(BlockType::Empty)); + f.instruction(&Instruction::Unreachable); + f.instruction(&Instruction::End); + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::LocalGet(0)); + f.instruction(&Instruction::ArrayNew(ARRAY_TYPE_INDEX)); + } + StorageOperation::Read => { + f.instruction(&Instruction::LocalGet(0)); + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::ArrayGet(ARRAY_TYPE_INDEX)); + } + StorageOperation::Write => { + f.instruction(&Instruction::LocalGet(0)); + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::ArraySet(ARRAY_TYPE_INDEX)); + } + StorageOperation::Trap => { + f.instruction(&Instruction::Unreachable); + } + } + f.instruction(&Instruction::End); + f +} + +#[cfg(test)] +mod tests; diff --git a/crates/psrs-runtime/src/storage/tests.rs b/crates/psrs-runtime/src/storage/tests.rs new file mode 100644 index 00000000..64bf5acc --- /dev/null +++ b/crates/psrs-runtime/src/storage/tests.rs @@ -0,0 +1,178 @@ +use super::*; +use std::process::{Command, Output}; +use std::sync::atomic::{AtomicU64, Ordering}; + +const IMPORTS: &str = r#" + (type $array (array (mut eqref))) + (type $box (struct (field i32))) + (import "psrs:runtime-storage" "array_fill" + (func $fill (param i32 eqref) (result (ref $array)))) + (import "psrs:runtime-storage" "array_read" + (func $read (param (ref $array) i32) (result eqref))) + (import "psrs:runtime-storage" "array_write" + (func $write (param (ref $array) i32 eqref))) + (import "psrs:runtime-storage" "trap" (func $trap)) +"#; + +static NEXT: AtomicU64 = AtomicU64::new(0); + +fn run(body: &str) -> Option { + run_with_imports(IMPORTS, body) +} + +fn run_with_imports(imports: &str, body: &str) -> Option { + let version = Command::new("wasmtime").arg("--version").output(); + if !version.is_ok_and(|v| v.status.success()) { + assert_ne!( + std::env::var("PSRS_REQUIRE_WASMTIME").as_deref(), + Ok("1"), + "required Wasmtime is unavailable" + ); + eprintln!("skipping GC storage execution: Wasmtime is unavailable"); + return None; + } + let directory = std::env::temp_dir().join(format!( + "psrs-runtime-storage-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed), + )); + std::fs::create_dir(&directory).unwrap(); + let provider = directory.join("storage.wasm"); + let guest = directory.join("guest.wat"); + std::fs::write(&provider, encode_module()).unwrap(); + std::fs::write(&guest, format!("(module {imports} {body})")).unwrap(); + let output = Command::new("wasmtime") + .args(["run", "--preload"]) + .arg(format!("{}={}", crate::STORAGE_MODULE, provider.display())) + .args(["--invoke", "test"]) + .arg(&guest) + .output() + .unwrap(); + std::fs::remove_dir_all(directory).unwrap(); + Some(output) +} + +#[test] +fn provider_validates_without_memory_or_state_storage() { + let bytes = encode_module(); + wasmparser::Validator::new_with_features(wasmparser::WasmFeatures::all()) + .validate_all(&bytes) + .unwrap(); + assert_eq!(bytes, encode_module()); + let mut exports = Vec::new(); + for payload in wasmparser::Parser::new(0).parse_all(&bytes) { + match payload.unwrap() { + wasmparser::Payload::ExportSection(section) => { + exports.extend(section.into_iter().map(|e| e.unwrap().name.to_owned())); + } + wasmparser::Payload::MemorySection(_) + | wasmparser::Payload::GlobalSection(_) + | wasmparser::Payload::ImportSection(_) + | wasmparser::Payload::StartSection { .. } => { + panic!("GC storage must not allocate state or linear-memory handles") + } + _ => {} + } + } + assert_eq!( + exports, + StorageOperation::ALL.map(|operation| operation.abi().export) + ); +} + +#[test] +fn writes_preserve_aliases_and_repeated_reads_observe_values() { + let Some(output) = run(r#" + (func (export "test") (result i32) + (local $a (ref null $array)) (local $alias (ref null $array)) + (local $initial (ref null $box)) (local $updated (ref null $box)) + (local.set $initial (struct.new $box (i32.const 7))) + (local.set $updated (struct.new $box (i32.const 19))) + (local.set $a (call $fill (i32.const 2) (local.get $initial))) + (local.set $alias (local.get $a)) + (if (i32.ne (array.len (local.get $a)) (i32.const 2)) + (then unreachable)) + (if (i32.eqz (ref.eq (call $read (ref.as_non_null (local.get $a)) + (i32.const 1)) (local.get $initial))) (then unreachable)) + (call $write (ref.as_non_null (local.get $a)) (i32.const 1) + (local.get $updated)) + (if (i32.eqz (ref.eq (call $read (ref.as_non_null (local.get $alias)) + (i32.const 1)) (local.get $updated))) (then unreachable)) + (if (i32.ne (struct.get $box 0 + (ref.cast (ref $box) (call $read + (ref.as_non_null (local.get $alias)) (i32.const 1)))) + (i32.const 19)) (then unreachable)) + (call $write (ref.as_non_null (local.get $alias)) (i32.const 1) + (local.get $initial)) + (struct.get $box 0 (ref.cast (ref $box) + (call $read (ref.as_non_null (local.get $a)) (i32.const 1))))) + "#) else { + return; + }; + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "7"); +} + +#[test] +fn zero_length_and_null_values_are_supported() { + let Some(output) = run(r#" + (func (export "test") (result i32) + (if (i32.ne (array.len (call $fill (i32.const 0) (ref.null eq))) + (i32.const 0)) (then unreachable)) + (ref.is_null (call $read (call $fill (i32.const 1) (ref.null eq)) + (i32.const 0)))) + "#) else { + return; + }; + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "1"); +} + +#[test] +fn incompatible_gc_array_import_is_rejected_before_execution() { + // An immutable array is not the mutable storage representation even when + // its element type agrees. Do not adapt an in-place write by copying it. + let imports = IMPORTS.replace("(array (mut eqref))", "(array eqref)"); + let Some(output) = run_with_imports( + &imports, + "(func (export \"test\") (result i32) (i32.const 123))", + ) else { + return; + }; + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("incompatible import type"), "{stderr}"); + assert!(output.stdout.is_empty()); +} + +#[test] +fn invalid_sizes_indices_and_explicit_trap_have_no_normal_return() { + for operation in [ + "(drop (call $fill (i32.const -1) (ref.null eq)))", + "(drop (call $read (call $fill (i32.const 0) (ref.null eq)) (i32.const 0)))", + "(drop (call $read (call $fill (i32.const 1) (ref.null eq)) (i32.const -1)))", + "(call $write (call $fill (i32.const 1) (ref.null eq)) (i32.const 1) (ref.null eq))", + "(call $trap)", + ] { + let Some(output) = run(&format!( + "(func (export \"test\") (result i32) {operation} (i32.const 123))" + )) else { + return; + }; + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("wasm trap"), "{stderr}"); + assert!( + output.stdout.is_empty(), + "a trap must not return a fabricated payload" + ); + } +} diff --git a/crates/psrs-runtime/tools/build-allocator.sh b/crates/psrs-runtime/tools/build-allocator.sh new file mode 100755 index 00000000..8c2101f5 --- /dev/null +++ b/crates/psrs-runtime/tools/build-allocator.sh @@ -0,0 +1,15 @@ +#!/bin/sh +set -eu +cd "$(dirname "$0")/../../.." +runtime_allocator_target="${CARGO_TARGET_DIR:-target}/runtime-allocator" +sysroot=$(rustc --print sysroot) +# The upper 16 KiB of the stack prefix is allocator-owned; verified frame +# bounds keep accesses above canonical scratch. Data starts above the stack. +# Paths in panic text must not capture this machine. +RUSTFLAGS="${RUSTFLAGS:-} -C no-redzone=yes --remap-path-prefix=${HOME}/.cargo/=/cargo/ --remap-path-prefix=${sysroot}=/rustc/ --remap-path-prefix=${PWD}=/psrs/" \ +cargo rustc --locked -p psrs-runtime --lib --no-default-features --features allocator \ + --target-dir "$runtime_allocator_target" --target wasm32-unknown-unknown --profile target-runtime -- \ + -C link-arg=--import-memory -C link-arg=--global-base=32768 \ + -C link-arg=--stack-first -C link-arg=-zstack-size=32768 +cp "$runtime_allocator_target/wasm32-unknown-unknown/target-runtime/psrs_runtime.wasm" \ + "${1:-crates/psrs-runtime/artifact/psrs_allocator.wasm}" diff --git a/crates/psrs-runtime/tools/build.sh b/crates/psrs-runtime/tools/build.sh new file mode 100644 index 00000000..da326d11 --- /dev/null +++ b/crates/psrs-runtime/tools/build.sh @@ -0,0 +1,15 @@ +#!/bin/sh +set -eu +cd "$(dirname "$0")/../../.." +runtime_number_target="${CARGO_TARGET_DIR:-target}/runtime-number" +# Apply to dependencies as well: libm force_eval uses stack temporaries. +# Explicit frames let the linker measure their storage without a red zone. +RUSTFLAGS="${RUSTFLAGS:-} -C no-redzone=yes" \ +cargo rustc --locked -p psrs-runtime --lib --no-default-features --features number \ + --target-dir "$runtime_number_target" --target wasm32-unknown-unknown --profile target-runtime -- \ + -C link-arg=--import-memory -C link-arg=--global-base=65536 \ + -C link-arg=-zstack-size=65536 -C link-arg=--export=__heap_base +cargo run --locked -p psrs-runtime --no-default-features --features artifact-tool \ + --bin psrs-runtime-package -- \ + "$runtime_number_target/wasm32-unknown-unknown/target-runtime/psrs_runtime.wasm" \ + "${1:-crates/psrs-runtime/artifact/psrs_runtime.wasm}" diff --git a/crates/psrs-runtime/tools/check-reproducible.sh b/crates/psrs-runtime/tools/check-reproducible.sh new file mode 100755 index 00000000..bb0cac8a --- /dev/null +++ b/crates/psrs-runtime/tools/check-reproducible.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# Rebuild both runtime artifacts without overwriting the reviewed bytes. +set -eu +cd "$(dirname "$0")/../../.." +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +sh crates/psrs-runtime/tools/build.sh "$tmp/psrs_runtime.wasm" +sh crates/psrs-runtime/tools/build-allocator.sh "$tmp/psrs_allocator.wasm" +for artifact in psrs_runtime psrs_allocator; do + if cmp -s "$tmp/$artifact.wasm" "crates/psrs-runtime/artifact/$artifact.wasm"; then + echo "$artifact reproduces byte-for-byte" + else + echo "$artifact differs from the reviewed bytes; update its provenance" >&2 + exit 1 + fi +done diff --git a/crates/psrs-runtime/tools/package.rs b/crates/psrs-runtime/tools/package.rs new file mode 100644 index 00000000..9fe1d720 --- /dev/null +++ b/crates/psrs-runtime/tools/package.rs @@ -0,0 +1,66 @@ +//! Places the linked runtime's private stack above its static data. +use std::{env, fs}; +use wasm_encoder::{ConstExpr, GlobalSection, Module, reencode::Reencode}; +use wasmparser::{GlobalSectionReader, Operator, Parser}; + +struct Reservation; + +impl Reencode for Reservation { + type Error = String; + + fn parse_global_section( + &mut self, + globals: &mut GlobalSection, + section: GlobalSectionReader<'_>, + ) -> Result<(), wasm_encoder::reencode::Error> { + let values = section.into_iter().collect::, _>>()?; + if values.len() != 2 || !values[0].ty.mutable || values[1].ty.mutable { + return Err(wasm_encoder::reencode::Error::UserError( + "runtime must define exactly the stack pointer and heap boundary".into(), + )); + } + for (index, global) in values.into_iter().enumerate() { + let mut ops = global.init_expr.get_operators_reader(); + let expected = if index == 0 { 65536 } else { 131072 }; + if global.ty.content_type != wasmparser::ValType::I32 + || !matches!(ops.read()?, Operator::I32Const { value } if value >= 65536 && value <= expected) + || !matches!(ops.read()?, Operator::End) + { + return Err(wasm_encoder::reencode::Error::UserError( + "runtime linker layout does not match the reservation recipe".into(), + )); + } + globals.global( + self.global_type(global.ty)?, + &ConstExpr::i32_const(psrs_runtime::HEAP_START as i32), + ); + } + Ok(()) + } + + fn memory_type( + &mut self, + memory: wasmparser::MemoryType, + ) -> Result> { + let mut memory = wasm_encoder::reencode::utils::memory_type(self, memory); + memory.minimum = u64::from(psrs_runtime::HEAP_START / 65536); + Ok(memory) + } +} + +fn main() -> Result<(), Box> { + let args: Vec<_> = env::args().collect(); + if args.len() != 3 { + return Err("usage: psrs-runtime-package ".into()); + } + let bytes = fs::read(&args[1])?; + wasmparser::Validator::new().validate_all(&bytes)?; + let mut module = Module::new(); + Reservation + .parse_core_module(&mut module, Parser::new(0), &bytes) + .map_err(|error| std::io::Error::other(error.to_string()))?; + let output = module.finish(); + wasmparser::Validator::new().validate_all(&output)?; + fs::write(&args[2], output)?; + Ok(()) +} diff --git a/crates/psrs-backend/wit/deps/cli.wit b/crates/psrs-runtime/wit/deps/cli.wit similarity index 100% rename from crates/psrs-backend/wit/deps/cli.wit rename to crates/psrs-runtime/wit/deps/cli.wit diff --git a/crates/psrs-backend/wit/deps/clocks.wit b/crates/psrs-runtime/wit/deps/clocks.wit similarity index 100% rename from crates/psrs-backend/wit/deps/clocks.wit rename to crates/psrs-runtime/wit/deps/clocks.wit diff --git a/crates/psrs-backend/wit/deps/filesystem.wit b/crates/psrs-runtime/wit/deps/filesystem.wit similarity index 100% rename from crates/psrs-backend/wit/deps/filesystem.wit rename to crates/psrs-runtime/wit/deps/filesystem.wit diff --git a/crates/psrs-backend/wit/deps/io.wit b/crates/psrs-runtime/wit/deps/io.wit similarity index 100% rename from crates/psrs-backend/wit/deps/io.wit rename to crates/psrs-runtime/wit/deps/io.wit diff --git a/crates/psrs-backend/wit/deps/random.wit b/crates/psrs-runtime/wit/deps/random.wit similarity index 100% rename from crates/psrs-backend/wit/deps/random.wit rename to crates/psrs-runtime/wit/deps/random.wit diff --git a/crates/psrs-backend/wit/deps/sockets.wit b/crates/psrs-runtime/wit/deps/sockets.wit similarity index 100% rename from crates/psrs-backend/wit/deps/sockets.wit rename to crates/psrs-runtime/wit/deps/sockets.wit diff --git a/crates/psrs-backend/wit/psrs-app.wit b/crates/psrs-runtime/wit/psrs-app.wit similarity index 100% rename from crates/psrs-backend/wit/psrs-app.wit rename to crates/psrs-runtime/wit/psrs-app.wit diff --git a/crates/psrs-span/Cargo.toml b/crates/psrs-span/Cargo.toml index 407dafba..02ce342f 100644 --- a/crates/psrs-span/Cargo.toml +++ b/crates/psrs-span/Cargo.toml @@ -5,3 +5,4 @@ edition.workspace = true license.workspace = true [dependencies] +stacker.workspace = true diff --git a/crates/psrs-span/src/lib.rs b/crates/psrs-span/src/lib.rs index 9ccb8717..6ebd852d 100644 --- a/crates/psrs-span/src/lib.rs +++ b/crates/psrs-span/src/lib.rs @@ -91,6 +91,19 @@ impl SourceFile { } } +/// Runs `work` where a nested expression spine cannot exhaust the native stack. +/// +/// Fixity resolution rebuilds a flat operator chain as a nested application or +/// operator spine. Walks, clones, and equality of that spine follow the source +/// length. Each recursive entry checks the remaining stack and continues on a +/// fresh heap segment before the platform stack is exhausted. The red zone +/// covers one walker frame and the unguarded callees that run before the next +/// entry. +#[inline(never)] +pub fn with_sufficient_stack(work: impl FnOnce() -> R) -> R { + stacker::maybe_grow(256 * 1024, 8 * 1024 * 1024, work) +} + impl fmt::Display for TextRange { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}..{}", self.start, self.end) @@ -101,6 +114,25 @@ impl fmt::Display for TextRange { mod tests { use super::*; + #[test] + fn deep_expression_walk_grows_onto_a_heap_stack() { + fn descend(depth: usize) -> usize { + with_sufficient_stack(|| { + let padding = std::hint::black_box([depth as u8; 24 * 1024]); + let here = usize::from(padding[depth % padding.len()]); + if depth == 0 { + here + } else { + here + descend(depth - 1) + } + }) + } + + let depth = 300; + let expected = (0..=depth).map(|value| value % 256).sum(); + assert_eq!(descend(depth), expected); + } + #[test] fn maps_byte_offsets_to_unicode_columns() { let source = SourceFile::new("test", "α\nxyz"); diff --git a/crates/psrs-syntax/src/parser/declaration/mod.rs b/crates/psrs-syntax/src/parser/declaration/mod.rs index 18e2717b..16f599a4 100644 --- a/crates/psrs-syntax/src/parser/declaration/mod.rs +++ b/crates/psrs-syntax/src/parser/declaration/mod.rs @@ -36,11 +36,21 @@ impl<'a> Parser<'a> { let pattern = self.parse_pattern()?; let equals_span = self.consume_raw(RawTokenKind::Equals)?.span; let value = self.parse_expression(0)?; - let span = TextRange::new(pattern.span.start, value.span.end); + let where_block = if self.at_raw(&RawTokenKind::Where) { + Some(self.parse_declaration_block()?) + } else { + None + }; + let end = where_block + .as_ref() + .map(|block| block.span.end) + .unwrap_or(value.span.end); + let span = TextRange::new(pattern.span.start, end); Ok(Declaration::Pattern(PatternDeclaration { pattern, equals_span, value, + where_block, span, })) } diff --git a/crates/psrs-syntax/src/parser/expr/atom/mod.rs b/crates/psrs-syntax/src/parser/expr/atom/mod.rs index b60494a3..1355f5f2 100644 --- a/crates/psrs-syntax/src/parser/expr/atom/mod.rs +++ b/crates/psrs-syntax/src/parser/expr/atom/mod.rs @@ -1,3 +1,4 @@ +mod postfix; mod records; mod sections; @@ -9,22 +10,8 @@ use super::super::{ParseError, Parser}; impl<'a> Parser<'a> { pub(super) fn parse_application(&mut self) -> Result { - let mut function = self.parse_atom()?; + let mut function = self.parse_postfix_atom()?; loop { - if self.at_raw(&RawTokenKind::Dot) && self.starts_label_at(1) { - let dot_span = self.bump().span; - let field = self.parse_label("record field")?; - let span = TextRange::new(function.span.start, field.span.end); - function = Expr { - kind: ExprKind::FieldAccess { - expression: Box::new(function), - dot_span, - field, - }, - span, - }; - continue; - } if let LayoutTokenKind::Raw(RawTokenKind::Operator(operator)) = &self.current().kind && operator == "@" && self.starts_type_atom_at(1) @@ -42,22 +29,10 @@ impl<'a> Parser<'a> { }; continue; } - if self.current().kind == LayoutTokenKind::Raw(RawTokenKind::LBrace) { - let checkpoint = self.cursor; - match self.parse_record_update(function.clone()) { - Ok(updated) => { - function = updated; - continue; - } - Err(_) => { - self.cursor = checkpoint; - } - } - } - if !self.starts_atom() { + if self.qualified_operator().is_some() || !self.starts_atom() { break; } - let argument = self.parse_atom()?; + let argument = self.parse_postfix_atom()?; let span = TextRange::new(function.span.start, argument.span.end); function = Expr { kind: ExprKind::Application(Box::new(function), Box::new(argument)), @@ -285,7 +260,7 @@ impl<'a> Parser<'a> { }) } - fn parse_qualified_value_name(&mut self) -> Result { + pub(super) fn parse_qualified_value_name(&mut self) -> Result { let token = self.current().clone(); let allow_qualification = matches!( &token.kind, @@ -331,6 +306,14 @@ impl<'a> Parser<'a> { self.bump(); let operator_token = self.current().clone(); let operator = match operator_token.kind { + LayoutTokenKind::Raw(RawTokenKind::Colon) => { + self.bump(); + ":".to_owned() + } + LayoutTokenKind::Raw(RawTokenKind::DotDot) => { + self.bump(); + "..".to_owned() + } LayoutTokenKind::Raw(RawTokenKind::Operator(text)) | LayoutTokenKind::Raw(RawTokenKind::LowerIdent(text)) | LayoutTokenKind::Raw(RawTokenKind::UpperIdent(text)) => { diff --git a/crates/psrs-syntax/src/parser/expr/atom/postfix.rs b/crates/psrs-syntax/src/parser/expr/atom/postfix.rs new file mode 100644 index 00000000..e1221c0c --- /dev/null +++ b/crates/psrs-syntax/src/parser/expr/atom/postfix.rs @@ -0,0 +1,74 @@ +//! Record projection and update bind to one atom before value application. +use super::*; +use psrs_cst::RecordAccessorField; + +impl Parser<'_> { + pub(super) fn parse_postfix_atom(&mut self) -> Result { + let mut function = self.parse_atom()?; + loop { + if self.at_raw(&RawTokenKind::Dot) && self.starts_label_at(1) { + let dot_span = self.bump().span; + let field = self.parse_label("record field")?; + let field_end = field.span.end; + function = match function.kind { + ExprKind::Name(name) if name.text == "_" => { + let marker_span = name.span; + Expr { + kind: ExprKind::RecordAccessor { + marker_span, + fields: vec![RecordAccessorField { dot_span, field }], + }, + span: TextRange::new(marker_span.start, field_end), + } + } + ExprKind::RecordAccessor { + marker_span, + mut fields, + } => { + fields.push(RecordAccessorField { dot_span, field }); + Expr { + kind: ExprKind::RecordAccessor { + marker_span, + fields, + }, + span: TextRange::new(marker_span.start, field_end), + } + } + kind => { + let span = TextRange::new(function.span.start, field.span.end); + Expr { + kind: ExprKind::FieldAccess { + expression: Box::new(Expr { + kind, + span: function.span, + }), + dot_span, + field, + }, + span, + } + } + }; + continue; + } + // Empty braces are a record argument, never a record update. + // Nonempty updates must contain at least one assignment. + if self.current().kind == LayoutTokenKind::Raw(RawTokenKind::LBrace) + && self.peek(1).kind != LayoutTokenKind::Raw(RawTokenKind::RBrace) + { + let checkpoint = self.cursor; + match self.parse_record_update(function.clone()) { + Ok(updated) => { + function = updated; + continue; + } + Err(_) => { + self.cursor = checkpoint; + } + } + } + break; + } + Ok(function) + } +} diff --git a/crates/psrs-syntax/src/parser/expr/atom/sections.rs b/crates/psrs-syntax/src/parser/expr/atom/sections.rs index 1d29f253..13c75eed 100644 --- a/crates/psrs-syntax/src/parser/expr/atom/sections.rs +++ b/crates/psrs-syntax/src/parser/expr/atom/sections.rs @@ -92,15 +92,22 @@ impl<'a> Parser<'a> { left, right, } = &first.kind - && matches!(&right.kind, ExprKind::Name(name) if name.text == "_") + && (matches!(&right.kind, ExprKind::Name(name) if name.text == "_") + || matches!(&left.kind, ExprKind::Name(name) if name.text == "_")) { + let (operand, side) = if matches!(&right.kind, ExprKind::Name(name) if name.text == "_") + { + (left.clone(), OperatorSectionSide::Left) + } else { + (right.clone(), OperatorSectionSide::Right) + }; let close_paren_span = self.consume_raw(RawTokenKind::RParen)?.span; let span = TextRange::new(open_paren_span.start, close_paren_span.end); return Ok(Expr { kind: ExprKind::OperatorSection { operator: operator.clone(), - operand: left.clone(), - side: OperatorSectionSide::Left, + operand, + side, }, span, }); diff --git a/crates/psrs-syntax/src/parser/expr/mod.rs b/crates/psrs-syntax/src/parser/expr/mod.rs index 4ced7903..1219dfe2 100644 --- a/crates/psrs-syntax/src/parser/expr/mod.rs +++ b/crates/psrs-syntax/src/parser/expr/mod.rs @@ -1,6 +1,7 @@ mod atom; mod compound; mod pattern; +mod qualified; use crate::{LayoutTokenKind, RawTokenKind}; use psrs_cst::{CstName, Expr, ExprKind}; @@ -26,16 +27,9 @@ impl<'a> Parser<'a> { if precedence < min_precedence { break; } - let is_simple = matches!( - &self.peek(1).kind, - LayoutTokenKind::Raw( - RawTokenKind::LowerIdent(_) - | RawTokenKind::UpperIdent(_) - | RawTokenKind::Operator(_) - ) - ) && self.peek(2).kind - == LayoutTokenKind::Raw(RawTokenKind::Backtick); - if is_simple { + // A qualified name such as `A.zip` is one operator, not an + // expression applied to the left operand. + if self.at_simple_backticked_operator() { self.bump(); let operator = self.parse_backticked_operator()?; self.consume_raw(RawTokenKind::Backtick)?; @@ -75,6 +69,26 @@ impl<'a> Parser<'a> { left = combined; continue; } + if let Some((operator, precedence, tokens)) = self.qualified_operator() { + if precedence < min_precedence { + break; + } + for _ in 0..tokens { + self.bump(); + } + let right = + self.parse_expression_impl(precedence + 1, allow_backtick, allow_section)?; + let span = TextRange::new(left.span.start, right.span.end); + left = Expr { + kind: ExprKind::Operator { + operator, + left: Box::new(left), + right: Box::new(right), + }, + span, + }; + continue; + } let (operator, operator_span) = match &self.current().kind { LayoutTokenKind::Raw(RawTokenKind::Operator(operator)) if operator != "@" => { (operator.clone(), self.current().span) @@ -124,14 +138,50 @@ impl<'a> Parser<'a> { Ok(left) } + fn at_simple_backticked_operator(&self) -> bool { + let close_at = match &self.peek(1).kind { + LayoutTokenKind::Raw( + RawTokenKind::LowerIdent(_) | RawTokenKind::Operator(_) | RawTokenKind::Colon, + ) => 2, + LayoutTokenKind::Raw(RawTokenKind::UpperIdent(_)) => { + let mut index = 1; + loop { + let name = self.peek(index); + let dot = self.peek(index + 1); + let part = self.peek(index + 2); + if !matches!(dot.kind, LayoutTokenKind::Raw(RawTokenKind::Dot)) + || dot.span.start != name.span.end + { + break; + } + let part_is_name = matches!( + part.kind, + LayoutTokenKind::Raw( + RawTokenKind::LowerIdent(_) | RawTokenKind::UpperIdent(_) + ) + ); + if !part_is_name || part.span.start != dot.span.end { + break; + } + index += 2; + } + index + 1 + } + _ => return false, + }; + self.peek(close_at).kind == LayoutTokenKind::Raw(RawTokenKind::Backtick) + } + fn parse_backticked_operator(&mut self) -> Result { + if matches!( + self.current().kind, + LayoutTokenKind::Raw(RawTokenKind::LowerIdent(_) | RawTokenKind::UpperIdent(_)) + ) { + return self.parse_qualified_value_name(); + } let token = self.current().clone(); match token.kind { - LayoutTokenKind::Raw( - RawTokenKind::LowerIdent(name) - | RawTokenKind::UpperIdent(name) - | RawTokenKind::Operator(name), - ) => { + LayoutTokenKind::Raw(RawTokenKind::Operator(name)) => { self.bump(); Ok(CstName::new(name, token.span)) } diff --git a/crates/psrs-syntax/src/parser/expr/pattern.rs b/crates/psrs-syntax/src/parser/expr/pattern.rs index 019de1f5..7f29f80b 100644 --- a/crates/psrs-syntax/src/parser/expr/pattern.rs +++ b/crates/psrs-syntax/src/parser/expr/pattern.rs @@ -10,6 +10,14 @@ impl<'a> Parser<'a> { let mut operands = vec![first]; let mut operators = Vec::new(); loop { + if let Some((operator, _, tokens)) = self.qualified_operator() { + for _ in 0..tokens { + self.bump(); + } + operators.push(operator); + operands.push(self.parse_pattern_prefix()?); + continue; + } let operator = match &self.current().kind { LayoutTokenKind::Raw(RawTokenKind::Operator(operator)) if operator != "@" => { operator.clone() @@ -96,23 +104,24 @@ impl<'a> Parser<'a> { } pub(crate) fn starts_pattern_atom(&self) -> bool { - matches!( - &self.current().kind, - LayoutTokenKind::Raw( - RawTokenKind::LowerIdent(_) - | RawTokenKind::UpperIdent(_) - | RawTokenKind::Integer(_) - | RawTokenKind::Number(_) - | RawTokenKind::String(_) - | RawTokenKind::Char(_) - | RawTokenKind::LParen - | RawTokenKind::LBracket - | RawTokenKind::LBrace - | RawTokenKind::As - | RawTokenKind::Hiding - | RawTokenKind::Role + self.qualified_operator().is_none() + && matches!( + &self.current().kind, + LayoutTokenKind::Raw( + RawTokenKind::LowerIdent(_) + | RawTokenKind::UpperIdent(_) + | RawTokenKind::Integer(_) + | RawTokenKind::Number(_) + | RawTokenKind::String(_) + | RawTokenKind::Char(_) + | RawTokenKind::LParen + | RawTokenKind::LBracket + | RawTokenKind::LBrace + | RawTokenKind::As + | RawTokenKind::Hiding + | RawTokenKind::Role + ) ) - ) } pub(crate) fn parse_pattern_atom(&mut self) -> Result { @@ -224,10 +233,29 @@ impl<'a> Parser<'a> { RawTokenKind::Hiding => "hiding", _ => "role", }; - Ok(Pattern { + let pattern = Pattern { kind: PatternKind::Var(CstName::new(text, token.span)), span: token.span, - }) + }; + // `as` is the fixity keyword, and it is also a legal binder. + // `merge as@(a : as')` is a named pattern, same as a lower-ident binder. + if self.at_operator_text("@") { + let at_span = self.bump().span; + let inner = self.parse_pattern_atom()?; + let span = TextRange::new(pattern.span.start, inner.span.end); + let PatternKind::Var(name) = pattern.kind else { + unreachable!("just constructed a variable pattern") + }; + return Ok(Pattern { + kind: PatternKind::Named { + name, + at_span, + pattern: Box::new(inner), + }, + span, + }); + } + Ok(pattern) } LayoutTokenKind::Raw(RawTokenKind::LParen) => self.parse_parenthesized_pattern(), LayoutTokenKind::Raw(RawTokenKind::LBracket) => self.parse_array_pattern(), diff --git a/crates/psrs-syntax/src/parser/expr/qualified.rs b/crates/psrs-syntax/src/parser/expr/qualified.rs new file mode 100644 index 00000000..45b589d7 --- /dev/null +++ b/crates/psrs-syntax/src/parser/expr/qualified.rs @@ -0,0 +1,51 @@ +use super::*; + +impl Parser<'_> { + /// Recognizes a contiguous module qualifier followed by a symbolic name. + /// The lexer maximally munches the final dot with the operator (`A.!!`). + pub(super) fn qualified_operator(&self) -> Option<(CstName, u8, usize)> { + let LayoutTokenKind::Raw(RawTokenKind::UpperIdent(first)) = &self.current().kind else { + return None; + }; + let start = self.current().span.start; + let mut end = self.current().span.end; + let mut qualifier = first.clone(); + let mut offset = 1; + loop { + let token = self.peek(offset); + if token.span.start != end { + return None; + } + if token.kind == LayoutTokenKind::Raw(RawTokenKind::Dot) { + let part = self.peek(offset + 1); + if let LayoutTokenKind::Raw(RawTokenKind::UpperIdent(name)) = &part.kind { + if part.span.start != token.span.end { + return None; + } + qualifier.push('.'); + qualifier.push_str(name); + end = part.span.end; + offset += 2; + continue; + } + } + let text = match &token.kind { + LayoutTokenKind::Raw(RawTokenKind::Operator(text)) => text.as_str(), + LayoutTokenKind::Raw(RawTokenKind::DotDot) => "..", + _ => return None, + }; + let operator = text.strip_prefix('.')?; + if operator.is_empty() || operator == "@" { + return None; + } + return Some(( + CstName::new( + format!("{qualifier}.{operator}"), + TextRange::new(start, token.span.end), + ), + precedence(operator), + offset + 1, + )); + } + } +} diff --git a/crates/psrs-syntax/src/parser/tests.rs b/crates/psrs-syntax/src/parser/tests/mod.rs similarity index 78% rename from crates/psrs-syntax/src/parser/tests.rs rename to crates/psrs-syntax/src/parser/tests/mod.rs index b61d7960..f5ee949a 100644 --- a/crates/psrs-syntax/src/parser/tests.rs +++ b/crates/psrs-syntax/src/parser/tests/mod.rs @@ -3,6 +3,25 @@ use crate::{add_layout, lex}; use psrs_cst::{Declaration, ExprKind, PatternKind, TypeExprKind, ValueRhs}; use psrs_span::SourceFile; +mod qualified_patterns; + +#[test] +fn parses_qualified_symbolic_operators_with_symbolic_dots_and_sections() { + let module = parse("module Main where\na = 2 A... 5\nb = 4 Foo.Bar.-#- 10\nc = (_ A.!! 1)\nd = (2 A.: _)\ne = A.(..) 2 5\nf = A.(:) 2 []\n").unwrap(); + for (declaration, expected) in module.declarations[..2].iter().zip(["A...", "Foo.Bar.-#-"]) { + let ExprKind::Operator { operator, .. } = &plain_value(as_value(declaration)).kind else { + panic!("expected qualified infix operator"); + }; + assert_eq!(operator.text, expected); + } + for declaration in &module.declarations[2..4] { + assert!(matches!( + plain_value(as_value(declaration)).kind, + ExprKind::OperatorSection { .. } + )); + } +} + fn parse(source: &str) -> Result { let source_file = SourceFile::new("test.purs", source); let (tokens, errors) = lex(source); @@ -76,6 +95,16 @@ fn parses_lambdas_conditionals_and_operator_precedence() { assert!(matches!(right.kind, ExprKind::Operator { ref operator, .. } if operator.text == "*")); } +#[test] +fn parses_a_qualified_name_between_backticks_as_one_operator() { + let module = parse("module Main where\nzip xs ys = xs `A.zip` ys\n").unwrap(); + let ExprKind::Operator { operator, .. } = &plain_value(as_value(&module.declarations[0])).kind + else { + panic!("expected an infix operator"); + }; + assert_eq!(operator.text, "A.zip"); +} + #[test] fn distinguishes_parenthesized_negation_from_explicit_operator_sections() { let module = parse( @@ -129,6 +158,23 @@ fn distinguishes_lowercase_record_fields_from_uppercase_qualified_values() { assert!(matches!(&qualified.kind, ExprKind::Name(name) if name.text == "Data.Array.map")); } +#[test] +fn parses_anonymous_record_field_accessors() { + let module = parse("module Main where\nproject = _.value.nested\n").unwrap(); + let accessor = plain_value(as_value(&module.declarations[0])); + let ExprKind::RecordAccessor { + marker_span, + fields, + } = &accessor.kind + else { + panic!("expected `_ .field` to have its own CST node"); + }; + assert_eq!(marker_span, &TextRange::new(28, 29)); + assert_eq!(fields.len(), 2); + assert_eq!(fields[0].field.text, "value"); + assert_eq!(fields[1].field.text, "nested"); +} + #[test] fn parses_single_line_let_blocks() { let module = parse("module Main where\nmain = let x = 1 in x\n").unwrap(); @@ -221,6 +267,18 @@ fn parses_forall_with_multiple_variables() { ); } +#[test] +fn parses_empty_parentheses_as_an_empty_row_type() { + let module = parse("module Main where\ntype Empty = ()\n").unwrap(); + let Declaration::TypeSynonym(declaration) = &module.declarations[0] else { + panic!("expected a type synonym"); + }; + assert!(matches!( + &declaration.body.kind, + TypeExprKind::Row { fields, tail: None, .. } if fields.is_empty() + )); +} + #[test] fn type_arrows_are_right_associative() { let module = parse("module Main where\nf :: a -> b -> c\nf x = x\n").unwrap(); @@ -386,3 +444,38 @@ fn keeps_a_deeper_indented_minus_in_a_case_rhs_as_an_operator() { }; assert!(matches!(&value.kind, ExprKind::Operator { operator, .. } if operator.text == "-")); } + +#[test] +fn record_projection_and_update_bind_before_value_application() { + let module = parse( + "module Main where\nproject r = consume r.value\nupdate r = consume r { value = 42 }\n", + ) + .unwrap(); + for (index, projection) in [(0, true), (1, false)] { + let expression = plain_value(as_value(&module.declarations[index])); + let ExprKind::Application(function, argument) = &expression.kind else { + panic!("record postfix must belong to the argument: {expression:?}"); + }; + assert!(matches!(&function.kind, ExprKind::Name(name) if name.text == "consume")); + if projection { + assert!( + matches!(&argument.kind, ExprKind::FieldAccess { expression, .. } if matches!(&expression.kind, ExprKind::Name(name) if name.text == "r")) + ); + } else { + assert!( + matches!(&argument.kind, ExprKind::RecordUpdate { expression, .. } if matches!(&expression.kind, ExprKind::Name(name) if name.text == "r")) + ); + } + } +} + +#[test] +fn empty_braces_are_a_record_argument() { + let module = parse("module Main where\nforce action = action {}\n").unwrap(); + let expression = plain_value(as_value(&module.declarations[0])); + let ExprKind::Application(function, argument) = &expression.kind else { + panic!("expected an application: {expression:?}"); + }; + assert!(matches!(&function.kind, ExprKind::Name(name) if name.text == "action")); + assert!(matches!(&argument.kind, ExprKind::Record { fields, .. } if fields.is_empty())); +} diff --git a/crates/psrs-syntax/src/parser/tests/qualified_patterns.rs b/crates/psrs-syntax/src/parser/tests/qualified_patterns.rs new file mode 100644 index 00000000..b3be9583 --- /dev/null +++ b/crates/psrs-syntax/src/parser/tests/qualified_patterns.rs @@ -0,0 +1,41 @@ +use super::*; + +#[test] +fn qualified_pattern_operators_preserve_names_spans_and_constructor_operands() { + for (pattern, expected, constructor) in [ + ("head L.: tail", "L.:", false), + ("Wrap head Foo.Bar.: tail", "Foo.Bar.:", true), + ("L.Wrap head Foo.Bar... tail", "Foo.Bar...", true), + ] { + let source = format!("module Main where\nmain = case value of\n {pattern} -> 42\n"); + let module = parse(&source).unwrap(); + let ExprKind::Case { alternatives, .. } = + &plain_value(as_value(&module.declarations[0])).kind + else { + panic!("expected a case"); + }; + let PatternKind::OperatorChain { + operands, + operators, + } = &alternatives[0].patterns[0].kind + else { + panic!("expected an operator chain"); + }; + assert_eq!(operands.len(), 2); + assert_eq!(operators.len(), 1); + assert_eq!(operators[0].text, expected); + let span = operators[0].span; + assert_eq!(&source[span.start as usize..span.end as usize], expected); + if constructor { + let PatternKind::Constructor { arguments, .. } = &operands[0].kind else { + panic!("expected the left constructor operand"); + }; + assert_eq!(arguments.len(), 1); + } + } +} + +#[test] +fn qualified_pattern_operator_before_unicode_guards_parses() { + parse("module Main where\nmain = case value of\n t L.: ts\n | t == tag → true\n | otherwise → go ts\n L.Nil → false\n").unwrap(); +} diff --git a/crates/psrs-syntax/src/parser/type_expr/mod.rs b/crates/psrs-syntax/src/parser/type_expr/mod.rs index b655529c..56f3a5a1 100644 --- a/crates/psrs-syntax/src/parser/type_expr/mod.rs +++ b/crates/psrs-syntax/src/parser/type_expr/mod.rs @@ -311,7 +311,12 @@ impl<'a> Parser<'a> { let close_paren_span = self.bump().span; let span = TextRange::new(open_paren_span.start, close_paren_span.end); return Ok(TypeExpr { - kind: TypeExprKind::Name(CstName::new("Unit", span)), + kind: TypeExprKind::Row { + open_paren_span, + fields: Vec::new(), + tail: None, + close_paren_span, + }, span, }); } diff --git a/crates/psrs-thir/src/evidence.rs b/crates/psrs-thir/src/evidence.rs index 43626727..d400a569 100644 --- a/crates/psrs-thir/src/evidence.rs +++ b/crates/psrs-thir/src/evidence.rs @@ -2,6 +2,19 @@ use super::TypeId; use psrs_hir::{LocalId, SymbolId, TypeId as ClassId}; use psrs_span::TextRange; +/// Authority for a conversion that is not an ordinary Coercible proof. +/// Newtype deriving relies on representation transparency and a selected +/// wrapped instance, as upstream dictionary reuse does. The checker owns that +/// validation; THIR verifies the named newtype and the conversion endpoints. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum UncheckedCoercionOrigin { + UnsafeCoerce, + NewtypeDeriving { + class_id: ClassId, + newtype_id: ClassId, + }, +} + /// A frontend-selected dictionary derivation retained in Typed Core. /// /// The class solver owns the meaning and coherence of this derivation. Core @@ -20,11 +33,16 @@ pub struct Evidence { #[derive(Clone, Debug, PartialEq, Eq)] pub enum EvidenceKind { + /// A compiler-constructed dictionary expressed as checked ordinary terms. + /// The verifier checks its fields, lexical scope, and dictionary type; + /// the frontend owns the class rule that authorizes construction. + DictionaryValue(Box), /// A dictionary parameter introduced by a constrained binding. Given(LocalId), /// A dictionary value already bound by the class elaborator. Global(SymbolId), /// A dictionary obtained from a superclass field of another dictionary. + /// Selects and forces a checked Unit thunk in the parent dictionary. Superclass { parent: Box, field: String, diff --git a/crates/psrs-thir/src/external_type_tests.rs b/crates/psrs-thir/src/external_type_tests.rs index c7023d31..5a3a107a 100644 --- a/crates/psrs-thir/src/external_type_tests.rs +++ b/crates/psrs-thir/src/external_type_tests.rs @@ -8,6 +8,7 @@ fn fixture() -> Module { id: ModuleId(2), name: "ExternalScheme".into(), externals: vec![ExternalSymbol { + source_module: None, symbol, name: "clock".into(), kind: ExternalKind::Wit { @@ -50,6 +51,17 @@ fn a_wit_scheme_is_required_even_without_a_raw_annotation() { rejects(&module, "no checked signature"); } +#[test] +fn a_library_scheme_is_required_even_without_a_raw_annotation() { + let mut module = fixture(); + module.externals[0].kind = ExternalKind::Library { + module: "Native".into(), + }; + module.verify().unwrap(); + module.external_types.clear(); + rejects(&module, "no checked signature"); +} + #[test] fn checked_external_schemes_are_unique_and_have_an_external_owner() { let mut module = fixture(); diff --git a/crates/psrs-thir/src/lib.rs b/crates/psrs-thir/src/lib.rs index 192819de..ab5add63 100644 --- a/crates/psrs-thir/src/lib.rs +++ b/crates/psrs-thir/src/lib.rs @@ -8,7 +8,7 @@ mod evidence; mod scope; mod verify; -pub use evidence::{Evidence, EvidenceKind}; +pub use evidence::{Evidence, EvidenceKind, UncheckedCoercionOrigin}; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct TypeId(pub u32); @@ -177,10 +177,11 @@ pub struct ConstructorInfo { pub parameters: Vec, } -/// The normalized checked scheme for one WIT value import. Unlike the HIR +/// The normalized checked scheme for one source foreign value import. Unlike the HIR /// signature kept for names and diagnostics, `ty` has had type synonyms -/// expanded by the type checker and uses this module's type table. Intrinsics -/// use registry-owned contracts and do not appear in this table. +/// expanded by the type checker and uses this module's type table. Bootstrap +/// intrinsics use registry-owned contracts; explicit primitive bindings still +/// require the checked source scheme recorded here. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ExternalType { pub symbol: SymbolId, @@ -249,7 +250,7 @@ pub struct Binding { pub span: TextRange, } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Debug, Eq)] pub struct Expr { pub kind: ExprKind, pub ty: TypeId, @@ -287,6 +288,16 @@ pub enum ExprKind { source_type: TypeId, target_type: TypeId, }, + /// An unchecked representational conversion with an explicit authority. Unlike + /// [`ExprKind::Coerce`] it carries no `Coercible` proof; the value crosses + /// its erased representation unchanged. Core lowers it to a + /// `RepresentationCast`. + UnsafeCoerce { + value: Box, + source_type: TypeId, + target_type: TypeId, + origin: UncheckedCoercionOrigin, + }, Application(Box, Box), Lambda { binder: Binder, @@ -307,6 +318,32 @@ pub enum ExprKind { }, } +impl Clone for Expr { + fn clone(&self) -> Self { + psrs_span::with_sufficient_stack(|| clone_expr(self)) + } +} + +impl PartialEq for Expr { + fn eq(&self, other: &Self) -> bool { + psrs_span::with_sufficient_stack(|| expr_eq(self, other)) + } +} + +#[inline(never)] +fn clone_expr(expression: &Expr) -> Expr { + Expr { + kind: expression.kind.clone(), + ty: expression.ty, + span: expression.span, + } +} + +#[inline(never)] +fn expr_eq(left: &Expr, right: &Expr) -> bool { + left.ty == right.ty && left.span == right.span && left.kind == right.kind +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct CaseBranch { pub pattern: Pattern, diff --git a/crates/psrs-thir/src/rank_n_tests.rs b/crates/psrs-thir/src/rank_n_tests.rs index ec2705af..0e717419 100644 --- a/crates/psrs-thir/src/rank_n_tests.rs +++ b/crates/psrs-thir/src/rank_n_tests.rs @@ -392,3 +392,73 @@ fn verifier_rejects_two_residuals_for_one_row_variable() { "global reference is not a valid scheme instance", ); } + +#[test] +fn quantified_let_scopes_its_locals_and_rejects_unbound_local_variables() { + for unbound in [false, true] { + let outer = TypeVariableId(10); + let inner = if unbound { TypeVariableId(11) } else { outer }; + let mut types = vec![Type::Variable(outer), Type::Variable(inner)]; + let outer_arrow = arrow(&mut types, TypeId(0), TypeId(0)); + let inner_arrow = arrow(&mut types, TypeId(1), TypeId(1)); + let quantified = TypeId(types.len() as u32); + types.push(Type::ForAll { + variables: vec![outer], + body: outer_arrow, + }); + let binder = Binder { + id: LocalId(0), + name: "ident".into(), + ty: inner_arrow, + span: SPAN, + }; + let local_value = Expr { + kind: ExprKind::Lambda { + binder: Binder { + id: LocalId(1), + name: "x".into(), + ty: TypeId(1), + span: SPAN, + }, + body: Box::new(Expr { + kind: ExprKind::Local(LocalId(1)), + ty: TypeId(1), + span: SPAN, + }), + }, + ty: inner_arrow, + span: SPAN, + }; + let value = Expr { + kind: ExprKind::Let { + bindings: vec![Binding { + binder, + quantified: Vec::new(), + value: local_value, + span: SPAN, + }], + body: Box::new(Expr { + kind: ExprKind::Local(LocalId(0)), + ty: outer_arrow, + span: SPAN, + }), + }, + ty: quantified, + span: SPAN, + }; + let module = module(types, vec![declaration(0, Vec::new(), quantified, value)]); + if unbound { + assert!( + module + .verify() + .unwrap_err() + .iter() + .any(|error| error.message == "type variable is outside its quantifier scope") + ); + } else { + module + .verify() + .expect("the enclosing forall scopes monomorphic local definitions"); + } + } +} diff --git a/crates/psrs-thir/src/scope/expr.rs b/crates/psrs-thir/src/scope/expr.rs new file mode 100644 index 00000000..d52b6d45 --- /dev/null +++ b/crates/psrs-thir/src/scope/expr.rs @@ -0,0 +1,195 @@ +use super::{ + enter_binders, leading_foralls, open_child_binders, open_expression_binders, + verify_evidence_scope, verify_pattern_scope, verify_type_scope, +}; +use crate::{Expr, ExprKind, Type, VerifyError}; +use psrs_hir::TypeVariableId; +use std::collections::HashSet; + +pub(super) fn verify_expr_scope( + expression: &Expr, + types: &[Type], + scope: &mut HashSet, + errors: &mut Vec, +) { + psrs_span::with_sufficient_stack(|| verify_expr_scope_inner(expression, types, scope, errors)) +} + +fn verify_expr_scope_inner( + expression: &Expr, + types: &[Type], + scope: &mut HashSet, + errors: &mut Vec, +) { + verify_type_scope( + expression.ty, + types, + scope, + expression.span, + &mut HashSet::new(), + errors, + ); + match &expression.kind { + ExprKind::Local(_) + | ExprKind::Global(_) + | ExprKind::Integer(_) + | ExprKind::Number(_) + | ExprKind::Boolean(_) + | ExprKind::String(_) + | ExprKind::Char(_) => {} + ExprKind::Array(elements) => { + let binders = leading_foralls(types, expression.ty); + for element in elements { + let mut element_scope = scope.clone(); + open_child_binders(element, &binders, types, &mut element_scope, errors); + verify_expr_scope(element, types, &mut element_scope, errors); + } + } + ExprKind::Record(fields) => { + let field_types = crate::record_fields(types, expression.ty).unwrap_or_default(); + for (label, value) in fields { + let binders = field_types + .iter() + .find(|(field_label, _)| field_label == label) + .map(|(_, ty)| leading_foralls(types, *ty)) + .unwrap_or_default(); + let mut field_scope = scope.clone(); + open_child_binders(value, &binders, types, &mut field_scope, errors); + verify_expr_scope(value, types, &mut field_scope, errors); + } + } + ExprKind::RecordUpdate { expression, fields } => { + verify_expr_scope(expression, types, scope, errors); + for (_, value) in fields { + verify_expr_scope(value, types, scope, errors); + } + } + ExprKind::FieldAccess { + expression: record, .. + } => { + let binders = leading_foralls(types, expression.ty); + let mut record_scope = scope.clone(); + open_child_binders(record, &binders, types, &mut record_scope, errors); + verify_expr_scope(record, types, &mut record_scope, errors) + } + ExprKind::Evidence(evidence) => verify_evidence_scope(evidence, types, scope, errors), + ExprKind::Coerce { + value, + evidence, + source_type, + target_type, + } => { + verify_expr_scope(value, types, scope, errors); + verify_evidence_scope(evidence, types, scope, errors); + verify_type_scope( + *source_type, + types, + scope, + evidence.span, + &mut HashSet::new(), + errors, + ); + verify_type_scope( + *target_type, + types, + scope, + evidence.span, + &mut HashSet::new(), + errors, + ); + } + ExprKind::UnsafeCoerce { + value, + source_type, + target_type, + .. + } => { + verify_expr_scope(value, types, scope, errors); + verify_type_scope( + *source_type, + types, + scope, + expression.span, + &mut HashSet::new(), + errors, + ); + verify_type_scope( + *target_type, + types, + scope, + expression.span, + &mut HashSet::new(), + errors, + ); + } + ExprKind::Application(function, argument) => { + let binders = leading_foralls(types, expression.ty); + let mut function_scope = scope.clone(); + open_child_binders(function, &binders, types, &mut function_scope, errors); + verify_expr_scope(function, types, &mut function_scope, errors); + let mut argument_scope = scope.clone(); + open_child_binders(argument, &binders, types, &mut argument_scope, errors); + verify_expr_scope(argument, types, &mut argument_scope, errors); + } + ExprKind::Lambda { binder, body } => { + let mut body_scope = scope.clone(); + open_expression_binders(expression, types, &mut body_scope, errors); + verify_type_scope( + binder.ty, + types, + &body_scope, + binder.span, + &mut HashSet::new(), + errors, + ); + verify_expr_scope(body, types, &mut body_scope, errors); + } + ExprKind::Let { bindings, body } => { + let mut body_scope = scope.clone(); + open_expression_binders(expression, types, &mut body_scope, errors); + for binding in bindings { + let mut binding_scope = body_scope.clone(); + enter_binders( + &binding.quantified, + &mut binding_scope, + binding.span, + "binding quantifiers must be unique and lexically distinct", + errors, + ); + verify_type_scope( + binding.binder.ty, + types, + &binding_scope, + binding.binder.span, + &mut HashSet::new(), + errors, + ); + verify_expr_scope(&binding.value, types, &mut binding_scope, errors); + } + verify_expr_scope(body, types, &mut body_scope, errors); + } + ExprKind::If { + condition, + then_branch, + else_branch, + } => { + verify_expr_scope(condition, types, scope, errors); + let mut branch_scope = scope.clone(); + open_expression_binders(expression, types, &mut branch_scope, errors); + verify_expr_scope(then_branch, types, &mut branch_scope, errors); + verify_expr_scope(else_branch, types, &mut branch_scope, errors); + } + ExprKind::Case { + scrutinee, + branches, + } => { + verify_expr_scope(scrutinee, types, scope, errors); + let mut branch_scope = scope.clone(); + open_expression_binders(expression, types, &mut branch_scope, errors); + for branch in branches { + verify_pattern_scope(&branch.pattern, types, &branch_scope, errors); + verify_expr_scope(&branch.value, types, &mut branch_scope, errors); + } + } + } +} diff --git a/crates/psrs-thir/src/scope/mod.rs b/crates/psrs-thir/src/scope/mod.rs index 83984fc2..712bb110 100644 --- a/crates/psrs-thir/src/scope/mod.rs +++ b/crates/psrs-thir/src/scope/mod.rs @@ -1,12 +1,15 @@ use crate::{ - Evidence, EvidenceKind, Expr, ExprKind, Module, Pattern, PatternKind, Type, TypeId, VerifyError, + Evidence, EvidenceKind, Expr, Module, Pattern, PatternKind, Type, TypeId, VerifyError, }; use psrs_hir::TypeVariableId; use psrs_span::TextRange; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; +mod expr; mod free_type_variables; -use free_type_variables::free_type_variables; +mod polymorphic; +use expr::verify_expr_scope; +use polymorphic::{leading_foralls, open_child_binders}; pub(super) fn verify_module(module: &Module) -> Vec { let mut errors = Vec::new(); @@ -121,10 +124,12 @@ fn verify_type_scope( return; } match types.get(id.0 as usize) { - Some(Type::Variable(variable)) if !scope.contains(variable) => errors.push(VerifyError { - span, - message: "type variable is outside its quantifier scope", - }), + Some(Type::Variable(variable)) if !scope.contains(variable) => { + errors.push(VerifyError { + span, + message: "type variable is outside its quantifier scope", + }); + } Some(Type::Application(function, argument)) => { verify_type_scope(*function, types, scope, span, active, errors); verify_type_scope(*argument, types, scope, span, active, errors); @@ -172,145 +177,6 @@ fn enter_binders( scope.extend(local); } -fn verify_expr_scope( - expression: &Expr, - types: &[Type], - scope: &mut HashSet, - errors: &mut Vec, -) { - verify_type_scope( - expression.ty, - types, - scope, - expression.span, - &mut HashSet::new(), - errors, - ); - match &expression.kind { - ExprKind::Local(_) - | ExprKind::Global(_) - | ExprKind::Integer(_) - | ExprKind::Number(_) - | ExprKind::Boolean(_) - | ExprKind::String(_) - | ExprKind::Char(_) => {} - ExprKind::Array(elements) => { - for element in elements { - verify_expr_scope(element, types, scope, errors); - } - } - ExprKind::Record(fields) => { - for (_, value) in fields { - verify_expr_scope(value, types, scope, errors); - } - } - ExprKind::RecordUpdate { expression, fields } => { - verify_expr_scope(expression, types, scope, errors); - for (_, value) in fields { - verify_expr_scope(value, types, scope, errors); - } - } - ExprKind::FieldAccess { expression, .. } => { - verify_expr_scope(expression, types, scope, errors) - } - ExprKind::Evidence(evidence) => verify_evidence_scope(evidence, types, scope, errors), - ExprKind::Coerce { - value, - evidence, - source_type, - target_type, - } => { - verify_expr_scope(value, types, scope, errors); - verify_evidence_scope(evidence, types, scope, errors); - verify_type_scope( - *source_type, - types, - scope, - evidence.span, - &mut HashSet::new(), - errors, - ); - verify_type_scope( - *target_type, - types, - scope, - evidence.span, - &mut HashSet::new(), - errors, - ); - } - ExprKind::Application(function, argument) => { - let binders = leading_foralls(types, expression.ty); - let mut function_scope = scope.clone(); - open_child_binders(function, &binders, types, &mut function_scope, errors); - verify_expr_scope(function, types, &mut function_scope, errors); - let mut argument_scope = scope.clone(); - open_child_binders(argument, &binders, types, &mut argument_scope, errors); - verify_expr_scope(argument, types, &mut argument_scope, errors); - } - ExprKind::Lambda { binder, body } => { - let mut body_scope = scope.clone(); - open_expression_binders(expression, types, &mut body_scope, errors); - verify_type_scope( - binder.ty, - types, - &body_scope, - binder.span, - &mut HashSet::new(), - errors, - ); - verify_expr_scope(body, types, &mut body_scope, errors); - } - ExprKind::Let { bindings, body } => { - for binding in bindings { - let mut binding_scope = scope.clone(); - enter_binders( - &binding.quantified, - &mut binding_scope, - binding.span, - "binding quantifiers must be unique and lexically distinct", - errors, - ); - verify_type_scope( - binding.binder.ty, - types, - &binding_scope, - binding.binder.span, - &mut HashSet::new(), - errors, - ); - verify_expr_scope(&binding.value, types, &mut binding_scope, errors); - } - let mut body_scope = scope.clone(); - open_expression_binders(expression, types, &mut body_scope, errors); - verify_expr_scope(body, types, &mut body_scope, errors); - } - ExprKind::If { - condition, - then_branch, - else_branch, - } => { - verify_expr_scope(condition, types, scope, errors); - let mut branch_scope = scope.clone(); - open_expression_binders(expression, types, &mut branch_scope, errors); - verify_expr_scope(then_branch, types, &mut branch_scope, errors); - verify_expr_scope(else_branch, types, &mut branch_scope, errors); - } - ExprKind::Case { - scrutinee, - branches, - } => { - verify_expr_scope(scrutinee, types, scope, errors); - let mut branch_scope = scope.clone(); - open_expression_binders(expression, types, &mut branch_scope, errors); - for branch in branches { - verify_pattern_scope(&branch.pattern, types, &branch_scope, errors); - verify_expr_scope(&branch.value, types, &mut branch_scope, errors); - } - } - } -} - fn open_expression_binders( expression: &Expr, types: &[Type], @@ -334,42 +200,6 @@ fn open_expression_binders( scope.extend(local); } -fn open_child_binders( - child: &Expr, - candidates: &[TypeVariableId], - types: &[Type], - scope: &mut HashSet, - errors: &mut Vec, -) { - let mut free = HashSet::new(); - free_type_variables( - child.ty, - types, - &mut HashMap::new(), - &mut HashSet::new(), - &mut free, - ); - let relevant = candidates - .iter() - .copied() - .filter(|variable| free.contains(variable)) - .collect::>(); - if relevant.is_empty() { - return; - } - let mut local = HashSet::new(); - if relevant - .iter() - .any(|variable| !local.insert(*variable) || scope.contains(variable)) - { - errors.push(VerifyError { - span: child.span, - message: "forall binder shadows an active type variable", - }); - } - scope.extend(local); -} - fn verify_pattern_scope( pattern: &Pattern, types: &[Type], @@ -425,6 +255,9 @@ fn verify_evidence_scope( errors, ); match &evidence.kind { + EvidenceKind::DictionaryValue(value) => { + verify_expr_scope(value, types, &mut scope.clone(), errors) + } EvidenceKind::Given(_) | EvidenceKind::Global(_) => {} EvidenceKind::Coercible { source_type, @@ -481,20 +314,3 @@ fn verify_evidence_scope( } } } - -fn leading_foralls(types: &[Type], mut id: TypeId) -> Vec { - let mut variables = Vec::new(); - let mut seen = HashSet::new(); - while let Some(Type::ForAll { - variables: binders, - body, - }) = types.get(id.0 as usize) - { - if !seen.insert(id) { - break; - } - variables.extend(binders.iter().copied()); - id = *body; - } - variables -} diff --git a/crates/psrs-thir/src/scope/polymorphic.rs b/crates/psrs-thir/src/scope/polymorphic.rs new file mode 100644 index 00000000..5374f6b5 --- /dev/null +++ b/crates/psrs-thir/src/scope/polymorphic.rs @@ -0,0 +1,57 @@ +use super::free_type_variables::free_type_variables; +use crate::{Expr, Type, TypeId, VerifyError}; +use psrs_hir::TypeVariableId; +use std::collections::{HashMap, HashSet}; + +pub(super) fn open_child_binders( + child: &Expr, + candidates: &[TypeVariableId], + types: &[Type], + scope: &mut HashSet, + errors: &mut Vec, +) { + let mut free = HashSet::new(); + free_type_variables( + child.ty, + types, + &mut HashMap::new(), + &mut HashSet::new(), + &mut free, + ); + let relevant = candidates + .iter() + .copied() + .filter(|variable| free.contains(variable)) + .collect::>(); + if relevant.is_empty() { + return; + } + let mut local = HashSet::new(); + if relevant + .iter() + .any(|variable| !local.insert(*variable) || scope.contains(variable)) + { + errors.push(VerifyError { + span: child.span, + message: "forall binder shadows an active type variable", + }); + } + scope.extend(local); +} + +pub(super) fn leading_foralls(types: &[Type], mut id: TypeId) -> Vec { + let mut variables = Vec::new(); + let mut seen = HashSet::new(); + while let Some(Type::ForAll { + variables: binders, + body, + }) = types.get(id.0 as usize) + { + if !seen.insert(id) { + break; + } + variables.extend(binders.iter().copied()); + id = *body; + } + variables +} diff --git a/crates/psrs-thir/src/tests/constructed_dictionary.rs b/crates/psrs-thir/src/tests/constructed_dictionary.rs new file mode 100644 index 00000000..04d564fd --- /dev/null +++ b/crates/psrs-thir/src/tests/constructed_dictionary.rs @@ -0,0 +1,61 @@ +use super::*; + +#[test] +fn a_runtime_dictionary_cannot_authorize_a_coercion() { + let span = TextRange::new(0, 1); + let dictionary = Evidence { + kind: EvidenceKind::DictionaryValue(Box::new(Expr { + kind: ExprKind::Record(Vec::new()), + ty: TypeId(3), + span, + })), + class_id: psrs_hir::TypeId::COERCIBLE, + ty: TypeId(3), + span, + }; + let module = Module { + id: ModuleId(0), + name: "Main".into(), + type_names: Vec::new(), + externals: Vec::new(), + external_types: Vec::new(), + types: vec![ + Type::Constructor(TypeConstructor::Int), + Type::RowEmpty, + Type::Constructor(TypeConstructor::Record), + Type::Application(TypeId(2), TypeId(1)), + ], + newtype_ids: Vec::new(), + opaque_ids: Vec::new(), + callable_types: Vec::new(), + constructors: Vec::new(), + declarations: vec![Declaration { + symbol: SymbolId::new(ModuleId(0), 0), + name: "main".into(), + name_span: span, + quantified: Vec::new(), + ty: TypeId(0), + span, + value: Expr { + kind: ExprKind::Coerce { + value: Box::new(Expr { + kind: ExprKind::Integer(42), + ty: TypeId(0), + span, + }), + evidence: dictionary, + source_type: TypeId(0), + target_type: TypeId(0), + }, + ty: TypeId(0), + span, + }, + }], + span, + }; + let errors = module + .verify() + .expect_err("dictionary terms are not proof boundaries"); + assert!(errors.iter().any(|error| error.message + == "coercion expression requires an explicit Coercible proof boundary")); +} diff --git a/crates/psrs-thir/src/tests/deep_expr.rs b/crates/psrs-thir/src/tests/deep_expr.rs new file mode 100644 index 00000000..50371b6e --- /dev/null +++ b/crates/psrs-thir/src/tests/deep_expr.rs @@ -0,0 +1,30 @@ +use super::*; + +/// Clone and equality follow a deep application spine on a heap stack. +/// Ordinary recursive destruction also fits the worker stack at this depth. +#[test] +fn deep_application_clone_equality_and_drop_stay_within_a_worker_stack() { + let mut expression = Expr { + kind: ExprKind::Boolean(true), + ty: TypeId(0), + span: TextRange::default(), + }; + for _ in 0..800 { + expression = Expr { + kind: ExprKind::Application( + Box::new(Expr { + kind: ExprKind::Boolean(false), + ty: TypeId(0), + span: TextRange::default(), + }), + Box::new(expression), + ), + ty: TypeId(0), + span: TextRange::default(), + }; + } + let cloned = expression.clone(); + assert_eq!(expression, cloned); + drop(cloned); + drop(expression); +} diff --git a/crates/psrs-thir/src/tests.rs b/crates/psrs-thir/src/tests/mod.rs similarity index 70% rename from crates/psrs-thir/src/tests.rs rename to crates/psrs-thir/src/tests/mod.rs index edc82c8d..c81e25ed 100644 --- a/crates/psrs-thir/src/tests.rs +++ b/crates/psrs-thir/src/tests/mod.rs @@ -93,6 +93,46 @@ fn verifier_checks_instance_context_against_constructor_parameters() { assert!(errors.iter().any(|error| { error.message == "instance evidence does not match its context parameter" })); + + let mut equivalent = module.clone(); + for variable in [TypeVariableId(0), TypeVariableId(1)] { + let base = equivalent.types.len() as u32; + equivalent.types.extend([ + Type::Variable(variable), + Type::Application(TypeId(4), TypeId(base)), + Type::Application(TypeId(base + 1), TypeId(base)), + Type::ForAll { + variables: vec![variable], + body: TypeId(base + 2), + }, + Type::RowExtend { + label: "method".into(), + ty: TypeId(base + 3), + tail: TypeId(2), + }, + Type::Application(TypeId(1), TypeId(base + 4)), + ]); + } + equivalent.types.extend([ + Type::Application(TypeId(4), TypeId(12)), + Type::Application(TypeId(19), dictionary), + ]); + let ExprKind::Evidence(evidence) = &mut equivalent.declarations[0].value.kind else { + unreachable!() + }; + let EvidenceKind::Instance { + constructor_type, + context, + .. + } = &mut evidence.kind + else { + unreachable!() + }; + *constructor_type = TypeId(20); + context[0].ty = TypeId(18); + equivalent + .verify() + .expect("alpha-equivalent context dictionaries are valid"); } #[test] @@ -157,6 +197,96 @@ fn verifier_requires_superclass_evidence_to_name_a_well_typed_field() { ); } +#[test] +fn verifier_accepts_alpha_equivalent_superclass_field_types() { + let left_variable = TypeVariableId(10); + let right_variable = TypeVariableId(11); + let module = Module { + type_names: Vec::new(), + id: ModuleId(0), + name: "Main".into(), + externals: Vec::new(), + external_types: Vec::new(), + types: vec![ + Type::RowEmpty, + Type::Variable(left_variable), + Type::Constructor(TypeConstructor::Function), + Type::Application(TypeId(2), TypeId(1)), + Type::Application(TypeId(3), TypeId(1)), + Type::ForAll { + variables: vec![left_variable], + body: TypeId(4), + }, + Type::Variable(right_variable), + Type::Application(TypeId(2), TypeId(6)), + Type::Application(TypeId(7), TypeId(6)), + Type::ForAll { + variables: vec![right_variable], + body: TypeId(8), + }, + Type::RowExtend { + label: "method".into(), + ty: TypeId(5), + tail: TypeId(0), + }, + Type::Constructor(TypeConstructor::Record), + Type::Application(TypeId(11), TypeId(10)), + Type::RowExtend { + label: "super".into(), + ty: TypeId(18), + tail: TypeId(0), + }, + Type::Constructor(TypeConstructor::Record), + Type::Application(TypeId(14), TypeId(13)), + Type::Constructor(TypeConstructor::Unit), + Type::Application(TypeId(2), TypeId(16)), + Type::Application(TypeId(17), TypeId(12)), + ], + newtype_ids: Vec::new(), + opaque_ids: Vec::new(), + callable_types: Vec::new(), + constructors: Vec::new(), + declarations: vec![Declaration { + symbol: SymbolId::new(ModuleId(0), 0), + name: "main".into(), + name_span: TextRange::new(0, 4), + quantified: Vec::new(), + ty: TypeId(12), + value: Expr { + kind: ExprKind::Evidence(Evidence { + kind: EvidenceKind::Superclass { + parent: Box::new(Evidence { + kind: EvidenceKind::Given(LocalId(0)), + class_id: psrs_hir::TypeId::new(ModuleId(0), 1), + ty: TypeId(15), + span: TextRange::new(8, 9), + }), + field: "super".into(), + }, + class_id: psrs_hir::TypeId::new(ModuleId(0), 0), + ty: TypeId(12), + span: TextRange::new(8, 18), + }), + ty: TypeId(12), + span: TextRange::new(8, 18), + }, + span: TextRange::new(0, 18), + }], + span: TextRange::new(0, 18), + }; + + assert!(module.verify().is_ok()); + let mut invalid = module; + invalid.types[16] = Type::Constructor(TypeConstructor::Boolean); + assert!( + invalid + .verify() + .unwrap_err() + .iter() + .any(|error| error.message == "superclass evidence field has the wrong type") + ); +} + /// A module carrying `Proxy 1` and `Proxy 2` and one global reference at each. /// A literal is decided, so the verifier compares the two by value instead of /// treating both as applications of the same nominal head. @@ -323,3 +453,6 @@ fn verifier_rejects_coercion_evidence_for_a_different_boundary() { error.message == "coercion expression requires an explicit Coercible proof boundary" })); } + +mod constructed_dictionary; +mod deep_expr; diff --git a/crates/psrs-thir/src/verify/mod.rs b/crates/psrs-thir/src/verify/mod.rs index 20e2a369..c8c813ae 100644 --- a/crates/psrs-thir/src/verify/mod.rs +++ b/crates/psrs-thir/src/verify/mod.rs @@ -1,7 +1,6 @@ use crate::{ Evidence, EvidenceKind, Expr, ExprKind, Module, Pattern, PatternKind, Type, TypeId, VerifyError, }; -use psrs_hir::ExternalKind; use psrs_span::TextRange; mod semantics; @@ -34,7 +33,7 @@ pub(super) fn verify_module(module: &Module) -> Result<(), Vec> { ); } for external in &module.externals { - if matches!(&external.kind, ExternalKind::Wit { .. }) + if external.kind.requires_checked_signature() && !external_type_symbols.contains(&external.symbol) { errors.push(VerifyError { @@ -85,7 +84,7 @@ pub(super) fn verify_module(module: &Module) -> Result<(), Vec> { declaration.name_span, &mut errors, ); - verify_expr(&declaration.value, &module.types, &mut errors); + verify_expr(&declaration.value, module, &mut errors); } if !errors.is_empty() { return Err(errors); @@ -98,7 +97,12 @@ pub(super) fn verify_module(module: &Module) -> Result<(), Vec> { } } -fn verify_expr(expression: &Expr, types: &[Type], errors: &mut Vec) { +fn verify_expr(expression: &Expr, module: &Module, errors: &mut Vec) { + psrs_span::with_sufficient_stack(|| verify_expr_inner(expression, module, errors)) +} + +fn verify_expr_inner(expression: &Expr, module: &Module, errors: &mut Vec) { + let types = &module.types; verify_type_id(expression.ty, types.len(), expression.span, errors); match &expression.kind { ExprKind::Local(_) @@ -110,30 +114,30 @@ fn verify_expr(expression: &Expr, types: &[Type], errors: &mut Vec) | ExprKind::Char(_) => {} ExprKind::Array(elements) => { for element in elements { - verify_expr(element, types, errors); + verify_expr(element, module, errors); } } ExprKind::Record(fields) => { for (_, value) in fields { - verify_expr(value, types, errors); + verify_expr(value, module, errors); } } ExprKind::RecordUpdate { expression, fields } => { - verify_expr(expression, types, errors); + verify_expr(expression, module, errors); for (_, value) in fields { - verify_expr(value, types, errors); + verify_expr(value, module, errors); } } - ExprKind::FieldAccess { expression, .. } => verify_expr(expression, types, errors), - ExprKind::Evidence(evidence) => verify_evidence(evidence, types, errors), + ExprKind::FieldAccess { expression, .. } => verify_expr(expression, module, errors), + ExprKind::Evidence(evidence) => verify_evidence(evidence, module, errors), ExprKind::Coerce { value, evidence, source_type, target_type, } => { - verify_expr(value, types, errors); - verify_evidence(evidence, types, errors); + verify_expr(value, module, errors); + verify_evidence(evidence, module, errors); if evidence.class_id != psrs_hir::TypeId::COERCIBLE { errors.push(VerifyError { span: evidence.span, @@ -163,46 +167,88 @@ fn verify_expr(expression: &Expr, types: &[Type], errors: &mut Vec) }), } } + ExprKind::UnsafeCoerce { + value, + source_type, + target_type, + origin, + } => { + verify_expr(value, module, errors); + if *source_type != value.ty || *target_type != expression.ty { + errors.push(VerifyError { + span: expression.span, + message: "unsafe coercion boundary types do not match its value and result", + }); + } + if let crate::UncheckedCoercionOrigin::NewtypeDeriving { newtype_id, .. } = origin { + let constructors = module + .constructors + .iter() + .filter(|constructor| constructor.type_id == *newtype_id) + .collect::>(); + if !module.newtype_ids.contains(newtype_id) + || constructors.len() != 1 + || constructors[0].field_count != 1 + || constructors[0].field_types.len() != 1 + { + errors.push(VerifyError { + span: expression.span, + message: "newtype deriving conversion requires a declared single-field newtype", + }); + } + } + } ExprKind::Application(function, argument) => { - verify_expr(function, types, errors); - verify_expr(argument, types, errors); + verify_expr(function, module, errors); + verify_expr(argument, module, errors); } ExprKind::Lambda { binder, body } => { verify_type_id(binder.ty, types.len(), binder.span, errors); - verify_expr(body, types, errors); + verify_expr(body, module, errors); } ExprKind::Let { bindings, body } => { for binding in bindings { verify_type_id(binding.binder.ty, types.len(), binding.binder.span, errors); - verify_expr(&binding.value, types, errors); + verify_expr(&binding.value, module, errors); } - verify_expr(body, types, errors); + verify_expr(body, module, errors); } ExprKind::If { condition, then_branch, else_branch, } => { - verify_expr(condition, types, errors); - verify_expr(then_branch, types, errors); - verify_expr(else_branch, types, errors); + verify_expr(condition, module, errors); + verify_expr(then_branch, module, errors); + verify_expr(else_branch, module, errors); } ExprKind::Case { scrutinee, branches, } => { - verify_expr(scrutinee, types, errors); + verify_expr(scrutinee, module, errors); for branch in branches { verify_pattern(&branch.pattern, types.len(), errors); - verify_expr(&branch.value, types, errors); + verify_expr(&branch.value, module, errors); } } } } -fn verify_evidence(evidence: &Evidence, types: &[Type], errors: &mut Vec) { +fn verify_evidence(evidence: &Evidence, module: &Module, errors: &mut Vec) { + let types = &module.types; verify_type_id(evidence.ty, types.len(), evidence.span, errors); match &evidence.kind { + EvidenceKind::DictionaryValue(value) => { + verify_expr(value, module, errors); + if !semantics::types_equal(value.ty, evidence.ty, module) { + errors.push(VerifyError { + span: evidence.span, + message: "constructed evidence has the wrong dictionary type", + }); + } + } + EvidenceKind::Given(_) | EvidenceKind::Global(_) => {} EvidenceKind::Coercible { source_type, @@ -227,7 +273,7 @@ fn verify_evidence(evidence: &Evidence, types: &[Type], errors: &mut Vec { - verify_evidence(parent, types, errors); + verify_evidence(parent, module, errors); let Some(fields) = crate::record_fields(types, parent.ty) else { errors.push(VerifyError { span: evidence.span, @@ -236,7 +282,15 @@ fn verify_evidence(evidence: &Evidence, types: &[Type], errors: &mut Vec {} + Some((_, field_ty)) + if crate::arrow_parts(types, *field_ty).is_some_and( + |(parameter, result)| { + matches!( + types.get(parameter.0 as usize), + Some(Type::Constructor(crate::TypeConstructor::Unit)) + ) && semantics::types_equal(result, evidence.ty, module) + }, + ) => {} _ => errors.push(VerifyError { span: evidence.span, message: "superclass evidence field has the wrong type", @@ -251,7 +305,7 @@ fn verify_evidence(evidence: &Evidence, types: &[Type], errors: &mut Vec bool { + Matcher::new(module).equal(left, right, &mut HashSet::new()) +} + struct Matcher<'a> { module: &'a Module, flexible: HashSet, @@ -116,35 +120,27 @@ impl<'a> Matcher<'a> { body: actual_body, } = actual_type { - if let Type::ForAll { - variables: expected_variables, - body: expected_body, - } = expected_type - { - if actual_variables.len() != expected_variables.len() - || actual_variables - .iter() - .any(|variable| self.alpha.contains_key(variable)) - { - self.active.remove(&(actual, expected)); - return false; - } - for (actual, expected) in actual_variables.iter().zip(expected_variables) { - self.alpha.insert(*actual, *expected); - } - let result = self.subsumes(*actual_body, *expected_body); - for variable in actual_variables { - self.alpha.remove(variable); - } - self.active.remove(&(actual, expected)); - return result; - } let added = actual_variables .iter() .copied() .filter(|variable| self.flexible.insert(*variable)) .collect::>(); - let result = self.subsumes(*actual_body, expected); + let (expected_body, expected_flexible) = match expected_type { + Type::ForAll { variables, body } => { + let previous = variables + .iter() + .map(|variable| (*variable, self.flexible.remove(variable))) + .collect::>(); + (*body, previous) + } + _ => (expected, Vec::new()), + }; + let result = self.subsumes(*actual_body, expected_body); + for (variable, was_flexible) in expected_flexible { + if was_flexible { + self.flexible.insert(variable); + } + } for variable in added { self.flexible.remove(&variable); self.replacements.remove(&variable); diff --git a/crates/psrs-thir/src/verify/semantics/matching/tests.rs b/crates/psrs-thir/src/verify/semantics/matching/tests.rs index 0b1cf2ed..201348e0 100644 --- a/crates/psrs-thir/src/verify/semantics/matching/tests.rs +++ b/crates/psrs-thir/src/verify/semantics/matching/tests.rs @@ -157,6 +157,51 @@ fn alpha_equal_foralls_match_reordered_rows_inside_nested_proxy_types() { assert!(Matcher::new(&module).equal(left, right, &mut HashSet::new())); } +#[test] +fn a_more_polymorphic_function_matches_a_rank_n_instance_method() { + let mut types = Vec::new(); + let x = TypeVariableId(50); + let y = TypeVariableId(51); + let z = TypeVariableId(52); + let x_ty = push(&mut types, Type::Variable(x)); + let y_ty = push(&mut types, Type::Variable(y)); + let z_ty = push(&mut types, Type::Variable(z)); + let y_to_z = arrow(&mut types, y_ty, z_ty); + let x_to_y = arrow(&mut types, x_ty, y_ty); + let x_to_z = arrow(&mut types, x_ty, z_ty); + let function_after_first = arrow(&mut types, x_to_y, x_to_z); + let actual_body = arrow(&mut types, y_to_z, function_after_first); + let actual = push( + &mut types, + Type::ForAll { + variables: vec![x, y, z], + body: actual_body, + }, + ); + + let a = TypeVariableId(53); + let b = TypeVariableId(54); + let r = TypeVariableId(55); + let a_ty = push(&mut types, Type::Variable(a)); + let b_ty = push(&mut types, Type::Variable(b)); + let r_ty = push(&mut types, Type::Variable(r)); + let a_to_b = arrow(&mut types, a_ty, b_ty); + let r_to_a = arrow(&mut types, r_ty, a_ty); + let r_to_b = arrow(&mut types, r_ty, b_ty); + let function_after_first = arrow(&mut types, r_to_a, r_to_b); + let expected_body = arrow(&mut types, a_to_b, function_after_first); + let expected = push( + &mut types, + Type::ForAll { + variables: vec![a, b], + body: expected_body, + }, + ); + let module = module(types, Vec::new()); + + assert!(compatible(actual, expected, &module)); +} + #[test] fn scheme_instantiation_rejects_inconsistent_reuses_of_a_generic_row_tail() { let mut types = vec![Type::RowEmpty]; diff --git a/crates/psrs-thir/src/verify/semantics/mod.rs b/crates/psrs-thir/src/verify/semantics/mod.rs index f556cb92..5ed65a91 100644 --- a/crates/psrs-thir/src/verify/semantics/mod.rs +++ b/crates/psrs-thir/src/verify/semantics/mod.rs @@ -6,6 +6,10 @@ use std::collections::HashMap; mod matching; +pub(super) fn types_equal(left: TypeId, right: TypeId, module: &Module) -> bool { + matching::equivalent(left, right, module) +} + #[derive(Clone)] struct Scheme { ty: TypeId, @@ -47,7 +51,27 @@ struct Context<'a> { } impl Context<'_> { + fn evidence(&mut self, evidence: &crate::Evidence) { + match &evidence.kind { + crate::EvidenceKind::DictionaryValue(value) => self.expr(value, Some(evidence.ty)), + crate::EvidenceKind::Superclass { parent, .. } => self.evidence(parent), + crate::EvidenceKind::Instance { context, .. } => { + for child in context { + self.evidence(child); + } + } + crate::EvidenceKind::Given(_) + | crate::EvidenceKind::Global(_) + | crate::EvidenceKind::Coercible { .. } + | crate::EvidenceKind::Primitive { .. } => {} + } + } + fn expr(&mut self, expression: &Expr, expected: Option) { + psrs_span::with_sufficient_stack(|| self.expr_inner(expression, expected)) + } + + fn expr_inner(&mut self, expression: &Expr, expected: Option) { if let Some(expected) = expected { self.compatible(expression.ty, expected, expression.span); } @@ -143,7 +167,7 @@ impl Context<'_> { self.error(expression.span, "record field is not declared"); } } - ExprKind::Evidence(_) => {} + ExprKind::Evidence(evidence) => self.evidence(evidence), ExprKind::Coerce { value, source_type, @@ -153,6 +177,15 @@ impl Context<'_> { self.expr(value, Some(*source_type)); self.compatible(*target_type, expression.ty, expression.span); } + ExprKind::UnsafeCoerce { + value, + source_type, + target_type, + .. + } => { + self.expr(value, Some(*source_type)); + self.compatible(*target_type, expression.ty, expression.span); + } ExprKind::Application(function, argument) => { self.expr(function, None); self.expr(argument, None); @@ -351,6 +384,7 @@ fn primitive_type(module: &Module, constructor: TypeConstructor) -> Option Option { + let id = strip_leading_foralls(module, id); let Type::Application(head, element) = module.types.get(id.0 as usize)? else { return None; }; diff --git a/crates/psrs-typecheck/src/typecheck/classes/deriving/bifunctor.rs b/crates/psrs-typecheck/src/typecheck/classes/deriving/bifunctor.rs index 78f7012d..dd03a045 100644 --- a/crates/psrs-typecheck/src/typecheck/classes/deriving/bifunctor.rs +++ b/crates/psrs-typecheck/src/typecheck/classes/deriving/bifunctor.rs @@ -1,15 +1,5 @@ use super::super::super::*; -use super::types::{contains_parameter, flatten_type_application}; -use super::{apply_expr, flatten_spine, global_expr, local_expr}; - -struct BifunctorFieldMap<'a> { - left_parameter: &'a str, - right_parameter: &'a str, - left_mapper: &'a hir::Expr, - right_mapper: &'a hir::Expr, - bimap_method: SymbolId, - span: TextRange, -} +use super::KnownClass; impl Checker { pub(super) fn derive_bifunctor_method( @@ -18,170 +8,6 @@ impl Checker { class_arguments: &[InferType], span: TextRange, ) -> Option { - let Some(instance_type) = class_arguments.first() else { - return self.deriving_error(span, "Bifunctor deriving requires one type argument"); - }; - let instance_type = self.resolve_type(instance_type.clone()); - let (head, arguments) = flatten_spine(&instance_type); - let InferType::Constructor(TypeConstructor::User(type_id)) = head else { - return self - .deriving_error(span, "Bifunctor deriving requires a local type constructor"); - }; - let Some(declaration) = self.env.type_declarations.get(type_id).cloned() else { - return self - .deriving_error(span, "cannot find the data declaration to derive Bifunctor"); - }; - if type_id.module != self.env.module_id - || !matches!( - declaration.kind, - hir::TypeDeclarationKind::Data | hir::TypeDeclarationKind::Newtype - ) - || declaration.parameters.len() < 2 - || arguments.len() + 2 != declaration.parameters.len() - { - return self.deriving_error( - span, - "Bifunctor deriving requires a local type constructor applied to all but its final two parameters", - ); - } - if method.name != "bimap" { - return self.deriving_error(span, "Bifunctor deriving requires a bimap method"); - } - let left_parameter = &declaration.parameters[declaration.parameters.len() - 2].name; - let right_parameter = &declaration.parameters[declaration.parameters.len() - 1].name; - let left_mapper = self.fresh_deriving_binder("__derived_lmap", span); - let right_mapper = self.fresh_deriving_binder("__derived_rmap", span); - let left_mapper_expr = local_expr(left_mapper.id, span); - let right_mapper_expr = local_expr(right_mapper.id, span); - let field_map = BifunctorFieldMap { - left_parameter, - right_parameter, - left_mapper: &left_mapper_expr, - right_mapper: &right_mapper_expr, - bimap_method: method.symbol, - span, - }; - let value = self.fresh_deriving_binder("__derived_value", span); - let mut branches = Vec::with_capacity(declaration.constructors.len()); - for constructor in &declaration.constructors { - let binders = constructor - .fields - .iter() - .map(|_| self.fresh_deriving_binder("__derived_field", span)) - .collect::>(); - let mut result = global_expr(constructor.symbol, span); - for (field_type, binder) in constructor.fields.iter().zip(&binders) { - let field_type = self.normalize_deriving_type(field_type); - let field_value = local_expr(binder.id, span); - let mapped = match map_bifunctor_field(&field_type, &field_map, &field_value) { - Ok(mapped) => mapped, - Err(message) => return self.deriving_error(span, &message), - }; - result = apply_expr(result, mapped, span); - } - branches.push(hir::CaseBranch { - coverage: hir::CaseBranchCoverage::Source, - pattern: hir::Pattern { - kind: hir::PatternKind::Constructor { - symbol: constructor.symbol, - name_span: constructor.name_span, - arguments: binders - .into_iter() - .map(|binder| hir::Pattern { - kind: hir::PatternKind::Var(binder), - span, - }) - .collect(), - }, - span, - }, - value: result, - span, - }); - } - let implementation = hir::Expr { - kind: hir::ExprKind::Lambda { - binder: left_mapper.clone(), - body: Box::new(hir::Expr { - kind: hir::ExprKind::Lambda { - binder: right_mapper.clone(), - body: Box::new(hir::Expr { - kind: hir::ExprKind::Lambda { - binder: value.clone(), - body: Box::new(hir::Expr { - kind: hir::ExprKind::Case { - scrutinee: Box::new(local_expr(value.id, span)), - branches, - }, - span, - }), - }, - span, - }), - }, - span, - }), - }, - span, - }; - self.infer_derived_method(method, class_arguments, &implementation) - } -} - -fn map_bifunctor_field( - ty: &hir::Type, - context: &BifunctorFieldMap<'_>, - value: &hir::Expr, -) -> Result { - let BifunctorFieldMap { - left_parameter, - right_parameter, - left_mapper, - right_mapper, - bimap_method, - span, - } = *context; - let uses_left = contains_parameter(ty, left_parameter); - let uses_right = contains_parameter(ty, right_parameter); - if !uses_left && !uses_right { - return Ok(value.clone()); - } - if matches!(&ty.kind, hir::TypeKind::Variable(name) if name == left_parameter) { - return Ok(apply_expr(left_mapper.clone(), value.clone(), span)); - } - if matches!(&ty.kind, hir::TypeKind::Variable(name) if name == right_parameter) { - return Ok(apply_expr(right_mapper.clone(), value.clone(), span)); - } - let (head, arguments) = flatten_type_application(ty); - if !contains_parameter(head, left_parameter) - && !contains_parameter(head, right_parameter) - && arguments.len() >= 2 - && matches!(&arguments[arguments.len() - 2].kind, hir::TypeKind::Variable(name) if name == left_parameter) - && matches!(&arguments[arguments.len() - 1].kind, hir::TypeKind::Variable(name) if name == right_parameter) - && arguments[..arguments.len() - 2].iter().all(|argument| { - !contains_parameter(argument, left_parameter) - && !contains_parameter(argument, right_parameter) - }) - { - return Ok(apply_expr( - apply_expr( - apply_expr(global_expr(bimap_method, span), left_mapper.clone(), span), - right_mapper.clone(), - span, - ), - value.clone(), - span, - )); - } - if uses_left && !uses_right { - return Err( - "Bifunctor deriving does not support this nested left-parameter occurrence".to_owned(), - ); - } - if uses_right && !uses_left { - return Err( - "Bifunctor deriving does not support this nested right-parameter occurrence".to_owned(), - ); + self.derive_mapping_method(KnownClass::Bifunctor, method, class_arguments, span) } - Err("Bifunctor deriving does not support this parameter occurrence".to_owned()) } diff --git a/crates/psrs-typecheck/src/typecheck/classes/deriving/contravariant.rs b/crates/psrs-typecheck/src/typecheck/classes/deriving/contravariant.rs index d1960ece..4deb336e 100644 --- a/crates/psrs-typecheck/src/typecheck/classes/deriving/contravariant.rs +++ b/crates/psrs-typecheck/src/typecheck/classes/deriving/contravariant.rs @@ -1,6 +1,5 @@ use super::super::super::*; -use super::types::{contains_parameter, flatten_type_application}; -use super::{apply_expr, flatten_spine, global_expr, local_expr}; +use super::KnownClass; impl Checker { pub(super) fn derive_contravariant_method( @@ -9,195 +8,6 @@ impl Checker { class_arguments: &[InferType], span: TextRange, ) -> Option { - let Some(instance_type) = class_arguments.first() else { - return self.deriving_error(span, "Contravariant deriving requires one type argument"); - }; - let instance_type = self.resolve_type(instance_type.clone()); - let (head, arguments) = flatten_spine(&instance_type); - let InferType::Constructor(TypeConstructor::User(type_id)) = head else { - return self.deriving_error( - span, - "Contravariant deriving requires a local type constructor", - ); - }; - let Some(declaration) = self.env.type_declarations.get(type_id).cloned() else { - return self.deriving_error( - span, - "cannot find the data declaration to derive Contravariant", - ); - }; - if type_id.module != self.env.module_id - || !matches!( - declaration.kind, - hir::TypeDeclarationKind::Data | hir::TypeDeclarationKind::Newtype - ) - || declaration.parameters.is_empty() - || arguments.len() + 1 != declaration.parameters.len() - { - return self.deriving_error( - span, - "Contravariant deriving requires a local type constructor applied to all but its final parameter", - ); - } - if method.name != "cmap" { - return self.deriving_error(span, "Contravariant deriving requires a cmap method"); - } - let parameter = &declaration.parameters.last()?.name; - let profunctor_lcmap = self.profunctor_lcmap_symbol(); - let mapper = self.fresh_deriving_binder("__derived_contramap", span); - let value = self.fresh_deriving_binder("__derived_value", span); - let mut branches = Vec::with_capacity(declaration.constructors.len()); - for constructor in &declaration.constructors { - let binders = constructor - .fields - .iter() - .map(|_| self.fresh_deriving_binder("__derived_field", span)) - .collect::>(); - let mut result = global_expr(constructor.symbol, span); - for (field_type, binder) in constructor.fields.iter().zip(&binders) { - let field_type = self.normalize_deriving_type(field_type); - let field_value = local_expr(binder.id, span); - let mapped = match contramap_field_value( - &field_type, - parameter, - &local_expr(mapper.id, span), - &field_value, - profunctor_lcmap, - span, - ) { - Ok(mapped) => mapped, - Err(message) => return self.deriving_error(span, &message), - }; - result = apply_expr(result, mapped, span); - } - branches.push(hir::CaseBranch { - coverage: hir::CaseBranchCoverage::Source, - pattern: hir::Pattern { - kind: hir::PatternKind::Constructor { - symbol: constructor.symbol, - name_span: constructor.name_span, - arguments: binders - .into_iter() - .map(|binder| hir::Pattern { - kind: hir::PatternKind::Var(binder), - span, - }) - .collect(), - }, - span, - }, - value: result, - span, - }); - } - let implementation = hir::Expr { - kind: hir::ExprKind::Lambda { - binder: mapper.clone(), - body: Box::new(hir::Expr { - kind: hir::ExprKind::Lambda { - binder: value.clone(), - body: Box::new(hir::Expr { - kind: hir::ExprKind::Case { - scrutinee: Box::new(local_expr(value.id, span)), - branches, - }, - span, - }), - }, - span, - }), - }, - span, - }; - self.infer_derived_method(method, class_arguments, &implementation) + self.derive_mapping_method(KnownClass::Contravariant, method, class_arguments, span) } - - fn profunctor_lcmap_symbol(&self) -> Option { - self.env.classes.iter().find_map(|(class_id, class)| { - (self.env.type_names.get(class_id).map(String::as_str) == Some("Profunctor") - && self.env.type_modules.get(class_id).map(String::as_str) - == Some("Data.Profunctor")) - .then(|| { - class - .methods - .iter() - .find(|method| method.name == "lcmap") - .map(|method| method.symbol) - }) - .flatten() - }) - } -} - -fn contramap_field_value( - ty: &hir::Type, - parameter: &str, - mapper: &hir::Expr, - value: &hir::Expr, - profunctor_lcmap: Option, - span: TextRange, -) -> Result { - if !contains_parameter(ty, parameter) { - return Ok(value.clone()); - } - if let Some(lcmap) = - contravariant_profunctor_application(ty, parameter, mapper, value, profunctor_lcmap, span)? - { - return Ok(lcmap); - } - if let hir::TypeKind::Function { - parameter: input, - result, - } = &ty.kind - && matches!(&input.kind, hir::TypeKind::Variable(name) if name == parameter) - && !contains_parameter(result, parameter) - { - let Some(lcmap) = profunctor_lcmap else { - return Err( - "Contravariant deriving through a function requires Data.Profunctor.lcmap".into(), - ); - }; - return Ok(apply_expr( - apply_expr(global_expr(lcmap, span), mapper.clone(), span), - value.clone(), - span, - )); - } - Err("Contravariant deriving requires each occurrence to be a direct function input".into()) -} - -fn contravariant_profunctor_application( - ty: &hir::Type, - parameter: &str, - mapper: &hir::Expr, - value: &hir::Expr, - profunctor_lcmap: Option, - span: TextRange, -) -> Result, String> { - let (head, arguments) = flatten_type_application(ty); - if arguments.len() < 2 - || contains_parameter(head, parameter) - || !matches!( - &arguments[arguments.len() - 2].kind, - hir::TypeKind::Variable(name) if name == parameter - ) - || arguments[..arguments.len() - 2] - .iter() - .any(|argument| contains_parameter(argument, parameter)) - || arguments[arguments.len() - 1..] - .iter() - .any(|argument| contains_parameter(argument, parameter)) - { - return Ok(None); - } - let Some(lcmap) = profunctor_lcmap else { - return Err( - "Contravariant deriving through a profunctor requires Data.Profunctor.lcmap".into(), - ); - }; - Ok(Some(apply_expr( - apply_expr(global_expr(lcmap, span), mapper.clone(), span), - value.clone(), - span, - ))) } diff --git a/crates/psrs-typecheck/src/typecheck/classes/deriving/eq.rs b/crates/psrs-typecheck/src/typecheck/classes/deriving/eq.rs new file mode 100644 index 00000000..049a0cec --- /dev/null +++ b/crates/psrs-typecheck/src/typecheck/classes/deriving/eq.rs @@ -0,0 +1,220 @@ +use super::*; +use crate::typecheck::classes::deriving::syntax::{ + case_expr, constructor_pattern, if_expr, lambda, wildcard_pattern, +}; + +impl Checker { + pub(super) fn derive_eq_method( + &mut self, + method: &MethodInfo, + class_arguments: &[InferType], + span: TextRange, + ) -> Option { + // A field whose type is an applied type variable `f a` is compared + // through `Eq1`'s `eq1`, exactly as the official deriving rule does. + // The lookup is lazy because a data type with no such field does not + // need `Eq1` to exist in the environment at all. + let needs_eq1 = class_arguments + .first() + .and_then(|argument| { + let instance_type = self.resolve_type(argument.clone()); + let (head, _) = flatten_spine(&instance_type); + let InferType::Constructor(TypeConstructor::User(type_id)) = head else { + return None; + }; + self.env.type_declarations.get(type_id).map(|declaration| { + declaration.constructors.iter().any(|constructor| { + constructor + .fields + .iter() + .any(|field| is_applied_variable(&self.normalize_deriving_type(field))) + }) + }) + }) + .unwrap_or(false); + let eq1_method = if needs_eq1 { + match self.known_method(KnownClass::Eq1, "eq1") { + Some(method) => Some(method), + None => { + return self.deriving_error( + TypeCheckErrorKind::CannotDerive, + span, + "cannot find the Eq1 method for Eq deriving", + ); + } + } + } else { + None + }; + self.derive_structural_eq(method, class_arguments, method.symbol, eq1_method, span) + } + + pub(super) fn derive_eq1_method( + &mut self, + method: &MethodInfo, + class_arguments: &[InferType], + span: TextRange, + ) -> Option { + // The official rule derives `Eq1` by delegating to `Eq` at the applied + // argument: `eq1 = eq`. The matching `Eq` instance supplies the + // dictionary, so its context must be visible alongside the method's. + let Some(eq_method) = self.known_method(KnownClass::Eq, "eq") else { + return self.deriving_error( + TypeCheckErrorKind::CannotDerive, + span, + "cannot find the Eq method for Eq1 deriving", + ); + }; + let implementation = global_expr(eq_method, span); + self.infer_derived_method(method, class_arguments, &implementation) + } + + fn derive_structural_eq( + &mut self, + method: &MethodInfo, + class_arguments: &[InferType], + eq_method: SymbolId, + eq1_method: Option, + span: TextRange, + ) -> Option { + let Some(instance_type) = class_arguments.first() else { + return self.deriving_error( + TypeCheckErrorKind::InvalidDerivedInstance, + span, + "equality deriving requires one type argument", + ); + }; + let instance_type = self.resolve_type(instance_type.clone()); + let (head, arguments) = flatten_spine(&instance_type); + let InferType::Constructor(TypeConstructor::User(type_id)) = head else { + return self.deriving_error( + TypeCheckErrorKind::ExpectedTypeConstructor, + span, + "Eq deriving requires a local data or newtype constructor", + ); + }; + let Some(declaration) = self.env.type_declarations.get(type_id).cloned() else { + return self.deriving_error( + TypeCheckErrorKind::CannotFindDerivingType, + span, + "cannot find the data declaration to derive equality", + ); + }; + if type_id.module != self.env.module_id + || !matches!( + declaration.kind, + hir::TypeDeclarationKind::Data | hir::TypeDeclarationKind::Newtype + ) + || declaration.parameters.len() != arguments.len() + { + return self.deriving_error( + TypeCheckErrorKind::ExpectedTypeConstructor, + span, + "Eq deriving requires a locally declared, fully applied data type", + ); + } + + let left = self.fresh_deriving_binder("__derived_left", span); + let right = self.fresh_deriving_binder("__derived_right", span); + let mut left_case_branches = Vec::new(); + for constructor in &declaration.constructors { + let left_fields = constructor + .fields + .iter() + .map(|_| self.fresh_deriving_binder("__derived_l", span)) + .collect::>(); + let right_fields = constructor + .fields + .iter() + .map(|_| self.fresh_deriving_binder("__derived_r", span)) + .collect::>(); + let field_types = constructor + .fields + .iter() + .map(|field| self.normalize_deriving_type(field)) + .collect::>(); + let body = derive_eq_field_tests( + &field_types, + &left_fields, + &right_fields, + eq_method, + eq1_method, + span, + ); + let same_constructor = hir::CaseBranch { + coverage: hir::CaseBranchCoverage::Source, + pattern: constructor_pattern(constructor, &right_fields, span), + value: body, + span, + }; + let mismatch = hir::CaseBranch { + coverage: hir::CaseBranchCoverage::Source, + pattern: wildcard_pattern(span), + value: boolean_literal(false, span), + span, + }; + let right_case = case_expr( + local_expr(right.id, span), + vec![same_constructor, mismatch], + span, + ); + left_case_branches.push(hir::CaseBranch { + coverage: hir::CaseBranchCoverage::Source, + pattern: constructor_pattern(constructor, &left_fields, span), + value: right_case, + span, + }); + } + if left_case_branches.is_empty() { + left_case_branches.push(hir::CaseBranch { + coverage: hir::CaseBranchCoverage::Source, + pattern: wildcard_pattern(span), + value: boolean_literal(true, span), + span, + }); + } + let implementation = lambda( + left.clone(), + lambda( + right.clone(), + case_expr(local_expr(left.id, span), left_case_branches, span), + span, + ), + span, + ); + self.infer_derived_method(method, class_arguments, &implementation) + } +} + +fn derive_eq_field_tests( + fields: &[hir::Type], + left_fields: &[hir::LocalBinder], + right_fields: &[hir::LocalBinder], + eq_method: SymbolId, + eq1_method: Option, + span: TextRange, +) -> hir::Expr { + fields + .iter() + .zip(left_fields) + .zip(right_fields) + .map(|((field, left), right)| { + let method = if is_applied_variable(field) { + eq1_method.unwrap_or(eq_method) + } else { + eq_method + }; + let method = global_expr(method, span); + apply_expr( + apply_expr(method, local_expr(left.id, span), span), + local_expr(right.id, span), + span, + ) + }) + .collect::>() + .into_iter() + .rev() + .fold(boolean_literal(true, span), |rest, test| { + if_expr(test, rest, boolean_literal(false, span), span) + }) +} diff --git a/crates/psrs-typecheck/src/typecheck/classes/deriving/foldable/branches.rs b/crates/psrs-typecheck/src/typecheck/classes/deriving/foldable/branches.rs new file mode 100644 index 00000000..92d51420 --- /dev/null +++ b/crates/psrs-typecheck/src/typecheck/classes/deriving/foldable/branches.rs @@ -0,0 +1,385 @@ +use super::*; +use crate::typecheck::classes::deriving::syntax::field_expr; +use crate::typecheck::classes::deriving::syntax::lambda; + +impl Checker { + fn require_fold_symbol( + &mut self, + symbol: Option, + span: TextRange, + operation: &str, + ) -> Option { + symbol.or_else(|| { + self.deriving_error( + TypeCheckErrorKind::CannotFindDerivingType, + span, + &format!("cannot find the `{operation}` value required to derive a fold"), + ) + }) + } + + pub(super) fn fold_map_branch( + &mut self, + binders: &[hir::LocalBinder], + usages: &[FieldUsage], + context: &FoldContext<'_>, + ) -> Option { + let FoldContext { + ref ops, + left, + right, + span, + } = *context; + let mut contributions = Vec::new(); + for (binder, usage) in binders.iter().zip(usages) { + if *usage == FieldUsage::Inert { + continue; + } + let field = local_expr(binder.id, span); + let function = self.fold_map_function(usage, ops, left, right, span)?; + contributions.push(apply_expr(function, field, span)); + } + self.fold_contributions(contributions, ops, span) + } + + fn fold_contributions( + &mut self, + mut contributions: Vec, + ops: &FoldOps, + span: TextRange, + ) -> Option { + let mut body = match contributions.pop() { + Some(last) => last, + None => { + return Some(global_expr( + self.require_fold_symbol(ops.mempty, span, "mempty")?, + span, + )); + } + }; + for contribution in contributions.into_iter().rev() { + body = apply_expr( + apply_expr( + global_expr(self.require_fold_symbol(ops.append, span, "append")?, span), + contribution, + span, + ), + body, + span, + ); + } + Some(body) + } + + /// A function `x -> m` for a field occurrence. + fn fold_map_function( + &mut self, + usage: &FieldUsage, + ops: &FoldOps, + left: &hir::Expr, + right: &hir::Expr, + span: TextRange, + ) -> Option { + Some(match usage { + FieldUsage::Inert => { + let ignored = self.fresh_deriving_binder("__derived_ignored", span); + lambda( + ignored, + global_expr(self.require_fold_symbol(ops.mempty, span, "mempty")?, span), + span, + ) + } + FieldUsage::Record(fields) => { + let binder = self.fresh_deriving_binder("__derived_record", span); + let record = local_expr(binder.id, span); + let mut contributions = Vec::new(); + for (label, usage) in fields { + if *usage != FieldUsage::Inert { + let function = self.fold_map_function(usage, ops, left, right, span)?; + contributions.push(apply_expr( + function, + field_expr(record.clone(), label, span), + span, + )); + } + } + let body = self.fold_contributions(contributions, ops, span)?; + lambda(binder, body, span) + } + FieldUsage::Param => right.clone(), + FieldUsage::LParam => left.clone(), + FieldUsage::Mono(inner) => apply_expr( + global_expr( + self.require_fold_symbol(ops.fold_map, span, "foldMap")?, + span, + ), + self.fold_map_function(inner, ops, left, right, span)?, + span, + ), + FieldUsage::Bi(l, r) => apply_expr( + apply_expr( + global_expr( + self.require_fold_symbol(ops.bifold_map, span, "bifoldMap")?, + span, + ), + self.fold_map_function(l, ops, left, right, span)?, + span, + ), + self.fold_map_function(r, ops, left, right, span)?, + span, + ), + FieldUsage::Contra(inner) | FieldUsage::Pro(inner, _) => { + self.fold_map_function(inner, ops, left, right, span)? + } + }) + } + + pub(super) fn fold_r_branch( + &mut self, + binders: &[hir::LocalBinder], + usages: &[FieldUsage], + accumulator: &hir::Expr, + context: &FoldContext<'_>, + ) -> Option { + let FoldContext { + ref ops, + left, + right, + span, + } = *context; + let mut body = accumulator.clone(); + for (binder, usage) in binders.iter().zip(usages).rev() { + if *usage == FieldUsage::Inert { + continue; + } + let field = local_expr(binder.id, span); + let step = self.fold_r_step(usage, ops, left, right, span)?; + body = apply_expr(apply_expr(step, field, span), body, span); + } + Some(body) + } + + /// A `foldr` step `x -> rest -> b`. + fn fold_r_step( + &mut self, + usage: &FieldUsage, + ops: &FoldOps, + left: &hir::Expr, + right: &hir::Expr, + span: TextRange, + ) -> Option { + Some(match usage { + FieldUsage::Inert => self.ignore_step(span, false), + FieldUsage::Record(fields) => { + self.record_fold_step(fields, ops, left, right, span, false)? + } + FieldUsage::Param => right.clone(), + FieldUsage::LParam => left.clone(), + FieldUsage::Mono(inner) => { + let step = self.fold_r_step(inner, ops, left, right, span)?; + let method = self.require_fold_symbol(ops.fold_r, span, "foldr")?; + self.nested_fold_step(method, step, span, false) + } + FieldUsage::Bi(l, r) => { + let l_step = self.fold_r_step(l, ops, left, right, span)?; + let r_step = self.fold_r_step(r, ops, left, right, span)?; + let method = self.require_fold_symbol(ops.bifold_r, span, "bifoldr")?; + self.nested_bifold_step(method, l_step, r_step, span, false) + } + FieldUsage::Contra(inner) | FieldUsage::Pro(inner, _) => { + let step = self.fold_r_step(inner, ops, left, right, span)?; + let method = self.require_fold_symbol(ops.fold_r, span, "foldr")?; + self.nested_fold_step(method, step, span, false) + } + }) + } + + pub(super) fn fold_l_branch( + &mut self, + binders: &[hir::LocalBinder], + usages: &[FieldUsage], + accumulator: &hir::Expr, + context: &FoldContext<'_>, + ) -> Option { + let FoldContext { + ref ops, + left, + right, + span, + } = *context; + let mut body = accumulator.clone(); + for (binder, usage) in binders.iter().zip(usages) { + if *usage == FieldUsage::Inert { + continue; + } + let field = local_expr(binder.id, span); + let step = self.fold_l_step(usage, ops, left, right, span)?; + body = apply_expr(apply_expr(step, body, span), field, span); + } + Some(body) + } + + /// A `foldl` step `rest -> x -> b`. + fn fold_l_step( + &mut self, + usage: &FieldUsage, + ops: &FoldOps, + left: &hir::Expr, + right: &hir::Expr, + span: TextRange, + ) -> Option { + Some(match usage { + FieldUsage::Inert => self.ignore_step(span, true), + FieldUsage::Record(fields) => { + self.record_fold_step(fields, ops, left, right, span, true)? + } + FieldUsage::Param => right.clone(), + FieldUsage::LParam => left.clone(), + FieldUsage::Mono(inner) => { + let step = self.fold_l_step(inner, ops, left, right, span)?; + let method = self.require_fold_symbol(ops.fold_l, span, "foldl")?; + self.nested_fold_step(method, step, span, true) + } + FieldUsage::Bi(l, r) => { + let l_step = self.fold_l_step(l, ops, left, right, span)?; + let r_step = self.fold_l_step(r, ops, left, right, span)?; + let method = self.require_fold_symbol(ops.bifold_l, span, "bifoldl")?; + self.nested_bifold_step(method, l_step, r_step, span, true) + } + FieldUsage::Contra(inner) | FieldUsage::Pro(inner, _) => { + let step = self.fold_l_step(inner, ops, left, right, span)?; + let method = self.require_fold_symbol(ops.fold_l, span, "foldl")?; + self.nested_fold_step(method, step, span, true) + } + }) + } + + fn record_fold_step( + &mut self, + fields: &[(String, FieldUsage)], + ops: &FoldOps, + left: &hir::Expr, + right: &hir::Expr, + span: TextRange, + forward: bool, + ) -> Option { + let record = self.fresh_deriving_binder("__derived_record", span); + let acc = self.fresh_deriving_binder("__derived_acc", span); + let mut body = local_expr(acc.id, span); + let ordered: Box> = if forward { + Box::new(fields.iter()) + } else { + Box::new(fields.iter().rev()) + }; + for (label, usage) in ordered { + if *usage == FieldUsage::Inert { + continue; + } + let field = field_expr(local_expr(record.id, span), label, span); + let step = if forward { + self.fold_l_step(usage, ops, left, right, span)? + } else { + self.fold_r_step(usage, ops, left, right, span)? + }; + body = if forward { + apply_expr(apply_expr(step, body, span), field, span) + } else { + apply_expr(apply_expr(step, field, span), body, span) + }; + } + Some(if forward { + lambda(acc, lambda(record, body, span), span) + } else { + lambda(record, lambda(acc, body, span), span) + }) + } + + /// A step that ignores its element and returns the accumulator. + fn ignore_step(&mut self, span: TextRange, left: bool) -> hir::Expr { + let accumulator = self.fresh_deriving_binder("__derived_rest", span); + let element = self.fresh_deriving_binder("__derived_ignored", span); + let (first, second) = if left { + (accumulator.clone(), element) + } else { + (element, accumulator.clone()) + }; + lambda( + first, + lambda(second, local_expr(accumulator.id, span), span), + span, + ) + } + + /// `\x rest -> method step rest x` (right) or `\rest x -> method step rest x` (left). + fn nested_fold_step( + &mut self, + method: SymbolId, + step: hir::Expr, + span: TextRange, + left: bool, + ) -> hir::Expr { + let accumulator = self.fresh_deriving_binder("__derived_rest", span); + let element = self.fresh_deriving_binder("__derived_x", span); + let (first, second) = if left { + (accumulator.clone(), element.clone()) + } else { + (element.clone(), accumulator.clone()) + }; + lambda( + first, + lambda( + second, + apply_expr( + apply_expr( + apply_expr(global_expr(method, span), step, span), + local_expr(accumulator.id, span), + span, + ), + local_expr(element.id, span), + span, + ), + span, + ), + span, + ) + } + + /// The bipartite version of `nested_fold_step`. + fn nested_bifold_step( + &mut self, + method: SymbolId, + l_step: hir::Expr, + r_step: hir::Expr, + span: TextRange, + left: bool, + ) -> hir::Expr { + let accumulator = self.fresh_deriving_binder("__derived_rest", span); + let element = self.fresh_deriving_binder("__derived_x", span); + let (first, second) = if left { + (accumulator.clone(), element.clone()) + } else { + (element.clone(), accumulator.clone()) + }; + lambda( + first, + lambda( + second, + apply_expr( + apply_expr( + apply_expr( + apply_expr(global_expr(method, span), l_step, span), + r_step, + span, + ), + local_expr(accumulator.id, span), + span, + ), + local_expr(element.id, span), + span, + ), + span, + ), + span, + ) + } +} diff --git a/crates/psrs-typecheck/src/typecheck/classes/deriving/foldable/mod.rs b/crates/psrs-typecheck/src/typecheck/classes/deriving/foldable/mod.rs new file mode 100644 index 00000000..c5234c3c --- /dev/null +++ b/crates/psrs-typecheck/src/typecheck/classes/deriving/foldable/mod.rs @@ -0,0 +1,193 @@ +//! `Foldable` and `Bifoldable` deriving. +//! +//! `foldMap` combines each field's monoidal contribution; `foldr`/`foldl` +//! thread an accumulator through the contributing fields in right or left +//! order. The field usage tree decides which occurrences fold through the +//! nested `Foldable`/`Bifoldable` instance and which are inert. + +use crate::typecheck::classes::deriving::syntax::{case_expr, constructor_pattern, lambda}; +mod branches; + +use super::super::super::*; +use super::usage::FieldUsage; +use super::{KnownClass, MappingClasses, apply_expr, flatten_spine, global_expr, local_expr}; + +/// The method symbols a fold rule uses. +#[derive(Clone, Copy)] +struct FoldOps { + fold_map: Option, + fold_r: Option, + fold_l: Option, + bifold_map: Option, + bifold_r: Option, + bifold_l: Option, + append: Option, + mempty: Option, +} + +/// The shared inputs of the per-method branch builders. +#[derive(Clone, Copy)] +struct FoldContext<'a> { + ops: FoldOps, + left: &'a hir::Expr, + right: &'a hir::Expr, + span: TextRange, +} + +impl Checker { + pub(super) fn derive_foldable_method( + &mut self, + known: KnownClass, + method: &MethodInfo, + class_arguments: &[InferType], + span: TextRange, + ) -> Option { + let is_bi = known == KnownClass::Bifoldable; + let Some(instance_type) = class_arguments.first() else { + return self.deriving_error( + TypeCheckErrorKind::InvalidDerivedInstance, + span, + "fold deriving requires one type argument", + ); + }; + let instance_type = self.resolve_type(instance_type.clone()); + let (head, arguments) = flatten_spine(&instance_type); + let InferType::Constructor(TypeConstructor::User(type_id)) = head else { + return self.deriving_error( + TypeCheckErrorKind::ExpectedTypeConstructor, + span, + "fold deriving requires a local type constructor", + ); + }; + let Some(declaration) = self.env.type_declarations.get(type_id).cloned() else { + return self.deriving_error( + TypeCheckErrorKind::CannotFindDerivingType, + span, + "cannot find the data declaration to derive a fold", + ); + }; + let arity = if is_bi { 2 } else { 1 }; + if type_id.module != self.env.module_id + || !matches!( + declaration.kind, + hir::TypeDeclarationKind::Data | hir::TypeDeclarationKind::Newtype + ) + || declaration.parameters.len() < arity + || arguments.len() + arity != declaration.parameters.len() + { + return self.deriving_error( + TypeCheckErrorKind::ExpectedTypeConstructor, + span, + "fold deriving requires a local type constructor applied to all but its final parameters", + ); + } + let lparam = is_bi.then(|| { + declaration.parameters[declaration.parameters.len() - 2] + .name + .as_str() + }); + let param = &declaration.parameters[declaration.parameters.len() - 1].name; + let usages = match self.validate_field_usage( + &declaration, + MappingClasses::foldable(), + lparam, + param, + (false, false), + &arguments, + ) { + Ok(usages) => usages, + Err(offending) => { + return self.deriving_error( + TypeCheckErrorKind::CannotDeriveInvalidConstructorArg, + offending, + "fold deriving cannot map a parameter occurrence in this field", + ); + } + }; + let ops = self.fold_ops(); + let left = self.fresh_deriving_binder("__derived_f", span); + let right = if is_bi { + self.fresh_deriving_binder("__derived_g", span) + } else { + left.clone() + }; + let accumulator = matches!( + method.name.as_str(), + "foldr" | "bifoldr" | "foldl" | "bifoldl" + ) + .then(|| self.fresh_deriving_binder("__derived_z", span)); + let left_expr = local_expr(left.id, span); + let right_expr = local_expr(right.id, span); + let accumulator_expr = accumulator + .as_ref() + .map(|binder| local_expr(binder.id, span)); + let context = FoldContext { + ops, + left: &left_expr, + right: &right_expr, + span, + }; + let mut branches = Vec::with_capacity(declaration.constructors.len()); + for (constructor, field_usages) in declaration.constructors.iter().zip(&usages) { + let binders = constructor + .fields + .iter() + .map(|_| self.fresh_deriving_binder("__derived_field", span)) + .collect::>(); + let value = match method.name.as_str() { + "foldMap" | "bifoldMap" => { + self.fold_map_branch(&binders, field_usages, &context)? + } + "foldr" | "bifoldr" => self.fold_r_branch( + &binders, + field_usages, + accumulator_expr.as_ref()?, + &context, + )?, + "foldl" | "bifoldl" => self.fold_l_branch( + &binders, + field_usages, + accumulator_expr.as_ref()?, + &context, + )?, + _ => { + return self.deriving_error( + TypeCheckErrorKind::CannotDerive, + span, + "the known-class deriving rule is unavailable for this class method", + ); + } + }; + branches.push(hir::CaseBranch { + coverage: hir::CaseBranchCoverage::Source, + pattern: constructor_pattern(constructor, &binders, span), + value, + span, + }); + } + let value = self.fresh_deriving_binder("__derived_value", span); + let mut body = case_expr(local_expr(value.id, span), branches, span); + body = lambda(value, body, span); + if let Some(accumulator) = accumulator { + body = lambda(accumulator, body, span); + } + if is_bi { + body = lambda(right, body, span); + } + let implementation = lambda(left, body, span); + self.infer_derived_method(method, class_arguments, &implementation) + } + + fn fold_ops(&self) -> FoldOps { + FoldOps { + fold_map: self.known_method(KnownClass::Foldable, "foldMap"), + fold_r: self.known_method(KnownClass::Foldable, "foldr"), + fold_l: self.known_method(KnownClass::Foldable, "foldl"), + bifold_map: self.known_method(KnownClass::Bifoldable, "bifoldMap"), + bifold_r: self.known_method(KnownClass::Bifoldable, "bifoldr"), + bifold_l: self.known_method(KnownClass::Bifoldable, "bifoldl"), + append: self.env.deriving.append(), + mempty: self.env.deriving.mempty(), + } + } +} diff --git a/crates/psrs-typecheck/src/typecheck/classes/deriving/functor.rs b/crates/psrs-typecheck/src/typecheck/classes/deriving/functor.rs index cd5a4a99..9978f99b 100644 --- a/crates/psrs-typecheck/src/typecheck/classes/deriving/functor.rs +++ b/crates/psrs-typecheck/src/typecheck/classes/deriving/functor.rs @@ -1,6 +1,5 @@ use super::super::super::*; -use super::types::contains_parameter; -use super::{apply_expr, flatten_spine, global_expr, local_expr}; +use super::KnownClass; impl Checker { pub(super) fn derive_functor_method( @@ -9,192 +8,6 @@ impl Checker { class_arguments: &[InferType], span: TextRange, ) -> Option { - let Some(instance_type) = class_arguments.first() else { - return self.deriving_error(span, "Functor deriving requires one type argument"); - }; - let instance_type = self.resolve_type(instance_type.clone()); - let (head, arguments) = flatten_spine(&instance_type); - let InferType::Constructor(TypeConstructor::User(type_id)) = head else { - return self.deriving_error(span, "Functor deriving requires a local type constructor"); - }; - let Some(declaration) = self.env.type_declarations.get(type_id).cloned() else { - return self.deriving_error(span, "cannot find the data declaration to derive Functor"); - }; - if type_id.module != self.env.module_id - || !matches!( - declaration.kind, - hir::TypeDeclarationKind::Data | hir::TypeDeclarationKind::Newtype - ) - || declaration.parameters.is_empty() - || arguments.len() + 1 != declaration.parameters.len() - { - return self.deriving_error( - span, - "Functor deriving requires a local type constructor applied to all but its final parameter", - ); - } - if method.name != "map" { - return self.deriving_error(span, "Functor deriving requires a map method"); - } - - let parameter = declaration - .parameters - .last() - .map(|parameter| parameter.name.as_str())?; - let mapper = self.fresh_deriving_binder("__derived_map", span); - let value = self.fresh_deriving_binder("__derived_value", span); - let mut branches = Vec::with_capacity(declaration.constructors.len()); - for constructor in &declaration.constructors { - let binders = constructor - .fields - .iter() - .map(|_| self.fresh_deriving_binder("__derived_field", span)) - .collect::>(); - let mut result = global_expr(constructor.symbol, span); - for (field_type, binder) in constructor.fields.iter().zip(&binders) { - let input = local_expr(binder.id, span); - let field_type = self.normalize_deriving_type(field_type); - let mapped = match map_field_value( - &field_type, - parameter, - &local_expr(mapper.id, span), - &input, - method.symbol, - span, - ) { - Ok(mapped) => mapped, - Err(message) => return self.deriving_error(span, &message), - }; - result = apply_expr(result, mapped, span); - } - branches.push(hir::CaseBranch { - coverage: hir::CaseBranchCoverage::Source, - pattern: hir::Pattern { - kind: hir::PatternKind::Constructor { - symbol: constructor.symbol, - name_span: constructor.name_span, - arguments: binders - .into_iter() - .map(|binder| hir::Pattern { - kind: hir::PatternKind::Var(binder), - span, - }) - .collect(), - }, - span, - }, - value: result, - span, - }); - } - let implementation = hir::Expr { - kind: hir::ExprKind::Lambda { - binder: mapper.clone(), - body: Box::new(hir::Expr { - kind: hir::ExprKind::Lambda { - binder: value.clone(), - body: Box::new(hir::Expr { - kind: hir::ExprKind::Case { - scrutinee: Box::new(local_expr(value.id, span)), - branches, - }, - span, - }), - }, - span, - }), - }, - span, - }; - self.infer_derived_method(method, class_arguments, &implementation) - } -} - -fn map_field_value( - ty: &hir::Type, - parameter: &str, - mapper: &hir::Expr, - value: &hir::Expr, - map_method: SymbolId, - span: TextRange, -) -> Result { - if !contains_parameter(ty, parameter) { - return Ok(value.clone()); - } - if matches!(&ty.kind, hir::TypeKind::Variable(name) if name == parameter) { - return Ok(apply_expr(mapper.clone(), value.clone(), span)); - } - match &ty.kind { - hir::TypeKind::Application(function, argument) - if contains_parameter(argument, parameter) - && !contains_parameter(function, parameter) => - { - let mapper = mapping_function(argument, parameter, mapper, map_method, span)?; - Ok(apply_expr( - apply_expr(global_expr(map_method, span), mapper, span), - value.clone(), - span, - )) - } - hir::TypeKind::Function { - parameter: input, - result, - } if !contains_parameter(input, parameter) && contains_parameter(result, parameter) => { - let mapper = mapping_function(result, parameter, mapper, map_method, span)?; - Ok(apply_expr( - apply_expr(global_expr(map_method, span), mapper, span), - value.clone(), - span, - )) - } - hir::TypeKind::Function { - parameter: input, .. - } if contains_parameter(input, parameter) => { - Err("Functor deriving cannot map a type parameter in a function input".to_owned()) - } - hir::TypeKind::Application(_, _) => Err( - "Functor deriving cannot map a type parameter in a higher-kinded application head" - .to_owned(), - ), - hir::TypeKind::Record { .. } | hir::TypeKind::Row { .. } => { - Err("Functor deriving does not support a record containing its parameter".to_owned()) - } - _ => Err("Functor deriving does not support this parameter occurrence".to_owned()), - } -} - -fn mapping_function( - ty: &hir::Type, - parameter: &str, - mapper: &hir::Expr, - map_method: SymbolId, - span: TextRange, -) -> Result { - if matches!(&ty.kind, hir::TypeKind::Variable(name) if name == parameter) { - return Ok(mapper.clone()); - } - if !contains_parameter(ty, parameter) { - return Err("Functor deriving expected a parameter-containing field type".to_owned()); - } - match &ty.kind { - hir::TypeKind::Application(function, argument) - if contains_parameter(argument, parameter) - && !contains_parameter(function, parameter) => - { - let inner = mapping_function(argument, parameter, mapper, map_method, span)?; - Ok(apply_expr(global_expr(map_method, span), inner, span)) - } - hir::TypeKind::Function { - parameter: input, - result, - } if !contains_parameter(input, parameter) && contains_parameter(result, parameter) => { - let result_mapper = mapping_function(result, parameter, mapper, map_method, span)?; - Ok(apply_expr( - global_expr(map_method, span), - result_mapper, - span, - )) - } - _ => Err("Functor deriving does not support this nested parameter occurrence".to_owned()), + self.derive_mapping_method(KnownClass::Functor, method, class_arguments, span) } } diff --git a/crates/psrs-typecheck/src/typecheck/classes/deriving/generic.rs b/crates/psrs-typecheck/src/typecheck/classes/deriving/generic.rs new file mode 100644 index 00000000..f661c3ae --- /dev/null +++ b/crates/psrs-typecheck/src/typecheck/classes/deriving/generic.rs @@ -0,0 +1,435 @@ +use super::super::super::*; +use super::KnownClass; +use super::{apply_expr, flatten_spine, global_expr, local_expr}; +use crate::typecheck::classes::deriving::syntax::{ + case_expr, lambda, symbol_pattern as constructor_pattern, variable_pattern, wildcard_pattern, +}; + +impl Checker { + pub(in crate::typecheck::classes) fn generic_representation( + &mut self, + instance_type: &InferType, + span: TextRange, + ) -> Option { + let instance_type = self.resolve_type(instance_type.clone()); + let (head, arguments) = flatten_spine(&instance_type); + let InferType::Constructor(TypeConstructor::User(type_id)) = head else { + return self.deriving_error( + TypeCheckErrorKind::ExpectedTypeConstructor, + span, + "Generic deriving requires a local data type", + ); + }; + let Some(declaration) = self.env.type_declarations.get(type_id).cloned() else { + return self.deriving_error( + TypeCheckErrorKind::CannotFindDerivingType, + span, + "cannot find the data declaration for Generic", + ); + }; + if type_id.module != self.env.module_id + || declaration.kind != hir::TypeDeclarationKind::Data + || declaration.parameters.len() != arguments.len() + { + return self.deriving_error( + TypeCheckErrorKind::ExpectedTypeConstructor, + span, + "Generic deriving requires a locally declared, fully applied data type", + ); + } + if declaration.constructors.is_empty() { + return self.generic_type("NoConstructors", Vec::new()).or_else(|| { + self.deriving_error( + TypeCheckErrorKind::CannotFindDerivingType, + span, + "cannot find Data.Generic.Rep.NoConstructors", + ) + }); + } + + let parameter_types = declaration + .parameters + .iter() + .map(|parameter| parameter.name.clone()) + .zip(arguments) + .collect::>(); + let mut constructor_representations = Vec::new(); + for constructor in &declaration.constructors { + let mut fields = Vec::new(); + for field in &constructor.fields { + let mut variables = parameter_types.clone(); + let field_type = self.elaborate_type(field, &mut variables); + let Some(argument) = self.generic_type("Argument", vec![field_type]) else { + return self.deriving_error( + TypeCheckErrorKind::CannotFindDerivingType, + span, + "cannot find Data.Generic.Rep.Argument", + ); + }; + fields.push(argument); + } + let product = if fields.is_empty() { + let Some(no_arguments) = self.generic_type("NoArguments", Vec::new()) else { + return self.deriving_error( + TypeCheckErrorKind::CannotFindDerivingType, + span, + "cannot find Data.Generic.Rep.NoArguments", + ); + }; + no_arguments + } else { + let Some(product) = self.generic_product_type(fields) else { + return self.deriving_error( + TypeCheckErrorKind::CannotFindDerivingType, + span, + "cannot find Data.Generic.Rep.Product", + ); + }; + product + }; + let Some(representation) = self.generic_type( + "Constructor", + vec![ + InferType::TypeLevelString(constructor.name.clone()), + product, + ], + ) else { + return self.deriving_error( + TypeCheckErrorKind::CannotFindDerivingType, + span, + "cannot find Data.Generic.Rep.Constructor", + ); + }; + constructor_representations.push(representation); + } + if constructor_representations.len() == 1 { + return constructor_representations.pop(); + } + self.generic_sum_type(constructor_representations) + .or_else(|| { + self.deriving_error( + TypeCheckErrorKind::CannotFindDerivingType, + span, + "cannot find Data.Generic.Rep.Sum", + ) + }) + } + + pub(super) fn derive_generic_method( + &mut self, + method: &MethodInfo, + class_arguments: &[InferType], + span: TextRange, + ) -> Option { + let Some(instance_type) = class_arguments.first() else { + return self.deriving_error( + TypeCheckErrorKind::InvalidDerivedInstance, + span, + "Generic deriving requires its data type argument", + ); + }; + let instance_type = self.resolve_type(instance_type.clone()); + let (head, arguments) = flatten_spine(&instance_type); + let InferType::Constructor(TypeConstructor::User(type_id)) = head else { + return self.deriving_error( + TypeCheckErrorKind::ExpectedTypeConstructor, + span, + "Generic deriving requires a local data type", + ); + }; + let Some(declaration) = self.env.type_declarations.get(type_id).cloned() else { + return self.deriving_error( + TypeCheckErrorKind::CannotFindDerivingType, + span, + "cannot find the data declaration for Generic", + ); + }; + if type_id.module != self.env.module_id + || declaration.kind != hir::TypeDeclarationKind::Data + || declaration.parameters.len() != arguments.len() + { + return self.deriving_error( + TypeCheckErrorKind::ExpectedTypeConstructor, + span, + "Generic deriving requires a locally declared, fully applied data type", + ); + } + + let value = self.fresh_deriving_binder("__generic_value", span); + let implementation = match method.name.as_str() { + "from" => self.derive_generic_from(&declaration, &value, span)?, + "to" => self.derive_generic_to(&declaration, &value, span)?, + _ => { + return self.deriving_error( + TypeCheckErrorKind::CannotDerive, + span, + "Generic derives only its `to` and `from` methods", + ); + } + }; + self.infer_derived_method(method, class_arguments, &implementation) + } + + fn derive_generic_from( + &mut self, + declaration: &hir::TypeDeclaration, + value: &hir::LocalBinder, + span: TextRange, + ) -> Option { + let mut branches = Vec::with_capacity(declaration.constructors.len()); + let count = declaration.constructors.len(); + for (index, constructor) in declaration.constructors.iter().enumerate() { + let fields = constructor + .fields + .iter() + .map(|_| self.fresh_deriving_binder("__generic_field", span)) + .collect::>(); + let product = self.generic_product_expression(&fields, span)?; + let wrapped = self.generic_constructor_expression("Constructor", product, span)?; + let encoded = self.generic_sum_expression(wrapped, index, count, span)?; + branches.push(hir::CaseBranch { + coverage: hir::CaseBranchCoverage::Source, + pattern: data_constructor_pattern(constructor, &fields, span), + value: encoded, + span, + }); + } + if branches.is_empty() { + let recursive = apply_expr( + global_expr(self.current_generic_method("from")?, span), + local_expr(value.id, span), + span, + ); + return Some(lambda(value.clone(), recursive, span)); + } + Some(lambda( + value.clone(), + case_expr(local_expr(value.id, span), branches, span), + span, + )) + } + + fn derive_generic_to( + &mut self, + declaration: &hir::TypeDeclaration, + value: &hir::LocalBinder, + span: TextRange, + ) -> Option { + let count = declaration.constructors.len(); + let mut branches = Vec::with_capacity(count); + for (index, constructor) in declaration.constructors.iter().enumerate() { + let fields = constructor + .fields + .iter() + .map(|_| self.fresh_deriving_binder("__generic_field", span)) + .collect::>(); + let product = self.generic_product_pattern(&fields, span)?; + let wrapped = self.generic_constructor_pattern("Constructor", product, span)?; + let encoded = self.generic_sum_pattern(wrapped, index, count, span)?; + let value = fields + .iter() + .fold(global_expr(constructor.symbol, span), |function, field| { + apply_expr(function, local_expr(field.id, span), span) + }); + branches.push(hir::CaseBranch { + coverage: hir::CaseBranchCoverage::Source, + pattern: encoded, + value, + span, + }); + } + if branches.is_empty() { + let recursive = apply_expr( + global_expr(self.current_generic_method("to")?, span), + local_expr(value.id, span), + span, + ); + return Some(lambda(value.clone(), recursive, span)); + } + Some(lambda( + value.clone(), + case_expr(local_expr(value.id, span), branches, span), + span, + )) + } + + fn generic_product_expression( + &self, + fields: &[hir::LocalBinder], + span: TextRange, + ) -> Option { + if fields.is_empty() { + return Some(global_expr( + self.generic_rep_constructor("NoArguments")?, + span, + )); + } + let argument = self.generic_rep_constructor("Argument")?; + let product = self.generic_rep_constructor("Product")?; + let mut values = fields + .iter() + .map(|field| { + apply_expr( + global_expr(argument, span), + local_expr(field.id, span), + span, + ) + }) + .rev(); + let mut result = values.next()?; + for value in values { + result = apply_expr( + apply_expr(global_expr(product, span), value, span), + result, + span, + ); + } + Some(result) + } + + fn generic_product_pattern( + &self, + fields: &[hir::LocalBinder], + span: TextRange, + ) -> Option { + match fields { + [] => Some(wildcard_pattern(span)), + [field] => { + self.generic_constructor_pattern("Argument", variable_pattern(field, span), span) + } + [first, rest @ ..] => Some(constructor_pattern( + self.generic_rep_constructor("Product")?, + vec![ + self.generic_constructor_pattern( + "Argument", + variable_pattern(first, span), + span, + )?, + self.generic_product_pattern(rest, span)?, + ], + span, + )), + } + } + + fn generic_constructor_expression( + &self, + name: &str, + argument: hir::Expr, + span: TextRange, + ) -> Option { + Some(apply_expr( + global_expr(self.generic_rep_constructor(name)?, span), + argument, + span, + )) + } + + fn generic_constructor_pattern( + &self, + name: &str, + argument: hir::Pattern, + span: TextRange, + ) -> Option { + Some(constructor_pattern( + self.generic_rep_constructor(name)?, + vec![argument], + span, + )) + } + + fn generic_sum_expression( + &self, + expression: hir::Expr, + index: usize, + count: usize, + span: TextRange, + ) -> Option { + if count <= 1 { + return Some(expression); + } + let name = if index == 0 { "Inl" } else { "Inr" }; + let nested = if index == 0 { + expression + } else { + self.generic_sum_expression(expression, index - 1, count - 1, span)? + }; + Some(apply_expr( + global_expr(self.generic_rep_constructor(name)?, span), + nested, + span, + )) + } + + fn generic_sum_pattern( + &self, + pattern: hir::Pattern, + index: usize, + count: usize, + span: TextRange, + ) -> Option { + if count <= 1 { + return Some(pattern); + } + let name = if index == 0 { "Inl" } else { "Inr" }; + let nested = if index == 0 { + pattern + } else { + self.generic_sum_pattern(pattern, index - 1, count - 1, span)? + }; + Some(constructor_pattern( + self.generic_rep_constructor(name)?, + vec![nested], + span, + )) + } + + fn generic_product_type(&self, fields: Vec) -> Option { + let mut fields = fields.into_iter().rev(); + let mut result = fields.next()?; + for field in fields { + result = self.generic_type("Product", vec![field, result])?; + } + Some(result) + } + + fn generic_sum_type(&self, constructors: Vec) -> Option { + let mut constructors = constructors.into_iter().rev(); + let mut result = constructors.next()?; + for constructor in constructors { + result = self.generic_type("Sum", vec![constructor, result])?; + } + Some(result) + } + + fn generic_type(&self, name: &str, arguments: Vec) -> Option { + let type_id = self.env.deriving.generic_rep()?.type_id(name)?; + Some(arguments.into_iter().fold( + InferType::Constructor(TypeConstructor::User(type_id)), + |function, argument| InferType::Application(Box::new(function), Box::new(argument)), + )) + } + + fn generic_rep_constructor(&self, name: &str) -> Option { + self.env.deriving.generic_rep()?.constructor(name) + } + + fn current_generic_method(&self, name: &str) -> Option { + self.known_method(KnownClass::Generic, name) + } +} + +fn data_constructor_pattern( + constructor: &hir::Constructor, + fields: &[hir::LocalBinder], + span: TextRange, +) -> hir::Pattern { + constructor_pattern( + constructor.symbol, + fields + .iter() + .map(|field| variable_pattern(field, span)) + .collect(), + span, + ) +} diff --git a/crates/psrs-typecheck/src/typecheck/classes/deriving/mapping.rs b/crates/psrs-typecheck/src/typecheck/classes/deriving/mapping.rs new file mode 100644 index 00000000..a64e472a --- /dev/null +++ b/crates/psrs-typecheck/src/typecheck/classes/deriving/mapping.rs @@ -0,0 +1,131 @@ +//! Usage-driven generation shared by the four structural mapping classes. +use super::super::super::*; +use super::syntax::{apply_expr, case_expr, constructor_pattern, global_expr, lambda, local_expr}; +use super::usage::{MappingClasses, MappingMethods}; +use super::{KnownClass, flatten_spine}; + +impl Checker { + pub(super) fn derive_mapping_method( + &mut self, + known: KnownClass, + method: &MethodInfo, + class_arguments: &[InferType], + span: TextRange, + ) -> Option { + let Some(instance_type) = class_arguments.first() else { + return self.deriving_error( + TypeCheckErrorKind::InvalidDerivedInstance, + span, + "mapping deriving requires one type argument", + ); + }; + let instance_type = self.resolve_type(instance_type.clone()); + let (head, arguments) = flatten_spine(&instance_type); + let InferType::Constructor(TypeConstructor::User(type_id)) = head else { + return self.deriving_error( + TypeCheckErrorKind::ExpectedTypeConstructor, + span, + "mapping deriving requires a local type constructor", + ); + }; + let Some(declaration) = self.env.type_declarations.get(type_id).cloned() else { + return self.deriving_error( + TypeCheckErrorKind::CannotFindDerivingType, + span, + "cannot find the data declaration to derive a mapping", + ); + }; + let is_bi = matches!(known, KnownClass::Bifunctor | KnownClass::Profunctor); + let arity = if is_bi { 2 } else { 1 }; + if type_id.module != self.env.module_id + || !matches!( + declaration.kind, + hir::TypeDeclarationKind::Data | hir::TypeDeclarationKind::Newtype + ) + || declaration.parameters.len() < arity + || arguments.len() + arity != declaration.parameters.len() + { + return self.deriving_error(TypeCheckErrorKind::ExpectedTypeConstructor, span, "mapping deriving requires a local type constructor applied to all but its mapped parameters"); + } + let left_parameter = is_bi.then(|| { + declaration.parameters[declaration.parameters.len() - 2] + .name + .as_str() + }); + let right_parameter = &declaration.parameters.last()?.name; + let usages = match self.validate_field_usage( + &declaration, + MappingClasses::covariant(), + left_parameter, + right_parameter, + ( + known == KnownClass::Profunctor, + known == KnownClass::Contravariant, + ), + &arguments, + ) { + Ok(usages) => usages, + Err(offending) => { + return self.deriving_error( + TypeCheckErrorKind::CannotDeriveInvalidConstructorArg, + offending, + "mapping deriving cannot map a parameter occurrence in this field", + ); + } + }; + let methods = MappingMethods { + mono: self.known_method(KnownClass::Functor, "map"), + bi: self.known_method(KnownClass::Bifunctor, "bimap"), + contra: self.known_method(KnownClass::Contravariant, "cmap"), + pro: self.known_method(KnownClass::Profunctor, "dimap"), + pro_left: self.known_method(KnownClass::Profunctor, "lcmap"), + }; + let left = self.fresh_deriving_binder("__derived_f", span); + let right = if is_bi { + self.fresh_deriving_binder("__derived_g", span) + } else { + left.clone() + }; + let left_expr = local_expr(left.id, span); + let right_expr = local_expr(right.id, span); + let value = self.fresh_deriving_binder("__derived_value", span); + let mut branches = Vec::new(); + for (constructor, usages) in declaration.constructors.iter().zip(usages) { + let binders = constructor + .fields + .iter() + .map(|_| self.fresh_deriving_binder("__derived_field", span)) + .collect::>(); + let mut result = global_expr(constructor.symbol, span); + for (binder, usage) in binders.iter().zip(usages) { + let field = local_expr(binder.id, span); + let Some(mapped) = + self.map_field_usage(&usage, &methods, &left_expr, &right_expr, &field, span) + else { + return self.deriving_error( + TypeCheckErrorKind::CannotFindDerivingType, + span, + "cannot find the class method required by the checked field usage", + ); + }; + result = apply_expr(result, mapped, span); + } + branches.push(hir::CaseBranch { + coverage: hir::CaseBranchCoverage::Source, + pattern: constructor_pattern(constructor, &binders, span), + value: result, + span, + }); + } + let mut body = lambda( + value.clone(), + case_expr(local_expr(value.id, span), branches, span), + span, + ); + if is_bi { + body = lambda(right, body, span); + } + let implementation = lambda(left, body, span); + self.infer_derived_method(method, class_arguments, &implementation) + } +} diff --git a/crates/psrs-typecheck/src/typecheck/classes/deriving/mod.rs b/crates/psrs-typecheck/src/typecheck/classes/deriving/mod.rs index 6a06b1ad..0229d89d 100644 --- a/crates/psrs-typecheck/src/typecheck/classes/deriving/mod.rs +++ b/crates/psrs-typecheck/src/typecheck/classes/deriving/mod.rs @@ -2,41 +2,45 @@ use super::super::*; mod bifunctor; mod contravariant; +mod eq; +mod foldable; mod functor; +mod generic; +mod mapping; mod newtype; mod ord; +mod profunctor; +mod registry; +mod syntax; +mod traversable; mod types; +mod usage; -pub(crate) use types::contains_wildcard; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum KnownDerivingClass { - Eq, - Ord, - Functor, - Bifunctor, - Contravariant, -} +pub(in crate::typecheck) use registry::{DerivingRegistry, KnownClass}; -impl KnownDerivingClass { - fn identity(self) -> (&'static str, &'static str) { - match self { - Self::Eq => ("Data.Eq", "Eq"), - Self::Ord => ("Data.Ord", "Ord"), - Self::Functor => ("Data.Functor", "Functor"), - Self::Bifunctor => ("Data.Bifunctor", "Bifunctor"), - Self::Contravariant => ("Data.Functor.Contravariant", "Contravariant"), - } - } +pub(crate) use types::contains_wildcard; - fn method(self) -> &'static str { - match self { - Self::Eq => "eq", - Self::Ord => "compare", - Self::Functor => "map", - Self::Bifunctor => "bimap", - Self::Contravariant => "cmap", - } +use syntax::{apply_expr, boolean_literal, global_expr, local_expr}; +use usage::MappingClasses; + +/// Whether a structural rule produces this class method. Used only to reject a +/// method the rule does not derive. +fn handles_method(known: KnownClass, name: &str) -> bool { + match known { + KnownClass::Eq => name == "eq", + KnownClass::Eq1 => name == "eq1", + KnownClass::Ord => name == "compare", + KnownClass::Ord1 => name == "compare1", + KnownClass::Functor => name == "map", + KnownClass::Bifunctor => name == "bimap", + KnownClass::Contravariant => name == "cmap", + KnownClass::Profunctor => name == "dimap", + KnownClass::Foldable => matches!(name, "foldMap" | "foldr" | "foldl"), + KnownClass::Bifoldable => matches!(name, "bifoldMap" | "bifoldr" | "bifoldl"), + KnownClass::Traversable => matches!(name, "traverse" | "sequence"), + KnownClass::Bitraversable => matches!(name, "bitraverse" | "bisequence"), + KnownClass::Newtype => name == "wrap", + KnownClass::Generic => matches!(name, "to" | "from"), } } @@ -57,10 +61,7 @@ impl Checker { self.infer_expr_with_expected(implementation, Some(expected)) } - /// Generates the structural `Eq` method for a local data or newtype type. - /// Field comparisons remain ordinary class-method selections, so explicit - /// instance-context dictionaries and imported instances use the existing - /// evidence solver. + /// Synthesizes the methods of a compiler-supported derived class. pub(super) fn derive_known_class_method( &mut self, class_id: hir::TypeId, @@ -69,195 +70,117 @@ impl Checker { class_arguments: &[InferType], span: TextRange, ) -> Option { - if class.parameters.len() != 1 { - return self.deriving_error(span, "known-class deriving requires a unary class"); - } - let Some(known_class) = self.known_deriving_class(class_id) else { + let Some(known_class) = self.env.deriving.known_class(class_id) else { return self.deriving_error( + TypeCheckErrorKind::CannotDerive, span, - "the known-class deriving rule is unavailable for this class", + "the compiler has no deriving rule for this class", ); }; - if method.name != known_class.method() { + if known_class == KnownClass::Newtype { return self.deriving_error( + TypeCheckErrorKind::CannotDerive, span, - "the known-class deriving rule is unavailable for this class method", + "Newtype has no derivable class methods", ); } - match known_class { - KnownDerivingClass::Functor => { - return self.derive_functor_method(method, class_arguments, span); - } - KnownDerivingClass::Bifunctor => { - return self.derive_bifunctor_method(method, class_arguments, span); - } - KnownDerivingClass::Contravariant => { - return self.derive_contravariant_method(method, class_arguments, span); - } - KnownDerivingClass::Ord => { - return self.derive_ord_method(method, class_arguments, span); + if known_class == KnownClass::Generic { + if !matches!(method.name.as_str(), "to" | "from") { + return self.deriving_error( + TypeCheckErrorKind::CannotDerive, + span, + "Generic derives only its `to` and `from` methods", + ); } - KnownDerivingClass::Eq => {} + return self.derive_generic_method(method, class_arguments, span); } - let Some(instance_type) = class_arguments.first() else { - return self.deriving_error(span, "Eq deriving requires one type argument"); - }; - let instance_type = self.resolve_type(instance_type.clone()); - let (head, arguments) = flatten_spine(&instance_type); - let InferType::Constructor(TypeConstructor::User(type_id)) = head else { - return self.deriving_error( - span, - "Eq deriving requires a local data or newtype constructor", - ); - }; - let Some(declaration) = self.env.type_declarations.get(type_id).cloned() else { - return self.deriving_error(span, "cannot find the data declaration to derive Eq"); - }; - if type_id.module != self.env.module_id - || !matches!( - declaration.kind, - hir::TypeDeclarationKind::Data | hir::TypeDeclarationKind::Newtype - ) - || arguments.len() != declaration.parameters.len() - { + if class.parameters.len() != 1 { return self.deriving_error( + TypeCheckErrorKind::InvalidDerivedInstance, span, - "Eq deriving requires a locally declared, fully applied data type", + "known-class deriving requires a unary class", ); } - - let left = self.fresh_deriving_binder("__derived_left", span); - let right = self.fresh_deriving_binder("__derived_right", span); - let mut left_case_branches = Vec::new(); - for constructor in &declaration.constructors { - let left_fields = constructor - .fields - .iter() - .map(|_| self.fresh_deriving_binder("__derived_l", span)) - .collect::>(); - let right_fields = constructor - .fields - .iter() - .map(|_| self.fresh_deriving_binder("__derived_r", span)) - .collect::>(); - let body = Self::derive_eq_field_tests( - constructor, - &left_fields, - &right_fields, - method.symbol, + if !handles_method(known_class, &method.name) { + return self.deriving_error( + TypeCheckErrorKind::CannotDerive, span, + "the known-class deriving rule is unavailable for this class method", ); - let same_constructor = hir::CaseBranch { - coverage: hir::CaseBranchCoverage::Source, - pattern: hir::Pattern { - kind: hir::PatternKind::Constructor { - symbol: constructor.symbol, - name_span: constructor.name_span, - arguments: right_fields - .iter() - .cloned() - .map(|binder| hir::Pattern { - kind: hir::PatternKind::Var(binder), - span, - }) - .collect(), - }, - span, - }, - value: body, - span, - }; - let mismatch = hir::CaseBranch { - coverage: hir::CaseBranchCoverage::Source, - pattern: hir::Pattern { - kind: hir::PatternKind::Wildcard, - span, - }, - value: boolean_literal(false, span), - span, - }; - let right_case = hir::Expr { - kind: hir::ExprKind::Case { - scrutinee: Box::new(local_expr(right.id, span)), - branches: vec![same_constructor, mismatch], - }, - span, - }; - left_case_branches.push(hir::CaseBranch { - coverage: hir::CaseBranchCoverage::Source, - pattern: hir::Pattern { - kind: hir::PatternKind::Constructor { - symbol: constructor.symbol, - name_span: constructor.name_span, - arguments: left_fields - .iter() - .cloned() - .map(|binder| hir::Pattern { - kind: hir::PatternKind::Var(binder), - span, - }) - .collect(), - }, - span, - }, - value: right_case, - span, - }); } - if left_case_branches.is_empty() { - left_case_branches.push(hir::CaseBranch { - coverage: hir::CaseBranchCoverage::Source, - pattern: hir::Pattern { - kind: hir::PatternKind::Wildcard, - span, - }, - value: boolean_literal(true, span), - span, - }); + match known_class { + KnownClass::Eq => self.derive_eq_method(method, class_arguments, span), + KnownClass::Eq1 => self.derive_eq1_method(method, class_arguments, span), + KnownClass::Ord => self.derive_ord_method(method, class_arguments, span), + KnownClass::Ord1 => self.derive_ord1_method(method, class_arguments, span), + KnownClass::Functor => self.derive_functor_method(method, class_arguments, span), + KnownClass::Bifunctor => self.derive_bifunctor_method(method, class_arguments, span), + KnownClass::Contravariant => { + self.derive_contravariant_method(method, class_arguments, span) + } + KnownClass::Profunctor => self.derive_profunctor_method(method, class_arguments, span), + KnownClass::Foldable | KnownClass::Bifoldable => { + self.derive_foldable_method(known_class, method, class_arguments, span) + } + KnownClass::Traversable | KnownClass::Bitraversable => { + self.derive_traversable_method(known_class, method, class_arguments, span) + } + KnownClass::Newtype | KnownClass::Generic => unreachable!("handled above"), } - let implementation = hir::Expr { - kind: hir::ExprKind::Lambda { - binder: left.clone(), - body: Box::new(hir::Expr { - kind: hir::ExprKind::Lambda { - binder: right.clone(), - body: Box::new(hir::Expr { - kind: hir::ExprKind::Case { - scrutinee: Box::new(local_expr(left.id, span)), - branches: left_case_branches, - }, - span, - }), - }, - span, - }), - }, - span, - }; - self.infer_derived_method(method, class_arguments, &implementation) } pub(super) fn validate_known_deriving_class( &mut self, class_id: hir::TypeId, class: &ClassInfo, + head: &hir::Type, + head_arguments: &[InferType], span: TextRange, ) -> Option<()> { - let Some(known_class) = self.known_deriving_class(class_id) else { + let Some(known_class) = self.env.deriving.known_class(class_id) else { return self.deriving_error( + TypeCheckErrorKind::CannotDerive, span, - "the known-class deriving rule is unavailable for this class", + "the compiler has no deriving rule for this class", ); }; - if class.parameters.len() != 1 { - return self.deriving_error(span, "known-class deriving requires a unary class"); + let expected_arity = match known_class { + KnownClass::Newtype | KnownClass::Generic => 2, + _ => 1, + }; + if class.parameters.len() != expected_arity || head_arguments.len() != expected_arity { + return self.deriving_error( + TypeCheckErrorKind::InvalidDerivedInstance, + span, + "known-class deriving requires the class's supported parameter arity", + ); } - if !class - .methods - .iter() - .any(|method| method.name == known_class.method()) - { + if known_class.is_head_shape() { + let (_, raw_arguments) = types::flatten_type_application(head); + let is_wildcard = raw_arguments + .last() + .is_some_and(|argument| matches!(argument.kind, hir::TypeKind::Wildcard)); + if !is_wildcard { + return self.deriving_error( + TypeCheckErrorKind::ExpectedWildcard, + span, + "the derived class's final type argument must be a type wildcard", + ); + } + } + let has_required_methods = match known_class { + KnownClass::Newtype => class.methods.is_empty(), + KnownClass::Generic => ["to", "from"] + .iter() + .all(|name| class.methods.iter().any(|method| method.name == *name)), + _ => class + .methods + .iter() + .any(|method| handles_method(known_class, &method.name)), + }; + if !has_required_methods { return self.deriving_error( + TypeCheckErrorKind::CannotDerive, span, "the class is missing the method required by its known deriving rule", ); @@ -265,52 +188,9 @@ impl Checker { Some(()) } - fn known_deriving_class(&self, class_id: hir::TypeId) -> Option { - let module = self.env.type_modules.get(&class_id)?.as_str(); - let name = self.env.type_names.get(&class_id)?.as_str(); - [ - KnownDerivingClass::Eq, - KnownDerivingClass::Ord, - KnownDerivingClass::Functor, - KnownDerivingClass::Bifunctor, - KnownDerivingClass::Contravariant, - ] - .into_iter() - .find(|known| known.identity() == (module, name)) - } - - fn derive_eq_field_tests( - constructor: &hir::Constructor, - left_fields: &[hir::LocalBinder], - right_fields: &[hir::LocalBinder], - eq_method: SymbolId, - span: TextRange, - ) -> hir::Expr { - let tests = constructor - .fields - .iter() - .zip(left_fields) - .zip(right_fields) - .map(|((_field, left), right)| { - let method = global_expr(eq_method, span); - apply_expr( - apply_expr(method, local_expr(left.id, span), span), - local_expr(right.id, span), - span, - ) - }) - .collect::>(); - tests - .into_iter() - .rev() - .fold(boolean_literal(true, span), |rest, test| hir::Expr { - kind: hir::ExprKind::If { - condition: Box::new(test), - then_branch: Box::new(rest), - else_branch: Box::new(boolean_literal(false, span)), - }, - span, - }) + /// A known class's method symbol by method name, read from the registry. + fn known_method(&self, known: KnownClass, name: &str) -> Option { + self.env.deriving.method(known, name) } fn fresh_deriving_local(&mut self, prefix: &str, span: TextRange) -> hir::LocalBinder { @@ -329,18 +209,29 @@ impl Checker { pub(in crate::typecheck::classes) fn deriving_error( &mut self, + kind: TypeCheckErrorKind, span: TextRange, message: &str, ) -> Option { - self.state.errors.push(TypeCheckError::new( - TypeCheckErrorKind::UnsupportedClass, - span, - message, - )); + self.state + .errors + .push(TypeCheckError::new(kind, span, message)); None } } +/// Whether the type is an application whose immediate head is a type +/// variable, such as `f a`. The official `Eq`/`Ord` deriving rules compare +/// exactly these fields through the class's higher-kinded `eq1`/`compare1` +/// counterpart; a deeper application such as `f a b` is not one, matching the +/// official `isAppliedVar` test. +fn is_applied_variable(ty: &hir::Type) -> bool { + matches!( + &ty.kind, + hir::TypeKind::Application(function, _) if matches!(function.kind, hir::TypeKind::Variable(_)) + ) +} + fn flatten_spine(ty: &InferType) -> (&InferType, Vec) { let mut head = ty; let mut arguments = Vec::new(); @@ -351,35 +242,3 @@ fn flatten_spine(ty: &InferType) -> (&InferType, Vec) { arguments.reverse(); (head, arguments) } - -fn local_expr(local: LocalId, span: TextRange) -> hir::Expr { - hir::Expr { - kind: hir::ExprKind::Local(local), - span, - } -} - -fn global_expr(symbol: SymbolId, span: TextRange) -> hir::Expr { - hir::Expr { - kind: hir::ExprKind::Global(symbol), - span, - } -} - -fn apply_expr(function: hir::Expr, argument: hir::Expr, span: TextRange) -> hir::Expr { - hir::Expr { - kind: hir::ExprKind::Application(Box::new(function), Box::new(argument)), - span, - } -} - -fn boolean_literal(value: bool, span: TextRange) -> hir::Expr { - global_expr( - if value { - hir::Intrinsic::BoolTrue.symbol() - } else { - hir::Intrinsic::BoolFalse.symbol() - }, - span, - ) -} diff --git a/crates/psrs-typecheck/src/typecheck/classes/deriving/newtype.rs b/crates/psrs-typecheck/src/typecheck/classes/deriving/newtype.rs index 8ca1b091..8862eb62 100644 --- a/crates/psrs-typecheck/src/typecheck/classes/deriving/newtype.rs +++ b/crates/psrs-typecheck/src/typecheck/classes/deriving/newtype.rs @@ -3,10 +3,16 @@ use super::super::super::*; use super::super::evidence::record_field_type; use super::flatten_spine; +struct CheckedNewtype { + type_id: hir::TypeId, + underlying: InferType, +} + impl Checker { /// Builds the method implementation for a `derive newtype` instance. /// The source method is selected from the wrapped type's dictionary, then - /// adapted at each function boundary through checked Coercible evidence. + /// adapted through the trusted representation-transparent newtype contract. + /// This is dictionary reuse, not a claim of ordinary Coercible entailment. pub(in crate::typecheck::classes) fn derive_newtype_method( &mut self, class_id: hir::TypeId, @@ -16,9 +22,18 @@ impl Checker { span: TextRange, ) -> Option { if class.parameters.len() != class_arguments.len() { - return self.deriving_error(span, "derive newtype class head has the wrong arity"); + return self.deriving_error( + TypeCheckErrorKind::InvalidNewtypeInstance, + span, + "derive newtype class head has the wrong arity", + ); } - let underlying = self.newtype_underlying_type(class_arguments, span)?; + let checked = self.checked_newtype(class_arguments, span, false)?; + let origin = thir::UncheckedCoercionOrigin::NewtypeDeriving { + class_id, + newtype_id: checked.type_id, + }; + let underlying = checked.underlying; let mut underlying_arguments = class_arguments.to_vec(); *underlying_arguments.last_mut()? = underlying; @@ -74,12 +89,17 @@ impl Checker { ty: underlying_method_type.clone(), span, }; - self.adapt_newtype_method( - selected_method, - underlying_method_type, - derived_method_type, - span, - ) + let mut method_foralls = Vec::new(); + leading_forall_variables(&derived_method_type, &mut method_foralls); + self.with_skolem_scope(&method_foralls, |checker| { + checker.adapt_newtype_method( + selected_method, + underlying_method_type, + derived_method_type, + span, + origin, + ) + }) } pub(in crate::typecheck::classes) fn validate_newtype_deriving_instance( @@ -87,16 +107,28 @@ impl Checker { class_arguments: &[InferType], span: TextRange, ) -> Option { - self.newtype_underlying_type(class_arguments, span) + self.newtype_underlying_type(class_arguments, span, false) } - fn newtype_underlying_type( + pub(in crate::typecheck::classes) fn newtype_underlying_type( &mut self, class_arguments: &[InferType], span: TextRange, + deriving_newtype_class: bool, ) -> Option { + self.checked_newtype(class_arguments, span, deriving_newtype_class) + .map(|checked| checked.underlying) + } + + fn checked_newtype( + &mut self, + class_arguments: &[InferType], + span: TextRange, + deriving_newtype_class: bool, + ) -> Option { let Some(newtype) = class_arguments.last() else { return self.deriving_error( + TypeCheckErrorKind::InvalidNewtypeInstance, span, "derive newtype requires a class with a final type parameter", ); @@ -105,27 +137,46 @@ impl Checker { let (head, arguments) = flatten_spine(&resolved_newtype); let InferType::Constructor(TypeConstructor::User(type_id)) = head else { return self.deriving_error( + TypeCheckErrorKind::InvalidNewtypeInstance, span, "derive newtype requires its final class argument to be a newtype constructor", ); }; let Some(declaration) = self.env.type_declarations.get(type_id).cloned() else { - return self.deriving_error(span, "cannot find the newtype declaration to derive"); + return self.deriving_error( + TypeCheckErrorKind::CannotFindDerivingType, + span, + "cannot find the newtype declaration to derive", + ); }; if declaration.kind != hir::TypeDeclarationKind::Newtype || type_id.module != self.env.module_id || arguments.len() > declaration.parameters.len() { + let kind = if deriving_newtype_class + && declaration.kind == hir::TypeDeclarationKind::Data + && type_id.module == self.env.module_id + { + TypeCheckErrorKind::CannotDeriveNewtypeForData + } else { + TypeCheckErrorKind::InvalidNewtypeInstance + }; return self.deriving_error( + kind, span, "derive newtype requires a locally declared newtype constructor", ); } let Some(constructor) = declaration.constructors.first() else { - return self.deriving_error(span, "the newtype has no data constructor"); + return self.deriving_error( + TypeCheckErrorKind::InvalidNewtypeInstance, + span, + "the newtype has no data constructor", + ); }; let [field] = constructor.fields.as_slice() else { return self.deriving_error( + TypeCheckErrorKind::InvalidNewtypeInstance, span, "derive newtype requires a constructor with exactly one field", ); @@ -146,11 +197,15 @@ impl Checker { let underlying = self.elaborate_type(field, &mut newtype_variables); let Some(underlying) = strip_newtype_arguments(underlying, &omitted_arguments, self) else { return self.deriving_error( + TypeCheckErrorKind::InvalidNewtypeInstance, span, "the wrapped type must end in every unapplied newtype parameter", ); }; - Some(underlying) + Some(CheckedNewtype { + type_id: *type_id, + underlying, + }) } fn adapt_newtype_method( @@ -159,6 +214,7 @@ impl Checker { source: InferType, target: InferType, span: TextRange, + origin: thir::UncheckedCoercionOrigin, ) -> Option { let source = self.resolve_type(source); let target = self.resolve_type(target); @@ -187,6 +243,7 @@ impl Checker { source_body.as_ref().clone(), target_body.as_ref().clone(), span, + origin, )?; return Some(InferredExpr { ty: target, @@ -219,6 +276,7 @@ impl Checker { target_parameter.clone(), source_parameter, span, + origin, ); let applied = InferredExpr { kind: InferredExprKind::Application(Box::new(value), Box::new(converted_argument)), @@ -230,6 +288,7 @@ impl Checker { source_result.clone(), target_result.clone(), span, + origin, )?; return Some(InferredExpr { kind: InferredExprKind::Lambda { @@ -243,7 +302,7 @@ impl Checker { span, }); } - Some(self.apply_newtype_coercion(value, source, target, span)) + Some(self.apply_newtype_coercion(value, source, target, span, origin)) } fn apply_newtype_coercion( @@ -252,20 +311,14 @@ impl Checker { source: InferType, target: InferType, span: TextRange, + origin: thir::UncheckedCoercionOrigin, ) -> InferredExpr { - let constraint = ClassConstraint { - class_id: hir::TypeId::COERCIBLE, - arguments: vec![source.clone(), target.clone()], - span, - }; - let dictionary_type = self.dictionary_type(&constraint); - let wanted = self.push_wanted(constraint, dictionary_type); let function_type = arrow(source.clone(), target.clone()); let function = InferredExpr { - kind: InferredExprKind::CoerceFunction { - wanted, + kind: InferredExprKind::UnsafeCoerceFunction { source, target: target.clone(), + origin, }, ty: function_type, span, @@ -310,3 +363,17 @@ fn strip_newtype_arguments( } Some(ty) } + +fn leading_forall_variables(ty: &InferType, variables: &mut Vec) { + match ty { + InferType::ForAll { + variables: binders, + body, + } => { + variables.extend(binders); + leading_forall_variables(body, variables); + } + InferType::Constrained { body, .. } => leading_forall_variables(body, variables), + _ => {} + } +} diff --git a/crates/psrs-typecheck/src/typecheck/classes/deriving/ord.rs b/crates/psrs-typecheck/src/typecheck/classes/deriving/ord.rs index a3b709d4..e83181bc 100644 --- a/crates/psrs-typecheck/src/typecheck/classes/deriving/ord.rs +++ b/crates/psrs-typecheck/src/typecheck/classes/deriving/ord.rs @@ -1,9 +1,13 @@ use super::super::super::*; -use super::flatten_spine; +use super::{KnownClass, apply_expr, flatten_spine, global_expr, is_applied_variable, local_expr}; +use crate::typecheck::classes::deriving::syntax::{ + case_expr, lambda, symbol_pattern, variable_pattern, +}; #[derive(Clone, Copy)] struct OrdFieldContext { method: SymbolId, + field_method: Option, less: SymbolId, equal: SymbolId, greater: SymbolId, @@ -16,56 +20,127 @@ impl Checker { method: &MethodInfo, class_arguments: &[InferType], span: TextRange, + ) -> Option { + // A field whose type is an applied type variable `f a` is compared + // through `Ord1`'s `compare1`, exactly as the official deriving rule + // does. The lookup is lazy because a data type with no such field does + // not need `Ord1` to exist in the environment at all. + let needs_ord1 = class_arguments + .first() + .and_then(|argument| { + let instance_type = self.resolve_type(argument.clone()); + let (head, _) = flatten_spine(&instance_type); + let InferType::Constructor(TypeConstructor::User(type_id)) = head else { + return None; + }; + self.env.type_declarations.get(type_id).map(|declaration| { + declaration.constructors.iter().any(|constructor| { + constructor + .fields + .iter() + .any(|field| is_applied_variable(&self.normalize_deriving_type(field))) + }) + }) + }) + .unwrap_or(false); + let field_method = if needs_ord1 { + match self.known_method(KnownClass::Ord1, "compare1") { + Some(method) => Some(method), + None => { + return self.deriving_error( + TypeCheckErrorKind::CannotDerive, + span, + "cannot find the Ord1 method for Ord deriving", + ); + } + } + } else { + None + }; + self.derive_ord_method_using(method, class_arguments, method.symbol, field_method, span) + } + + pub(super) fn derive_ord1_method( + &mut self, + method: &MethodInfo, + class_arguments: &[InferType], + span: TextRange, + ) -> Option { + // The official rule derives `Ord1` by delegating to `Ord` at the + // applied argument: `compare1 = compare`. The matching `Ord` instance + // supplies the dictionary, so its context must be visible alongside the + // method's. + let Some(compare_method) = self.known_method(KnownClass::Ord, "compare") else { + return self.deriving_error( + TypeCheckErrorKind::CannotDerive, + span, + "cannot find the Ord method for Ord1 deriving", + ); + }; + let implementation = global_expr(compare_method, span); + self.infer_derived_method(method, class_arguments, &implementation) + } + + fn derive_ord_method_using( + &mut self, + method: &MethodInfo, + class_arguments: &[InferType], + compare_method: SymbolId, + field_method: Option, + span: TextRange, ) -> Option { let Some(instance_type) = class_arguments.first() else { - return self.deriving_error(span, "Ord deriving requires one type argument"); + return self.deriving_error( + TypeCheckErrorKind::InvalidDerivedInstance, + span, + "Ord deriving requires one type argument", + ); }; let instance_type = self.resolve_type(instance_type.clone()); let (head, arguments) = flatten_spine(&instance_type); let InferType::Constructor(TypeConstructor::User(type_id)) = head else { return self.deriving_error( + TypeCheckErrorKind::ExpectedTypeConstructor, span, "Ord deriving requires a local data or newtype constructor", ); }; let Some(declaration) = self.env.type_declarations.get(type_id).cloned() else { - return self.deriving_error(span, "cannot find the data declaration to derive Ord"); + return self.deriving_error( + TypeCheckErrorKind::CannotFindDerivingType, + span, + "cannot find the data declaration to derive Ord", + ); }; if type_id.module != self.env.module_id || !matches!( declaration.kind, hir::TypeDeclarationKind::Data | hir::TypeDeclarationKind::Newtype ) - || arguments.len() != declaration.parameters.len() + || declaration.parameters.len() != arguments.len() { return self.deriving_error( + TypeCheckErrorKind::ExpectedTypeConstructor, span, "Ord deriving requires a locally declared, fully applied data type", ); } - let Some(ordering_id) = ordering_result_id(&method.signature) else { + let Some(ordering) = self.env.deriving.ordering() else { return self.deriving_error( + TypeCheckErrorKind::CannotFindDerivingType, span, - "Ord deriving requires a method returning the Ordering data type", + "cannot find the Data.Ordering data declaration", ); }; - let Some(ordering) = self.env.type_declarations.get(&ordering_id) else { - return self.deriving_error(span, "cannot find the Ordering data declaration"); - }; - let Some(less) = nullary_constructor(ordering, "LT") else { - return self.deriving_error(span, "Ordering must define a nullary LT constructor"); - }; - let Some(equal) = nullary_constructor(ordering, "EQ") else { - return self.deriving_error(span, "Ordering must define a nullary EQ constructor"); - }; - let Some(greater) = nullary_constructor(ordering, "GT") else { - return self.deriving_error(span, "Ordering must define a nullary GT constructor"); - }; + let less = ordering.lt; + let equal = ordering.eq; + let greater = ordering.gt; let left = self.fresh_deriving_binder("__derived_left", span); let right = self.fresh_deriving_binder("__derived_right", span); let field_context = OrdFieldContext { - method: method.symbol, + method: compare_method, + field_method, less, equal, greater, @@ -78,6 +153,11 @@ impl Checker { .iter() .map(|_| self.fresh_deriving_binder("__derived_l", span)) .collect::>(); + let field_types = constructor + .fields + .iter() + .map(|field| self.normalize_deriving_type(field)) + .collect::>(); let mut right_branches = Vec::new(); for (right_index, other) in declaration.constructors.iter().enumerate() { let result_symbol = match left_index.cmp(&right_index) { @@ -91,12 +171,7 @@ impl Checker { .map(|_| self.fresh_deriving_binder("__derived_r", span)) .collect::>(); let value = if left_index == right_index { - derive_ord_field_tests( - &constructor.fields, - &left_fields, - &right_fields, - field_context, - ) + derive_ord_field_tests(&field_types, &left_fields, &right_fields, field_context) } else { global_expr(result_symbol, span) }; @@ -107,13 +182,7 @@ impl Checker { span, }); } - let right_case = hir::Expr { - kind: hir::ExprKind::Case { - scrutinee: Box::new(local_expr(right.id, span)), - branches: right_branches, - }, - span, - }; + let right_case = case_expr(local_expr(right.id, span), right_branches, span); left_branches.push(hir::CaseBranch { coverage: hir::CaseBranchCoverage::Source, pattern: constructor_pattern(constructor.symbol, &left_fields, span), @@ -124,33 +193,20 @@ impl Checker { if left_branches.is_empty() { left_branches.push(hir::CaseBranch { coverage: hir::CaseBranchCoverage::Source, - pattern: hir::Pattern { - kind: hir::PatternKind::Wildcard, - span, - }, + pattern: crate::typecheck::classes::deriving::syntax::wildcard_pattern(span), value: global_expr(equal, span), span, }); } - let implementation = hir::Expr { - kind: hir::ExprKind::Lambda { - binder: left.clone(), - body: Box::new(hir::Expr { - kind: hir::ExprKind::Lambda { - binder: right.clone(), - body: Box::new(hir::Expr { - kind: hir::ExprKind::Case { - scrutinee: Box::new(local_expr(left.id, span)), - branches: left_branches, - }, - span, - }), - }, - span, - }), - }, + let implementation = lambda( + left.clone(), + lambda( + right.clone(), + case_expr(local_expr(left.id, span), left_branches, span), + span, + ), span, - }; + ); self.infer_derived_method(method, class_arguments, &implementation) } } @@ -163,6 +219,7 @@ fn derive_ord_field_tests( ) -> hir::Expr { let OrdFieldContext { method, + field_method, less, equal, greater, @@ -170,23 +227,26 @@ fn derive_ord_field_tests( } = context; fields.iter().zip(left).zip(right).rev().fold( global_expr(equal, span), - |rest, ((_field, left), right)| { + |rest, ((field, left), right)| { + let method = if is_applied_variable(field) { + field_method.unwrap_or(method) + } else { + method + }; let compared = apply_expr( apply_expr(global_expr(method, span), local_expr(left.id, span), span), local_expr(right.id, span), span, ); - hir::Expr { - kind: hir::ExprKind::Case { - scrutinee: Box::new(compared), - branches: vec![ - ordering_branch(less, global_expr(less, span), span), - ordering_branch(equal, rest, span), - ordering_branch(greater, global_expr(greater, span), span), - ], - }, + case_expr( + compared, + vec![ + ordering_branch(less, global_expr(less, span), span), + ordering_branch(equal, rest, span), + ordering_branch(greater, global_expr(greater, span), span), + ], span, - } + ) }, ) } @@ -205,60 +265,12 @@ fn constructor_pattern( arguments: &[hir::LocalBinder], span: TextRange, ) -> hir::Pattern { - hir::Pattern { - kind: hir::PatternKind::Constructor { - symbol, - name_span: span, - arguments: arguments - .iter() - .map(|binder| hir::Pattern { - kind: hir::PatternKind::Var(binder.clone()), - span, - }) - .collect(), - }, + symbol_pattern( + symbol, + arguments + .iter() + .map(|binder| variable_pattern(binder, span)) + .collect(), span, - } -} - -fn nullary_constructor(declaration: &hir::TypeDeclaration, name: &str) -> Option { - declaration - .constructors - .iter() - .find(|constructor| constructor.name == name && constructor.fields.is_empty()) - .map(|constructor| constructor.symbol) -} - -fn ordering_result_id(mut ty: &hir::Type) -> Option { - loop { - match &ty.kind { - hir::TypeKind::Forall { body, .. } | hir::TypeKind::Constrained { body, .. } => { - ty = body; - } - hir::TypeKind::Function { result, .. } => ty = result, - hir::TypeKind::Named(id) => return Some(*id), - _ => return None, - } - } -} - -fn local_expr(local: LocalId, span: TextRange) -> hir::Expr { - hir::Expr { - kind: hir::ExprKind::Local(local), - span, - } -} - -fn global_expr(symbol: SymbolId, span: TextRange) -> hir::Expr { - hir::Expr { - kind: hir::ExprKind::Global(symbol), - span, - } -} - -fn apply_expr(function: hir::Expr, argument: hir::Expr, span: TextRange) -> hir::Expr { - hir::Expr { - kind: hir::ExprKind::Application(Box::new(function), Box::new(argument)), - span, - } + ) } diff --git a/crates/psrs-typecheck/src/typecheck/classes/deriving/profunctor.rs b/crates/psrs-typecheck/src/typecheck/classes/deriving/profunctor.rs new file mode 100644 index 00000000..0ddc7208 --- /dev/null +++ b/crates/psrs-typecheck/src/typecheck/classes/deriving/profunctor.rs @@ -0,0 +1,13 @@ +use super::super::super::*; +use super::KnownClass; + +impl Checker { + pub(super) fn derive_profunctor_method( + &mut self, + method: &MethodInfo, + class_arguments: &[InferType], + span: TextRange, + ) -> Option { + self.derive_mapping_method(KnownClass::Profunctor, method, class_arguments, span) + } +} diff --git a/crates/psrs-typecheck/src/typecheck/classes/deriving/registry.rs b/crates/psrs-typecheck/src/typecheck/classes/deriving/registry.rs new file mode 100644 index 00000000..3a38d5a6 --- /dev/null +++ b/crates/psrs-typecheck/src/typecheck/classes/deriving/registry.rs @@ -0,0 +1,321 @@ +//! The single table of compiler-known deriving classes and the representation +//! declarations their rules name. +//! +//! The table is built once from the resolved declarations and read by +//! `TypeId`; no deriving rule compares a class module or name at use time. The +//! declaring module and short name are inputs to construction only, so a +//! re-export keeps the defining identity and an unrelated same-name class gains +//! no rule. See [deriving](../../../../docs/design/frontend/type-system/deriving.md). + +use super::super::super::*; + +/// A class with a compiler-supported deriving rule. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(in crate::typecheck) enum KnownClass { + Eq, + Eq1, + Ord, + Ord1, + Functor, + Bifunctor, + Contravariant, + Profunctor, + Foldable, + Bifoldable, + Traversable, + Bitraversable, + Newtype, + Generic, +} + +const ALL_KNOWN_CLASSES: [KnownClass; 14] = [ + KnownClass::Eq, + KnownClass::Eq1, + KnownClass::Ord, + KnownClass::Ord1, + KnownClass::Functor, + KnownClass::Bifunctor, + KnownClass::Contravariant, + KnownClass::Profunctor, + KnownClass::Foldable, + KnownClass::Bifoldable, + KnownClass::Traversable, + KnownClass::Bitraversable, + KnownClass::Newtype, + KnownClass::Generic, +]; + +impl KnownClass { + /// The declaring module and class name this rule is pinned to. This is a + /// construction-time key, not a use-time predicate. + pub(in crate::typecheck) fn identity(self) -> (&'static str, &'static str) { + match self { + Self::Eq => ("Data.Eq", "Eq"), + Self::Eq1 => ("Data.Eq", "Eq1"), + Self::Ord => ("Data.Ord", "Ord"), + Self::Ord1 => ("Data.Ord", "Ord1"), + Self::Functor => ("Data.Functor", "Functor"), + Self::Bifunctor => ("Data.Bifunctor", "Bifunctor"), + Self::Contravariant => ("Data.Functor.Contravariant", "Contravariant"), + Self::Profunctor => ("Data.Profunctor", "Profunctor"), + Self::Foldable => ("Data.Foldable", "Foldable"), + Self::Bifoldable => ("Data.Bifoldable", "Bifoldable"), + Self::Traversable => ("Data.Traversable", "Traversable"), + Self::Bitraversable => ("Data.Bitraversable", "Bitraversable"), + Self::Newtype => ("Data.Newtype", "Newtype"), + Self::Generic => ("Data.Generic.Rep", "Generic"), + } + } + + /// Whether the rule only fixes the instance head's final type argument + /// (Newtype, Generic) rather than deriving method bodies from fields. + pub(in crate::typecheck) fn is_head_shape(self) -> bool { + matches!(self, Self::Newtype | Self::Generic) + } +} + +/// The `Data.Ordering` constructors an `Ord` rule names. +#[derive(Clone, Copy, Debug)] +pub(in crate::typecheck) struct OrderingRep { + pub(in crate::typecheck) lt: SymbolId, + pub(in crate::typecheck) eq: SymbolId, + pub(in crate::typecheck) gt: SymbolId, +} + +/// The `Data.Generic.Rep` types and constructors a `Generic` rule names. +#[derive(Clone, Copy, Debug)] +pub(in crate::typecheck) struct GenericRep { + constructor_ty: hir::TypeId, + no_constructors: hir::TypeId, + no_arguments: hir::TypeId, + argument: hir::TypeId, + product: hir::TypeId, + sum: hir::TypeId, + constructor_ctor: SymbolId, + no_arguments_ctor: SymbolId, + argument_ctor: SymbolId, + product_ctor: SymbolId, + inl_ctor: SymbolId, + inr_ctor: SymbolId, +} + +impl GenericRep { + /// The representation type named `name`. + pub(in crate::typecheck) fn type_id(&self, name: &str) -> Option { + match name { + "Constructor" => Some(self.constructor_ty), + "NoConstructors" => Some(self.no_constructors), + "NoArguments" => Some(self.no_arguments), + "Argument" => Some(self.argument), + "Product" => Some(self.product), + "Sum" => Some(self.sum), + _ => None, + } + } + + /// The representation constructor named `name`. `Inl` and `Inr` are the + /// `Sum` constructors. + pub(in crate::typecheck) fn constructor(&self, name: &str) -> Option { + match name { + "Constructor" => Some(self.constructor_ctor), + "NoArguments" => Some(self.no_arguments_ctor), + "Argument" => Some(self.argument_ctor), + "Product" => Some(self.product_ctor), + "Inl" => Some(self.inl_ctor), + "Inr" => Some(self.inr_ctor), + _ => None, + } + } +} + +impl OrderingRep { + fn build(env: &SemanticEnv) -> Option { + let (_, declaration) = find_std_type(env, "Data.Ordering", "Ordering")?; + Some(Self { + lt: nullary_constructor(declaration, "LT")?, + eq: nullary_constructor(declaration, "EQ")?, + gt: nullary_constructor(declaration, "GT")?, + }) + } +} + +impl GenericRep { + fn build(env: &SemanticEnv) -> Option { + let (constructor_ty, constructor_declaration) = + find_std_type(env, "Data.Generic.Rep", "Constructor")?; + let (no_constructors, _) = find_std_type(env, "Data.Generic.Rep", "NoConstructors")?; + let (no_arguments, no_arguments_declaration) = + find_std_type(env, "Data.Generic.Rep", "NoArguments")?; + let (argument, argument_declaration) = find_std_type(env, "Data.Generic.Rep", "Argument")?; + let (product, product_declaration) = find_std_type(env, "Data.Generic.Rep", "Product")?; + let (sum, sum_declaration) = find_std_type(env, "Data.Generic.Rep", "Sum")?; + Some(Self { + constructor_ty, + no_constructors, + no_arguments, + argument, + product, + sum, + constructor_ctor: constructor_symbol(constructor_declaration, "Constructor")?, + no_arguments_ctor: constructor_symbol(no_arguments_declaration, "NoArguments")?, + argument_ctor: constructor_symbol(argument_declaration, "Argument")?, + product_ctor: constructor_symbol(product_declaration, "Product")?, + inl_ctor: constructor_symbol(sum_declaration, "Inl")?, + inr_ctor: constructor_symbol(sum_declaration, "Inr")?, + }) + } +} + +/// The single source of "which classes the compiler knows" and the symbols +/// their rules name. Built once when the semantic environment is constructed. +#[derive(Clone, Debug, Default)] +pub(in crate::typecheck) struct DerivingRegistry { + classes: HashMap, + ids: HashMap, + methods: HashMap<(KnownClass, String), SymbolId>, + values: HashMap<(String, String), SymbolId>, + ordering: Option, + generic: Option, +} + +impl DerivingRegistry { + pub(in crate::typecheck) fn build( + env: &SemanticEnv, + known_values: &[hir::Declaration], + module_names: &HashMap, + ) -> Self { + let mut registry = Self::default(); + for (class_id, class) in &env.classes { + let Some(module) = env.type_modules.get(class_id).map(String::as_str) else { + continue; + }; + let Some(name) = env.type_names.get(class_id).map(String::as_str) else { + continue; + }; + let Some(known) = ALL_KNOWN_CLASSES + .into_iter() + .find(|known| known.identity() == (module, name)) + else { + continue; + }; + registry.classes.insert(*class_id, known); + registry.ids.insert(known, *class_id); + for method in &class.methods { + registry + .methods + .insert((known, method.name.clone()), method.symbol); + } + } + // A class method is a value too: `append`, `mempty`, `apply`, and + // `pure` are methods of classes that are not deriving classes, so the + // fold and traversal rules reach them through the value table. + for (class_id, class) in &env.classes { + let Some(module) = env.type_modules.get(class_id) else { + continue; + }; + for method in &class.methods { + registry + .values + .insert((module.clone(), method.name.clone()), method.symbol); + } + } + for declaration in known_values { + if let Some(module) = module_names.get(&declaration.symbol.module) { + registry.values.insert( + (module.clone(), declaration.name.clone()), + declaration.symbol, + ); + } + } + registry.ordering = OrderingRep::build(env); + registry.generic = GenericRep::build(env); + registry + } + + /// The deriving rule for a resolved class identity, if the compiler knows + /// one. + pub(in crate::typecheck) fn known_class(&self, class_id: hir::TypeId) -> Option { + self.classes.get(&class_id).copied() + } + + /// The resolved class identity of a known class, for consulting the visible + /// instance environment. + pub(in crate::typecheck) fn class_id(&self, known: KnownClass) -> Option { + self.ids.get(&known).copied() + } + + /// A known class's method symbol by method name. + pub(in crate::typecheck) fn method(&self, known: KnownClass, name: &str) -> Option { + self.methods.get(&(known, name.to_owned())).copied() + } + + pub(in crate::typecheck) fn ordering(&self) -> Option { + self.ordering + } + + pub(in crate::typecheck) fn generic_rep(&self) -> Option { + self.generic + } + + /// A program value by declaring module and name. + pub(in crate::typecheck) fn value(&self, module: &str, name: &str) -> Option { + self.values + .get(&(module.to_owned(), name.to_owned())) + .copied() + } + + /// `Data.Semigroup.append`. + pub(in crate::typecheck) fn append(&self) -> Option { + self.value("Data.Semigroup", "append") + } + + /// `Data.Monoid.mempty`. + pub(in crate::typecheck) fn mempty(&self) -> Option { + self.value("Data.Monoid", "mempty") + } + + /// The canonical `Control.Category.identity`, re-exported by Data.Function. + /// Standalone source fixtures may provide the ordinary function directly. + pub(in crate::typecheck) fn identity(&self) -> Option { + self.value("Control.Category", "identity") + .or_else(|| self.value("Data.Function", "identity")) + } + + /// `Control.Apply.apply`. + pub(in crate::typecheck) fn apply(&self) -> Option { + self.value("Control.Apply", "apply") + } + + /// `Control.Applicative.pure`. + pub(in crate::typecheck) fn pure(&self) -> Option { + self.value("Control.Applicative", "pure") + } +} + +/// The declaration of `module.name` among the program's resolved types. +fn find_std_type<'a>( + env: &'a SemanticEnv, + module: &str, + name: &str, +) -> Option<(hir::TypeId, &'a hir::TypeDeclaration)> { + env.type_declarations.iter().find_map(|(id, declaration)| { + (declaration.name == name && env.type_modules.get(id).map(String::as_str) == Some(module)) + .then_some((*id, declaration)) + }) +} + +fn nullary_constructor(declaration: &hir::TypeDeclaration, name: &str) -> Option { + declaration + .constructors + .iter() + .find(|constructor| constructor.name == name && constructor.fields.is_empty()) + .map(|constructor| constructor.symbol) +} + +fn constructor_symbol(declaration: &hir::TypeDeclaration, name: &str) -> Option { + declaration + .constructors + .iter() + .find(|constructor| constructor.name == name) + .map(|constructor| constructor.symbol) +} diff --git a/crates/psrs-typecheck/src/typecheck/classes/deriving/syntax.rs b/crates/psrs-typecheck/src/typecheck/classes/deriving/syntax.rs new file mode 100644 index 00000000..a128f32d --- /dev/null +++ b/crates/psrs-typecheck/src/typecheck/classes/deriving/syntax.rs @@ -0,0 +1,153 @@ +//! The one term builder every structural deriving rule uses. +//! +//! A rule states its traversal; the lambdas, cases, applications, and literals +//! it emits are constructed here, so no rule builds resolved HIR by hand. The +//! output is ordinary resolved HIR and is checked by the normal inference path. + +use super::super::super::*; + +pub(super) fn local_expr(local: LocalId, span: TextRange) -> hir::Expr { + hir::Expr { + kind: hir::ExprKind::Local(local), + span, + } +} + +pub(super) fn global_expr(symbol: SymbolId, span: TextRange) -> hir::Expr { + hir::Expr { + kind: hir::ExprKind::Global(symbol), + span, + } +} + +pub(super) fn apply_expr(function: hir::Expr, argument: hir::Expr, span: TextRange) -> hir::Expr { + hir::Expr { + kind: hir::ExprKind::Application(Box::new(function), Box::new(argument)), + span, + } +} + +pub(super) fn boolean_literal(value: bool, span: TextRange) -> hir::Expr { + global_expr( + if value { + hir::Intrinsic::BoolTrue.symbol() + } else { + hir::Intrinsic::BoolFalse.symbol() + }, + span, + ) +} + +pub(super) fn lambda(binder: hir::LocalBinder, body: hir::Expr, span: TextRange) -> hir::Expr { + hir::Expr { + kind: hir::ExprKind::Lambda { + binder, + body: Box::new(body), + }, + span, + } +} + +pub(super) fn case_expr( + value: hir::Expr, + branches: Vec, + span: TextRange, +) -> hir::Expr { + hir::Expr { + kind: hir::ExprKind::Case { + scrutinee: Box::new(value), + branches, + }, + span, + } +} + +pub(super) fn constructor_pattern( + constructor: &hir::Constructor, + binders: &[hir::LocalBinder], + span: TextRange, +) -> hir::Pattern { + hir::Pattern { + kind: hir::PatternKind::Constructor { + symbol: constructor.symbol, + name_span: constructor.name_span, + arguments: binders + .iter() + .map(|binder| hir::Pattern { + kind: hir::PatternKind::Var(binder.clone()), + span, + }) + .collect(), + }, + span, + } +} + +pub(super) fn field_expr(value: hir::Expr, field: &str, span: TextRange) -> hir::Expr { + hir::Expr { + kind: hir::ExprKind::FieldAccess { + expression: Box::new(value), + field: field.into(), + }, + span, + } +} + +pub(super) fn record_update( + value: hir::Expr, + fields: Vec<(String, hir::Expr)>, + span: TextRange, +) -> hir::Expr { + hir::Expr { + kind: hir::ExprKind::RecordUpdate { + expression: Box::new(value), + fields, + }, + span, + } +} + +pub(super) fn if_expr( + condition: hir::Expr, + then_branch: hir::Expr, + else_branch: hir::Expr, + span: TextRange, +) -> hir::Expr { + hir::Expr { + kind: hir::ExprKind::If { + condition: Box::new(condition), + then_branch: Box::new(then_branch), + else_branch: Box::new(else_branch), + }, + span, + } +} + +pub(super) fn variable_pattern(binder: &hir::LocalBinder, span: TextRange) -> hir::Pattern { + hir::Pattern { + kind: hir::PatternKind::Var(binder.clone()), + span, + } +} + +pub(super) fn wildcard_pattern(span: TextRange) -> hir::Pattern { + hir::Pattern { + kind: hir::PatternKind::Wildcard, + span, + } +} + +pub(super) fn symbol_pattern( + symbol: SymbolId, + arguments: Vec, + span: TextRange, +) -> hir::Pattern { + hir::Pattern { + kind: hir::PatternKind::Constructor { + symbol, + name_span: span, + arguments, + }, + span, + } +} diff --git a/crates/psrs-typecheck/src/typecheck/classes/deriving/traversable.rs b/crates/psrs-typecheck/src/typecheck/classes/deriving/traversable.rs new file mode 100644 index 00000000..da91ad92 --- /dev/null +++ b/crates/psrs-typecheck/src/typecheck/classes/deriving/traversable.rs @@ -0,0 +1,395 @@ +//! `Traversable` and `Bitraversable` deriving. +//! +//! `traverse` runs an effect for each parameter occurrence and rebuilds the +//! constructor inside the applicative: `map C e1 <*> e2 ...`. Inert fields are +//! carried through unchanged, and a nested occurrence recurses through the +//! `Traversable`/`Bitraversable` instance. `sequence` is `traverse identity`. + +use super::super::super::*; +use super::syntax::{constructor_pattern, field_expr, record_update}; +use super::usage::FieldUsage; +use super::{KnownClass, MappingClasses, apply_expr, flatten_spine, global_expr, local_expr}; +use crate::typecheck::classes::deriving::syntax::{case_expr, lambda}; + +#[derive(Clone, Copy)] +struct TraverseOps { + traverse: Option, + bitraverse: Option, + map: Option, + apply: Option, + pure: Option, + identity: Option, +} + +#[derive(Clone, Copy)] +struct TraverseContext<'a> { + ops: TraverseOps, + left: &'a hir::Expr, + right: &'a hir::Expr, + span: TextRange, +} + +impl Checker { + pub(super) fn derive_traversable_method( + &mut self, + known: KnownClass, + method: &MethodInfo, + class_arguments: &[InferType], + span: TextRange, + ) -> Option { + let is_bi = known == KnownClass::Bitraversable; + let Some(instance_type) = class_arguments.first() else { + return self.deriving_error( + TypeCheckErrorKind::InvalidDerivedInstance, + span, + "traversal deriving requires one type argument", + ); + }; + let instance_type = self.resolve_type(instance_type.clone()); + let (head, arguments) = flatten_spine(&instance_type); + let InferType::Constructor(TypeConstructor::User(type_id)) = head else { + return self.deriving_error( + TypeCheckErrorKind::ExpectedTypeConstructor, + span, + "traversal deriving requires a local type constructor", + ); + }; + let Some(declaration) = self.env.type_declarations.get(type_id).cloned() else { + return self.deriving_error( + TypeCheckErrorKind::CannotFindDerivingType, + span, + "cannot find the data declaration to derive a traversal", + ); + }; + let arity = if is_bi { 2 } else { 1 }; + if type_id.module != self.env.module_id + || !matches!( + declaration.kind, + hir::TypeDeclarationKind::Data | hir::TypeDeclarationKind::Newtype + ) + || declaration.parameters.len() < arity + || arguments.len() + arity != declaration.parameters.len() + { + return self.deriving_error( + TypeCheckErrorKind::ExpectedTypeConstructor, + span, + "traversal deriving requires a local type constructor applied to all but its final parameters", + ); + } + let lparam = is_bi.then(|| { + declaration.parameters[declaration.parameters.len() - 2] + .name + .as_str() + }); + let param = &declaration.parameters[declaration.parameters.len() - 1].name; + let usages = match self.validate_field_usage( + &declaration, + MappingClasses::traversable(), + lparam, + param, + (false, false), + &arguments, + ) { + Ok(usages) => usages, + Err(offending) => { + return self.deriving_error( + TypeCheckErrorKind::CannotDeriveInvalidConstructorArg, + offending, + "traversal deriving cannot map a parameter occurrence in this field", + ); + } + }; + let ops = self.traverse_ops(); + if matches!(method.name.as_str(), "traverse" | "bitraverse") { + let traversal_method = if is_bi { ops.bitraverse } else { ops.traverse }; + if ops.map.is_none() + || ops.apply.is_none() + || ops.pure.is_none() + || traversal_method.is_none() + { + return self.deriving_error( + TypeCheckErrorKind::CannotFindDerivingType, + span, + "traversal deriving requires the Functor, Apply, and Applicative classes", + ); + } + } else { + let traversal_method = if is_bi { ops.bitraverse } else { ops.traverse }; + if ops.identity.is_none() || traversal_method.is_none() { + return self.deriving_error( + TypeCheckErrorKind::CannotFindDerivingType, + span, + "sequence deriving requires the registered identity and the traversal method", + ); + } + } + let value = self.fresh_deriving_binder("__derived_value", span); + let implementation = if matches!(method.name.as_str(), "sequence" | "bisequence") { + let identity = global_expr(ops.identity?, span); + let traversing = if is_bi { + apply_expr( + apply_expr(global_expr(ops.bitraverse?, span), identity.clone(), span), + identity, + span, + ) + } else { + apply_expr(global_expr(ops.traverse?, span), identity, span) + }; + lambda( + value.clone(), + apply_expr(traversing, local_expr(value.id, span), span), + span, + ) + } else { + let left = self.fresh_deriving_binder("__derived_f", span); + let right = if is_bi { + self.fresh_deriving_binder("__derived_g", span) + } else { + left.clone() + }; + let left_expr = local_expr(left.id, span); + let right_expr = local_expr(right.id, span); + let context = TraverseContext { + ops, + left: &left_expr, + right: &right_expr, + span, + }; + let mut branches = Vec::with_capacity(declaration.constructors.len()); + for (constructor, field_usages) in declaration.constructors.iter().zip(&usages) { + let binders = constructor + .fields + .iter() + .map(|_| self.fresh_deriving_binder("__derived_field", span)) + .collect::>(); + let body = self.traverse_branch(constructor, &binders, field_usages, &context)?; + branches.push(hir::CaseBranch { + coverage: hir::CaseBranchCoverage::Source, + pattern: constructor_pattern(constructor, &binders, span), + value: body, + span, + }); + } + let mut body = case_expr(local_expr(value.id, span), branches, span); + body = lambda(value, body, span); + if is_bi { + body = lambda(right, body, span); + } + lambda(left, body, span) + }; + self.infer_derived_method(method, class_arguments, &implementation) + } + + fn traverse_ops(&self) -> TraverseOps { + TraverseOps { + traverse: self.known_method(KnownClass::Traversable, "traverse"), + bitraverse: self.known_method(KnownClass::Bitraversable, "bitraverse"), + map: self.known_method(KnownClass::Functor, "map"), + apply: self.env.deriving.apply(), + pure: self.env.deriving.pure(), + identity: self.env.deriving.identity(), + } + } + + fn traverse_branch( + &mut self, + constructor: &hir::Constructor, + binders: &[hir::LocalBinder], + usages: &[FieldUsage], + context: &TraverseContext<'_>, + ) -> Option { + let TraverseContext { + ref ops, + left, + right, + span, + } = *context; + let mut arguments = Vec::new(); + let mut effects = Vec::new(); + let mut effect_binders = Vec::new(); + for (binder, usage) in binders.iter().zip(usages) { + let field = local_expr(binder.id, span); + if *usage == FieldUsage::Inert { + arguments.push(field); + } else { + let effect_binder = self.fresh_deriving_binder("__derived_effect", span); + arguments.push(local_expr(effect_binder.id, span)); + effects.push(self.traverse_effect(usage, &field, ops, left, right, span)?); + effect_binders.push(effect_binder); + } + } + let mut constructed = global_expr(constructor.symbol, span); + for argument in arguments { + constructed = apply_expr(constructed, argument, span); + } + self.combine_traversal(constructed, effects, effect_binders, ops, span) + } + + fn combine_traversal( + &mut self, + constructed: hir::Expr, + effects: Vec, + effect_binders: Vec, + ops: &TraverseOps, + span: TextRange, + ) -> Option { + if effects.is_empty() { + return Some(apply_expr(global_expr(ops.pure?, span), constructed, span)); + } + let mut combine = constructed; + for binder in effect_binders.into_iter().rev() { + combine = lambda(binder, combine, span); + } + let mut effects = effects.into_iter(); + let first = effects.next()?; + let mut body = apply_expr( + apply_expr(global_expr(ops.map?, span), combine, span), + first, + span, + ); + for effect in effects { + body = apply_expr( + apply_expr(global_expr(ops.apply?, span), body, span), + effect, + span, + ); + } + Some(body) + } + + /// The effectful expression for one field occurrence. + fn traverse_effect( + &mut self, + usage: &FieldUsage, + field: &hir::Expr, + ops: &TraverseOps, + left: &hir::Expr, + right: &hir::Expr, + span: TextRange, + ) -> Option { + let function = match usage { + FieldUsage::Record(fields) => { + return self.traverse_record(fields, field, ops, left, right, span); + } + FieldUsage::Param => right.clone(), + FieldUsage::LParam => left.clone(), + FieldUsage::Mono(inner) => apply_expr( + global_expr(ops.traverse?, span), + self.traverse_function(inner, ops, left, right, span)?, + span, + ), + FieldUsage::Bi(l, r) => apply_expr( + apply_expr( + global_expr(ops.bitraverse?, span), + self.traverse_argument(l, ops, left, right, span)?, + span, + ), + self.traverse_argument(r, ops, left, right, span)?, + span, + ), + FieldUsage::Contra(inner) | FieldUsage::Pro(inner, _) => { + return self.traverse_effect(inner, field, ops, left, right, span); + } + FieldUsage::Inert => return None, + }; + Some(apply_expr(function, field.clone(), span)) + } + + /// A traversing function `x -> f y` for a nested occurrence. + fn traverse_function( + &mut self, + usage: &FieldUsage, + ops: &TraverseOps, + left: &hir::Expr, + right: &hir::Expr, + span: TextRange, + ) -> Option { + Some(match usage { + FieldUsage::Record(fields) => { + let binder = self.fresh_deriving_binder("__derived_record", span); + let body = self.traverse_record( + fields, + &local_expr(binder.id, span), + ops, + left, + right, + span, + )?; + lambda(binder, body, span) + } + FieldUsage::Param => right.clone(), + FieldUsage::LParam => left.clone(), + FieldUsage::Mono(inner) => apply_expr( + global_expr(ops.traverse?, span), + self.traverse_function(inner, ops, left, right, span)?, + span, + ), + FieldUsage::Bi(l, r) => apply_expr( + apply_expr( + global_expr(ops.bitraverse?, span), + self.traverse_argument(l, ops, left, right, span)?, + span, + ), + self.traverse_argument(r, ops, left, right, span)?, + span, + ), + FieldUsage::Inert => global_expr(ops.pure?, span), + FieldUsage::Contra(inner) | FieldUsage::Pro(inner, _) => { + self.traverse_function(inner, ops, left, right, span)? + } + }) + } + + fn traverse_record( + &mut self, + fields: &[(String, FieldUsage)], + record: &hir::Expr, + ops: &TraverseOps, + left: &hir::Expr, + right: &hir::Expr, + span: TextRange, + ) -> Option { + let mut updates = Vec::new(); + let mut effects = Vec::new(); + let mut binders = Vec::new(); + for (label, usage) in fields { + if *usage == FieldUsage::Inert { + continue; + } + let binder = self.fresh_deriving_binder("__derived_effect", span); + updates.push((label.clone(), local_expr(binder.id, span))); + effects.push(self.traverse_effect( + usage, + &field_expr(record.clone(), label, span), + ops, + left, + right, + span, + )?); + binders.push(binder); + } + self.combine_traversal( + record_update(record.clone(), updates, span), + effects, + binders, + ops, + span, + ) + } + + /// A traversing argument, using `pure` for an inert side. + fn traverse_argument( + &mut self, + usage: &FieldUsage, + ops: &TraverseOps, + left: &hir::Expr, + right: &hir::Expr, + span: TextRange, + ) -> Option { + if *usage == FieldUsage::Inert { + Some(global_expr(ops.pure?, span)) + } else { + self.traverse_function(usage, ops, left, right, span) + } + } +} diff --git a/crates/psrs-typecheck/src/typecheck/classes/deriving/types.rs b/crates/psrs-typecheck/src/typecheck/classes/deriving/types.rs index 13b431a2..3242288e 100644 --- a/crates/psrs-typecheck/src/typecheck/classes/deriving/types.rs +++ b/crates/psrs-typecheck/src/typecheck/classes/deriving/types.rs @@ -14,6 +14,9 @@ impl Checker { /// `TypeChecker.checkTypeClassInstance` with `InvalidInstanceHead`; see /// `failing/TypeWildcards3.purs`. A wildcard in an instance *context* is a /// different matter and stays legal, as `passing/WildcardInInstance.purs` needs. +/// The compiler's known `Newtype` and `Generic` derivations also accept one +/// final wildcard: the wrapped field or generated representation determines +/// that class argument. pub(crate) fn contains_wildcard(ty: &hir::Type) -> bool { match &ty.kind { hir::TypeKind::Wildcard => true, diff --git a/crates/psrs-typecheck/src/typecheck/classes/deriving/usage/mapping.rs b/crates/psrs-typecheck/src/typecheck/classes/deriving/usage/mapping.rs new file mode 100644 index 00000000..41021f81 --- /dev/null +++ b/crates/psrs-typecheck/src/typecheck/classes/deriving/usage/mapping.rs @@ -0,0 +1,104 @@ +use super::super::syntax::{ + apply_expr, field_expr, global_expr, lambda, local_expr, record_update, +}; +use super::*; + +#[derive(Clone, Copy)] +pub(in crate::typecheck::classes::deriving) struct MappingMethods { + pub mono: Option, + pub bi: Option, + pub contra: Option, + pub pro: Option, + pub pro_left: Option, +} + +impl Checker { + /// Builds exactly the mapping accepted by field-usage analysis. An inert + /// nested argument receives identity, including the unused side of bimap. + pub(in crate::typecheck::classes::deriving) fn usage_function( + &mut self, + usage: &FieldUsage, + methods: &MappingMethods, + left: &hir::Expr, + right: &hir::Expr, + span: TextRange, + ) -> Option { + Some(match usage { + FieldUsage::Inert => { + let binder = self.fresh_deriving_binder("__derived_identity", span); + lambda(binder.clone(), local_expr(binder.id, span), span) + } + FieldUsage::Param => right.clone(), + FieldUsage::LParam => left.clone(), + FieldUsage::Mono(inner) => apply_expr( + global_expr(methods.mono?, span), + self.usage_function(inner, methods, left, right, span)?, + span, + ), + FieldUsage::Contra(inner) => apply_expr( + global_expr(methods.contra?, span), + self.usage_function(inner, methods, left, right, span)?, + span, + ), + FieldUsage::Bi(l, r) => apply_expr( + apply_expr( + global_expr(methods.bi?, span), + self.usage_function(l, methods, left, right, span)?, + span, + ), + self.usage_function(r, methods, left, right, span)?, + span, + ), + FieldUsage::Pro(l, r) if **r == FieldUsage::Inert && methods.pro_left.is_some() => { + apply_expr( + global_expr(methods.pro_left?, span), + self.usage_function(l, methods, left, right, span)?, + span, + ) + } + FieldUsage::Pro(l, r) => apply_expr( + apply_expr( + global_expr(methods.pro?, span), + self.usage_function(l, methods, left, right, span)?, + span, + ), + self.usage_function(r, methods, left, right, span)?, + span, + ), + FieldUsage::Record(fields) => { + let binder = self.fresh_deriving_binder("__derived_record", span); + let value = local_expr(binder.id, span); + let mut updates = Vec::new(); + for (label, usage) in fields { + if *usage != FieldUsage::Inert { + let field = field_expr(value.clone(), label, span); + updates.push(( + label.clone(), + self.map_field_usage(usage, methods, left, right, &field, span)?, + )); + } + } + lambda(binder, record_update(value, updates, span), span) + } + }) + } + + pub(in crate::typecheck::classes::deriving) fn map_field_usage( + &mut self, + usage: &FieldUsage, + methods: &MappingMethods, + left: &hir::Expr, + right: &hir::Expr, + value: &hir::Expr, + span: TextRange, + ) -> Option { + match usage { + FieldUsage::Inert => Some(value.clone()), + _ => Some(apply_expr( + self.usage_function(usage, methods, left, right, span)?, + value.clone(), + span, + )), + } + } +} diff --git a/crates/psrs-typecheck/src/typecheck/classes/deriving/usage/mod.rs b/crates/psrs-typecheck/src/typecheck/classes/deriving/usage/mod.rs new file mode 100644 index 00000000..c323abc4 --- /dev/null +++ b/crates/psrs-typecheck/src/typecheck/classes/deriving/usage/mod.rs @@ -0,0 +1,388 @@ +//! Field-usage and variance analysis for structural deriving rules. +//! +//! A rule does not decide per field whether to map, recurse, or reject from the +//! field's shape alone. It first walks the field with the class's variance and +//! the visible instance environment: a field occurrence is acceptable only when +//! the mapping class it would use has a visible instance, and a parameter used +//! at the wrong polarity is rejected. The walk returns a [`FieldUsage`] tree so +//! the generator maps exactly the occurrences the analysis accepted. The +//! official compiler performs the same walk in `TypeChecker/Deriving.hs` +//! (`validateParamsInTypeConstructors` / `typeToUsageOf`). + +use super::types::{contains_parameter, flatten_type_application}; +use super::{KnownClass, flatten_spine}; +use crate::typecheck::*; + +mod mapping; +pub(super) use mapping::MappingMethods; + +/// The classes a `Functor`-shaped rule uses to map a field: a monomorphic +/// class, a bipartite class, and the contravariant/profunctor counterparts used +/// when a field occurrence flips polarity. +#[derive(Clone, Copy)] +pub(super) struct MappingClasses { + pub mono: KnownClass, + pub bi: KnownClass, + pub contra: Option, + pub pro: Option, +} + +impl MappingClasses { + /// The covariant classes shared by `Functor`, `Bifunctor`, `Contravariant`, + /// and `Profunctor` deriving. + pub(super) fn covariant() -> Self { + Self { + mono: KnownClass::Functor, + bi: KnownClass::Bifunctor, + contra: Some(KnownClass::Contravariant), + pro: Some(KnownClass::Profunctor), + } + } + + /// The fold classes (`Foldable`/`Bifoldable`), which have no contravariant + /// counterpart. + pub(super) fn foldable() -> Self { + Self { + mono: KnownClass::Foldable, + bi: KnownClass::Bifoldable, + contra: None, + pro: None, + } + } + + /// The traversal classes (`Traversable`/`Bitraversable`). + pub(super) fn traversable() -> Self { + Self { + mono: KnownClass::Traversable, + bi: KnownClass::Bitraversable, + contra: None, + pro: None, + } + } +} + +/// How one constructor field uses the parameters. The generator maps exactly +/// the occurrences this tree records. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(super) enum FieldUsage { + /// The field does not mention a mapped parameter. + Inert, + /// The field is the final (covariant) parameter itself. + Param, + /// The field is the left parameter itself (bipartite rules). + LParam, + /// The field maps through the monomorphic class, e.g. `Array a`. + Mono(Box), + /// The field maps through the bipartite class, e.g. `Either e a`. + Bi(Box, Box), + /// The field maps through the contravariant class. + Contra(Box), + /// The field maps through the profunctor class, e.g. `p a c`. + Pro(Box, Box), + /// Each record field is traversed independently in canonical label order. + Record(Vec<(String, FieldUsage)>), +} + +/// The head a field application maps through. `Arrow` is the function type, +/// which every mapping class reaches through its own instance. +#[derive(Clone, Copy)] +enum MappingHead<'a> { + Arrow, + Constructor(&'a hir::Type), +} + +struct UsageContext<'a> { + classes: MappingClasses, + fixed: HashMap, + lparam: Option<&'a str>, + param: &'a str, + lparam_contra: bool, + param_contra: bool, +} + +impl Checker { + /// Validates every constructor field of a structural derivation against the + /// class's variance and the visible instance environment, returning the + /// usage tree per constructor and field. The first unmappable occurrence's + /// span is the error; the caller reports it as + /// `CannotDeriveInvalidConstructorArg`. + pub(super) fn validate_field_usage( + &self, + declaration: &hir::TypeDeclaration, + classes: MappingClasses, + lparam: Option<&str>, + param: &str, + variance: (bool, bool), + prefix: &[InferType], + ) -> Result>, TextRange> { + let context = UsageContext { + classes, + fixed: declaration + .parameters + .iter() + .zip(prefix) + .map(|(parameter, argument)| (parameter.name.clone(), argument.clone())) + .collect(), + lparam, + param, + lparam_contra: variance.0, + param_contra: variance.1, + }; + let mut constructors = Vec::with_capacity(declaration.constructors.len()); + for constructor in &declaration.constructors { + let mut fields = Vec::with_capacity(constructor.fields.len()); + for field in &constructor.fields { + let field = self.normalize_deriving_type(field); + fields.push(self.check_field(&field, &context, false)?); + } + constructors.push(fields); + } + Ok(constructors) + } + + fn check_field( + &self, + ty: &hir::Type, + context: &UsageContext<'_>, + negative: bool, + ) -> Result { + if !mentions_parameter(ty, context.lparam, context.param) { + return Ok(FieldUsage::Inert); + } + match &ty.kind { + hir::TypeKind::Variable(name) => { + let contra = if name == context.param { + context.param_contra + } else if context.lparam == Some(name.as_str()) { + context.lparam_contra + } else { + return Ok(FieldUsage::Inert); + }; + if contra != negative { + return Err(ty.span); + } + if name == context.param { + Ok(FieldUsage::Param) + } else { + Ok(FieldUsage::LParam) + } + } + hir::TypeKind::Function { parameter, result } => { + self.try_bipartite(context, MappingHead::Arrow, parameter, result, negative) + } + hir::TypeKind::Record { fields, tail } => { + if tail + .as_deref() + .is_some_and(|tail| mentions_parameter(tail, context.lparam, context.param)) + { + return Err(ty.span); + } + let mut fields = fields + .iter() + .map(|field| { + Ok(( + field.label.clone(), + self.check_field(&field.ty, context, negative)?, + )) + }) + .collect::, TextRange>>()?; + fields.sort_by(|left, right| left.0.cmp(&right.0)); + Ok(FieldUsage::Record(fields)) + } + hir::TypeKind::Row { .. } => Err(ty.span), + hir::TypeKind::Forall { variables, body } => { + let shadows = |name: &str| variables.iter().any(|variable| variable.name == name); + let scoped = UsageContext { + classes: context.classes, + fixed: context + .fixed + .iter() + .filter(|(name, _)| !shadows(name)) + .map(|(name, ty)| (name.clone(), ty.clone())) + .collect(), + lparam: context.lparam.filter(|name| !shadows(name)), + param: if shadows(context.param) { + "" + } else { + context.param + }, + lparam_contra: context.lparam_contra, + param_contra: context.param_contra, + }; + self.check_field(body, &scoped, negative) + } + hir::TypeKind::Constrained { body, .. } => self.check_field(body, context, negative), + hir::TypeKind::Application(function, argument) => { + let (head, _) = flatten_type_application(ty); + if mentions_parameter(head, context.lparam, context.param) { + return Err(head.span); + } + match &function.kind { + hir::TypeKind::Application(inner, l_arg) => { + if mentions_parameter(inner, context.lparam, context.param) { + return Err(inner.span); + } + let head = flatten_type_application(inner).0; + self.try_bipartite( + context, + MappingHead::Constructor(head), + l_arg, + argument, + negative, + ) + } + _ => { + if mentions_parameter(function, context.lparam, context.param) { + return Err(function.span); + } + let head = flatten_type_application(function).0; + self.try_mono(context, MappingHead::Constructor(head), argument, negative) + } + } + } + _ => Ok(FieldUsage::Inert), + } + } + + /// A two-argument application: map through the bipartite class, or the + /// profunctor class with the left argument contravariant, or fall back to + /// the mono class on the final argument with the left one inert. + fn try_bipartite( + &self, + context: &UsageContext<'_>, + head: MappingHead<'_>, + l_arg: &hir::Type, + r_arg: &hir::Type, + negative: bool, + ) -> Result { + if self.has_mapping_instance(context.classes.bi, head, context) { + let left = self.check_field(l_arg, context, negative)?; + let right = self.check_field(r_arg, context, negative)?; + if left == FieldUsage::Inert + && self.has_mapping_instance(context.classes.mono, head, context) + { + return Ok(FieldUsage::Mono(Box::new(right))); + } + return Ok(FieldUsage::Bi(Box::new(left), Box::new(right))); + } + if let Some(pro) = context.classes.pro + && self.has_mapping_instance(pro, head, context) + { + let left = self.check_field(l_arg, context, !negative)?; + let right = self.check_field(r_arg, context, negative)?; + if left == FieldUsage::Inert + && self.has_mapping_instance(context.classes.mono, head, context) + { + return Ok(FieldUsage::Mono(Box::new(right))); + } + return Ok(FieldUsage::Pro(Box::new(left), Box::new(right))); + } + if mentions_parameter(l_arg, context.lparam, context.param) { + return Err(l_arg.span); + } + self.try_mono(context, head, r_arg, negative) + } + + /// A one-argument application. When no mapping instance is visible, the + /// occurrence is only inert if it does not mention a parameter. + fn try_mono( + &self, + context: &UsageContext<'_>, + head: MappingHead<'_>, + argument: &hir::Type, + negative: bool, + ) -> Result { + if self.has_mapping_instance(context.classes.mono, head, context) { + let inner = self.check_field(argument, context, negative)?; + return Ok(FieldUsage::Mono(Box::new(inner))); + } + if let Some(contra) = context.classes.contra + && self.has_mapping_instance(contra, head, context) + { + let inner = self.check_field(argument, context, !negative)?; + return Ok(FieldUsage::Contra(Box::new(inner))); + } + if mentions_parameter(argument, context.lparam, context.param) { + Err(argument.span) + } else { + Ok(FieldUsage::Inert) + } + } + + /// Whether the visible instance environment proves `known` at `head`. + fn has_mapping_instance( + &self, + known: KnownClass, + head: MappingHead<'_>, + context: &UsageContext<'_>, + ) -> bool { + let Some(class_id) = self.env.deriving.class_id(known) else { + return false; + }; + let wanted = match head { + MappingHead::Arrow => InferType::Constructor(TypeConstructor::Function), + MappingHead::Constructor(ty) => { + if let Some(constructor) = field_head_constructor(ty) { + InferType::Constructor(constructor) + } else if let hir::TypeKind::Variable(name) = &ty.kind { + let Some(actual) = context.fixed.get(name) else { + return false; + }; + flatten_spine(&self.resolve_type(actual.clone())).0.clone() + } else { + return false; + } + } + }; + let agrees = |argument: &InferType| { + let resolved = self.resolve_type(argument.clone()); + flatten_spine(&resolved).0 == &wanted + }; + if self.scope.givens.iter().any(|(constraint, _)| { + constraint.class_id == class_id + && constraint.arguments.len() == 1 + && agrees(&constraint.arguments[0]) + }) { + return true; + } + let visible = self.instance_candidate_modules(class_id, std::slice::from_ref(&wanted)); + self.env.instances.iter().any(|info| { + info.class_id == class_id + && visible.contains(&info.symbol.module) + && info.head_arguments.len() == 1 + && agrees(&info.head_arguments[0]) + }) + } +} + +fn mentions_parameter(ty: &hir::Type, lparam: Option<&str>, param: &str) -> bool { + contains_parameter(ty, param) || lparam.is_some_and(|lparam| contains_parameter(ty, lparam)) +} + +/// The constructor a field head names, or `None` when the head is a type +/// variable or a form that cannot carry an instance. +fn field_head_constructor(ty: &hir::Type) -> Option { + match &ty.kind { + hir::TypeKind::Constructor(builtin) => Some(builtin_constructor(*builtin)), + hir::TypeKind::Named(id) | hir::TypeKind::Opaque(id) => Some(TypeConstructor::User(*id)), + _ => None, + } +} + +fn builtin_constructor(builtin: hir::BuiltinType) -> TypeConstructor { + match builtin { + hir::BuiltinType::Int => TypeConstructor::Int, + hir::BuiltinType::Number => TypeConstructor::Number, + hir::BuiltinType::Boolean => TypeConstructor::Boolean, + hir::BuiltinType::String => TypeConstructor::String, + hir::BuiltinType::Char => TypeConstructor::Char, + hir::BuiltinType::Unit => TypeConstructor::Unit, + hir::BuiltinType::Array => TypeConstructor::Array, + hir::BuiltinType::Function => TypeConstructor::Function, + hir::BuiltinType::Record => TypeConstructor::Record, + hir::BuiltinType::Row => TypeConstructor::Row, + hir::BuiltinType::Type => TypeConstructor::Type, + hir::BuiltinType::Constraint => TypeConstructor::Constraint, + hir::BuiltinType::Symbol => TypeConstructor::Symbol, + } +} diff --git a/crates/psrs-typecheck/src/typecheck/classes/environment/compiler.rs b/crates/psrs-typecheck/src/typecheck/classes/environment/compiler.rs new file mode 100644 index 00000000..5e79a99f --- /dev/null +++ b/crates/psrs-typecheck/src/typecheck/classes/environment/compiler.rs @@ -0,0 +1,76 @@ +use super::*; + +impl Checker { + pub(super) fn validate_compiler_class( + &mut self, + declaration: &hir::TypeDeclaration, + ) -> Option { + let identity = declaration.compiler_class?; + let valid = match identity { + hir::CompilerClass::IsSymbol => self.valid_symbol_interface(declaration, identity), + }; + if valid { + Some(identity) + } else { + self.state.errors.push(TypeCheckError::new( + TypeCheckErrorKind::UnsupportedClass, + declaration.span, + "compiler class declaration does not match its dictionary contract", + )); + None + } + } + + fn valid_symbol_interface( + &mut self, + declaration: &hir::TypeDeclaration, + identity: hir::CompilerClass, + ) -> bool { + if declaration.parameters.len() != 1 + || !declaration.superclasses.is_empty() + || !declaration.fundeps.is_empty() + || declaration.members.len() != 1 + || declaration.members[0].name != identity.method() + { + return false; + } + let Some(kind) = self.env.checked_kinds.kind_scheme(declaration.id).cloned() else { + return false; + }; + let Kind::Function(parameter_kind, _) = self.instantiate_kind_scheme(&kind) else { + return false; + }; + if *parameter_kind != Kind::Builtin(hir::BuiltinType::Symbol) { + return false; + } + let Some(signature) = declaration.members[0].signature.as_ref() else { + return false; + }; + let argument = self.fresh(); + let InferType::Variable(id) = argument else { + unreachable!() + }; + self.record_variable_kind(id, *parameter_kind); + self.with_skolem_scope(&[id], |checker| { + let mut variables = + HashMap::from([(declaration.parameters[0].name.clone(), argument.clone())]); + let ty = checker.elaborate_type_mode(signature, &mut variables, true); + let ty = checker.resolve_type(ty); + // Validate the elaborated contract, so synonyms and imported aliases + // receive exactly the same treatment as the written arrow. + let InferType::Application(function, result) = ty else { + return false; + }; + let InferType::Application(head, parameter) = *function else { + return false; + }; + let InferType::Application(proxy, parameter_argument) = *parameter else { + return false; + }; + *head == InferType::Constructor(TypeConstructor::Function) + && *result == InferType::Constructor(TypeConstructor::String) + && matches!(*proxy, InferType::Constructor(TypeConstructor::User(_))) + && *parameter_argument == argument + }) + } +} diff --git a/crates/psrs-typecheck/src/typecheck/classes/environment/mod.rs b/crates/psrs-typecheck/src/typecheck/classes/environment/mod.rs index 2fc2e075..c6179110 100644 --- a/crates/psrs-typecheck/src/typecheck/classes/environment/mod.rs +++ b/crates/psrs-typecheck/src/typecheck/classes/environment/mod.rs @@ -1,6 +1,8 @@ use super::super::signature::flatten_spine; use super::super::*; -use super::deriving::contains_wildcard; +use super::deriving::{KnownClass, contains_wildcard}; +use super::fundeps::collect_infer_variables; +mod compiler; mod method; use method::validate_method_signature; @@ -28,6 +30,7 @@ impl Checker { self.env.classes.insert( hir::TypeId::COERCIBLE, ClassInfo { + compiler_class: None, parameters: vec!["source".to_owned(), "target".to_owned()], superclasses: Vec::new(), fundeps: Vec::new(), @@ -82,9 +85,11 @@ impl Checker { .insert(member.symbol, (declaration.id, method.clone())); methods.push(method); } + let compiler_class = self.validate_compiler_class(declaration); self.env.classes.insert( declaration.id, ClassInfo { + compiler_class, parameters, superclasses: Vec::new(), fundeps, @@ -242,7 +247,20 @@ impl Checker { return; } let (_, arguments) = flatten_spine(&instance.head); - if local && arguments.iter().any(|argument| contains_wildcard(argument)) { + let head_shape_wildcard = local + && instance.derivation == Some(hir::DerivationStrategy::KnownClass) + && self + .env + .deriving + .known_class(instance.class_id) + .is_some_and(|known| known.is_head_shape()) + && arguments.len() == 2 + && !contains_wildcard(arguments[0]) + && matches!(arguments[1].kind, hir::TypeKind::Wildcard); + let has_unsupported_wildcard = arguments.iter().enumerate().any(|(index, argument)| { + contains_wildcard(argument) && !(head_shape_wildcard && index == 1) + }); + if local && has_unsupported_wildcard { self.state.errors.push(TypeCheckError::new( TypeCheckErrorKind::InvalidInstanceHead, instance.head.span, @@ -253,7 +271,7 @@ impl Checker { if arguments.len() != class.parameters.len() { if local { self.state.errors.push(TypeCheckError::new( - TypeCheckErrorKind::UnsupportedClass, + TypeCheckErrorKind::ClassInstanceArityMismatch, instance.span, "an instance head must apply its class to one type argument per parameter", )); @@ -261,11 +279,29 @@ impl Checker { return; } let mut variables = HashMap::new(); - let head_arguments = arguments + let mut head_arguments = arguments .iter() .map(|argument| self.elaborate_type(argument, &mut variables)) .collect::>(); - let head_variable_types = variables.clone(); + // A `Newtype`/`Generic` derivation's trailing wildcard is resolved to + // the wrapped type or representation before the instance is recorded, + // so the searchable head is concrete. The rule is chosen from the + // registry, not from a class name. + if head_shape_wildcard { + let resolved = match self.env.deriving.known_class(instance.class_id) { + Some(KnownClass::Newtype) => { + self.newtype_underlying_type(&head_arguments[..1], instance.span, true) + } + Some(KnownClass::Generic) => { + self.generic_representation(&head_arguments[0], instance.span) + } + _ => None, + }; + let Some(resolved) = resolved else { + return; + }; + head_arguments[1] = resolved; + } let mut context = Vec::with_capacity(instance.context.len()); let mut context_parameters = Vec::with_capacity(instance.context.len()); let mut valid = true; @@ -286,13 +322,21 @@ impl Checker { if !valid { return; } + let instance_variables = variables.clone(); if local { let head_names = head_variables(&arguments); + let determined = + instance_context_determined_variables(&self.env.classes, &context, &head_arguments); for constraint in &instance.context { let mut used = Vec::new(); collect_variables(constraint, &mut used); for name in used { - if !head_names.contains(&name) { + let determined_by_fundep = variables.get(&name).is_some_and(|ty| { + let mut variables = HashSet::new(); + collect_infer_variables(ty, &mut variables); + !variables.is_empty() && variables.is_subset(&determined) + }); + if !head_names.contains(&name) && !determined_by_fundep { self.state.errors.push(TypeCheckError::new( TypeCheckErrorKind::UnsupportedClass, constraint.span, @@ -312,13 +356,62 @@ impl Checker { chain_id: instance.chain_id, chain_position: instance.chain_position, head_arguments, - head_variables: head_variable_types, + instance_variables, context, context_parameters, }); } } +/// The head arguments determine their own variables, and class functional +/// dependencies may determine additional variables in instance contexts. Take +/// the closure across context constraints so chained dependencies work too. +fn instance_context_determined_variables( + classes: &HashMap, + context: &[ClassConstraint], + head_arguments: &[InferType], +) -> HashSet { + let mut determined = HashSet::new(); + for argument in head_arguments { + collect_infer_variables(argument, &mut determined); + } + loop { + let mut changed = false; + for constraint in context { + let Some(class) = classes.get(&constraint.class_id) else { + continue; + }; + for fundep in &class.fundeps { + let determining = fundep + .determining + .iter() + .filter_map(|&index| constraint.arguments.get(index)) + .flat_map(|argument| { + let mut variables = HashSet::new(); + collect_infer_variables(argument, &mut variables); + variables + }) + .collect::>(); + if !determining.is_subset(&determined) { + continue; + } + for &index in &fundep.determined { + if let Some(argument) = constraint.arguments.get(index) { + let mut variables = HashSet::new(); + collect_infer_variables(argument, &mut variables); + for variable in variables { + changed |= determined.insert(variable); + } + } + } + } + } + if !changed { + return determined; + } + } +} + /// Depth-first search for a superclass cycle through `classes`. fn superclass_cycle( id: hir::TypeId, diff --git a/crates/psrs-typecheck/src/typecheck/classes/evidence/finalize.rs b/crates/psrs-typecheck/src/typecheck/classes/evidence/finalize.rs index 07f5c3ef..8603a324 100644 --- a/crates/psrs-typecheck/src/typecheck/classes/evidence/finalize.rs +++ b/crates/psrs-typecheck/src/typecheck/classes/evidence/finalize.rs @@ -128,6 +128,9 @@ impl Checker { generics: &HashSet, ) -> Option { Some(match solution { + WantedSolution::DictionaryValue(value) => thir::EvidenceKind::DictionaryValue( + Box::new(self.finalize_expr(*value, interner, generics)?), + ), WantedSolution::Given(id) | WantedSolution::Abstracted(id) => { thir::EvidenceKind::Given(id) } diff --git a/crates/psrs-typecheck/src/typecheck/classes/evidence/mod.rs b/crates/psrs-typecheck/src/typecheck/classes/evidence/mod.rs index 9f9102be..aefb57ae 100644 --- a/crates/psrs-typecheck/src/typecheck/classes/evidence/mod.rs +++ b/crates/psrs-typecheck/src/typecheck/classes/evidence/mod.rs @@ -51,22 +51,29 @@ impl Checker { ) { (InferType::Variable(a), InferType::Variable(b)) => a == b, (InferType::Constructor(a), InferType::Constructor(b)) => a == b, + (InferType::TypeLevelString(a), InferType::TypeLevelString(b)) => a == b, + (InferType::TypeLevelInt(a), InferType::TypeLevelInt(b)) => a == b, (InferType::Application(f1, a1), InferType::Application(f2, a2)) => { self.infer_types_equal(&f1, &f2) && self.infer_types_equal(&a1, &a2) } - (InferType::RowEmpty, InferType::RowEmpty) => true, - ( - InferType::RowExtend { - label: l1, - ty: t1, - tail: r1, - }, - InferType::RowExtend { - label: l2, - ty: t2, - tail: r2, - }, - ) => l1 == l2 && self.infer_types_equal(&t1, &t2) && self.infer_types_equal(&r1, &r2), + (left, right) if self.is_row(&left) && self.is_row(&right) => { + let span = TextRange::new(0, 0); + let (Ok(mut left), Ok(mut right)) = ( + self.normalize_row(left, span), + self.normalize_row(right, span), + ) else { + return false; + }; + left.fields.sort_by(|a, b| a.0.cmp(&b.0)); + right.fields.sort_by(|a, b| a.0.cmp(&b.0)); + left.tail == right.tail + && left.fields.len() == right.fields.len() + && left + .fields + .iter() + .zip(&right.fields) + .all(|((ll, lt), (rl, rt))| ll == rl && self.infer_types_equal(lt, rt)) + } _ => false, } } @@ -95,7 +102,10 @@ impl Checker { } } -pub(super) fn record_field_type(record: &InferType, wanted: &str) -> Option { +pub(in crate::typecheck) fn record_field_type( + record: &InferType, + wanted: &str, +) -> Option { let mut row = super::super::record_row(record)?; loop { match row { diff --git a/crates/psrs-typecheck/src/typecheck/classes/evidence/typing.rs b/crates/psrs-typecheck/src/typecheck/classes/evidence/typing.rs index a32b285e..da7ce507 100644 --- a/crates/psrs-typecheck/src/typecheck/classes/evidence/typing.rs +++ b/crates/psrs-typecheck/src/typecheck/classes/evidence/typing.rs @@ -49,7 +49,7 @@ impl Checker { } /// The dictionary record type for a class constraint: one field per - /// superclass (holding that superclass's dictionary) followed by one field + /// superclass (holding a Unit thunk for that superclass's dictionary) followed by one field /// per method, with the class parameters substituted. pub(in crate::typecheck) fn dictionary_type( &mut self, @@ -71,7 +71,10 @@ impl Checker { self.superclass_constraints(constraint.class_id, &constraint.arguments) { let field_ty = self.dictionary_type(&super_constraint); - fields.push((field, field_ty)); + fields.push(( + field, + arrow(InferType::Constructor(TypeConstructor::Unit), field_ty), + )); } for method in &class.methods { let mut method_variables = variables.clone(); diff --git a/crates/psrs-typecheck/src/typecheck/classes/fundeps.rs b/crates/psrs-typecheck/src/typecheck/classes/fundeps/mod.rs similarity index 85% rename from crates/psrs-typecheck/src/typecheck/classes/fundeps.rs rename to crates/psrs-typecheck/src/typecheck/classes/fundeps/mod.rs index 1b302772..5c04c4e3 100644 --- a/crates/psrs-typecheck/src/typecheck/classes/fundeps.rs +++ b/crates/psrs-typecheck/src/typecheck/classes/fundeps/mod.rs @@ -1,5 +1,9 @@ use super::super::unify::substitute; use super::super::*; +use std::collections::HashMap; +mod support; + +use support::solution_uses_lexical_given; /// Functional-dependency improvement and ambiguity checking for wanted /// constraints. Improvement assigns only the positions a dependency determines @@ -47,13 +51,18 @@ impl Checker { if class.fundeps.is_empty() { return false; } + let givens = constraint + .givens + .iter() + .flat_map(|(given, _)| self.superclass_closure(given)) + .collect::>(); let mut changed = false; for fundep in &class.fundeps { if !self.determiners_are_known(&constraint.arguments, &fundep.determining) { continue; } let mut sources: Vec> = Vec::new(); - for (given, _) in constraint.givens.clone() { + for given in &givens { if given.class_id == constraint.class_id && fundep.determining.iter().all(|&index| { self.infer_types_equal( @@ -138,6 +147,18 @@ impl Checker { if self.infer_types_equal(&wanted, &source) { return false; } + // Row order is semantic, not the written extension spine. A known + // source can refine only the wanted row through the shared normalizer; + // unresolved source variables remain conservative as below. + if self.is_row(&wanted) && self.is_row(&source) && !self.has_flexible_variable(&source) { + if !self.has_flexible_variable(&wanted) { + self.report_fundep_conflict(&wanted, &source, span); + return false; + } + let substitutions_before = self.state.substitutions.len(); + self.unify_rows(wanted, source, span); + return self.state.substitutions.len() != substitutions_before; + } match (&wanted, &source) { (InferType::Variable(variable), _) => { if self.state.rigid.contains(variable) { @@ -230,6 +251,17 @@ impl Checker { if start == self.state.wanted.len() { return; } + let solutions = self + .state + .wanted + .iter() + .filter_map(|constraint| { + constraint + .solution + .clone() + .map(|solution| (constraint.id, solution)) + }) + .collect::>(); let mut determined = HashSet::new(); collect_infer_variables(&self.resolve_type(result.clone()), &mut determined); self.fundep_determined(&self.state.wanted[start..], &mut determined); @@ -237,19 +269,23 @@ impl Checker { if constraint.solution.is_none() { continue; } - // A wanted discharged directly from a lexical dictionary is - // already determined by that dictionary's scope. This commonly - // occurs inside a rank-N method body, where the skolem appears in - // the wanted but intentionally does not escape through the - // method's result type. Do not apply this exemption to an - // instance dictionary merely because one of its context - // constraints comes from a given: the instance head itself may - // still contain an ambiguous variable. - if constraint - .solution - .as_ref() - .is_some_and(solution_uses_lexical_given) - { + // An abstracted dictionary belongs to a nested binding that already + // measured the constraint against its own result. This declaration's + // residuals are still unsolved here; `check_residual_ambiguity` + // measures them after solving returns. + if matches!(constraint.solution, Some(WantedSolution::Abstracted(_))) { + continue; + } + // A wanted discharged using a lexical dictionary, directly or + // through the context of a selected instance, is determined by + // that dictionary's scope. This commonly occurs inside a rank-N + // method body, where the skolem appears in the wanted but + // intentionally does not escape through the enclosing instance's + // result type. A context-free instance does not provide this + // evidence: its head may still contain an ambiguous variable. + if constraint.solution.as_ref().is_some_and(|solution| { + solution_uses_lexical_given(solution, &solutions, &mut HashSet::new()) + }) { continue; } let mut variables = HashSet::new(); @@ -258,6 +294,15 @@ impl Checker { } let ambiguous = variables .difference(&determined) + // A rigid variable belongs to an enclosing forall or given + // scope. Its obligation is checked at that use site and does + // not become an unconstrained variable of the declaration + // currently discharging the worklist. + .filter(|variable| !self.state.rigid.contains(variable)) + // A solved local obligation may still refer to its binding's + // quantified variables. They are parameters of that checked + // local scheme, not unknowns of the enclosing dictionary. + .filter(|variable| !self.state.generic_variables.contains(variable)) .copied() .collect::>(); if ambiguous.is_empty() { @@ -394,23 +439,6 @@ impl Checker { } } -fn solution_uses_lexical_given(solution: &WantedSolution) -> bool { - match solution { - WantedSolution::Given(_) => true, - WantedSolution::Superclass { parent, .. } => parent - .solution - .as_ref() - .is_some_and(solution_uses_lexical_given), - // An abstracted dictionary is a parameter of the declaration itself, so - // it determines nothing the result type and the dependencies do not. - WantedSolution::Global(_) - | WantedSolution::Instance { .. } - | WantedSolution::Abstracted(_) - | WantedSolution::Coercible { .. } - | WantedSolution::Primitive { .. } => false, - } -} - /// The inference variables used anywhere in a type. pub(in crate::typecheck) fn collect_infer_variables(ty: &InferType, out: &mut HashSet) { match ty { diff --git a/crates/psrs-typecheck/src/typecheck/classes/fundeps/support.rs b/crates/psrs-typecheck/src/typecheck/classes/fundeps/support.rs new file mode 100644 index 00000000..bc5524f4 --- /dev/null +++ b/crates/psrs-typecheck/src/typecheck/classes/fundeps/support.rs @@ -0,0 +1,69 @@ +use super::*; + +pub(super) fn solution_uses_lexical_given( + solution: &WantedSolution, + solutions: &HashMap, + visited: &mut HashSet, +) -> bool { + match solution { + WantedSolution::Given(_) => true, + WantedSolution::Superclass { parent, .. } => parent + .solution + .as_ref() + .is_some_and(|solution| solution_uses_lexical_given(solution, solutions, visited)), + WantedSolution::Instance { context, .. } => context.iter().any(|id| { + if !visited.insert(*id) { + return false; + } + let uses_given = solutions + .get(id) + .is_some_and(|solution| solution_uses_lexical_given(solution, solutions, visited)); + visited.remove(id); + uses_given + }), + // An abstracted dictionary is a parameter of the declaration itself, so + // it determines nothing the result type and the dependencies do not. + WantedSolution::DictionaryValue(_) + | WantedSolution::Global(_) + | WantedSolution::Abstracted(_) + | WantedSolution::Coercible { .. } + | WantedSolution::Primitive { .. } => false, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use psrs_hir::{LocalId, ModuleId, SymbolId}; + + #[test] + fn instance_context_using_a_given_determines_its_rank_n_wanted() { + let selected = WantedSolution::Instance { + constructor: SymbolId::new(ModuleId(0), 1), + constructor_type: InferType::Variable(0), + context: vec![7], + }; + let solutions = HashMap::from([(7, WantedSolution::Given(LocalId(2)))]); + + assert!(solution_uses_lexical_given( + &selected, + &solutions, + &mut HashSet::new() + )); + } + + #[test] + fn context_free_instance_does_not_determine_its_wanted_variables() { + let selected = WantedSolution::Instance { + constructor: SymbolId::new(ModuleId(0), 1), + constructor_type: InferType::Variable(0), + context: Vec::new(), + }; + + assert!(!solution_uses_lexical_given( + &selected, + &HashMap::new(), + &mut HashSet::new() + )); + } +} diff --git a/crates/psrs-typecheck/src/typecheck/classes/instance.rs b/crates/psrs-typecheck/src/typecheck/classes/instance.rs index ef7a0781..38abe7b8 100644 --- a/crates/psrs-typecheck/src/typecheck/classes/instance.rs +++ b/crates/psrs-typecheck/src/typecheck/classes/instance.rs @@ -21,7 +21,7 @@ impl Checker { instance: &hir::InstanceDeclaration, ) -> Option { let class = self.env.classes.get(&instance.class_id).cloned()?; - let (head_arguments, head_variables, context, context_parameters) = { + let (head_arguments, instance_variables, context, context_parameters) = { let info = self .env .instances @@ -29,7 +29,7 @@ impl Checker { .find(|info| info.symbol == instance.symbol)?; ( info.head_arguments.clone(), - info.head_variables.clone(), + info.instance_variables.clone(), info.context.clone(), info.context_parameters.clone(), ) @@ -53,12 +53,19 @@ impl Checker { } let derived_newtype_underlying = match instance.derivation { Some(hir::DerivationStrategy::KnownClass) => { - self.validate_known_deriving_class(instance.class_id, &class, instance.span)?; + self.validate_known_deriving_class( + instance.class_id, + &class, + &instance.head, + &head_arguments, + instance.span, + )?; None } Some(hir::DerivationStrategy::Newtype) => { if class.parameters.len() != head_arguments.len() { return self.deriving_error( + TypeCheckErrorKind::InvalidNewtypeInstance, instance.span, "derive newtype class head has the wrong arity", ); @@ -94,11 +101,30 @@ impl Checker { { let super_dictionary = self.dictionary_type(&super_constraint); let wanted = self.push_wanted(super_constraint, super_dictionary.clone()); + // Superclass construction must be delayed: method closures may + // reference a subclass instance whose superclass is this instance. + let parameter = InferType::Constructor(TypeConstructor::Unit); + let id = LocalId(self.state.next_dictionary_local); + self.state.next_dictionary_local += 1; fields.push(( field, InferredExpr { - kind: InferredExprKind::Evidence(wanted), - ty: super_dictionary, + kind: InferredExprKind::Lambda { + binder: InferredBinder { + binder: LocalBinder { + id, + name: "superclass_unit".into(), + span: instance.span, + }, + scheme: Scheme::monomorphic(parameter.clone()), + }, + body: Box::new(InferredExpr { + kind: InferredExprKind::Evidence(wanted), + ty: super_dictionary.clone(), + span: instance.span, + }), + }, + ty: arrow(parameter, super_dictionary), span: instance.span, }, )); @@ -116,9 +142,9 @@ impl Checker { Some(member) => { let mut method_variables = variables.clone(); let expected = self.elaborate_type(&method.signature, &mut method_variables); - let head_variables = head_variables.clone(); + let annotation_variables = instance_variables.clone(); let value = self.with_scope(|checker| { - checker.scope.annotation_variables = head_variables; + checker.scope.annotation_variables = annotation_variables; checker.in_nested_level(|checker| { checker.infer_expr_with_expected(&member.value, Some(expected)) }) @@ -200,8 +226,19 @@ impl Checker { ); // An instance head may contain type variables (for example a // `ToInt (Array a)` head); generalize the dictionary constructor over - // them so the declaration is polymorphic in the head variables. - let scheme = self.generalize(&[], &value.ty, &[], TOP_LEVEL); + // them so the declaration is polymorphic in the head variables. Keep + // variables that only occur in erased evidence too: a `Coercible` + // superclass proof still needs them while its type is finalized. + let mut head_variables = HashSet::new(); + for variable in instance_variables.values() { + super::fundeps::collect_infer_variables( + &self.resolve_type(variable.clone()), + &mut head_variables, + ); + } + let head_variables = head_variables.into_iter().collect::>(); + let scheme = self.generalize_instance_dictionary(&head_variables, &value.ty); + let scheme = self.generalize_body(scheme, &value, TOP_LEVEL); Some(InferredDeclaration { symbol: instance.symbol, name: instance.name.clone(), diff --git a/crates/psrs-typecheck/src/typecheck/classes/locals.rs b/crates/psrs-typecheck/src/typecheck/classes/locals.rs index 2ff25dcd..45680d39 100644 --- a/crates/psrs-typecheck/src/typecheck/classes/locals.rs +++ b/crates/psrs-typecheck/src/typecheck/classes/locals.rs @@ -32,7 +32,7 @@ fn scan_expr(expression: &hir::Expr, max: &mut Option) { scan_expr(element, max); } } - hir::ExprKind::Record(fields) => { + hir::ExprKind::Record(fields) | hir::ExprKind::MatchProduct(fields) => { for (_, value) in fields { scan_expr(value, max); } diff --git a/crates/psrs-typecheck/src/typecheck/classes/mod.rs b/crates/psrs-typecheck/src/typecheck/classes/mod.rs index 3e731f2f..e7fe51bd 100644 --- a/crates/psrs-typecheck/src/typecheck/classes/mod.rs +++ b/crates/psrs-typecheck/src/typecheck/classes/mod.rs @@ -12,6 +12,7 @@ mod coherence; mod deriving; mod environment; mod evidence; +pub(in crate::typecheck) use evidence::record_field_type; mod fundeps; mod instance; mod locals; @@ -19,6 +20,7 @@ mod matching; mod solve; mod superclass; +pub(in crate::typecheck) use deriving::DerivingRegistry; pub(in crate::typecheck) use fundeps::collect_infer_variables; pub(in crate::typecheck) use locals::next_local_id; pub(in crate::typecheck) use solve::{SolveDepth, UnsolvedPolicy}; diff --git a/crates/psrs-typecheck/src/typecheck/classes/solve/entry.rs b/crates/psrs-typecheck/src/typecheck/classes/solve/entry.rs index 948b7910..43da20a0 100644 --- a/crates/psrs-typecheck/src/typecheck/classes/solve/entry.rs +++ b/crates/psrs-typecheck/src/typecheck/classes/solve/entry.rs @@ -121,9 +121,7 @@ impl Checker { .iter() .any(|error| error.kind.reports_constraint_failure()); if constraint.solution.is_none() && !reported_resolution_error { - if unsolved == UnsolvedPolicy::Retain - && self.can_generalize_constraint(&constraint) - { + if self.policy_keeps_unsolved(unsolved, &constraint) { retained.push(index); } else { let rendered = @@ -151,6 +149,21 @@ impl Checker { retained } + /// Whether `unsolved` keeps this constraint instead of reporting it. + /// + /// `Retain` keeps one only while it can still be quantified by the + /// enclosing declaration. + pub(in crate::typecheck) fn policy_keeps_unsolved( + &self, + unsolved: UnsolvedPolicy, + constraint: &WantedConstraint, + ) -> bool { + match unsolved { + UnsolvedPolicy::Retain => self.can_generalize_constraint(constraint), + UnsolvedPolicy::RequireSolved => false, + } + } + /// A constraint that still mentions something generalization /// could quantify. A nullary class constraint is generalized on its own, and /// any argument that is still a flexible inference variable makes the diff --git a/crates/psrs-typecheck/src/typecheck/classes/solve/search.rs b/crates/psrs-typecheck/src/typecheck/classes/solve/search.rs index ab0f4049..fff0dadc 100644 --- a/crates/psrs-typecheck/src/typecheck/classes/solve/search.rs +++ b/crates/psrs-typecheck/src/typecheck/classes/solve/search.rs @@ -2,10 +2,7 @@ //! rule's entry into it. use super::super::super::prim::requeue::RequeueChain; -use super::super::super::prim::{ - PrimitiveDispatch, is_report_only, primitive_rule_precedes_givens, - primitive_rule_skips_given_lookup, -}; +use super::super::super::prim::{PrimitiveDispatch, is_report_only}; use super::super::super::unify::substitute; use super::super::fundeps::collect_infer_variables; use super::entry::{SolveDepth, UnsolvedPolicy}; @@ -14,14 +11,10 @@ impl Checker { /// Solves one wanted constraint, consulting givens, the primitive rule /// table, and instance search in that order. /// - /// The order is the one the primitive design fixes, and the one place it - /// varies is where a `Proof` member's rule is consulted: - /// [`primitive_rule_precedes_givens`] is the predicate, and it is false for - /// every other member. A `Proof` member's evidence is a checked boundary - /// rather than a dictionary, so a matching given cannot supply it — THIR - /// rejects a coercion whose evidence is not an explicit proof boundary — and - /// the rule has to derive the proof before the givens are consulted, which is - /// also what official solving does. + /// The registered rule's evidence classification owns its position. Proof + /// boundaries and runtime relation dictionaries precede givens; report rules + /// may propagate through a lexical dictionary first. A declined rule leaves + /// the ordinary given, superclass, and instance paths available. /// /// An instance's context is solved recursively before the instance is /// selected. @@ -47,10 +40,21 @@ impl Checker { if !depth.within_bound() { return None; } + // Instance contexts are created after the outer wanted-list fixed point. + // Improve every recursive obligation before exact given lookup or head + // selection, using the same dependencies as the outer worklist. + let mut improved = constraint.clone(); + self.improve_one(&mut improved); + let constraint = &improved; let class_id = constraint.class_id; let arguments = constraint.arguments.clone(); - let solves_before_givens = primitive_rule_precedes_givens(class_id); - let skips_given_lookup = primitive_rule_skips_given_lookup(class_id); + let rule = self.registered_primitive_rule(class_id); + let solves_before_givens = rule + .as_ref() + .is_some_and(|rule| rule.evidence.precedes_givens()); + let skips_given_lookup = rule + .as_ref() + .is_some_and(|rule| !rule.evidence.accepts_a_given()); // Relation rules precede ordinary dictionary lookup, as in // Entailment.hs:204-223. A checked proof also precedes givens, but its @@ -73,10 +77,7 @@ impl Checker { if let Some(solution) = self.given_solution(constraint, depth) { return Some(solution); } - if policy == UnsolvedPolicy::Retain - && is_report_only(class_id) - && self.can_generalize_constraint(constraint) - { + if is_report_only(class_id) && self.policy_keeps_unsolved(policy, constraint) { return None; } } @@ -141,11 +142,7 @@ impl Checker { ) -> Option { for (given, solution) in self.scope.givens.clone() { if given.class_id == constraint.class_id - && self.constraint_arguments_match_or_unify( - &given.arguments, - &constraint.arguments, - constraint.span, - ) + && self.constraint_arguments_match(&given.arguments, &constraint.arguments) { return Some(solution); } @@ -196,11 +193,7 @@ impl Checker { field, }; if edge.class_id == wanted.class_id - && self.constraint_arguments_match_or_unify( - &edge.arguments, - &wanted.arguments, - wanted.span, - ) + && self.constraint_arguments_match(&edge.arguments, &wanted.arguments) { return Some(solution); } @@ -217,28 +210,15 @@ impl Checker { None } - /// Matches a wanted constraint against a given or projected superclass. - /// Wanted type variables can be refined to the known argument types, but a - /// failed candidate must leave no substitution, level, kind, or evidence - /// behind. Its diagnostic is discarded, because a candidate that does not - /// match is not itself an error. - fn constraint_arguments_match_or_unify( - &mut self, - expected: &[InferType], - actual: &[InferType], - span: TextRange, - ) -> bool { - if expected.len() != actual.len() { - return false; - } - self.speculate(|checker| { - let errors_before = checker.state.errors.len(); - for (expected, actual) in expected.iter().zip(actual) { - checker.unify(actual.clone(), expected.clone(), span); - } - (checker.state.errors.len() == errors_before).then_some(()) - }) - .is_some() + /// A given proves only its existing argument types. Entailment must not + /// choose an unknown wanted argument by unifying it with a dictionary in + /// scope; functional-dependency improvement owns permitted refinement. + fn constraint_arguments_match(&self, expected: &[InferType], actual: &[InferType]) -> bool { + expected.len() == actual.len() + && expected + .iter() + .zip(actual) + .all(|(expected, actual)| self.infer_types_equal(expected, actual)) } /// Solves one instance's context and, on success, returns the instance @@ -270,10 +250,7 @@ impl Checker { let has_nested_diagnostic = self.state.errors[errors_before..] .iter() .any(|error| error.kind.reports_constraint_failure()); - if !has_nested_diagnostic - && policy == UnsolvedPolicy::Retain - && self.can_generalize_constraint(&wanted) - { + if !has_nested_diagnostic && self.policy_keeps_unsolved(policy, &wanted) { // The selected instance's dictionary needs this context // dictionary; the declaration can supply it as a generalized // parameter. The stable id lets evidence elaboration read the diff --git a/crates/psrs-typecheck/src/typecheck/classes/superclass.rs b/crates/psrs-typecheck/src/typecheck/classes/superclass.rs index f2553aca..a4f2f6db 100644 --- a/crates/psrs-typecheck/src/typecheck/classes/superclass.rs +++ b/crates/psrs-typecheck/src/typecheck/classes/superclass.rs @@ -161,6 +161,26 @@ pub(in crate::typecheck) fn instantiate_template( } impl Checker { + /// The constraint and all instantiated superclass constraints reachable + /// from it. The checked class environment rejects superclass cycles, and + /// each path retains its argument types even when paths share a class. + pub(in crate::typecheck) fn superclass_closure( + &self, + constraint: &ClassConstraint, + ) -> Vec { + let mut pending = vec![constraint.clone()]; + let mut closure = Vec::new(); + while let Some(constraint) = pending.pop() { + pending.extend( + self.superclass_constraints(constraint.class_id, &constraint.arguments) + .into_iter() + .map(|(_, superclass)| superclass), + ); + closure.push(constraint); + } + closure + } + /// The superclass constraints a dictionary for `class_id arguments` stores, /// each with the dictionary field that holds it, in edge order. Each edge is /// instantiated over the subclass's arguments through the shared diff --git a/crates/psrs-typecheck/src/typecheck/entry.rs b/crates/psrs-typecheck/src/typecheck/entry.rs index 06643a04..9512f46e 100644 --- a/crates/psrs-typecheck/src/typecheck/entry.rs +++ b/crates/psrs-typecheck/src/typecheck/entry.rs @@ -88,6 +88,7 @@ pub fn typecheck_module_with_checked_kinds( effect_runtime_representation, TypecheckContext { known_types, + known_values: &[], imported_instances, module_names: &module_names, checked_kinds, @@ -160,14 +161,13 @@ pub fn typecheck_module_with_checked_kinds_and_module_names_and_warnings( return Err(checker.state.errors); } let inferred = inferred.into_iter().flatten().collect::>(); - let mut types = TypeInterner::default(); let mut generics = checker.state.generic_variables.clone(); let external_types = module .externals .iter() .filter_map(|external| { - if !matches!(external.kind, hir::ExternalKind::Wit { .. }) { + if !external.kind.requires_checked_signature() { return None; } let signature = external.signature.as_ref()?; @@ -176,14 +176,18 @@ pub fn typecheck_module_with_checked_kinds_and_module_names_and_warnings( checker.state.errors.push(TypeCheckError::new( TypeCheckErrorKind::UnsupportedType, signature.span, - "class-constrained WIT imports are not supported by the WASI binding ABI", + if matches!(external.kind, hir::ExternalKind::Wit { .. }) { + "class-constrained WIT imports are not supported by the WASI binding ABI" + } else { + "class constraints are not allowed in foreign import signatures" + }, )); return None; } let ty = checker.finalize_type(&inferred, signature.span, &mut types, &generics)?; Some(thir::ExternalType { symbol: external.symbol, - source_module: module.id, + source_module: external.source_module.unwrap_or(module.id), ty, }) }) diff --git a/crates/psrs-typecheck/src/typecheck/error.rs b/crates/psrs-typecheck/src/typecheck/error.rs index 673780fc..ca6acbfa 100644 --- a/crates/psrs-typecheck/src/typecheck/error.rs +++ b/crates/psrs-typecheck/src/typecheck/error.rs @@ -40,6 +40,26 @@ pub enum TypeCheckErrorKind { InvalidInstanceHead, /// An inferred public value mentions a local type omitted from exports. TransitiveExport, + /// A class has no compiler-supported deriving rule. Official + /// `TypeChecker/Deriving.hs` raises `CannotDerive` in the same position. + CannotDerive, + /// A deriving head is not the local type constructor the rule expects. + ExpectedTypeConstructor, + /// A structural deriving head has the wrong number of type arguments. + InvalidDerivedInstance, + ClassInstanceArityMismatch, + /// A `derive newtype` head is not a locally declared newtype. + InvalidNewtypeInstance, + /// `derive newtype` was applied to a plain data type. + CannotDeriveNewtypeForData, + /// A constructor field cannot be mapped under the deriving class's + /// variance, the official `CannotDeriveInvalidConstructorArg`. + CannotDeriveInvalidConstructorArg, + /// The type a derivation refers to cannot be found. + CannotFindDerivingType, + /// A `Newtype` or `Generic` derivation is missing its trailing type + /// wildcard, the official `ExpectedWildcard`. + ExpectedWildcard, /// `e @T` where `e`'s type has no quantifier left to apply `T` to. /// `purs` raises this in `TypeChecker.Types.infer'` for /// `VisibleTypeApp`, where the operand's type is reported against the @@ -156,6 +176,17 @@ impl TypeCheckErrorKind { } TypeCheckErrorKind::InvalidInstanceHead => "InvalidInstanceHead", TypeCheckErrorKind::TransitiveExport => "TransitiveExportError", + TypeCheckErrorKind::CannotDerive => "CannotDerive", + TypeCheckErrorKind::ExpectedTypeConstructor => "ExpectedTypeConstructor", + TypeCheckErrorKind::InvalidDerivedInstance => "InvalidDerivedInstance", + TypeCheckErrorKind::ClassInstanceArityMismatch => "ClassInstanceArityMismatch", + TypeCheckErrorKind::InvalidNewtypeInstance => "InvalidNewtypeInstance", + TypeCheckErrorKind::CannotDeriveNewtypeForData => "CannotDeriveNewtypeForData", + TypeCheckErrorKind::CannotDeriveInvalidConstructorArg => { + "CannotDeriveInvalidConstructorArg" + } + TypeCheckErrorKind::CannotFindDerivingType => "CannotFindDerivingType", + TypeCheckErrorKind::ExpectedWildcard => "ExpectedWildcard", TypeCheckErrorKind::CannotApplyExpressionOfTypeOnType => { "CannotApplyExpressionOfTypeOnType" } diff --git a/crates/psrs-typecheck/src/typecheck/finalize.rs b/crates/psrs-typecheck/src/typecheck/finalize.rs index 24eb5064..5ac1c3d8 100644 --- a/crates/psrs-typecheck/src/typecheck/finalize.rs +++ b/crates/psrs-typecheck/src/typecheck/finalize.rs @@ -6,6 +6,17 @@ impl Checker { expression: InferredExpr, interner: &mut TypeInterner, generics: &HashSet, + ) -> Option { + psrs_span::with_sufficient_stack(|| { + self.finalize_expr_inner(expression, interner, generics) + }) + } + + fn finalize_expr_inner( + &mut self, + expression: InferredExpr, + interner: &mut TypeInterner, + generics: &HashSet, ) -> Option { let mut active_generics = generics.clone(); let mut scope = self.resolve_type(expression.ty.clone()); @@ -112,6 +123,43 @@ impl Checker { body: Box::new(body), } } + InferredExprKind::UnsafeCoerceFunction { + source, + target, + origin, + } => { + let source_type = + self.finalize_type(&source, expression.span, interner, generics)?; + let target_type = + self.finalize_type(&target, expression.span, interner, generics)?; + let local = LocalId(self.state.next_dictionary_local); + self.state.next_dictionary_local += 1; + let binder = thir::Binder { + id: local, + name: "__unsafe_coerce_value".to_owned(), + ty: source_type, + span: expression.span, + }; + let value = thir::Expr { + kind: thir::ExprKind::Local(local), + ty: source_type, + span: expression.span, + }; + let body = thir::Expr { + kind: thir::ExprKind::UnsafeCoerce { + value: Box::new(value), + source_type, + target_type, + origin, + }, + ty: target_type, + span: expression.span, + }; + thir::ExprKind::Lambda { + binder, + body: Box::new(body), + } + } InferredExprKind::Evidence(wanted) => { let evidence = self.wanted_evidence(wanted, interner, generics)?; thir::ExprKind::Evidence(evidence) diff --git a/crates/psrs-typecheck/src/typecheck/generalize/body.rs b/crates/psrs-typecheck/src/typecheck/generalize/body.rs new file mode 100644 index 00000000..a6af5e8a --- /dev/null +++ b/crates/psrs-typecheck/src/typecheck/generalize/body.rs @@ -0,0 +1,256 @@ +//! Quantifiers needed by checked implementation types, including phantom +//! instantiations that do not occur in a declaration's public result type. + +use super::*; + +#[derive(Default)] +struct BodyVariables { + candidates: Vec, + bound: HashSet, +} + +impl Checker { + /// Extends a finished binding scheme with flexible variables occurring only + /// in its implementation. Call after constraint solving and ambiguity checks; + /// this supplies lexical binders, never evidence for an unsolved constraint. + pub(in crate::typecheck) fn generalize_body( + &mut self, + scheme: Scheme, + value: &InferredExpr, + outer_level: u32, + ) -> Scheme { + let mut body_variables = BodyVariables::default(); + self.collect_body_variables(value, outer_level, &mut body_variables); + body_variables.candidates.retain(|variable| { + !body_variables.bound.contains(variable) + && !self.state.rigid.contains(variable) + && !self.state.generic_variables.contains(variable) + }); + let mut variables = scheme.variables; + variables.extend(body_variables.candidates); + self.scheme(variables, scheme.constraints, scheme.ty) + } + + fn collect_implementation_type(&self, ty: &InferType, level: u32, out: &mut BodyVariables) { + let ty = self.resolve_type(ty.clone()); + self.collect_generalizable(&ty, level, &mut out.candidates); + collect_structural_binders(&ty, &mut out.bound); + } + + fn collect_body_variables(&self, value: &InferredExpr, level: u32, out: &mut BodyVariables) { + let mut collect_type = |ty: &InferType| { + self.collect_implementation_type(ty, level, out); + }; + collect_type(&value.ty); + match &value.kind { + InferredExprKind::CoerceFunction { + wanted, + source, + target, + } => { + collect_type(source); + collect_type(target); + self.collect_wanted_variables(*wanted, level, out, &mut HashSet::new()); + } + InferredExprKind::UnsafeCoerceFunction { source, target, .. } => { + collect_type(source); + collect_type(target); + } + InferredExprKind::Lambda { binder, body } => { + collect_type(&binder.scheme.ty); + self.collect_body_variables(body, level, out); + } + InferredExprKind::Array(elements) => { + for element in elements { + self.collect_body_variables(element, level, out); + } + } + InferredExprKind::Record(fields) => { + for (_, field) in fields { + self.collect_body_variables(field, level, out); + } + } + InferredExprKind::RecordUpdate { expression, fields } => { + self.collect_body_variables(expression, level, out); + for (_, field) in fields { + self.collect_body_variables(field, level, out); + } + } + InferredExprKind::DictionaryApplication { function, wanted } => { + self.collect_body_variables(function, level, out); + self.collect_wanted_variables(*wanted, level, out, &mut HashSet::new()); + } + InferredExprKind::FieldAccess { expression, .. } => { + self.collect_body_variables(expression, level, out); + } + InferredExprKind::Application(function, argument) => { + self.collect_body_variables(function, level, out); + self.collect_body_variables(argument, level, out); + } + InferredExprKind::Let { bindings, body } => { + for binding in bindings { + self.collect_implementation_type(&binding.binder.scheme.ty, level, out); + self.collect_body_variables(&binding.value, level, out); + } + self.collect_body_variables(body, level, out); + } + InferredExprKind::If { + condition, + then_branch, + else_branch, + } => { + for child in [condition, then_branch, else_branch] { + self.collect_body_variables(child, level, out); + } + } + InferredExprKind::Case { + scrutinee, + branches, + } => { + self.collect_body_variables(scrutinee, level, out); + for branch in branches { + self.collect_pattern_variables(&branch.pattern, level, out); + self.collect_body_variables(&branch.value, level, out); + } + } + InferredExprKind::Local(_) + | InferredExprKind::Global(_) + | InferredExprKind::Integer(_) + | InferredExprKind::Number(_) + | InferredExprKind::Boolean(_) + | InferredExprKind::String(_) + | InferredExprKind::Char(_) => {} + InferredExprKind::Method { wanted, .. } | InferredExprKind::Evidence(wanted) => { + self.collect_wanted_variables(*wanted, level, out, &mut HashSet::new()); + } + } + } + + fn collect_wanted_variables( + &self, + index: usize, + level: u32, + out: &mut BodyVariables, + visited: &mut HashSet, + ) { + if let Some(wanted) = self.state.wanted.get(index) { + self.collect_evidence_variables(wanted, level, out, visited); + } + } + + fn collect_evidence_variables( + &self, + wanted: &WantedConstraint, + level: u32, + out: &mut BodyVariables, + visited: &mut HashSet, + ) { + let Some(solution) = &wanted.solution else { + return; + }; + if !visited.insert(wanted.id) { + return; + } + for ty in std::iter::once(&wanted.dictionary_type).chain(&wanted.arguments) { + self.collect_implementation_type(ty, level, out); + } + match solution { + WantedSolution::DictionaryValue(value) => { + self.collect_body_variables(value, level, out) + } + WantedSolution::Instance { + constructor_type, + context, + .. + } => { + self.collect_implementation_type(constructor_type, level, out); + for id in context { + if let Some(child) = self.state.wanted.iter().find(|wanted| wanted.id == *id) { + self.collect_evidence_variables(child, level, out, visited); + } + } + } + WantedSolution::Superclass { parent, .. } => { + self.collect_evidence_variables(parent, level, out, visited); + } + WantedSolution::Coercible { source, target } => { + for ty in [source, target] { + self.collect_implementation_type(ty, level, out); + } + } + WantedSolution::Primitive { arguments } => { + for ty in arguments { + self.collect_implementation_type(ty, level, out); + } + } + WantedSolution::Given(_) + | WantedSolution::Abstracted(_) + | WantedSolution::Global(_) => {} + } + } + + fn collect_pattern_variables( + &self, + pattern: &InferredPattern, + level: u32, + out: &mut BodyVariables, + ) { + self.collect_implementation_type(&pattern.ty, level, out); + match &pattern.kind { + InferredPatternKind::Array { elements } + | InferredPatternKind::Constructor { + arguments: elements, + .. + } => { + for element in elements { + self.collect_pattern_variables(element, level, out); + } + } + InferredPatternKind::Named { pattern, .. } => { + self.collect_pattern_variables(pattern, level, out); + } + InferredPatternKind::Record { fields } => { + for (_, field) in fields { + self.collect_pattern_variables(field, level, out); + } + } + InferredPatternKind::Var { ty, .. } => { + self.collect_implementation_type(ty, level, out); + } + InferredPatternKind::Wildcard | InferredPatternKind::Literal { .. } => {} + } + } +} + +// Binder identity belongs to the checked type, even after the scope that +// skolemized it has ended. Never recapture such a binder from a child node. +fn collect_structural_binders(ty: &InferType, out: &mut HashSet) { + match ty { + InferType::ForAll { variables, body } => { + out.extend(variables); + collect_structural_binders(body, out); + } + InferType::Application(function, argument) => { + collect_structural_binders(function, out); + collect_structural_binders(argument, out); + } + InferType::Constrained { constraints, body } => { + for argument in constraints + .iter() + .flat_map(|constraint| &constraint.arguments) + { + collect_structural_binders(argument, out); + } + collect_structural_binders(body, out); + } + InferType::RowExtend { ty, tail, .. } => { + collect_structural_binders(ty, out); + collect_structural_binders(tail, out); + } + InferType::Variable(_) + | InferType::Constructor(_) + | InferType::RowEmpty + | InferType::TypeLevelInt(_) + | InferType::TypeLevelString(_) => {} + } +} diff --git a/crates/psrs-typecheck/src/typecheck/generalize.rs b/crates/psrs-typecheck/src/typecheck/generalize/mod.rs similarity index 88% rename from crates/psrs-typecheck/src/typecheck/generalize.rs rename to crates/psrs-typecheck/src/typecheck/generalize/mod.rs index 4d509f95..359798b4 100644 --- a/crates/psrs-typecheck/src/typecheck/generalize.rs +++ b/crates/psrs-typecheck/src/typecheck/generalize/mod.rs @@ -9,6 +9,8 @@ use super::*; +mod body; + impl Checker { /// Generalizes a declaration into a scheme. /// @@ -17,7 +19,7 @@ impl Checker { /// them, because they are the polymorphism the source declared rather than a /// side effect of the level a binder was allocated at. A binder that no /// surviving part of the type or the constraints mentions is not quantified: - /// a quantifier no occurrence refers to has no meaning. + /// body-only occurrences are handled by `generalize_body` after solving. /// /// `outer_level` is the level the declaration's scope sits at. A variable /// at that level or below belongs to the enclosing scope and is left alone, @@ -47,6 +49,24 @@ impl Checker { self.scheme(variables, constraints.to_vec(), resolved) } + /// Generalizes an instance dictionary over every variable in its head, + /// including variables erased from the runtime dictionary shape. Compiler + /// evidence such as `Coercible (Additive a) a` still mentions those + /// variables while the dictionary constructor is checked and finalized. + pub(in crate::typecheck) fn generalize_instance_dictionary( + &mut self, + head_variables: &[u32], + ty: &InferType, + ) -> Scheme { + // Instance head variables belong to the dictionary constructor even if + // the runtime dictionary erases them. Its compile-time evidence still + // mentions them, so retain them alongside variables in the value type. + let inferred = self.generalize(&[], ty, &[], TOP_LEVEL); + let mut variables = inferred.variables; + variables.extend(head_variables.iter().copied()); + self.scheme(variables, inferred.constraints, inferred.ty) + } + /// The scheme a declaration exposes to the uses inside its binding group: the /// type its signature states, with that signature's `forall` binders /// quantified. A recursive use instantiates it, so a signature's diff --git a/crates/psrs-typecheck/src/typecheck/group.rs b/crates/psrs-typecheck/src/typecheck/group.rs index 47a4b1a2..bf64ac5b 100644 --- a/crates/psrs-typecheck/src/typecheck/group.rs +++ b/crates/psrs-typecheck/src/typecheck/group.rs @@ -282,6 +282,7 @@ impl Checker { }; let scheme = self.generalize(&member.quantified, &monomorphic, &retained, TOP_LEVEL); let value = self.wrap_dictionary_lambdas(value, &member.parameters); + let scheme = self.generalize_body(scheme, &value, TOP_LEVEL); self.state .pending_signatures .insert(member.symbol, member.parameters.clone()); diff --git a/crates/psrs-typecheck/src/typecheck/infer/case.rs b/crates/psrs-typecheck/src/typecheck/infer/case.rs index 4cd2d84e..8c7d76e2 100644 --- a/crates/psrs-typecheck/src/typecheck/infer/case.rs +++ b/crates/psrs-typecheck/src/typecheck/infer/case.rs @@ -20,10 +20,34 @@ impl Checker { let requires_monotype = branches .iter() .any(|branch| pattern_requires_monotype(&branch.pattern)); - let scrutinee = self.infer_pattern_scrutinee(scrutinee, requires_monotype)?; + let scrutinee = if let hir::ExprKind::MatchProduct(fields) = &scrutinee.kind { + let (kind, ty) = + self.infer_record_fields(fields, scrutinee.span, |checker, label, value| { + let requires_monotype = branches.iter().any(|branch| { + product_field_pattern(&branch.pattern, label) + .is_none_or(pattern_requires_monotype) + }); + checker.infer_pattern_scrutinee(value, requires_monotype) + })?; + InferredExpr { + kind, + ty, + span: scrutinee.span, + } + } else { + self.infer_pattern_scrutinee(scrutinee, requires_monotype)? + }; let mut result_ty = expected; let mut inferred = Vec::with_capacity(branches.len()); for branch in branches { + if branch.coverage == hir::CaseBranchCoverage::PartialFallback { + self.state.errors.push(TypeCheckError::new( + TypeCheckErrorKind::InvalidHir, + branch.span, + "partial fallback authorization must be issued by type checking", + )); + return None; + } let mut inserted = Vec::new(); let pattern = self.check_pattern(&branch.pattern, &scrutinee.ty, &mut inserted)?; let value = self.infer_expr_with_expected(&branch.value, result_ty.clone())?; @@ -42,6 +66,37 @@ impl Checker { }); } let ty = result_ty.unwrap_or_else(|| self.fresh()); + // Report-only Partial has no runtime dictionary, but its checked + // authorization must survive erasure. Reuse the generated empty-case + // failure representation used by guard desugaring; the marked fallback + // contributes coverage and never hides redundant source alternatives. + if !branches.is_empty() + && self.scope.givens.iter().any(|(constraint, _)| { + constraint.class_id == hir::TypeId::PRIM_PARTIAL && constraint.arguments.is_empty() + }) + { + inferred.push(InferredCaseBranch { + pattern: InferredPattern { + kind: InferredPatternKind::Wildcard, + ty: scrutinee.ty.clone(), + span, + }, + value: InferredExpr { + kind: InferredExprKind::Case { + scrutinee: Box::new(InferredExpr { + kind: InferredExprKind::Integer(0), + ty: InferType::Constructor(TypeConstructor::Int), + span, + }), + branches: Vec::new(), + }, + ty: ty.clone(), + span, + }, + span, + coverage: hir::CaseBranchCoverage::PartialFallback, + }); + } Some(InferredExpr { kind: InferredExprKind::Case { scrutinee: Box::new(scrutinee), @@ -55,7 +110,7 @@ impl Checker { /// A local pattern-matching scrutinee retains structural `forall` types so /// a typed binder can check a rank-N pattern against the value's declared /// type. Ordinary local references still instantiate those quantifiers. - fn infer_pattern_scrutinee( + pub(super) fn infer_pattern_scrutinee( &mut self, expression: &hir::Expr, requires_monotype: bool, @@ -125,6 +180,17 @@ impl Checker { } } +fn product_field_pattern<'a>(pattern: &'a hir::Pattern, label: &str) -> Option<&'a hir::Pattern> { + match &pattern.kind { + hir::PatternKind::Record { fields, .. } => fields + .iter() + .find(|(field, _)| field == label) + .map(|(_, pattern)| pattern), + hir::PatternKind::Named { pattern, .. } => product_field_pattern(pattern, label), + _ => None, + } +} + fn pattern_requires_monotype(pattern: &hir::Pattern) -> bool { match &pattern.kind { hir::PatternKind::Wildcard | hir::PatternKind::Var(_) => false, diff --git a/crates/psrs-typecheck/src/typecheck/infer/construct.rs b/crates/psrs-typecheck/src/typecheck/infer/construct.rs index 05f2c305..bac575b0 100644 --- a/crates/psrs-typecheck/src/typecheck/infer/construct.rs +++ b/crates/psrs-typecheck/src/typecheck/infer/construct.rs @@ -8,6 +8,7 @@ impl Checker { ) -> Self { let TypecheckContext { known_types, + known_values, imported_instances, module_names, checked_kinds, @@ -44,9 +45,7 @@ impl Checker { .map(|declaration| (declaration.id, module.name.clone())) .collect::>(); for declaration in known_types { - if declaration.kind == hir::TypeDeclarationKind::Class - && let Some(name) = module_names.get(&declaration.id.module) - { + if let Some(name) = module_names.get(&declaration.id.module) { type_modules.insert(declaration.id, name.clone()); } } @@ -139,6 +138,7 @@ impl Checker { classes: HashMap::new(), class_methods: HashMap::new(), instances: Vec::new(), + deriving: classes::DerivingRegistry::default(), }, state: InferState { substitutions: HashMap::new(), @@ -167,6 +167,11 @@ impl Checker { // declarations, which also supports forward and mutually referring // class/data declarations. checker.build_class_environment(module, known_types); + // The deriving registry reads the class environment, the resolved type + // identities, and the program's value identities, so it is built once + // here and read afterwards. + checker.env.deriving = + classes::DerivingRegistry::build(&checker.env, known_values, module_names); checker.register_constructors(); checker.state.next_dictionary_local = classes::next_local_id(module); checker.build_instance_environment(module, imported_instances); diff --git a/crates/psrs-typecheck/src/typecheck/infer/expected.rs b/crates/psrs-typecheck/src/typecheck/infer/expected.rs index 9a7bec13..cc0f3c11 100644 --- a/crates/psrs-typecheck/src/typecheck/infer/expected.rs +++ b/crates/psrs-typecheck/src/typecheck/infer/expected.rs @@ -8,6 +8,16 @@ impl Checker { &mut self, expression: &hir::Expr, expected: Option, + ) -> Option { + psrs_span::with_sufficient_stack(|| { + self.infer_expr_with_expected_inner(expression, expected) + }) + } + + fn infer_expr_with_expected_inner( + &mut self, + expression: &hir::Expr, + expected: Option, ) -> Option { let Some(expected) = expected else { return self.infer_expr(expression); @@ -235,8 +245,26 @@ impl Checker { ); } - let mut inferred = self.infer_expr(expression)?; - self.subsume(inferred.ty.clone(), expected.clone(), expression.span); + let inferred = self.infer_expr(expression)?; + // A bare class-method use keeps its method-local quantifiers and + // constraints on the inferred type. Instantiating them here matches the + // application path, so `eq = eq1` provides the `Eq a` dictionary the + // method signature requires instead of unifying the constraint with a + // plain function type. + let scheme = Scheme::monomorphic(inferred.ty.clone()); + let mut inferred = self.instantiate_expression_use(inferred, &scheme, expression.span); + if matches!( + inferred.kind, + InferredExprKind::CoerceFunction { .. } | InferredExprKind::UnsafeCoerceFunction { .. } + ) { + // A cast's source and target are exact boundary types, rather than + // a function implementation adapted by contravariant subsumption. + // In particular, keep a contextual rank-N input quantified in the + // source metadata that finalization emits as the cast's binder. + self.unify(expected.clone(), inferred.ty.clone(), expression.span); + } else { + self.subsume(inferred.ty.clone(), expected.clone(), expression.span); + } inferred.ty = expected; Some(inferred) } diff --git a/crates/psrs-typecheck/src/typecheck/infer/intrinsics.rs b/crates/psrs-typecheck/src/typecheck/infer/intrinsics.rs index 0fae1aea..909ef832 100644 --- a/crates/psrs-typecheck/src/typecheck/infer/intrinsics.rs +++ b/crates/psrs-typecheck/src/typecheck/infer/intrinsics.rs @@ -23,12 +23,30 @@ impl Checker { ) } + /// `Unsafe.Coerce.unsafeCoerce`: the same value shape as `coerce` with no + /// `Coercible` wanted. Its result type is fixed by the context. + pub(super) fn unsafe_coercion_function( + &mut self, + _span: TextRange, + ) -> (InferredExprKind, InferType) { + let source = self.fresh(); + let target = self.fresh(); + ( + InferredExprKind::UnsafeCoerceFunction { + source: source.clone(), + target: target.clone(), + origin: psrs_thir::UncheckedCoercionOrigin::UnsafeCoerce, + }, + arrow(source, target), + ) + } + /// The type of an intrinsic, instantiated from its descriptor's scheme. /// - /// `Coerce` is handled by the caller before this point, because it needs a - /// `Coercible` wanted and a distinct expression node. `Prim.undefined`'s - /// scheme `forall a. a` needs no special case: instantiating it yields the - /// fresh variable the use decides. + /// `Coerce` and `UnsafeCoerce` are handled by the caller before this point, + /// because they need a distinct expression node (and, for `Coerce`, a + /// `Coercible` wanted). `Prim.undefined`'s scheme `forall a. a` needs no + /// special case: instantiating it yields the fresh variable the use decides. pub(super) fn intrinsic_type(&mut self, intrinsic: Intrinsic) -> InferType { let descriptor = intrinsic.descriptor(); self.elaborate_imported_signature(&(descriptor.scheme)()) diff --git a/crates/psrs-typecheck/src/typecheck/infer/let_expr.rs b/crates/psrs-typecheck/src/typecheck/infer/let_expr.rs new file mode 100644 index 00000000..c5dbc81c --- /dev/null +++ b/crates/psrs-typecheck/src/typecheck/infer/let_expr.rs @@ -0,0 +1,91 @@ +//! Local `let` and `where` bindings share the enclosing declaration's unknowns. +//! +//! Unannotated locals are monomorphic, including their class obligations. +//! Solving and generalization belong to the enclosing declaration. An explicit +//! polymorphic annotation retains its checked quantifiers and is instantiated +//! independently at each local use. + +use super::super::*; + +impl Checker { + pub(in crate::typecheck) fn infer_let_expression( + &mut self, + bindings: &[hir::LocalBinding], + body: &hir::Expr, + expected: Option, + ) -> Option<(InferredExprKind, InferType)> { + let previous_locals = self.scope.locals.clone(); + let result = self.with_scope(|checker| { + let mut binders = Vec::with_capacity(bindings.len()); + for binding in bindings { + // An annotated local is predeclared at its written type so a + // recursive use instantiates the annotation instead of a fresh + // metavariable that a first recursive use would bind to an + // arrow. + let scheme = checker.predeclared_local_scheme(binding); + checker + .scope + .locals + .insert(binding.binder.id, scheme.clone()); + binders.push(InferredBinder { + binder: binding.binder.clone(), + scheme, + }); + } + let mut inferred = Vec::with_capacity(bindings.len()); + for (binding, mut binder) in bindings.iter().zip(binders) { + let value = checker.infer_expr(&binding.value)?; + checker.unify(binder.scheme.ty, value.ty.clone(), binding.span); + binder.scheme = Scheme::monomorphic(value.ty.clone()); + checker + .scope + .locals + .insert(binding.binder.id, binder.scheme.clone()); + inferred.push(InferredBinding { + binder, + value, + span: binding.span, + }); + } + let body = checker.infer_expr_with_expected(body, expected)?; + let ty = body.ty.clone(); + Some(( + InferredExprKind::Let { + bindings: inferred, + body: Box::new(body), + }, + ty, + )) + }); + self.scope.locals = previous_locals; + result + } + + /// The scheme a local binder is predeclared with. An explicit annotation is + /// elaborated here; an elaboration that reports a diagnostic is discarded so + /// the ascription reports it once. An unannotated local is monomorphic. + fn predeclared_local_scheme(&mut self, binding: &hir::LocalBinding) -> Scheme { + let Some(ty) = local_annotation(binding) else { + return Scheme::monomorphic(self.fresh()); + }; + let mut variables = self.scope.annotation_variables.clone(); + let annotation = self.speculate(|checker| { + let errors_before = checker.state.errors.len(); + let annotation = checker.elaborate_type(ty, &mut variables); + (checker.state.errors.len() == errors_before).then_some(annotation) + }); + match annotation { + Some(annotation) => Scheme::monomorphic(annotation), + None => Scheme::monomorphic(self.fresh()), + } + } +} + +/// The written type of an annotated local binding, which the resolver stores as +/// a `Typed` wrapper on the binding value. +fn local_annotation(binding: &hir::LocalBinding) -> Option<&hir::Type> { + match &binding.value.kind { + hir::ExprKind::Typed { ty, .. } => Some(ty), + _ => None, + } +} diff --git a/crates/psrs-typecheck/src/typecheck/infer/mod.rs b/crates/psrs-typecheck/src/typecheck/infer/mod.rs index 0b50ca88..65ce7e8e 100644 --- a/crates/psrs-typecheck/src/typecheck/infer/mod.rs +++ b/crates/psrs-typecheck/src/typecheck/infer/mod.rs @@ -5,6 +5,7 @@ mod case; mod construct; mod expected; mod intrinsics; +mod let_expr; mod pattern; mod records; mod visible_type_application; @@ -81,6 +82,10 @@ impl Checker { } pub(super) fn infer_expr(&mut self, expression: &hir::Expr) -> Option { + psrs_span::with_sufficient_stack(|| self.infer_expr_inner(expression)) + } + + fn infer_expr_inner(&mut self, expression: &hir::Expr) -> Option { let span = expression.span; let (kind, ty) = match &expression.kind { hir::ExprKind::Local(id) => match self.scope.locals.get(id).cloned() { @@ -139,17 +144,25 @@ impl Checker { Some(ExternalKind::Intrinsic(Intrinsic::Coerce)) => { self.coercion_function(span) } + Some(ExternalKind::Intrinsic(Intrinsic::UnsafeCoerce)) => { + self.unsafe_coercion_function(span) + } Some(ExternalKind::Intrinsic(intrinsic)) => ( InferredExprKind::Global(*symbol), self.intrinsic_type(intrinsic), ), - Some(ExternalKind::Wit { .. }) => { + Some( + ExternalKind::Wit { .. } + | ExternalKind::Library { .. } + | ExternalKind::Runtime { .. } + | ExternalKind::Primitive(_), + ) => { let Some(signature) = self.env.external_signatures.get(symbol).cloned() else { self.state.errors.push(TypeCheckError::new( TypeCheckErrorKind::InvalidHir, span, - "WIT import has no declared type", + "foreign import has no declared type", )); return None; }; @@ -167,12 +180,12 @@ impl Checker { } } } - hir::ExprKind::Integer(text) => match text.parse::() { - Ok(value) => ( + hir::ExprKind::Integer(text) => match parse_int_literal(text) { + Some(value) => ( InferredExprKind::Integer(value), InferType::Constructor(TypeConstructor::Int), ), - Err(_) => { + None => { self.state.errors.push(TypeCheckError::new( TypeCheckErrorKind::IntegerOutOfRange, span, @@ -235,6 +248,11 @@ impl Checker { } } hir::ExprKind::Record(fields) => self.infer_record(fields, span)?, + hir::ExprKind::MatchProduct(fields) => { + self.infer_record_fields(fields, span, |checker, _, value| { + checker.infer_pattern_scrutinee(value, false) + })? + } hir::ExprKind::RecordUpdate { expression, fields } => { self.infer_record_update(expression, fields, span)? } @@ -345,60 +363,24 @@ impl Checker { }; Some(InferredExpr { kind, ty, span }) } +} - pub(super) fn infer_let_expression( - &mut self, - bindings: &[hir::LocalBinding], - body: &hir::Expr, - expected: Option, - ) -> Option<(InferredExprKind, InferType)> { - // The binding bodies are inferred one level deeper, so their unknowns are - // generalized against this level; the body is checked back at it. - let outer_level = self.state.level; - let (inferred_bindings, body) = self.in_nested_level(|checker| { - let mut binders = Vec::with_capacity(bindings.len()); - for binding in bindings { - let ty = checker.fresh(); - checker - .scope - .locals - .insert(binding.binder.id, Scheme::monomorphic(ty.clone())); - binders.push(InferredBinder { - binder: binding.binder.clone(), - scheme: Scheme::monomorphic(ty), - }); - } - let mut inferred_bindings = Vec::with_capacity(bindings.len()); - for (binding, binder) in bindings.iter().zip(binders) { - if let Some(value) = checker.infer_expr(&binding.value) { - checker.unify(binder.scheme.ty.clone(), value.ty.clone(), binding.span); - inferred_bindings.push(InferredBinding { - binder, - value, - span: binding.span, - }); - } - } - for (binding, inferred) in bindings.iter().zip(inferred_bindings.iter_mut()) { - let scheme = checker.generalize(&[], &inferred.binder.scheme.ty, &[], outer_level); - inferred.binder.scheme = scheme.clone(); - checker.scope.locals.insert(binding.binder.id, scheme); - } - checker.state.level = outer_level; - let body = checker.infer_expr_with_expected(body, expected); - for binding in bindings { - checker.scope.locals.remove(&binding.binder.id); - } - (inferred_bindings, body) - }); - let body = body?; - let ty = body.ty.clone(); - Some(( - InferredExprKind::Let { - bindings: inferred_bindings, - body: Box::new(body), - }, - ty, - )) - } +/// Decimal and hexadecimal integer literals, within signed 32-bit `Int`. +pub(super) fn parse_int_literal(text: &str) -> Option { + let (sign, digits) = if let Some(digits) = text.strip_prefix('-') { + (-1i64, digits) + } else if let Some(digits) = text.strip_prefix('+') { + (1, digits) + } else { + (1, text) + }; + let magnitude = if let Some(hexadecimal) = digits + .strip_prefix("0x") + .or_else(|| digits.strip_prefix("0X")) + { + i64::from_str_radix(hexadecimal, 16).ok()? + } else { + digits.parse::().ok()? + }; + i32::try_from(magnitude.checked_mul(sign)?).ok() } diff --git a/crates/psrs-typecheck/src/typecheck/infer/pattern.rs b/crates/psrs-typecheck/src/typecheck/infer/pattern.rs index dafb4c2b..60e34d3a 100644 --- a/crates/psrs-typecheck/src/typecheck/infer/pattern.rs +++ b/crates/psrs-typecheck/src/typecheck/infer/pattern.rs @@ -18,7 +18,7 @@ impl Checker { } } hir::PatternKind::Integer(text) => { - let Ok(value) = text.parse::() else { + let Some(value) = super::parse_int_literal(text) else { self.state.errors.push(TypeCheckError::new( TypeCheckErrorKind::IntegerOutOfRange, span, diff --git a/crates/psrs-typecheck/src/typecheck/infer/records.rs b/crates/psrs-typecheck/src/typecheck/infer/records.rs index 79d704ff..97a7c315 100644 --- a/crates/psrs-typecheck/src/typecheck/infer/records.rs +++ b/crates/psrs-typecheck/src/typecheck/infer/records.rs @@ -13,35 +13,24 @@ impl Checker { .. } = self.normalize_row_or_report(expected_row, span); let expected_fields = expected_fields.into_iter().collect::>(); - let mut inferred = Vec::with_capacity(fields.len()); - let mut labels = HashSet::new(); - for (label, value) in fields { - if !labels.insert(label) { - self.state.errors.push(TypeCheckError::new( - TypeCheckErrorKind::TypeMismatch, - span, - format!("record label `{label}` occurs more than once"), - )); - return None; - } - let value = - self.infer_expr_with_expected(value, expected_fields.get(label).cloned())?; - inferred.push((label.clone(), value)); - } - let actual = record_type( - inferred - .iter() - .map(|(label, value)| (label.clone(), value.ty.clone())) - .collect(), - InferType::RowEmpty, - ); - Some((InferredExprKind::Record(inferred), actual)) + self.infer_record_fields(fields, span, |checker, label, value| { + checker.infer_expr_with_expected(value, expected_fields.get(label).cloned()) + }) } pub(super) fn infer_record( &mut self, fields: &[(String, hir::Expr)], span: TextRange, + ) -> Option<(InferredExprKind, InferType)> { + self.infer_record_fields(fields, span, |checker, _, value| checker.infer_expr(value)) + } + + pub(super) fn infer_record_fields( + &mut self, + fields: &[(String, hir::Expr)], + span: TextRange, + mut infer_field: impl FnMut(&mut Self, &str, &hir::Expr) -> Option, ) -> Option<(InferredExprKind, InferType)> { let mut inferred = Vec::with_capacity(fields.len()); let mut labels = HashSet::new(); @@ -54,13 +43,15 @@ impl Checker { )); return None; } - inferred.push((label.clone(), self.infer_expr(value)?)); + inferred.push((label.clone(), infer_field(self, label, value)?)); } - let record_fields = inferred - .iter() - .map(|(label, value)| (label.clone(), value.ty.clone())) - .collect::>(); - let ty = record_type(record_fields, InferType::RowEmpty); + let ty = record_type( + inferred + .iter() + .map(|(label, value)| (label.clone(), value.ty.clone())) + .collect(), + InferType::RowEmpty, + ); Some((InferredExprKind::Record(inferred), ty)) } diff --git a/crates/psrs-typecheck/src/typecheck/mod.rs b/crates/psrs-typecheck/src/typecheck/mod.rs index 60b24c47..29ccd2ab 100644 --- a/crates/psrs-typecheck/src/typecheck/mod.rs +++ b/crates/psrs-typecheck/src/typecheck/mod.rs @@ -36,6 +36,10 @@ pub struct TypeCheckOutput { #[derive(Clone, Copy)] pub struct TypecheckContext<'a> { pub known_types: &'a [hir::TypeDeclaration], + /// Every value declaration in the resolved program, used to pin the core + /// library values a deriving rule names (`mempty`, `append`, `identity`, + /// `apply`, `pure`) to their declaring identity. + pub known_values: &'a [hir::Declaration], pub imported_instances: &'a [hir::InstanceDeclaration], pub module_names: &'a HashMap, pub checked_kinds: &'a CheckedKindEnv, diff --git a/crates/psrs-typecheck/src/typecheck/order.rs b/crates/psrs-typecheck/src/typecheck/order.rs index 8486cfc1..89aa3296 100644 --- a/crates/psrs-typecheck/src/typecheck/order.rs +++ b/crates/psrs-typecheck/src/typecheck/order.rs @@ -49,6 +49,10 @@ pub(super) fn declaration_order(module: &hir::Module) -> Vec { } fn collect_globals(expression: &hir::Expr, out: &mut Vec) { + psrs_span::with_sufficient_stack(|| collect_globals_inner(expression, out)) +} + +fn collect_globals_inner(expression: &hir::Expr, out: &mut Vec) { match &expression.kind { hir::ExprKind::Local(_) | hir::ExprKind::Integer(_) @@ -60,7 +64,7 @@ fn collect_globals(expression: &hir::Expr, out: &mut Vec) { collect_globals(element, out); } } - hir::ExprKind::Record(fields) => { + hir::ExprKind::Record(fields) | hir::ExprKind::MatchProduct(fields) => { for (_, value) in fields { collect_globals(value, out); } @@ -97,7 +101,12 @@ fn collect_globals(expression: &hir::Expr, out: &mut Vec) { operands, operators, } => { - out.extend(operators.iter().map(|operator| operator.symbol)); + out.extend( + operators + .iter() + .filter(|operator| operator.local.is_none()) + .map(|operator| operator.symbol), + ); for operand in operands { collect_globals(operand, out); } @@ -105,7 +114,9 @@ fn collect_globals(expression: &hir::Expr, out: &mut Vec) { hir::ExprKind::OperatorSection { operator, operand, .. } => { - out.push(operator.symbol); + if operator.local.is_none() { + out.push(operator.symbol); + } collect_globals(operand, out); } hir::ExprKind::Application(function, argument) => { diff --git a/crates/psrs-typecheck/src/typecheck/prim/coercible/givens.rs b/crates/psrs-typecheck/src/typecheck/prim/coercible/givens.rs index 84613420..f78b5033 100644 --- a/crates/psrs-typecheck/src/typecheck/prim/coercible/givens.rs +++ b/crates/psrs-typecheck/src/typecheck/prim/coercible/givens.rs @@ -8,18 +8,40 @@ impl Checker { let mut direct = Vec::new(); let mut edges = Vec::new(); let mut relations = Vec::new(); - for (given, _) in &self.scope.givens { - if given.class_id != hir::TypeId::COERCIBLE || given.arguments.len() != 2 { + let mut pending = self + .scope + .givens + .iter() + .map(|(given, _)| (given.class_id, given.arguments.clone())) + .collect::>(); + let mut visited: Vec<(hir::TypeId, Vec)> = Vec::new(); + while let Some((class_id, arguments)) = pending.pop() { + if visited.iter().any(|(seen_class, seen_arguments)| { + *seen_class == class_id + && seen_arguments.len() == arguments.len() + && seen_arguments + .iter() + .zip(&arguments) + .all(|(seen, current)| self.infer_types_equal(seen, current)) + }) { continue; } - let left = self.resolve_type(given.arguments[0].clone()); - let right = self.resolve_type(given.arguments[1].clone()); - add_edge(&mut edges, left.clone(), right.clone(), self); - add_edge(&mut edges, right.clone(), left.clone(), self); - direct.push((left.clone(), right.clone())); - if let Some(canonical) = self.canonical_given(&left, &right) { - push_relation(&mut relations, canonical, self); + visited.push((class_id, arguments.clone())); + if class_id == hir::TypeId::COERCIBLE && arguments.len() == 2 { + let left = self.resolve_type(arguments[0].clone()); + let right = self.resolve_type(arguments[1].clone()); + add_edge(&mut edges, left.clone(), right.clone(), self); + add_edge(&mut edges, right.clone(), left.clone(), self); + direct.push((left.clone(), right.clone())); + if let Some(canonical) = self.canonical_given(&left, &right) { + push_relation(&mut relations, canonical, self); + } } + pending.extend( + self.superclass_constraints(class_id, &arguments) + .into_iter() + .map(|(_, constraint)| (constraint.class_id, constraint.arguments)), + ); } // A given can discharge its exact relation (in either direction), and diff --git a/crates/psrs-typecheck/src/typecheck/prim/coercible/mod.rs b/crates/psrs-typecheck/src/typecheck/prim/coercible/mod.rs index 3b838962..1ed4ec98 100644 --- a/crates/psrs-typecheck/src/typecheck/prim/coercible/mod.rs +++ b/crates/psrs-typecheck/src/typecheck/prim/coercible/mod.rs @@ -81,10 +81,45 @@ impl Checker { } let source = self.resolve_type(source.clone()); let target = self.resolve_type(target.clone()); + // Reflexivity does not depend on learning a kind: identical types + // necessarily have the same representation, even while their kind is + // still being inferred. + if self.infer_types_equal(&source, &target) { + return true; + } + // A constrained function is represented as one dictionary argument per + // constraint followed by its body. When the constraints are identical, + // the dictionary prefix has the same representation on both sides and + // the body can be checked using the ordinary coercion rules. + if let ( + InferType::Constrained { + constraints: source_constraints, + body: source_body, + }, + InferType::Constrained { + constraints: target_constraints, + body: target_body, + }, + ) = (&source, &target) + { + if !constraints_equal(source_constraints, target_constraints, self) { + // Constraints are part of the representation prefix, so + // different dictionaries cannot be skipped. + return false; + } + let mut nested = path.clone(); + return self.proves_coercible_inner( + source_body, + target_body, + span, + depth + 1, + &mut nested, + ); + } if !self.coercion_kinds_compatible(&source, &target, span) { return false; } - if self.infer_types_equal(&source, &target) || self.given_coercible(&source, &target) { + if self.given_coercible(&source, &target) { return true; } match (&source, &target) { @@ -238,6 +273,23 @@ impl Checker { } } +fn constraints_equal( + left: &[ClassConstraint], + right: &[ClassConstraint], + checker: &Checker, +) -> bool { + left.len() == right.len() + && left.iter().zip(right).all(|(left, right)| { + left.class_id == right.class_id + && left.arguments.len() == right.arguments.len() + && left + .arguments + .iter() + .zip(&right.arguments) + .all(|(left, right)| checker.infer_types_equal(left, right)) + }) +} + pub(super) fn flatten_infer_spine(ty: &InferType) -> (&InferType, Vec) { let mut head = ty; let mut arguments = Vec::new(); diff --git a/crates/psrs-typecheck/src/typecheck/prim/compare/tests.rs b/crates/psrs-typecheck/src/typecheck/prim/compare/tests.rs index 191e63cc..bb301101 100644 --- a/crates/psrs-typecheck/src/typecheck/prim/compare/tests.rs +++ b/crates/psrs-typecheck/src/typecheck/prim/compare/tests.rs @@ -26,6 +26,7 @@ pub(super) fn checker() -> Checker { &HashMap::new(), TypecheckContext { known_types: &[], + known_values: &[], imported_instances: &[], module_names: &HashMap::from([(hir::ModuleId(0), "Main".to_owned())]), checked_kinds: &psrs_kind::CheckedKindEnv::default(), diff --git a/crates/psrs-typecheck/src/typecheck/prim/dispatch.rs b/crates/psrs-typecheck/src/typecheck/prim/dispatch.rs index 97a9e272..38d37d78 100644 --- a/crates/psrs-typecheck/src/typecheck/prim/dispatch.rs +++ b/crates/psrs-typecheck/src/typecheck/prim/dispatch.rs @@ -44,7 +44,7 @@ impl Checker { policy: UnsolvedPolicy, chain: &mut RequeueChain, ) -> PrimitiveDispatch { - let Some(rule) = primitive_rule(constraint.class_id) else { + let Some(rule) = self.registered_primitive_rule(constraint.class_id) else { return PrimitiveDispatch::None; }; // A wanted constraint whose arguments are not the member's own is not @@ -62,7 +62,7 @@ impl Checker { self.improve_one(&mut improved); let snapshot = self.state.snapshot(); let outcome = self.call_rule( - rule, + &rule, &PrimitiveArgs { constraint: &improved, }, diff --git a/crates/psrs-typecheck/src/typecheck/prim/int.rs b/crates/psrs-typecheck/src/typecheck/prim/int.rs index 594e6783..7466f39d 100644 --- a/crates/psrs-typecheck/src/typecheck/prim/int.rs +++ b/crates/psrs-typecheck/src/typecheck/prim/int.rs @@ -217,6 +217,7 @@ mod tests { &HashMap::new(), TypecheckContext { known_types: &[], + known_values: &[], imported_instances: &[], module_names: &HashMap::from([(hir::ModuleId(0), "Main".to_owned())]), checked_kinds: &psrs_kind::CheckedKindEnv::default(), diff --git a/crates/psrs-typecheck/src/typecheck/prim/mod.rs b/crates/psrs-typecheck/src/typecheck/prim/mod.rs index f2537469..b715eed7 100644 --- a/crates/psrs-typecheck/src/typecheck/prim/mod.rs +++ b/crates/psrs-typecheck/src/typecheck/prim/mod.rs @@ -23,6 +23,7 @@ mod coercible; mod compare; mod dispatch; mod int; +mod reflection; mod reports; pub(in crate::typecheck) mod requeue; mod row; @@ -58,7 +59,7 @@ impl EvidenceClass { /// dictionary can mask them. A report runs after givens so `Warn`, `Fail`, /// and `Partial` can propagate through an enclosing constraint. A proof /// runs first because a dictionary is not the proof boundary Core expects. - fn precedes_givens(self) -> bool { + pub(in crate::typecheck) fn precedes_givens(self) -> bool { matches!( self, EvidenceClass::CompileTimeProof | EvidenceClass::RuntimeDictionary @@ -69,7 +70,7 @@ impl EvidenceClass { /// /// `Coercible` is a checked proof boundary, so a given dictionary cannot /// discharge it. Its rule reads and composes proof givens itself. - fn accepts_a_given(self) -> bool { + pub(in crate::typecheck) fn accepts_a_given(self) -> bool { matches!( self, EvidenceClass::RuntimeDictionary @@ -80,6 +81,7 @@ impl EvidenceClass { } /// One primitive rule, as the table records it. +#[derive(Clone, Copy)] pub(in crate::typecheck) struct PrimitiveRule { /// The class identity the rule decides, and the key of its table entry. A /// qualification, an alias, or a re-export reaches the same identity, so @@ -94,9 +96,11 @@ pub(in crate::typecheck) struct PrimitiveRule { solve: fn(&mut Checker, &PrimitiveArgs) -> PrimitiveOutcome, } -/// The rules that exist. Every member with a rule appears here and nowhere -/// else, so adding a relation is a table entry and a rule function rather than -/// a branch at the dispatch site. +/// Rules for the compiler-owned `Prim` declaration inventory. Source library +/// interfaces enter the same dispatch through `registered_primitive_rule`, +/// after their interface identity and dictionary contract have been checked. +/// Adding a relation extends its owning registry and rule implementation, never +/// a source-name branch at the solving site. /// /// A member with no entry reaches instance search, and a member whose rule /// declines continues into instance search: a relation never depends on an @@ -148,6 +152,11 @@ pub(in crate::typecheck) fn is_report_only(class_id: hir::TypeId) -> bool { /// erases and records the arguments the rule decided, and a report has none. #[derive(Clone, Debug)] pub(in crate::typecheck) enum PrimitiveEvidence { + /// An ordinary checked dictionary whose methods have runtime values. + DictionaryValue { + arguments: Vec, + value: Box, + }, /// A checked boundary with no runtime value, recording the types it /// connects. Proof { @@ -183,6 +192,9 @@ impl PrimitiveEvidence { PrimitiveEvidence::Dictionary { arguments } => { Some(WantedSolution::Primitive { arguments }) } + PrimitiveEvidence::DictionaryValue { value, .. } => { + Some(WantedSolution::DictionaryValue(value)) + } PrimitiveEvidence::Report => None, } } @@ -288,13 +300,19 @@ pub(in crate::typecheck) enum PrimitiveDispatch { /// The evidence classification owns this position: type-level relations and /// proof boundaries precede givens; reports run after givens so they can /// propagate or emit their diagnostic at the unresolved boundary. +#[cfg(test)] pub(in crate::typecheck) fn primitive_rule_precedes_givens(class_id: hir::TypeId) -> bool { primitive_rule(class_id).is_some_and(|rule| rule.evidence.precedes_givens()) } -/// Whether a direct lexical given is forbidden from supplying this member's -/// evidence. `Coercible` is the only such member: its proof rule can consume -/// givens but must produce a verified proof boundary. -pub(in crate::typecheck) fn primitive_rule_skips_given_lookup(class_id: hir::TypeId) -> bool { - primitive_rule(class_id).is_some_and(|rule| !rule.evidence.accepts_a_given()) +impl Checker { + pub(in crate::typecheck) fn registered_primitive_rule( + &self, + class_id: hir::TypeId, + ) -> Option { + primitive_rule(class_id).copied().or_else(|| { + let identity = self.env.classes.get(&class_id)?.compiler_class?; + Some(reflection::rule(class_id, identity)) + }) + } } diff --git a/crates/psrs-typecheck/src/typecheck/prim/reflection.rs b/crates/psrs-typecheck/src/typecheck/prim/reflection.rs new file mode 100644 index 00000000..13e598fb --- /dev/null +++ b/crates/psrs-typecheck/src/typecheck/prim/reflection.rs @@ -0,0 +1,65 @@ +//! Runtime dictionaries for designated, validated library interfaces. +use super::*; +use crate::typecheck::classes::record_field_type; + +pub(super) fn rule(class_id: hir::TypeId, identity: hir::CompilerClass) -> PrimitiveRule { + match identity { + hir::CompilerClass::IsSymbol => PrimitiveRule { + class_id, + evidence: EvidenceClass::RuntimeDictionary, + arity: 1, + solve: is_symbol, + }, + } +} + +fn is_symbol(checker: &mut Checker, args: &PrimitiveArgs) -> PrimitiveOutcome { + let arguments = args.resolved(checker); + let [InferType::TypeLevelString(value)] = arguments.as_slice() else { + return PrimitiveOutcome::Undecided; + }; + let field = hir::CompilerClass::IsSymbol.method(); + let dictionary_type = checker.resolve_type(args.constraint.dictionary_type.clone()); + let Some(method_type) = record_field_type(&dictionary_type, field) else { + return PrimitiveOutcome::Undecided; + }; + let InferType::Application(function, _) = &method_type else { + return PrimitiveOutcome::Undecided; + }; + let InferType::Application(_, parameter) = function.as_ref() else { + return PrimitiveOutcome::Undecided; + }; + let span = args.span(); + let id = LocalId(checker.state.next_dictionary_local); + checker.state.next_dictionary_local += 1; + let method = InferredExpr { + kind: InferredExprKind::Lambda { + binder: InferredBinder { + binder: hir::LocalBinder { + id, + name: "$reflect".into(), + span, + }, + scheme: Scheme::monomorphic(parameter.as_ref().clone()), + }, + body: Box::new(InferredExpr { + kind: InferredExprKind::String(value.clone()), + ty: InferType::Constructor(TypeConstructor::String), + span, + }), + }, + ty: method_type, + span, + }; + PrimitiveOutcome::Solved { + evidence: PrimitiveEvidence::DictionaryValue { + arguments, + value: Box::new(InferredExpr { + kind: InferredExprKind::Record(vec![(field.into(), method)]), + ty: dictionary_type, + span, + }), + }, + deferred: Vec::new(), + } +} diff --git a/crates/psrs-typecheck/src/typecheck/prim/requeue.rs b/crates/psrs-typecheck/src/typecheck/prim/requeue.rs index a15bceff..dda4855c 100644 --- a/crates/psrs-typecheck/src/typecheck/prim/requeue.rs +++ b/crates/psrs-typecheck/src/typecheck/prim/requeue.rs @@ -141,7 +141,7 @@ impl Checker { { return; } - if policy == UnsolvedPolicy::Retain && self.can_generalize_constraint(&constraint) { + if self.policy_keeps_unsolved(policy, &constraint) { self.state.wanted.push(constraint); return; } diff --git a/crates/psrs-typecheck/src/typecheck/prim/row/tests/mod.rs b/crates/psrs-typecheck/src/typecheck/prim/row/tests/mod.rs index ad2335ed..9ab547c0 100644 --- a/crates/psrs-typecheck/src/typecheck/prim/row/tests/mod.rs +++ b/crates/psrs-typecheck/src/typecheck/prim/row/tests/mod.rs @@ -29,6 +29,7 @@ fn checker() -> Checker { &HashMap::new(), TypecheckContext { known_types: &[], + known_values: &[], imported_instances: &[], module_names: &HashMap::from([(hir::ModuleId(0), "Main".to_owned())]), checked_kinds: &psrs_kind::CheckedKindEnv::default(), diff --git a/crates/psrs-typecheck/src/typecheck/prim/tests/mod.rs b/crates/psrs-typecheck/src/typecheck/prim/tests/mod.rs index c8414568..75d2f0d7 100644 --- a/crates/psrs-typecheck/src/typecheck/prim/tests/mod.rs +++ b/crates/psrs-typecheck/src/typecheck/prim/tests/mod.rs @@ -360,6 +360,7 @@ fn checker() -> Checker { &HashMap::new(), TypecheckContext { known_types: &[], + known_values: &[], imported_instances: &[], module_names: &HashMap::from([(ModuleId(0), "Main".to_owned())]), checked_kinds: &psrs_kind::CheckedKindEnv::default(), diff --git a/crates/psrs-typecheck/src/typecheck/prim/verify.rs b/crates/psrs-typecheck/src/typecheck/prim/verify.rs index 7909be23..15a73523 100644 --- a/crates/psrs-typecheck/src/typecheck/prim/verify.rs +++ b/crates/psrs-typecheck/src/typecheck/prim/verify.rs @@ -55,8 +55,10 @@ impl Checker { args: &PrimitiveArgs, evidence: &PrimitiveEvidence, ) -> bool { - let PrimitiveEvidence::Dictionary { arguments } = evidence else { - return true; + let arguments = match evidence { + PrimitiveEvidence::Dictionary { arguments } + | PrimitiveEvidence::DictionaryValue { arguments, .. } => arguments, + _ => return true, }; let wanted = args.resolved(self); let span = args.span(); @@ -73,6 +75,18 @@ impl Checker { return None; } } + if let PrimitiveEvidence::DictionaryValue { value, .. } = evidence { + let errors_before = checker.state.errors.len(); + checker.unify( + value.ty.clone(), + args.constraint.dictionary_type.clone(), + span, + ); + if checker.state.errors.len() > errors_before { + checker.state.errors.truncate(errors_before + 1); + return None; + } + } Some(()) }) .is_some() diff --git a/crates/psrs-typecheck/src/typecheck/result.rs b/crates/psrs-typecheck/src/typecheck/result.rs index 587e4dc6..f4332aa0 100644 --- a/crates/psrs-typecheck/src/typecheck/result.rs +++ b/crates/psrs-typecheck/src/typecheck/result.rs @@ -76,6 +76,14 @@ pub(super) enum InferredExprKind { source: InferType, target: InferType, }, + /// An unchecked conversion with a recorded source or derivation authority. + /// It has no `Coercible` wanted; + /// finalization closes it into an unchecked representation cast. + UnsafeCoerceFunction { + source: InferType, + target: InferType, + origin: thir::UncheckedCoercionOrigin, + }, /// A dictionary solved for `wanted`, used directly (for example as an /// instance's superclass field). Evidence(usize), diff --git a/crates/psrs-typecheck/src/typecheck/signature.rs b/crates/psrs-typecheck/src/typecheck/signature.rs index b8aab276..4de37a11 100644 --- a/crates/psrs-typecheck/src/typecheck/signature.rs +++ b/crates/psrs-typecheck/src/typecheck/signature.rs @@ -83,6 +83,11 @@ impl Checker { rigid: bool, quantified: &mut Vec, ) -> (Vec, InferType) { + if let Some(expanded) = self.env.checked_kinds.expand_synonyms(ty) + && expanded != *ty + { + return self.elaborate_constraint_spine(&expanded, variables, rigid, quantified); + } match &ty.kind { hir::TypeKind::Forall { variables: binders, @@ -125,6 +130,13 @@ impl Checker { variables: &mut HashMap, rigid_variables: bool, ) -> InferType { + // Substitute in HIR before elaborating arguments: a saturated outer + // alias may apply a partial alias argument or discard it altogether. + if let Some(expanded) = self.env.checked_kinds.expand_synonyms(ty) + && expanded != *ty + { + return self.elaborate_type_mode(&expanded, variables, rigid_variables); + } match &ty.kind { hir::TypeKind::Wildcard => self.fresh(), hir::TypeKind::Variable(name) => { @@ -171,15 +183,34 @@ impl Checker { hir::TypeKind::Application(function, argument) => { let (head, arguments) = flatten_spine(ty); if let Some(id) = nominal_type_id(head) - && self.env.synonyms.contains_key(&id) + && let Some(arity) = self + .env + .synonyms + .get(&id) + .map(|synonym| synonym.parameters.len()) { - let arguments = arguments - .into_iter() + // Arguments past the synonym's own parameters apply to the + // expanded body. `C2 a z` has kind `k -> Type`, so + // `C2 a z x` is `(C2 a z) x`, not a third synonym parameter. + let elaborated = arguments + .iter() + .take(arity) .map(|argument| { self.elaborate_type_mode(argument, variables, rigid_variables) }) .collect(); - return self.expand_synonym(id, arguments, ty.span); + let mut expanded = self.expand_synonym(id, elaborated, ty.span); + for argument in arguments.iter().skip(arity) { + expanded = InferType::Application( + Box::new(expanded), + Box::new(self.elaborate_type_mode( + argument, + variables, + rigid_variables, + )), + ); + } + return expanded; } InferType::Application( Box::new(self.elaborate_type_mode(function, variables, rigid_variables)), diff --git a/crates/psrs-typecheck/src/typecheck/state.rs b/crates/psrs-typecheck/src/typecheck/state.rs index 9625be0d..c888f838 100644 --- a/crates/psrs-typecheck/src/typecheck/state.rs +++ b/crates/psrs-typecheck/src/typecheck/state.rs @@ -14,6 +14,7 @@ //! than its neighbour, and [`InferState::snapshot`] is the only way a snapshot //! is taken. +use super::classes::DerivingRegistry; use super::*; /// The read-only semantic inputs of one module being checked. @@ -43,6 +44,9 @@ pub(super) struct SemanticEnv { pub(super) classes: HashMap, pub(super) class_methods: HashMap, pub(super) instances: Vec, + /// The single table of compiler-known deriving classes and representation + /// declarations, built once from the resolved declarations. + pub(super) deriving: DerivingRegistry, } /// The mutable solver: everything a binding changes and a speculation must undo. @@ -209,9 +213,10 @@ impl Checker { } } - /// Runs `f` with `givens` as the given evidence in scope, and restores what - /// leaving the scope restores: the givens, the rigid variables their - /// arguments added, and the given-rigid record that tracks them. + /// Runs `f` with `givens` added to the enclosing given evidence, and + /// restores what leaving the scope restores: the givens, the rigid + /// variables their arguments added, and the given-rigid record that tracks + /// them. /// /// A given's variables are rigid only while the given is in scope, which is /// why this is not [`Self::with_skolem_scope`]: a skolem outlives its scope, @@ -221,10 +226,11 @@ impl Checker { givens: Vec<(ClassConstraint, WantedSolution)>, f: impl FnOnce(&mut Self) -> T, ) -> T { - let previous_givens = std::mem::replace(&mut self.scope.givens, givens); + let previous_givens = self.scope.givens.clone(); + self.scope.givens.extend(givens); let previous_rigid = self.state.rigid.clone(); let previous_given_rigid = std::mem::take(&mut self.scope.given_rigid); - for (constraint, _) in self.scope.givens.clone() { + for (constraint, _) in &self.scope.givens[previous_givens.len()..] { for argument in &constraint.arguments { let mut variables = HashSet::new(); classes::collect_infer_variables(argument, &mut variables); diff --git a/crates/psrs-typecheck/src/typecheck/tests/binding_kinds.rs b/crates/psrs-typecheck/src/typecheck/tests/binding_kinds.rs index 491b7c06..39b7fbf6 100644 --- a/crates/psrs-typecheck/src/typecheck/tests/binding_kinds.rs +++ b/crates/psrs-typecheck/src/typecheck/tests/binding_kinds.rs @@ -50,6 +50,7 @@ fn typecheck_program(source: &str) -> Result Checker { &HashMap::new(), TypecheckContext { known_types: &[], + known_values: &[], imported_instances: &[], module_names: &HashMap::from([(ModuleId(0), "Main".to_owned())]), checked_kinds: &psrs_kind::CheckedKindEnv::default(), diff --git a/crates/psrs-typecheck/src/typecheck/tests/mod.rs b/crates/psrs-typecheck/src/typecheck/tests/mod.rs index 8505a079..7ac11502 100644 --- a/crates/psrs-typecheck/src/typecheck/tests/mod.rs +++ b/crates/psrs-typecheck/src/typecheck/tests/mod.rs @@ -75,7 +75,7 @@ fn integer(value: &str, start: u32) -> HirExpr { #[test] fn infers_functions_arithmetic_conditionals_and_intrinsic_booleans() { - let add = Intrinsic::I32Add.symbol(); + let add = Intrinsic::IntAdd.symbol(); let true_symbol = Intrinsic::BoolTrue.symbol(); let increment = expr( HirExprKind::Lambda { @@ -287,7 +287,7 @@ fn rejects_a_body_that_does_not_match_its_signature() { } #[test] -fn generalizes_let_bound_functions() { +fn unannotated_let_bound_functions_are_monomorphic() { let let_expression = expr( HirExprKind::Let { bindings: vec![hir::LocalBinding { @@ -322,9 +322,13 @@ fn generalizes_let_bound_functions() { let resolved = module(vec![declaration(0, "main", 19, let_expression)], false); let resolved = psrs_desugar::desugar_module(resolved).unwrap(); - let typed = typecheck_module(resolved).unwrap(); - assert_eq!(typed.declarations[0].quantified.len(), 1); - typed.verify().unwrap(); + let errors = typecheck_module(resolved).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.kind == TypeCheckErrorKind::OccursCheck), + "{errors:?}" + ); } #[test] diff --git a/crates/psrs-typecheck/src/typecheck/tests/rows.rs b/crates/psrs-typecheck/src/typecheck/tests/rows.rs index 51e13e25..7d9bc19c 100644 --- a/crates/psrs-typecheck/src/typecheck/tests/rows.rs +++ b/crates/psrs-typecheck/src/typecheck/tests/rows.rs @@ -109,6 +109,7 @@ fn synonym(id: u32, name: &str, parameters: &[&str], body: HirType) -> psrs_hir: name: name.into(), name_span: TextRange::new(0, 1), kind: psrs_hir::TypeDeclarationKind::TypeSynonym, + compiler_class: None, parameters: parameters .iter() .map(|parameter| psrs_hir::TypeParameter { diff --git a/crates/psrs-typecheck/src/typecheck/tests/type_level_literals.rs b/crates/psrs-typecheck/src/typecheck/tests/type_level_literals.rs index b8094999..7b69ffe5 100644 --- a/crates/psrs-typecheck/src/typecheck/tests/type_level_literals.rs +++ b/crates/psrs-typecheck/src/typecheck/tests/type_level_literals.rs @@ -28,6 +28,7 @@ fn proxy_declaration() -> hir::TypeDeclaration { name: "Proxy".into(), name_span: TextRange::new(0, 5), kind: hir::TypeDeclarationKind::Data, + compiler_class: None, parameters: vec![psrs_hir::TypeParameter { name: "a".into(), name_span: TextRange::new(6, 7), diff --git a/crates/psrs-typecheck/src/typecheck/tests/user_types/mod.rs b/crates/psrs-typecheck/src/typecheck/tests/user_types/mod.rs index 70c084a8..4fb821c2 100644 --- a/crates/psrs-typecheck/src/typecheck/tests/user_types/mod.rs +++ b/crates/psrs-typecheck/src/typecheck/tests/user_types/mod.rs @@ -223,6 +223,7 @@ fn synonym(id: u32, name: &str, parameters: &[&str], body: HirType) -> psrs_hir: name: name.into(), name_span: TextRange::new(0, 1), kind: psrs_hir::TypeDeclarationKind::TypeSynonym, + compiler_class: None, parameters: parameters .iter() .map(|parameter| psrs_hir::TypeParameter { @@ -285,6 +286,49 @@ fn expands_type_synonyms_in_signatures() { typed.verify().unwrap(); } +#[test] +fn applies_extra_arguments_after_expanding_a_synonym() { + let element = applied( + applied( + named(0, 20), + builtin(psrs_hir::BuiltinType::Array, 24), + 20, + 30, + ), + builtin(psrs_hir::BuiltinType::Int, 31), + 20, + 34, + ); + let signature = HirType { + kind: HirTypeKind::Function { + parameter: Box::new(element.clone()), + result: Box::new(element), + }, + span: TextRange::new(20, 40), + }; + let declaration = declaration_with_signature(0, "f", 19, signature, identity_lambda(40)); + let mut resolved = module(vec![declaration], false); + resolved.types = vec![synonym(0, "Hom", &["f"], variable("f", 10))]; + + let typed = typecheck_module(resolved).unwrap(); + assert!( + typed + .types + .iter() + .any(|ty| matches!(ty, Type::Constructor(thir::TypeConstructor::Array))) + ); + assert!( + typed + .types + .iter() + .any(|ty| matches!(ty, Type::Constructor(thir::TypeConstructor::Int))) + ); + assert!(!typed.types.iter().any(|ty| matches!( + ty, + Type::Constructor(thir::TypeConstructor::User(id)) if id.index == 0 + ))); +} + #[test] fn rejects_a_partially_applied_synonym() { let signature = HirType { @@ -325,6 +369,7 @@ fn data_declaration( name: name.into(), name_span: TextRange::new(0, 1), kind: psrs_hir::TypeDeclarationKind::Data, + compiler_class: None, parameters: parameters .iter() .map(|parameter| psrs_hir::TypeParameter { diff --git a/crates/psrs-typecheck/src/typecheck/vocabulary.rs b/crates/psrs-typecheck/src/typecheck/vocabulary.rs index 0585330c..8fb262e4 100644 --- a/crates/psrs-typecheck/src/typecheck/vocabulary.rs +++ b/crates/psrs-typecheck/src/typecheck/vocabulary.rs @@ -87,6 +87,7 @@ pub(super) struct FundepInfo { /// functional dependencies. #[derive(Clone, Debug)] pub(super) struct ClassInfo { + pub(super) compiler_class: Option, pub(super) parameters: Vec, pub(super) superclasses: Vec, pub(super) fundeps: Vec, @@ -103,7 +104,10 @@ pub(super) struct InstanceInfo { pub(super) chain_id: u32, pub(super) chain_position: u32, pub(super) head_arguments: Vec, - pub(super) head_variables: HashMap, + /// Every type variable shared by the instance head and context. Context + /// variables determined through fundeps still need one identity in method + /// bodies and their type annotations. + pub(super) instance_variables: HashMap, pub(super) context: Vec, pub(super) context_parameters: Vec<(LocalId, InferType)>, } @@ -114,6 +118,8 @@ pub(super) struct InstanceInfo { /// whose dictionaries it applies the constructor to. #[derive(Clone, Debug)] pub(super) enum WantedSolution { + /// A compiler-constructed dictionary with checked ordinary term fields. + DictionaryValue(Box), Given(LocalId), Global(SymbolId), Instance { diff --git a/docs/README.md b/docs/README.md index 14022ba9..1e1f94b9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,12 +15,24 @@ Project documentation is written in English and grouped by purpose: | [F-01: Source inspection](feature/F-01-source-inspection.md) | [Frontend and IR boundaries](design/D-01-frontend-and-ir-boundaries.md) | In progress | | [F-02: Build portable programs](feature/F-02-portable-programs.md) | [Backend design](design/backend/README.md) | In progress | | [F-03: PSRS Explorer](feature/F-03-interactive-ir-explorer.md) | [PSRS Explorer](design/D-14-interactive-ir-explorer.md) | In progress | +| [F-04: Diagnose compile failures](feature/F-04-compile-diagnosis.md) | [Compile failure diagnosis](design/D-16-compile-diagnosis.md) | In progress | Decision records use the `DEC-XX` prefix. See [decision policy](decision/README.md). The [authoring guide](authoring-guide.md) is the reference for where a document goes, how it is named, the topic design document template, and how to state a measured number. +## Contributor workflows + +- [Compiler iteration SOP](workflow/compiler-iteration-sop.md): diagnose a + baseline, locate the responsible stage contract, implement a bounded fix, and + compare the same cases afterward. + +- [Standard-library conformance](workflow/stdlib-conformance.md): audit pinned + upstream sources and compare explicit runtime observations. +- [Standard-library boundaries](design/D-17-stdlib-and-conformance-boundaries.md): + independent package ownership and locked compiler consumption. + ## Compiler design The [frontend design](design/frontend/README.md) groups syntax, semantic diff --git a/docs/authoring-guide.md b/docs/authoring-guide.md index 8d114968..c582ebcf 100644 --- a/docs/authoring-guide.md +++ b/docs/authoring-guide.md @@ -15,6 +15,7 @@ except `AGENTS.md` and the root `README.md`. | Implementation and IR architecture | `docs/design/` | | A major, durable decision only | `docs/decision/DEC-XX-.md` | | Per-topic acceptance checklists and evidence | `docs/implementation/` | +| Contributor process and repeatable workflows | `docs/workflow/` | ## Naming and identity diff --git a/docs/decision/DEC-03-purescript-faithful-type-system.md b/docs/decision/DEC-03-purescript-faithful-type-system.md index f0b447cc..d6b34a09 100644 --- a/docs/decision/DEC-03-purescript-faithful-type-system.md +++ b/docs/decision/DEC-03-purescript-faithful-type-system.md @@ -1,7 +1,12 @@ # DEC-03 — PureScript-Faithful Type System and Effect Encoding **Status:** Accepted -**Date:** 2026-09-19 +**Date:** 2026-09-19 +**Amended by:** [DEC-19](DEC-19-compiler-generated-resumable-frames.md) +(2026-10-11). DEC-19 narrows this record's blanket exclusion of delimited +continuations: a generic, effect-agnostic control mechanism may be lowered below +Typed Core. The type-system decision here is unchanged — effects remain a library +encoding with no frontend effect rows and no compiler-known handlers. ## Context diff --git a/docs/decision/DEC-05-wasmtime-feature-set.md b/docs/decision/DEC-05-wasmtime-feature-set.md index ba9ad715..ba9d1a3f 100644 --- a/docs/decision/DEC-05-wasmtime-feature-set.md +++ b/docs/decision/DEC-05-wasmtime-feature-set.md @@ -30,8 +30,9 @@ can execute. The default profile is - The backend may emit only features enabled in the selected profile. The stable profile requires MVP Wasm plus the GC/reference/function-reference - subset used by the current runtime representation and the Component Model - with WASI 0.2. SIMD, tail calls, exceptions, threads, multi-memory, + subset used by the current runtime representation, tail calls for stack-safe + callable execution, and the Component Model with WASI 0.2. SIMD, exceptions, + threads, multi-memory, memory64, wide arithmetic, async components, and WASI 0.3 remain disabled. - A proposal is not `Implemented` because Wasmtime accepts it. It needs a lowering, profile validation, regression tests, and observable execution diff --git a/docs/decision/DEC-11-primitive-ffi-stdlib-wrappers.md b/docs/decision/DEC-11-primitive-ffi-stdlib-wrappers.md index d9353f9a..143aeb76 100644 --- a/docs/decision/DEC-11-primitive-ffi-stdlib-wrappers.md +++ b/docs/decision/DEC-11-primitive-ffi-stdlib-wrappers.md @@ -106,3 +106,31 @@ and supersedes this record's mechanism clause and its rule against recognizing wraps foreign imports in ordinary PureScript, and the compiler does not grow a parallel type vocabulary. The amendment above that admitted non-byte `Array` lists is subsumed by DEC-13. + +## Amendment — source target implementations over array storage primitives + +Array algorithms belong in the target standard library. Wasm requires concrete +allocation and element-storage operations, but it does not require compiler +implementations of `arrayApply`, `arrayBind` or `arrayExtend`. These +implementation slots may +use ordinary PureScript source over private `arrayFill`, `arrayWrite`, length +and index primitives while preserving the official signatures, exports and +other pure declarations. Mutating primitives require fresh buffer ownership; +fully initialized allocation avoids exposing invalid default reference slots. +The compiler owns checked primitive identity, typed occurrence elaboration, +representation and Wasm encoding. The library owns loops, callback sequencing, +snapshot/flatten behavior and overflow policy. Mandatory primitive elaboration +must retain aliasing instead of copying arrays through a polymorphic wrapper. +This amendment does not authorize rewriting valid official pure definitions, +introducing per-function naming heuristics, or treating unimplemented FFI as an +empty result. Source diffs and official behavior evidence remain required. + +The same boundary covers mutable references. `Control.Monad.ST` keeps its +official `ST`/`STRef` newtypes and class instances in the library module, because +a class instance whose type is imported from another module is an orphan that +this compiler does not resolve. A Prelude-free target module owns the suspended +action, the fresh cell over the private storage primitives, and the loops; the +official module's operations are thin adapters. A representation that differs +from the compiler's scalar-token `Effect` closure makes `unsafeCoerce`-based +conversions such as `Control.Monad.ST.Global.toEffect` linked but unsound, and +that limit must be recorded rather than hidden. diff --git a/docs/decision/DEC-13-wit-to-source-type-mapping.md b/docs/decision/DEC-13-wit-to-source-type-mapping.md index a9e380cc..0e578356 100644 --- a/docs/decision/DEC-13-wit-to-source-type-mapping.md +++ b/docs/decision/DEC-13-wit-to-source-type-mapping.md @@ -23,8 +23,11 @@ interfaces already require them: `result, error-code>`. PureScript already has idiomatic, common types for every one of these forms. -No new compiler type vocabulary is required: records (the frontend already treats a tuple as a closed record, FE-06), `Data.Maybe.Maybe`, `Data.Either.Either`, and -ordinary user data types. The functional shape of WIT maps directly onto them. +Native tuple syntax is a closed record (FE-06). The core library's +`Data.Tuple.Tuple` is an ordinary algebraic data type with a `Tuple` data +constructor; it is a separate library type. WIT tuples map to the closed-record +form so the ABI mapping remains structural and does not depend on importing +`Data.Tuple`. ## Decision @@ -70,6 +73,10 @@ source field is `Unit`. There is no `Unit`/trap special case: a unit-success type, the linking stage interns it and validates it against the WIT descriptor, CC derives the layout, and MIR lowers from the CC signature and the descriptor. `SourceType` is not reintroduced. +- FE-06 native tuple syntax and `Data.Tuple.Tuple` remain separate source forms: + `(a, b)` is the closed record `{ _1 :: a, _2 :: b }`, while `Tuple a b` is the + library ADT. This decision does not add a compiler tuple type or make the two + source forms interchangeable. - DEC-11's mechanism clause ("do not grow `SourceType`"; "do not recognize `Maybe`/`Either`/tuples") is superseded by this decision. DEC-11's two-layer rule stands: the standard library wraps foreign imports in ordinary @@ -92,14 +99,10 @@ source field is `Unit`. There is no `Unit`/trap special case: a unit-success Rejected alternatives: -- **Compiler builtins for `Maybe`, `Either`, or tuples.** The library types are - the idiomatic ones; a builtin would duplicate them and force a name on user - code. +- **Compiler builtins for `Maybe` or `Either`.** The library types are the + idiomatic ones; a builtin would duplicate them and force a name on user code. - **A dedicated WIT aggregate type in the compiler.** Reintroduces the vocabulary DEC-12 removed. - **Structural recognition of any two-case ADT by constructor shape.** Two unrelated ADTs would be treated as `option`/`result`; recognition is by the named library type instead. -- **A project-specific encoding of tuples as anything other than a record.** - FE-06 already fixes a tuple as `{ _1, _2, ... }`. - diff --git a/docs/decision/DEC-16-scalar-strings-and-utf8-storage.md b/docs/decision/DEC-16-scalar-strings-and-utf8-storage.md index e073e826..087174bc 100644 --- a/docs/decision/DEC-16-scalar-strings-and-utf8-storage.md +++ b/docs/decision/DEC-16-scalar-strings-and-utf8-storage.md @@ -140,10 +140,20 @@ The migration has landed: intentional differences, and L1 parse agreement is 904/908 with exactly those four cases as the remainder. -One part of the target is not implemented: there is no source-level string -length, indexing, or slicing operation yet, so scalar-value boundaries are -enforced inside the storage and boundary paths only. Adding those operations is -where the scalar/byte distinction first becomes visible to source. +Source-level scalar length, take, drop, slice and splitAt now execute through +Data.String.CodeUnits foreign-slot delegates to the independent PSRS.String +library. The library scans validated canonical UTF-8 and maps scalar indices +to byte boundaries before copying a range through PSRS.Array.sliceImpl. The +448-case pinned-FFI projection oracle makes the intentional scalar/UTF-16 +difference explicit; see the [acceptance checkpoint](../implementation/stdlib/string-slicing-2026-10-07/report.md). +Scalar charAt and toChar now also execute through typed foreign-slot delegates. +The private library decoder reconstructs one scalar from validated canonical +UTF-8 before using the existing Int-to-Char identity primitive. The original +rank-N constructors and public pure wrappers are preserved. Their 269 projected +FFI observations and 6 builder checks are recorded in the +[character checkpoint](../implementation/stdlib/string-characters-2026-10-07/report.md). +Other foreign slots, including unsafe character indexing and predicate +traversal, remain unsupported. This does not establish the full String API. This record is the semantic authority. The design documents state the contract, including @@ -154,4 +164,4 @@ including [rows and records](../design/frontend/type-system/rows-and-records.md), [classes and evidence](../design/frontend/type-system/classes-and-evidence.md), [module resolution](../design/frontend/semantics/modules-and-resolution.md), -and [frontend and IR boundaries](../design/D-01-frontend-and-ir-boundaries.md). \ No newline at end of file +and [frontend and IR boundaries](../design/D-01-frontend-and-ir-boundaries.md). diff --git a/docs/decision/DEC-17-representation-and-evidence.md b/docs/decision/DEC-17-representation-and-evidence.md new file mode 100644 index 00000000..71b652cd --- /dev/null +++ b/docs/decision/DEC-17-representation-and-evidence.md @@ -0,0 +1,142 @@ +# DEC-17 — Unified Runtime Representation and Checked-Boundary Model + +**Status:** Proposed +**Date:** 2026-10-05 + +## Context and constraints + +A source type does not determine a runtime representation. Representation is +representation-polymorphic wherever a type variable, an abstract constructor, a +class dictionary, an effect, or a newtype is involved. The frontend already +produces explicit evidence for these cases — class dictionaries and selected +instances, `Coercible` proofs, instantiation and subsumption — and the backend +already owns layout ([CC IR](../design/backend/fp/cc-ir.md), +[MIR](../design/backend/fp/mir.md)). What was missing is a single contract for +how the two meet. + +Without that contract, the abstract-constructor transport work grew private, +per-feature mechanisms: a HIR-`TypeId`-keyed constructor-protocol table inside +CC, a callable-protocol signature derived by enumerating signatures and taking +parameter prefixes, an effect token lowered to the constant `i32` `0`, and a +driver that skips the compiler-provided `Safe.Coerce` module to avoid a +recursive vendored body. Each is a local rule where a shared one is required, +each keys a representation decision on information its owner does not have, and +together they make erasure, dictionaries, effects, coercion, and partial +application agree only by coincidence. + +Established practice treats these as one problem. GHC represents a value of kind +`TYPE r` through its `RuntimeRep` `r`, carries that representation at a coercion +site, uses `Any` as the uniform boxed inhabitant, represents `IO` as a state +token, and exposes `unsafeCoerce#` as the primitive representation coercion. +Swift gives a function value a calling convention and reintroduces a changed one +with a reabstraction thunk, and represents dictionaries as witness tables. Java +erases generic type arguments and restores call compatibility with bridge +methods. Koka elaborates class and effect operations to explicit evidence. The +constraint is to adopt that one model instead of another feature-local rule, +without adding a runtime type tag and without putting a source or HIR identity +into the CC representation. + +## Decision + +Adopt one runtime representation model, one checked-boundary contract, and one +conversion planner, shared by every feature: + +- **RuntimeRep** (`Scalar`, `Box`, `Reference`, `Aggregate`, `Closure`, + `Erased`) is the target-neutral representation a value has at a boundary; its + `Layout` is the target-specific realization. `Erased` is the uniform + anyref slot, not a loss of information. +- **RepresentationPolicy** is the `RuntimeRep` a producer chose when the value + entered an `Erased` slot (`Boxed`, `Nominal`, `AggregateLayout`, `Callable`). + It is compiler evidence, not a runtime tag. +- **Boundary**, **Evidence** (instantiation, subsumption, dictionary selection, + coercion proof, effect plan), and **ConversionPlan** describe the crossing. +- **Adaptation** (`Identity`, `Box`, `Unbox`, `Cast`, `AggregateConvert`, + `Thunk`, `Coerce`) is planned from the evidence and the two representations; + it is never searched for. +- A **representation owner** maps a source constructor plus its checked + instantiation arguments to a policy. There is one registry, not a special case + per feature. + +Four facts have four owners: checking owns the source relation (P5/P6); the +producer or representation owner owns the policy; the checked use owns the +consumer requirement; P9 owns the layout. The model has two levels: the +`RuntimeRep` of a value at a boundary is target-neutral, and its `Layout` is +the target-specific realization, joined by a total mapping per target +(`heap_layout` for the Wasm GC language heap, `abi_layout` for the Canonical ABI +boundary). A `RuntimeRep` a target uses must have a layout, so the target-neutral +model plus a total, verified mapping is what makes the backend complete. +`RuntimeRep` is this compiler's analogue of GHC's `RuntimeRep`; source-level +representation polymorphism (`TYPE r`) is not a PureScript feature, so it is an +internal descriptor, not a kind. +Erasure, canonical +aggregates, callables, dictionaries, effects, coercion, and partial application +are instances of this model, specified in +[representation and evidence](../design/backend/fp/representation-and-evidence.md), +not separate mechanisms. + +Checked evidence and representation policies travel to P8 through an explicit +Core-to-CC side table, the same boundary discipline `ExternalBindings` uses for +WIT bindings. P8 must not read the Core type arena ambiently, must not key a +semantic decision on a HIR identity, and must not reconstruct a producer policy +from a consumer type or a declaration arity. Missing evidence is a reported +unsupported boundary. + +The model adopts the established naming: GHC `RuntimeRep`/`Any`/`unsafeCoerce#`, +Swift calling conventions and reabstraction thunks with witness tables, Java +erasure with bridge methods, and Koka-style explicit evidence. + +Bare polymorphic function values use one registered unary erased protocol. +Flattened arguments become successive segments, and recovery supplies them +through a checked adapter. This fixes arity at the storage boundary while +preserving the producer's concrete calling convention behind that adapter. +Constructor-owned callable protocols retain their checked fixed arguments; +the bare-slot protocol does not replace the Function domain or Effect token. + +## Consequences + +- The transitional mechanisms are removed, not extended: the HIR-keyed protocol + table, the signature-prefix protocol derivation, and the effect token's + special case are now a single Core-to-CC side table plus the one planner. +- A new constructor is added by registering one representation owner, not by + teaching each pass a new rule. +- `Safe.Coerce.coerce` and `Unsafe.Coerce.unsafeCoerce` are compiler-provided + primitive values. A module the compiler provides is never shadowed by a + vendored on-disk file, so the vendored source stays faithful to upstream. +- The effect token is the opaque, compiler-owned `State# RealWorld` analogue; the + earlier `i32` `0` placeholder is gone. +- Generic aggregate normalization and the aggregate conversion plan + (`ProductMap`/`ArrayMap`, canonical aggregate keys) are sections of the model + document, not a separate design; the concrete aggregate layouts and their + lowering stay in [data representation](../design/backend/fp/data-representation.md). +- The cost is a longer-lived boundary artifact: checked evidence and policies + must be preserved until P8 emits conversion operations, and the side table is + validated in both directions like `ExternalBindings`. That cost is accepted + because it is the only way a consumer can recover a value without guessing. +- CC remains target-neutral and free of source identity; MIR keeps sole + ownership of concrete layout. + +## Rejected alternatives + +- **Keep the per-feature rules (HIR-keyed protocol table and signature-prefix + derivation).** Rejected: they key a representation decision on an identity + owned by another stage, they give the same constructor two mechanisms, and + they cannot be extended without another local rule. +- **Recompute the checked relation inside the backend instead of carrying it.** + Rejected: it duplicates the checker's subsumption without preserving its + direction, scope, or substitutions, and it makes acceptance depend on the + backend's copy. +- **Runtime type tags or type passing.** Rejected: the typed core performs no + runtime type analysis, so a tag store and load on every polymorphic boundary + buys nothing; dictionaries already carry the operations a class needs + ([DEC-15](DEC-15-unified-type-representation.md), + [polymorphism and erasure](../design/backend/fp/polymorphism-and-erasure.md)). +- **A dedicated IR node or runtime object per feature (dictionary, effect, + coercion).** Rejected: dictionaries and effects are ordinary products and + closures, and a dedicated node couples the backend to one library and adds a + verifier path for no semantic gain. +- **Keying representation behavior on a source or HIR identity inside CC.** + Rejected: representation decisions belong to stable representation indices; + a HIR identity is not part of the CC contract. +- **Treating representation equality as nominal identity.** Rejected: + structurally equal signatures must share one `SignatureId`, or a `ref.func` + for one source type is not callable through a shared `call_ref`. diff --git a/docs/decision/DEC-18-unified-target-linking.md b/docs/decision/DEC-18-unified-target-linking.md new file mode 100644 index 00000000..837b964a --- /dev/null +++ b/docs/decision/DEC-18-unified-target-linking.md @@ -0,0 +1,111 @@ +# DEC-18 — Unified Target Linking + +**Status:** Accepted; core-Wasm formatter linking and explicit synchronous +guest interface composition are implemented. + +**Date:** 2026-10-07. + +## Context and constraints + +Language intrinsics, library foreign declarations, WIT interfaces, and executable +Wasm libraries meet at the target boundary. Giving each a separate name table, +dependency scan, or component attachment path obscures who provides a function, +which signature was checked, and which memory its pointers address. + +`numberToString :: Number -> String` exposes this problem. Adding `ryu-js` to the +native compiler does not implement runtime formatting in generated programs. +An embedded implementation also needs checked raw calls, output recovery, +storage reservations, dependency closure, and safe instantiation ordering. +These are linking obligations, not incidental details of component encoding. + +WIT packages supply interface definitions. Executable providers may be a host +or a guest component. A definition being available does not discharge an import. +Existing source typing, representation evidence, resource ownership, and the +WASI host policy must survive provider selection. + +## Decision + +Define one target-linking contract, specified in +[linking and runtime](../design/backend/wasm/linking-and-runtime.md). +Use a single checked link plan for implementation selection, artifact dependency +closure, memory layout, instantiation, and final import verification. + +The owners remain distinct: + +- HIR owns intrinsic identities and language schemes; Core checks their uses. +- The library owns PureScript wrappers and algorithms. +- The runtime package owns pinned WIT definitions, the default command world, + executable artifacts, and their raw contracts/provenance. Its data API has no + dependency on compiler IR or backend types. Move `psrs-backend/wit/` into + `psrs-runtime/wit/`; both ABI resolution and componentization consume this + single catalog. +- The target implementation catalog maps intrinsic identities to direct + instructions, generated helpers, or artifact-backed implementations. +- `psrs-linker` owns WIT definition loading and resolved-world identity. The + backend validates source/WIT types and lowers canonical calls using that same + world context. Provider bindings select host or guest implementations explicitly. +- The independent target linker validates these contracts together and produces an immutable + plan consumed by Wasm emission and component assembly. + +Source-module linking remains with the driver. It passes checked declarations +and package provenance to the backend; it does not resolve target exports. +The link plan is target metadata alongside MIR, not another language IR or a +WIT type system inside Core. + +WIT loading and target composition belong in `psrs-linker`; source/canonical +validation and language-value recovery remain in the backend. Runtime asset +ownership must not move compiler passes into target execution code. Metadata and +target-code build entry points are separate; consuming the catalog alone does +not link the formatter into the native compiler. Interface membership comes from +the resolved world rather than a second handwritten interface whitelist. + +Use the selected `ryu-js` route as the first artifact-backed intrinsic. Its +implementation accepts binary64 and a caller-owned output buffer, writes an +ECMAScript token, and returns initialized byte length. MIR copies UTF-8 into a +GC String and frees the buffer. The library owns the official `Show` `.0` rule, +character/string escaping, and ordinary array callback traversal. + +The formatter is a core-Wasm library with an explicit shared-memory contract. +Its Rust stack and static data are private execution storage, not a language +heap. The linker must validate their reservation and the allocator boundary +before emission; output recovery must preserve DEC-16 and DEC-10 ownership. +Guest components use their own canonical ABI memory and resource identities; +the linker must not impose the core helper's shared-memory model on them. + +Embed pinned compiler-owned runtime artifacts, including source, dependency, +toolchain, recipe, and artifact hashes. Normal compiler builds do not compile +target Rust or download runtime libraries. WIT definitions and independently +supplied guest artifacts require equally explicit provenance and binding data. +There is no implicit dependency discovery, provider fallback, or network fetch. + +## Consequences + +The linker is an independent crate with a checked target API. The dependency +direction is `backend -> linker -> runtime`, with backend also consuming runtime +catalog data. The linker depends on WIT/Wasm tooling and target records, never +backend or compiler IR crates. The backend converts IR requirements and maps +structured linker errors to source diagnostics. Component encoding executes +the plan; it does not rediscover dependencies by import-name heuristics. +Each requirement has a provider, source origin, verified boundary, and lifetime. +Unused definitions do not automatically add imports, code, or capabilities. +Required initializer dependencies remain live even without ordinary call edges. + +DEC-06 remains the host-interface decision: a private embedded helper import is +closed inside the component and never becomes a project-specific host ABI. +This proposal extends DEC-09/DEC-10 to admit explicitly reserved target-library +execution storage, while retaining Wasm GC as the only language heap and bounded +ownership for buffers carrying language values. + +Arbitrary object-file relocation, runtime dynamic loading, ABI coercion between +incompatible versions, and shared GC objects across separately built libraries +are not implied by this decision. Artifact composition must respect each chosen +boundary. Additional supported input formats need their own conversion and +verification contracts. + +The formatter implementation now carries one checked core link plan through +emission and composition. Explicit synchronous guest interface composition uses +a checked component graph after source-to-component lowering; it preserves +component-owned memory and canonical ownership rather than applying core memory +reservations to separate components. Its supported entry point and remaining +obligations are recorded in the design and acceptance checklist. Neither slice +establishes arbitrary dynamic component loading or shared GC representations. diff --git a/docs/decision/DEC-19-compiler-generated-resumable-frames.md b/docs/decision/DEC-19-compiler-generated-resumable-frames.md new file mode 100644 index 00000000..64e0d0ae --- /dev/null +++ b/docs/decision/DEC-19-compiler-generated-resumable-frames.md @@ -0,0 +1,114 @@ +# DEC-19 — Compiler-Generated Resumable Frames for Generic Handlers + +**Status:** Accepted +**Date:** 2026-10-11 +**Amends:** [DEC-03](DEC-03-purescript-faithful-type-system.md), the clause that +keeps delimited continuations out of every IR. +**Governed by:** [F-02](../feature/F-02-portable-programs.md), +[delimited control and resumable frames](../design/backend/fp/delimited-control-and-resumable-frames.md), +and [library-owned algebraic effects](../design/backend/fp/algebraic-effects-and-handlers.md). + +## Context and constraints + +A portable program needs a way for library code to define effectful operations +and the handlers that interpret them. [DEC-03](DEC-03-purescript-faithful-type-system.md) +chose to encode effects as a library (`purescript-run` style) and explicitly kept +"effect rows, handlers, or delimited continuations" out of CST, AST, HIR, THIR, +and Typed Core. The compiler-owned `Effect`/`State` mechanism was removed on +2026-10-11, leaving no replacement representation. + +A library-only encoding over a free/freer monad is portable and needs no compiler +change, but it forces every effectful computation through an explicit monadic +value and depends on library trampolining for stack safety. A generic mechanism +that can suspend a computation and capture the remainder — delimited control — +provides compiler-owned continuation sequencing while keeping every concrete +operation, row, and handler in library code. The selected `Control` surface is +monadic; it does not provide direct-style syntax or capture arbitrary pure calls. +[DEC-20](DEC-20-typed-control-elimination.md) clarifies its typed elimination and +clause-scope contract. + +Delimited control has exactly two viable implementations on this target: + +- **Compiler-generated resumable frames.** The compiler lowers the regions that + can suspend into an explicit heap-allocated frame chain (a defunctionalized CPS + / abstract machine) driven by a loop. This uses only features the pinned profile + already enables: Wasm GC, typed function references, and tail calls. +- **Target-native stack switching.** Lower suspension to the WebAssembly + stack-switching proposal (`cont`/`suspend`/`resume`) and let the runtime capture + the guest execution stack. + +A plain host import cannot implement either. A Rust/Wasm function called by the +guest cannot capture the caller's Wasm execution stack; Rust's linear-memory stack +is a different stack, so "runtime support" is not a normal binding. + +## Decision + +Adopt **compiler-generated resumable frames** as the generic, effect-agnostic +handler and interruption mechanism. + +- The compiler owns a small, abstract control surface: a suspension primitive, a + delimiter/handler boundary, and an abstract typed resumption. No pass recognizes + a concrete operation name (`State`, `Console`, `Run`, `Effect`), and the type + system gains no effect rows or handlers. +- The lowering is a **selective, defunctionalized CPS**: only computations that + can reach a suspension are converted; the continuation is an explicit chain of + GC frame objects resumed by a trampoline, so execution is stack-safe + independently of the native stack. +- Concrete operations, rows, and handlers remain in the independent standard + library and user libraries, built on the control surface. The compiler does not + own their meaning. +- **Do not adopt WebAssembly stack switching now.** It is a Phase 3 proposal and + not merged into the core specification. Wasmtime disables it by default + (`Config::wasm_stack_switching = false`), it depends on the `exceptions` + feature, and the pinned profile disables `exceptions`. Revisit it as a + target-profile revision only when it reaches Phase 4/5 and the pinned runtime + enables it by default; until then the artifact contract must not depend on it. + +### Amendment to DEC-03 + +DEC-03's prohibition of delimited continuations in the IRs is narrowed: a +*generic, effect-agnostic* control mechanism may be lowered below Typed Core (in +the backend MIR), and a small abstract control surface may appear at the boundary +that the library builds on. DEC-03's type-system decision is retained: there are +no frontend effect rows, no compiler-known handlers, and no compiler-native effect +system; effects remain a library encoding. No effect tracking or effect reasoning +is added to the type system: the compiler does not infer, propagate, or solve +effect requirements, and does not reason about which effects a function may +perform. + +## Alternatives considered + +- **WebAssembly stack switching (rejected for now).** It is a Phase 3 proposal, + disabled by default in the pinned runtime, depends on a second disabled feature + (`exceptions`), and would tie the artifact contract to a moving proposal and a + non-default runtime configuration. It remains a possible future accelerator + behind an explicit profile revision. +- **Library-only free monad (retained as the portable baseline, not sufficient + alone).** It needs no compiler change and remains valid for pure handlers, but + its stack safety is a library trampoline, not a compiler guarantee. + The selected compiler surface also uses explicit monadic computations; arbitrary + non-monadic interruption is not established by this decision. +- **Host-imported stack capture (rejected as impossible).** A Wasm import cannot + capture its caller's execution stack. +- **A compiler-native effect system (rejected, unchanged from DEC-03).** Adding + effect rows and handlers to the type system is a language change DEC-03 + rejected; this record does not revive it. + +## Consequences + +- The backend gains a new lowering obligation: selective CPS/defunctionalization, + an explicit frame representation, a trampoline, and verification that frame + identity, capture boundaries, and resumption consumption are preserved. Frames + are GC values, so captured roots are collected by the engine. +- The target profile is unchanged: GC, typed function references, and tail calls + already cover the mechanism; no new capability flag is required. +- The native mechanism and the portable library encoding are distinct and must not + be silently interchanged; runtime one-shot and library multi-shot behavior stay + distinguishable. +- Delimited control is powerful and dangerous. Suspension across a non-suspendable + host/foreign frame, multi-shot replay of mutable state, and continuation + lifetime are explicit contracts, not automatic guarantees. +- The design document + [`delimited-control-and-resumable-frames.md`](../design/backend/fp/delimited-control-and-resumable-frames.md) + is the source of truth for the mechanism, its lowering, and the comparison with + stack switching. Implementation follows it. diff --git a/docs/decision/DEC-20-typed-control-elimination.md b/docs/decision/DEC-20-typed-control-elimination.md new file mode 100644 index 00000000..258a0594 --- /dev/null +++ b/docs/decision/DEC-20-typed-control-elimination.md @@ -0,0 +1,49 @@ +# DEC-20 — Typed Source-to-Target Control Elimination + +**Status:** Accepted +**Date:** 2026-10-11 +**Clarifies:** [DEC-19](DEC-19-compiler-generated-resumable-frames.md) +**Governed by:** [F-02](../feature/F-02-portable-programs.md) and +[control elimination and handler scopes](../design/backend/fp/control-elimination-and-handler-scopes.md). + +## Context and constraints + +The draft five-argument `handle` is an open state fold: its clause returns a +source computation, which the driver interprets under the same handler. That +cannot implement fresh clause requests outside the handler while preserving its +source and target types. The retained ClauseScope program exposes the mismatch. +The design also promises library-selectable deep/shallow handlers, multi-shot +replay, delimiter expiry and a flat guest machine. A dispatch patch or mode flag +cannot repair the missing target-answer contract. + +## Decision + +Generalize the existing `Prim.Control.handle` to take a return clause, a rank-2 +operation clause, and a source computation. Its raw continuation returns the +original source computation; both clauses return a distinct target computation. +No new intrinsic is added. State and union projection are ordinary library +closures; deep code reinstalls `handle`, shallow code uses the raw remainder. +Shallow output retains the source effect collection unless library code explicitly +transforms it. The existing `run` remains a separate closed state fold. + +Source dispatch ends before clause execution. Its lifetime extent remains live +until the target answer finishes. Explicit heap contexts preserve this distinction, +caller capture boundaries, and replay activation identities. Tail finalization +coalescing may preserve common completion boundaries but cannot erase independently +observable expiry. The software-frame architecture and target profile of DEC-19 +remain unchanged; the selected source surface is monadic, not direct style. + +## Consequences + +This revises an unpublished draft API: `ControlHandle` keeps identity 96, but its +checked scheme and arity change. Compiler registry, verification, layouts, lowering, +independent package and acceptance cases require a coherent migration and explicit +artifact/package compatibility invalidation. Existing v1 execution evidence is +retained as historical evidence and cannot certify v2 scope semantics. + +Library expressibility is supported by official ordinary-language type checking +and a rewrite model; lifetime, GC, native stack and foreign cleanup still require +real target execution. The full contract is in the linked design; the +[checklist](../implementation/backend/algebraic-effects.md) records migration and +remaining verification. This decision does not add frontend effect typing or +claim acceptance of the upstream Run library. diff --git a/docs/decision/DEC-21-control-machine-construction-boundary.md b/docs/decision/DEC-21-control-machine-construction-boundary.md new file mode 100644 index 00000000..87e14114 --- /dev/null +++ b/docs/decision/DEC-21-control-machine-construction-boundary.md @@ -0,0 +1,89 @@ +# DEC-21 — Control Machine Construction and Verification Boundary + +**Status:** Accepted +**Date:** 2026-10-14 +**Governed by:** [F-02](../feature/F-02-portable-programs.md), +[DEC-16](DEC-16-scalar-strings-and-utf8-storage.md), +[DEC-17](DEC-17-representation-and-evidence.md) and +[DEC-19](DEC-19-compiler-generated-resumable-frames.md). +**Design:** [Control machine](../design/backend/fp/control-machine.md), +[delimited control](../design/backend/fp/delimited-control-and-resumable-frames.md) +and [handler scopes](../design/backend/fp/control-elimination-and-handler-scopes.md). +**Review:** [Delimited control lowering review, 2026-10-14](../implementation/backend/delimited-control-lowering-review-2026-10-14.md). + +## Context and constraints + +P8 currently expands each control primitive into ordinary CC plus a +`ControlEvidence` witness. The CC and MIR machine verifiers then recover the +machine's meaning from emitted projections, calls, casts, aliases, dispatch +paths and private-cell writes. Shared relations reduce duplication, but the +recognizers are coupled to the exact instruction forms that survive +optimization: every change in expansion or in a generic rewrite becomes a new +recognition obligation. The witness proves a factory occurrence, not the whole +generated executor action. + +There is no single named authority for the machine schema or for the set of +required transition actions. Frame cases and fields are declared positionally +and their offsets recur in layout, lowering and verification; queue, context, +extent and status roles are inferred from helper signature positions. Machine +meaning and ordinary IR validity are therefore validated by the same mechanism, +which does not by itself establish semantic preservation across optimization. + +The machine must remain a software-frame implementation of +[DEC-19](DEC-19-compiler-generated-resumable-frames.md): a persistent +sequencing spine, one extent per delimiter (with a permitted tail-finalization +coalescing group), and a flat compiler-generated guest loop. The source surface +remains the monadic one selected by [DEC-20](DEC-20-typed-control-elimination.md). + +## Decision + +Machine semantics are discharged during P8 construction at an explicit boundary, +not reconstructed after optimization. + +1. One **named machine schema** owns the machine's cases, fields, helper roles + and fixed signatures. Physical layout planning, P8 generation and every + verifier consume it. Semantic roles are not recovered from helper parameter + offsets or variant field positions. +2. P8 builds a temporary, effect-agnostic **checked construction plan** with + named operands, selected cases, projections, ordered + allocation/read/write/check/close/apply operations and exactly one terminal + outcome (`Advance`, `Publish` or `Reject`). The plan retains Core's checked + domain identities at source and callback boundaries and is checked against + the operational semantics before emission. +3. A **boundary checker** compares the emitted ordinary CC actions and their + actual exits against the checked plan, accounting for guards and exceptional + prefixes. An action identity tag alone is not a body proof. +4. After the boundary, ordinary P9 representation lowering and P10 optimizations + are part of the compiler's trusted implementation. They must preserve + ordinary call, mutation, trap and identity semantics and are validated by + their ordinary IR invariants and behavior-preservation tests. SSA/layout + validity is not treated as a semantic-preservation proof, and no certificate + may authorize instructions modified after it was issued. +5. P9 keeps the representation, capture, CFG and loopification contracts it + already owns. Facts explicitly discharged at the boundary are not re-derived; + P9 checks the facts it still owns rather than re-recognizing machine actions. + +This changes the cross-stage assurance contract, not the selected runtime +semantics. The plan is not a whole-program Control IR, a runtime interpreter or +a source effect system. No runtime type reflection or concrete operation name is +introduced; boundary freshness and source/checkpoint provenance are obligations +of the plan, not annotations the emitter may assert. + +## Consequences + +- The emitter/boundary checker and the ordinary P9/P10 passes become trusted + implementation, as for ordinary closures and mutable data. This is not a + formal correctness proof and no pass-level equivalence proof is claimed. +- Existing CC/MIR semantic checks remain in force during migration. A check may + be removed only after every positive and malformed-input case is mapped to its + new owner and the newly trusted transformation has optimized Wasm behavior + evidence. Negative coverage is not deleted to make removal easier. +- The schema and registry are independently checked against the operational + semantics; generating the emitter and its checker from one unchecked field + list would only duplicate the same defect. +- Source typing, payload adapters and closure protocols keep their existing + owners. Foreign borrowed-resource lifetime and caught-host-trap recovery remain + separate unresolved contracts and are not discharged here. +- The required sequencing representation keeps the precedent of *Reflection + without Remorse*; this decision does not change queue shape, replay cost or + Finish coalescing preconditions, which stay separately specified. diff --git a/docs/design/D-04-suite-roadmap.md b/docs/design/D-04-suite-roadmap.md index d47cdbfc..4a8ea6db 100644 --- a/docs/design/D-04-suite-roadmap.md +++ b/docs/design/D-04-suite-roadmap.md @@ -3,6 +3,15 @@ **Implements:** [F-02 — Build Portable Program Artifacts](../feature/F-02-portable-programs.md) **Status:** In progress +> **Withdrawn capability (2026-10-11).** The compiler-owned Effect and State +> mechanisms described below were removed: the trusted `Effect` synthesis, the +> `Prim.State` primitives, the `runWorld`/`runRegion` intrinsics, the Core/CC/MIR +> State analysis, and the runtime storage source-binding contract. The `Effects` +> gate row and the `BE-21` `Effect Unit` entry capability no longer describe the +> compiler. No replacement Effect representation has been selected, so any +> measurement whose cases used either mechanism is historical. See the +> [removal handoff](../implementation/backend/effect-state-removal-handoff-2026-10-11.md). + ## Purpose Turn the official PureScript test suite into an operational scoreboard for the @@ -390,6 +399,15 @@ fails, and a type synonym for the record fails identically. Changing the argumen shape would change the API the corpus calls, so the two functions stay out and the defect is filed as #137. That leaves 2 `passing` cases blocked on them. +**Latest full-board remeasurement (2026-10-07, annotations oracle):** M2 +failing agreement is **72/72**. Shared explicit import qualifiers combine names +by declaration identity; an ambiguous member of a pseudo-module re-export +reports `ScopeConflict`, so `failing/ConflictingQualifiedImports2.purs` agrees +without rejecting the official `Data.List` shared `as Exports` imports. +Passing modules resolve in **403/413** cases; the other 10 stop at P3 (6) or P0 +(4), with no harness blockers. Nineteen sibling modules load successfully, and no +case is blocked because the loader cannot use an imported sibling. + ### M3 — Kinds and higher-kinded types - **Suite:** `KindsDoNotUnify` (24), `PartiallyAppliedSynonym` (12), @@ -422,15 +440,11 @@ environment, which owns `ClassInstanceArityMismatch`. The driver exposes a lenient kind check and the `l3` scoreboard; the scoreboard also runs against the vendored corpus without `purs`. -**Measured current result (annotations oracle, 2026-10-04):** M3 failing -agreement is **35/48**. Per code: `CycleInKindDeclaration` 2/2, -`InfiniteKind` 2/2, `CycleInTypeSynonym` 3/4, `UndefinedTypeVariable` 3/4, -`PartiallyAppliedSynonym` 10/12, and `KindsDoNotUnify` 15/24. Thirteen expected -diagnostics still differ: some are blocked by absent cross-module libraries -such as `Data.Foldable`, `Data.Newtype`, `Effect.Console`, `Safe.Coerce`, or -`Prim.*`; the rest need kind checking in expressions, polykinded instantiation, -type-level row functions, or local scoped variables. The scoreboard output records -each case. +**Measured current result (2026-10-04, remeasured with the full boards):** M3 +failing agreement is **39/48**. Per code: `CycleInKindDeclaration` 2/2, +`InfiniteKind` 2/2, `CycleInTypeSynonym` 4/4, `UndefinedTypeVariable` 3/4, +`PartiallyAppliedSynonym` 12/12, and `KindsDoNotUnify` 16/24. The scoreboard +output records each remaining mismatch. `failing/DiffKindsSameName.purs` now agrees, and it is the case the single program-level environment was for: kind checking ran twice before, a program-level pass whose diagnostics were discarded and a per-module pass that gave an imported @@ -470,13 +484,13 @@ measurable until Phase 3 provides those modules. - **Acceptance:** Agreement on the `errorCode`s above. - **Prerequisite:** M3 and M6. -**Measured current result (2026-10-04, annotations oracle):** **35/50** failing -cases agree, per code: `TypesDoNotUnify` 32/41, `IntOutOfRange` 1/1, -`InfiniteType` 2/2, `CannotApplyExpressionOfTypeOnType` 1/2, `EscapedSkolem` -0/2, `ExpectedType` 0/2, and `AmbiguousTypeVariables` 0/1. `HoleInferredType` -has no mapped kind and contributes no case. The `<>` slice moves the aggregate -from 34/50: the cases that `<>` unblocked now reach type checking, and -`TypesDoNotUnify` rises from 29/41 to 32/41 as three of them report that code. +**Measured current result (2026-10-04, remeasured with the full boards):** +**39/50** failing cases agree. The scoreboard's aggregate and per-code counts +disagree by one case, so this remeasurement does not publish a per-code +decomposition. `HoleInferredType` has no mapped kind and contributes no case. +The `<>` slice moved the aggregate from 34/50: the cases that `<>` unblocked +now reach type checking, and `TypesDoNotUnify` rose from 29/41 to 32/41 as +three of them reported that code. #87 moves M4 from 30/47 to 32/50. The denominator grew by three because the visible type application files now reach type checking instead of stopping at P2, @@ -502,7 +516,7 @@ That change moved no count, which is the point worth keeping: `30/47` was true before it and true after it, and the board cannot distinguish a case that agrees because the compiler is right from one that agrees because a defect happened to emit the right code. Read the aggregate as a floor on agreement, not as -agreement. The current result is 35/50, and the same caution applies to it. +agreement. The later 35/50 measurement had the same limitation. The aggregate counts distinct cases, while per-code totals count expected annotations: `failing/MultipleErrors.purs` declares `TypesDoNotUnify` twice, so @@ -577,13 +591,20 @@ obligation to FE-13, and accepted mismatches still need type-checking fixes. - **Acceptance:** Agreement on the `errorCode`s above. - **Prerequisite:** M4. -**Measured current result (2026-10-04, annotations oracle, remeasured with the -Effect-entry run):** **53/80** failing cases agree. An earlier headline said -53/84; the per-code totals sum to 80, and this run confirms 80. Per-code agreement is -`OverlappingInstances` 8/8, `NoInstanceFound` 41/52, `MissingClassMember` 2/2, -`DuplicateInstance` 1/1, `InvalidInstanceHead` 1/5, and 0 for -`PossiblyInfiniteInstance` (1), `OrphanInstance` (6), `InvalidNewtypeInstance` -(1), `DuplicateTypeClass` (1), and `CannotDeriveInvalidConstructorArg` (7). +**Measured current result (2026-10-17, `886cd82c`, annotations oracle, vendored corpus):** **77/97** +failing cases agree. Per-code agreement is `OverlappingInstances` 8/8, +`NoInstanceFound` 48/53, `MissingClassMember` 2/2, +`InvalidNewtypeInstance` 6/6, `InvalidInstanceHead` 1/7, +`DuplicateInstance` 1/1, `ClassInstanceArityMismatch` 4/4, and +`CannotDeriveInvalidConstructorArg` 7/7. The remaining measured codes are +`PossiblyInfiniteInstance` 0/1, `OrphanInstance` 0/7, and +`DuplicateTypeClass` 0/1. Eleven cases are blocked before a mapped diagnostic: +five P2 expression cases, four P2 hole cases, and two superclass-cycle cases. +The four arity cases now report their official code at the common instance +recorder; all structural deriving families have source and execution evidence. +The 2026-10-06 result was 75/97, with `NoInstanceFound` 46/53. The 2026-10-04 result was 58/81. Denominators reflect cases with their +own mapped diagnostics; newly attributable deriving and arity cases enter the +board rather than being read as regressions. The historical 51/92 figure below predates `Eq`/`Ord`/`Semiring` and this slice; the 84 is the set of cases whose annotations are entirely M5 codes on this tree. The `<>` slice raises `NoInstanceFound` @@ -718,18 +739,31 @@ the code reference and an immutable capture array. file. - **Prerequisite:** M2–M6. -**Progress (measured by `runtime::l6_runtime_scoreboard`):** **124 of 413** -non-FFI `passing` files compile, validate, and run, with 26 excluded as FFI. -All 124 exit 0 and print a first stdout line. They are the same 124 files whose -previous first blocker was a `main` that was not a zero-argument `Int`. The 63 -files with no selected `main` stay blocked at P10. This measurement does not -emit an empty main and does not change the 413 denominator. The board compiles -each case with the on-disk standard library on the module path. Separate +**Progress (measured 2026-10-17 by `runtime::l6_runtime_scoreboard` at `886cd82c`):** **273 of 413** +non-FFI `passing` files compile, validate, and run; all 273 exit 0. The other 140 do not agree. The gate table below records the stage split. Twenty-six FFI files stay excluded. + +**Earlier progress:** **210 of 413** +non-FFI `passing` files compile, validate, and run; all exit 0. Twenty-six FFI +files are excluded. Of the other 203 cases, 201 block before runtime and 2 +trap while running. The largest current blockers are P5 typechecking (68), P10 +files with no selected `main` (47), P3 resolution (23), and P5 kind checking +(16). This measurement does not emit an empty main or change the 413 +denominator. The board compiles each case with the on-disk standard library on +the module path. Separate vertical execution tests run under mandatory Wasmtime for GC strings, arrays, closed records, erased newtypes, parameterized ADTs, closures, dictionaries, effects, the component path, and pattern-matrix behavior including the value-sensitive `1185.purs` and `2049.purs` shapes. +The 46-case move from the earlier 164 measurement combines the +[abstract-constructor representation policy](backend/fp/polymorphism-and-erasure.md) +and its indirect partial-application path. Recovering a stored closure protocol +and generating an adapter, instead of casting the erased closure onto the +consumer signature, cleared 29 of the 30 P8 CC verifier failures and 17 of the +19 runtime traps; allowing an under-applied local closure or dictionary method +to expose its remaining parameter cleared three more P8 closure-conversion +failures. Five of the recovered cases then surfaced at a later blocker. + Agreement here means the pipeline compiles the file, the component passes Wasm validation, and the guest runs to completion without trapping. The corpus vendors no execution goldens and upstream's `passing` suite is a compile-time @@ -739,14 +773,29 @@ failure must reach the guest as a trap to be visible, which is the only execution signal the corpus can express. Nothing in the corpus needs argv, stdin, or a preopened directory, so the runner passes none. -The 289 rejections, by the first phase that blocks them. The current figures come -from one `PSRS_REQUIRE_WASMTIME=1 PSRS_ORACLE=annotations` run of all five boards -on 2026-10-04 (Wasmtime 49.0.2, `purs` 0.15.16). L1–L5 did not move in that run. +The 203 non-agreements, by the first phase that blocks them or runtime outcome. +These are from the latest `PSRS_REQUIRE_WASMTIME=1 PSRS_ORACLE=annotations` run +of all five boards on 2026-10-05 (Wasmtime 49.0.2, `purs` 0.15.16): + +| Blocker | Cases | Recovered by | +| --- | --- | --- | +| P5 typecheck | 68 | Type and class inference gaps behind earlier-stage blockers. | +| P10 Wasm structuring | 47 | No selected `main`. | +| P3 resolve | 23 | Remaining name and import resolution gaps. | +| P5 kind check | 16 | Kind checking gaps. | +| P8 closure conversion | 15 | Unsupported or inconsistent runtime representations. | +| P0 lex | 4 | DEC-16 lone-surrogate cases, also recorded as L1 differences. | +| P7 Core verification | 3 | A typed Core expression has an inconsistent context type. | +| P8 CC verification | 1 | A call whose arguments do not match its signature. | +| Harness loading | 24 | Multi-module corpus inputs the current runner cannot assemble. | +| Runtime trap | 2 | The compiled guest traps under Wasmtime. | +| P2 surface lowering | 0 | No `passing` file stops in surface lowering. | + The tables after the current one are earlier measurements and are **not** additive with it or with each other; they are kept because the M2 paragraphs cite them. -After the `Effect Unit` command entry (the same run): +Earlier, after the `Effect Unit` command entry: | Blocker | Cases | Recovered by | | --- | --- | --- | @@ -841,7 +890,8 @@ The failure path is now landed rather than assumed: `Prelude.trap` is a message and then escapes through it, and the vertical tests assert the trap rather than an exit code (`PSRS_REQUIRE_WASMTIME=1 cargo test -p psrs-driver --lib tests::assertions`). That measurement's board was still 0/413. The -current board is the 125/413 table above. +current board is the 273/413 row in the gate table. The 164/413 and 210/413 +figures in this section are earlier measurements. The L2 run reports 19 sibling modules loaded and no case blocked because the loader could not use an on-disk sibling. Before #86, two such cases were @@ -1050,15 +1100,15 @@ for matrix status. | --- | --- | --- | --- | | L0 | Layout goldens | 15/15 official parse outcomes agree (12 accepted, 3 rejected), enforced by regression tests. | 15/15 agreement, with all layout cases covered by regression tests. | | L1 | Non-excluded parse behavior | 904/908 agreement using the annotations oracle; `passing` 410/413, `failing` 412/413, `warning` 67/67, `layout` 15/15, with the four remaining cases recorded as DEC-16 intentional differences | 100% agreement apart from the DEC-16 intentional differences. | -| L2 | Module, import, export, and name resolution | 72/72 failing cases. `passing` resolution is **276/413**, with 53 first-stage blockers on a missing module and 80 at P3; none at P2, and none blocked on assembly. One `PSRS_ORACLE=annotations` run of all boards on 2026-10-04 after `Data.Functor`. | The mapped resolution cases and all required passing-module cases agree. | -| L3 | Kinds and higher-kinded types | 36/48 failing cases (`KindsDoNotUnify` 16/24, `PartiallyAppliedSynonym` 10/12, and the other mapped code totals as measured in M3). `failing/3549.purs` now agrees. | 100% agreement for the mapped kind cases. | -| L4 | Core type checking | 35/50 failing cases; `TypesDoNotUnify` 32/41, `IntOutOfRange` 1/1, `InfiniteType` 2/2, `CannotApplyExpressionOfTypeOnType` 1/2, `EscapedSkolem` 0/2, `ExpectedType` 0/2, `AmbiguousTypeVariables` 0/1. | 100% agreement for the mapped type cases. | -| L5 | Classes and instances | 53/79 failing cases; `OverlappingInstances` 8/8, `NoInstanceFound` 41/52, `MissingClassMember` 2/2, `DuplicateInstance` 1/1, `InvalidInstanceHead` 1/5, and 0 for the other mapped codes. The per-code denominators sum to 79. `failing/NewtypeInstance5.purs` is no longer in this board. | 100% agreement for the mapped class cases. | -| L6/M7 | Runtime and standard library | 125/413 non-FFI passing files compile, validate, and run, all with exit code 0. Of the other 288, 53 stop on missing modules, 63 at P10 because no `main` was selected, 80 at P3, 49 at P5 typecheck, 17 at P5 kind checking, 16 at P8, 6 at P6, and 4 at P0; no P2 surface-lowering blockers and no harness-loading blockers. One `PSRS_REQUIRE_WASMTIME=1 PSRS_ORACLE=annotations` run of all boards on 2026-10-04 after `Data.Functor` (Wasmtime 49.0.2, `purs` 0.15.16). | Every in-scope passing file for the feature compiles, validates, and runs with the expected result. | +| L2 | Module, import, export, and name resolution | **72/72** failing cases. `passing` resolution is **403/413**; the remaining 10 stop at P3 (6) or P0 (4), with no harness, missing-library, or unusable-sibling blockers. Remeasured with the annotations oracle on 2026-10-07 after the shared-import-qualifier repair. | The mapped resolution cases and all required passing-module cases agree. | +| L3 | Kinds and higher-kinded types | 39/48 failing cases: `KindsDoNotUnify` 16/24, `PartiallyAppliedSynonym` 12/12, `CycleInTypeSynonym` 4/4, `CycleInKindDeclaration` 2/2, `InfiniteKind` 2/2, and `UndefinedTypeVariable` 3/4. | 100% agreement for the mapped kind cases. | +| L4 | Core type checking | 39/50 failing cases. The remaining mismatches include kind diagnostics reported in place of `ExpectedType`, missing `EscapedSkolem`, `VisibleTypeApplications1`, and five `Coercible` cases reported as `NoInstanceFound`. | 100% agreement for the mapped type cases. | +| L5 | Classes and instances | 77/97 failing cases: `OverlappingInstances` 8/8, `NoInstanceFound` 48/53, `MissingClassMember` 2/2, `InvalidNewtypeInstance` 6/6, `DuplicateInstance` 1/1, `InvalidInstanceHead` 1/7, `ClassInstanceArityMismatch` 4/4, `CannotDeriveInvalidConstructorArg` 7/7; `PossiblyInfiniteInstance` 0/1, `OrphanInstance` 0/7, `DuplicateTypeClass` 0/1. Remeasured with the annotations oracle on 2026-10-17 (`886cd82c`, vendored corpus). The 2026-10-06 result was 75/97. | 100% agreement for the mapped class cases. | +| L6/M7 | Runtime and standard library | **273/413** non-FFI passing files compile, validate, and run, all with exit code 0. No case traps. The other 140 do not agree: 69 fail while linking a library foreign (`Data.Symbol.unsafeCoerce` 66, `Record.Unsafe.unsafeGet` 2, `Test.Assert.checkThrows` 1), 34 stop at P5 typecheck, 13 at P8 closure conversion, 11 at P5 kind check, 6 at P3, 4 at P0, 1 at P7 command runner (`Sequence.purs`), 1 times out at runtime (`2288.purs`, 10s), and `BigFunction.purs` is killed before a diagnostic. Remeasured on 2026-10-17 at `886cd82c` against the vendored corpus (Wasmtime 49.0.3, `purs` 0.15.16). Each file ran in its own scoreboard process. The 2026-10-04 result was 164/413. | Every in-scope passing file for the feature compiles, validates, and runs with the expected result. | | M8-W | Warnings | 67 non-FFI warning files are in scope; no warning-code scoreboard exists | Warning-code agreement reaches 100% for the tracked warning corpus. | | M8-O | Optimization | 10 optimize files are in scope; they are not vendored and their goldens are JavaScript output | Expected optimize/CoreFn output agrees for all tracked optimize files. | -The gate rows above are the 2026-10-04 measurement after `Data.Functor`. Earlier M7 tables in the progress section record the `logShow`, `Show`, and Foldable runs; those figures are historical and are not added to this table. L5 is 53/79. An older headline of 53/84 does not match the per-code totals. +L1–L4 were reconfirmed on 2026-10-17 at `886cd82c` against the vendored corpus (`PSRS_ORACLE=annotations`, `purs` 0.15.16): L1 904/908, L2 72/72 and passing resolution 403/413, L3 39/48, L4 39/50. L5 moved from 75/97 to 77/97 the same day. L6/M7 moved from 164/413 to 273/413. Earlier M7 tables in the progress section record the `logShow`, `Show`, Foldable, and `Data.Functor` runs; those figures are historical and are not added to this table. The current L5 denominator is 97. ### Feature-to-gate crosswalk @@ -1076,6 +1126,7 @@ when its local implementation tests pass. | FE-19 | Non-FFI suite cases plus WIT-specific tests | JavaScript FFI remains excluded. | | FE-20 | L0–L5 diagnostics and M8-W | Error-code and warning-code scoreboards. | | FE-21 | M8-O and backend Core/MIR tests | Optimize output and semantics-preservation tests. | +| FE-22 | L5 and the corresponding L6/M7 cases | Deriving differential and execution tests; [deriving acceptance record](../implementation/frontend/deriving.md). | | BE-01–BE-11 | L6/M7 for source-visible behavior | CC/MIR lowering, representation, and execution tests. | | BE-12 | M8-O | Core/MIR optimization and output comparison. | | BE-13–BE-16 | L6/M7 for generated artifacts | Wasm feature-profile, validation, WAT, and runtime tests. | @@ -1095,9 +1146,9 @@ resolved, type checked, and represented in Typed Core as required. | ID | Feature | Current support | Status | Next landing | | --- | --- | --- | --- | --- | | FE-01 | Lexing, Unicode tokens, comments, literals, and layout | Lexer and layout agree with the L1 annotations scoreboard at 904/908, including 15/15 layout cases. The four differences are the DEC-16 intentional differences: a supplementary scalar is accepted as one `Char` (`failing/2434.purs`), and an unpaired surrogate escape is rejected in `StringEscapes.purs` and the two `StringEdgeCases` files. A paired surrogate escape decodes as one scalar, and no surrogate becomes U+FFFD. Parse agreement does not verify string values. | Partial | Cover the remaining literal forms the corpus exercises. | -| FE-02 | Module headers, imports, exports, qualified names, aliases, and hiding | Module graph, stable module IDs, value/type/constructor/class imports and exports, fixity aliases, virtual `Prim.*` type/class interfaces, instance dictionary identities, per-branch instance exports, and unary minus through ordinary `negate` resolution work in a subset; 72/72 mapped failing cases agree. `passing` resolution is **276/413**, with 53 first-stage blockers on a missing module and 80 at P3, from the 2026-10-04 `Data.Functor` remeasurement. A re-exported operator alias carries its target's identity and does not require the target's name unless the target is declared in the re-exporting module. Class-only imports do not import methods into the value namespace; selective imports still receive visible instances through the module dependency graph. P3 checks explicit signatures and declaration dependencies; P5 checks inferred public schemes by stable type identity. `Prim.undefined` has a compiler-owned identity, type, and interface export, but Core lowering still rejects it because no runtime representation is defined. The [primitives topic](frontend/type-system/prim.md) owns the `Prim.*` inventory, the evidence-class dispatch order, relation outcomes, and diagnostic behavior; #120 adds the missing relation and report paths. Broader pattern-binding support remains incomplete. | Partial | Complete pattern-binding support; add the `Prim.undefined` runtime representation and continue official-suite coverage for primitive solving. | +| FE-02 | Module headers, imports, exports, qualified names, aliases, and hiding | Module graph, stable module IDs, value/type/constructor/class imports and exports, fixity aliases, virtual `Prim.*` type/class interfaces, instance dictionary identities, per-branch instance exports, and unary minus through ordinary `negate` resolution work in a subset; the 2026-10-07 annotations run agrees on **72/72** mapped failing cases. `passing` resolution is **403/413**; 6 cases stop at P3 and 4 at P0, with no harness blockers. Shared qualifiers combine disjoint members and preserve canonical declaration identity; ambiguous qualified lookup or pseudo-module re-export reports `ScopeConflict`. A re-exported operator alias carries its target's identity and does not require the target's name unless the target is declared in the re-exporting module. Class-only imports do not import methods into the value namespace; selective imports still receive visible instances through the module dependency graph. P3 checks explicit signatures and declaration dependencies; P5 checks inferred public schemes by stable type identity. `Prim.undefined` has a compiler-owned identity, type, and interface export, but Core lowering still rejects it because no runtime representation is defined. The [primitives topic](frontend/type-system/prim.md) owns the `Prim.*` inventory, the evidence-class dispatch order, relation outcomes, and diagnostic behavior; #120 adds the missing relation and report paths. Broader pattern-binding support remains incomplete. | Partial | Complete pattern-binding support; add the `Prim.undefined` runtime representation and continue official-suite coverage for primitive solving. | | FE-03 | Value declarations, signatures, recursive groups, pattern bindings, and `where` | Named declarations, signatures, recursive local groups, and top-level SCC inference work; selected local pattern declarations, including `LetPattern`, lower through the pattern pipeline. The full declaration and `where` forms are not end-to-end. | Partial | Complete remaining pattern declarations and local `where` blocks. | -| FE-04 | Declaration forms: `data`, `newtype`, `type`, `class`, `instance`, `derive`, `foreign`, roles, fixities, and kind signatures | Data/newtype roles are inferred and checked, foreign role signatures enter the checked kind environment, and source role errors retain spans. Instance declarations resolve into dictionary-scoped members; signatures associate with consecutive equations, reject orphan/repeated declaration groups, and check against the class method specialized by the instance head. Deriving and several declaration forms remain incomplete. | Partial | Complete deriving and the remaining declaration-form semantics. | +| FE-04 | Declaration forms: `data`, `newtype`, `type`, `class`, `instance`, `derive`, `foreign`, roles, fixities, and kind signatures | Data/newtype roles are inferred and checked, foreign role signatures enter the checked kind environment, and source role errors retain spans. Instance declarations resolve into dictionary-scoped members; signatures associate with consecutive equations, reject orphan/repeated declaration groups, and check against the class method specialized by the instance head. Deriving and several declaration forms remain incomplete. | Partial | Complete the remaining declaration-form semantics; deriving is tracked under FE-22. | | FE-05 | Expressions: application, operators, lambdas, `if`, `let`, `case`, records, arrays, literals, sections, `do`, and `ado` | Application, value and type operators with resolved fixities, the `Data.Function` application operators `$` and `#` with their official associativity and precedence, unary minus through the ordinary in-scope `negate` value, lambdas, `if`, `let`, `case`, scalar arrays, empty array literals whose element type is determined, records, and selected literals work; `do`/`ado` lower to bind, discard, and `let`. The ascription `e :: T` is checked against its written type and remains explicit through Typed Core. Sections lower through P4 and have runtime coverage. Remaining literal and expression forms are open. | Partial | Complete the remaining literal and expression forms. | | FE-06 | Patterns: variables, wildcards, constructors, records, literals, tuples, arrays, guards, and binders | Variables, wildcards, multi-field constructors (`passing/1185.purs`), nested named record patterns (`passing/2049.purs`), literals, arrays, typed binders, operator patterns, tuple products, and local pattern declarations reach Typed Core and the shared pattern matrix. The 34 fixed pattern blockers lower through P2; value-sensitive source-shaped executions select the expected fields for both 1185 (85) and 2049 (84), and the matrix suite covers scalar, array, string, char, Number, record, and guard first-match behavior. Guard coverage provenance and guarded Boolean alternative warnings have source and matrix evidence under PM-14; the full L4/L6 feature gates remain open, and no `passing` file stops in surface lowering. | Partial | Complete broader official type/runtime coverage. | | FE-07 | Operators, sections, fixity declarations, and type/value operators | P2 retains unresolved value, constructor-pattern, and type operator chains and both section forms; P3 binds value/type aliases and attaches fixities; P4 reassociates the chains and expands sections. Official operator-alias failures agree and focused runtime cases cover custom associativity, precedence, constructor patterns, and both sections. Builtin `Prim.Function` and `Prim.Int` type-operator aliases retain identity through module re-exports; `Prim.Int` overapplication reaches the kind arity check. Full type checking still depends on cross-module higher-kinded schemes. | Partial | Complete surrounding type/runtime coverage. | @@ -1107,19 +1158,20 @@ resolved, type checked, and represented in Typed Core as required. | FE-11 | Kinds, kind signatures, higher-kinded types, kind annotations, and kind variables | Dedicated kind inference/checking covers several declarations, annotations, records/rows, and official kind errors. Kind checking runs once per program and produces the checked kind and role environment every module's type check consumes, with each diagnostic attributed to the module that declares the offending type and a missing scheme reported instead of inferred; one primitive table is the only reading of a primitive's kind, and the `Coercible` solver consumes that shared denotation and kind solver. A declaration's scheme quantifies only the kind unknowns its own definition leaves undetermined, but the well-scoped-quantification rule (`QuantificationCheckFailureInKind`) is not implemented, and an instance head whose class is declared in another module is still skipped. | Partial | Add the well-scoped-quantification rule, cross-module instance heads, kind checking in expressions, and type-level row functions. | | FE-12 | Algebraic data types, constructors, newtypes, and constructor typing | Data/newtype declarations, constructor schemes, constructor application, and basic case typing work. | Partial | Add full recursive/parameterized checking, exhaustiveness, and all pattern forms. | | FE-13 | Records, row types, row polymorphism, and variants | Closed concrete records, field access/update, partial source record patterns, exact generated product patterns, and row unification work. The row normalizer returns collected fields and tail or an `InvalidShape` diagnostic; it does not treat an unrecognized shape as a closed row. The `Prim.Row` and `Prim.RowList` relations use that normalizer and the shared kind solver. #87 tracks row syntax under FE-17; #120 covers the primitive row constraints under FE-14. Rigid-tail row unification still has known bugs. Labels are scalar sequences under [DEC-16](../decision/DEC-16-scalar-strings-and-utf8-storage.md); the current compiler still uses Rust strings and does not preserve lone surrogates. A label that exists only in an unknown tail is not accessed or updated. Open rows still have no runtime layout, and variants are not implemented. | Partial | Fix rigid-tail row unification; complete row syntax and official coverage, then add variants without choosing a runtime field layout. | -| FE-14 | Constraints, type classes, superclasses, class members, and instances | Source constraint elaboration, contextual and multi-parameter instances, superclass evidence, imported generic dictionaries, ordered source instance chains, and rank-1 polymorphic method signatures execute. Superclass edges use `TypeTemplate` substitution, including constructed arguments such as `C (Array a)`; `a_superclass_edge_over_a_constructed_argument_runs_when_wasmtime_is_available` executes the `Gamma (Array a)` edge through an `Epsilon Int` use. Instance member signatures are associated with consecutive equations, resolve instance-head variables, and are checked against specialized class method types; source and Wasmtime tests cover specialized and more-general signatures plus mismatches. A constrained instance-member annotation cannot yet be solved solely to specialize it to a monomorphic expected method type. Duplicate member groups, orphan member signatures, duplicate named instances, and ordinary-value/name collisions use official diagnostics. Explicit export lists filter instance branches by class/head/context visibility while preserving identity and chain positions; imports do not require the class in their selective list to receive visible instances. Class-only imports keep methods out of the ordinary value namespace. Constrained-forall expression ascriptions are checked in a complete rollback probe, then elaborated at their expected use type; ordinary class dictionaries survive both monomorphic and rank-N uses, with actual Wasmtime execution evidence. #120 adds compiler-owned rules for the twelve `Prim.Row`, `Prim.RowList`, `Prim.Symbol`, and `Prim.Int` relations and a report path for `Prim.TypeError.Fail`, `Warn`, and `Partial`. Open-row `Lacks` and `Union` preserve partial evidence and re-enter solving on residual constraints; the outcome contract belongs to each rule, while the shared framework checks returned dictionary arguments and bounds speculative work. `Warn` propagates through explicit scoped dictionaries; otherwise it emits at the enclosing value or instance declaration and discharges, so inferred schemes do not retain a `Warn` context; `Fail` renders a valid `Doc` as a custom error, while malformed `Doc` falls back to `NoInstanceFound`. `Partial` still lacks HIR exhaustiveness metadata and therefore reports the generic no-instance message. Five `Coercible` board mismatches remain because the proof evidence has no relation arguments for the shared dictionary verification path. The [primitives topic](frontend/type-system/prim.md) owns these semantics and their measured coverage. A wildcard in an instance head is rejected as `InvalidInstanceHead`; a wildcard in an instance context stays legal because the constraint is still solvable. Scoped method-local constraints and quantified method parameters have evidence in the [rank-N acceptance record](../implementation/frontend/rank-n.md). The standard library now declares its first classes: `Data.Semigroup` defines `Semigroup`, its `append` method, and the `<>` alias, with `String`, `Unit`, and `Array a` instances resolved across the module graph and re-exported from `Prelude`. `Data.Monoid` adds `mempty` for those three types, `Data.Show` adds the `Show` class and `show` for `Boolean`, `Int`, `Number`, `Char`, `String`, `Unit`, and `Array a`, and `Data.Foldable` adds `foldr`, `foldl`, and `foldMap` for `Array`, `Maybe`, and `Either a`. `fold` is not part of that surface yet, because a polymorphic `foldMap` of the identity does not lower. | Partial | Reconcile remaining class-rule mismatches and official-suite coverage; deriving is tracked under FE-16. | +| FE-14 | Constraints, type classes, superclasses, class members, and instances | Source constraint elaboration, contextual and multi-parameter instances, superclass evidence, imported generic dictionaries, ordered source instance chains, and rank-1 polymorphic method signatures execute. Superclass edges use `TypeTemplate` substitution, including constructed arguments such as `C (Array a)`; `a_superclass_edge_over_a_constructed_argument_runs_when_wasmtime_is_available` executes the `Gamma (Array a)` edge through an `Epsilon Int` use. Instance member signatures are associated with consecutive equations, resolve instance-head variables, and are checked against specialized class method types; source and Wasmtime tests cover specialized and more-general signatures plus mismatches. A constrained instance-member annotation cannot yet be solved solely to specialize it to a monomorphic expected method type. Duplicate member groups, orphan member signatures, duplicate named instances, and ordinary-value/name collisions use official diagnostics. Explicit export lists filter instance branches by class/head/context visibility while preserving identity and chain positions; imports do not require the class in their selective list to receive visible instances. Class-only imports keep methods out of the ordinary value namespace. Constrained-forall expression ascriptions are checked in a complete rollback probe, then elaborated at their expected use type; ordinary class dictionaries survive both monomorphic and rank-N uses, with actual Wasmtime execution evidence. #120 adds compiler-owned rules for the twelve `Prim.Row`, `Prim.RowList`, `Prim.Symbol`, and `Prim.Int` relations and a report path for `Prim.TypeError.Fail`, `Warn`, and `Partial`. Open-row `Lacks` and `Union` preserve partial evidence and re-enter solving on residual constraints; the outcome contract belongs to each rule, while the shared framework checks returned dictionary arguments and bounds speculative work. `Warn` propagates through explicit scoped dictionaries; otherwise it emits at the enclosing value or instance declaration and discharges, so inferred schemes do not retain a `Warn` context; `Fail` renders a valid `Doc` as a custom error, while malformed `Doc` falls back to `NoInstanceFound`. `Partial` still lacks HIR exhaustiveness metadata and therefore reports the generic no-instance message. Five `Coercible` board mismatches remain because the proof evidence has no relation arguments for the shared dictionary verification path. The [primitives topic](frontend/type-system/prim.md) owns these semantics and their measured coverage. A wildcard in an instance head is rejected as `InvalidInstanceHead`; a wildcard in an instance context stays legal because the constraint is still solvable. Scoped method-local constraints and quantified method parameters have evidence in the [rank-N acceptance record](../implementation/frontend/rank-n.md). The standard library now declares its first classes: `Data.Semigroup` defines `Semigroup`, its `append` method, and the `<>` alias, with `String`, `Unit`, and `Array a` instances resolved across the module graph and re-exported from `Prelude`. `Data.Monoid` adds `mempty` for those three types, `Data.Show` adds the `Show` class and `show` for `Boolean`, `Int`, `Number`, `Char`, `String`, `Unit`, and `Array a`, and `Data.Foldable` adds `foldr`, `foldl`, and `foldMap` for `Array`, `Maybe`, and `Either a`. `fold` is not part of that surface yet, because a polymorphic `foldMap` of the identity does not lower. | Partial | Reconcile remaining class-rule mismatches and official-suite coverage; deriving is tracked under FE-22. | | FE-15 | Functional dependencies | Source fundep improvement uses transitive determining closure and selected branches; independent class arguments still prove apartness. Ambiguity and consistency diagnostics are covered. One known divergence: a type wildcard standing for a fundep-determined position is reported as a fundep conflict, where `purs` only warns — `passing/WildcardInInstance.purs` says so in its own comment. It is unmeasurable until Phase 3 provides `Effect` and `Effect.Console`. | Partial | Reconcile official-suite fundep coverage and remaining advanced class forms. | -| FE-16 | Deriving, roles, `Coercible`, and newtype-based derivation | Role inference/checking (including imported aliases), compiler-owned `Coercible` solving, checked kind compatibility, higher-kinded given rewriting, canonical open-row alignment, constructor-visibility checks, explicit Typed Core evidence boundaries, and backend-planned conversions work for the covered subset. Source and Wasmtime tests cover phantom/nominal/representational roles, parameterized newtype scalar/function/array payloads, structural `Eq`/`Ord`, nested alias-aware `Functor.map`, direct `Bifunctor.bimap`, recursive `Eq`, checked `derive newtype` adapters, empty-class underlying-instance validation, and cross-module dictionaries. Differential tests also cover function-result traversal, `Contravariant` via `Profunctor.lcmap`, and resolved re-exported class identity. Runtime closure capture still blocks the function-based `Contravariant` case; other upstream deriving classes and open-row runtime conversion remain incomplete. Scoped method-local constraints on class methods are covered under FE-18. The `Coercible` solver reads primitive kinds through the shared kind checker; expression-level kind checking remains incomplete. Five `Coercible` board mismatches remain because a `Proof` member has no dictionary arguments for the framework's decided-argument check, so its answer still needs an explicit verification contract. | Partial | Implement the remaining upstream deriving classes; expand closure-capture runtime support and remaining coercion cases. See [roles and coercions acceptance](../implementation/frontend/roles-and-coercions.md). | +| FE-16 | Roles and `Coercible` | Role inference/checking (including imported aliases), compiler-owned `Coercible` solving, checked kind compatibility, higher-kinded given rewriting, canonical open-row alignment, constructor-visibility checks, explicit Typed Core evidence boundaries, and backend-planned conversions work for the covered subset. Source and Wasmtime tests cover phantom/nominal/representational roles, parameterized newtype scalar/function/array payloads, and cross-module dictionaries. Scoped method-local constraints on class methods are covered under FE-18. The `Coercible` solver reads primitive kinds through the shared kind checker; expression-level kind checking remains incomplete. Five `Coercible` board mismatches remain because a `Proof` member has no dictionary arguments for the framework's decided-argument check, so its answer still needs an explicit verification contract. | Partial | Expand closure-capture runtime support and the remaining coercion cases. See [roles and coercions acceptance](../implementation/frontend/roles-and-coercions.md). | | FE-17 | Visible type application, typed binders, type wildcards, holes, and advanced annotations | Typed binders preserve and check scoped annotations, and each source type wildcard receives fresh kind/type variables through the shared type spine. Type-level `String` and `Int` literals are ordinary spine nodes: a signature may contain them, they unify by value, and they survive into THIR where the verifier compares them. A wildcard in a value signature is solved by unification and is accepted in every shape `purs` accepts; a wildcard in an instance head is rejected as `InvalidInstanceHead`, while one in an instance context stays legal. The `1664.purs` wildcard binder lowers through P2. Visible term type application, wildcard warning/error behavior, higher-kinded application, and non-generalized hole diagnostics remain incomplete. The `Type`, `Constraint`, and `Symbol` heads are accepted as ordinary type constructors with their declared primitive kinds. Official's CST has no kind-application node; its kind checker synthesizes `KindApp` while instantiating a polymorphic kind, and this compiler performs that instantiation in the kind solver, so its source type spine needs no `KindApplication` node. The source forms that do name a kind or type explicitly are separate nodes. #87 lands both of the forms that blocked P2: a negative type-level integer prefix is the negative literal on the shared spine, and a visible type application `e @T` is elaborated by the checker, which substitutes the written argument for the operand's outermost quantifier after checking it against that quantifier's kind, and is erased at runtime. No P2 surface-lowering case remains. Three limits are recorded rather than approximated. A chained application `f @A @B` is reported, because the quantifiers an application leaves behind are scheme variables here and choosing between them needs the scheme to record which variables a visible application has consumed. A visible application on a class-method head is unresolved, which is `failing/ClassHeadNoVTA3.purs`. And this compiler's CST does not carry the binder visibility that official's `CST/Convert.hs` derives from `forall @a.`, so a plain `forall a.` binder is selectable where `purs` rejects it — the permissive direction, and the remaining half of `failing/VisibleTypeApplications1.purs`. `CannotApplyExpressionOfTypeOnType` and `CannotSkipTypeApplication` are the mapped codes. The primitive row relations themselves all have rules, and the row-side gap that remains is the rigid-tail unification defect under FE-13. | Partial | Model `forall` binder visibility so a visible application matches official, then resolve chained applications and class-method heads. | | FE-18 | Higher-rank types, subsumption, impredicativity, and higher-rank `forall` | Bidirectional checking preserves nested quantifiers, checks directional function/record subsumption, and rejects escaping skolems and specialized universal arguments. Source and GC execution cases cover rank-2 through rank-4, fields, returned and captured values, recursive annotations, higher-kinded parameters, and nested constraints. See the [rank-N acceptance record](../implementation/frontend/rank-n.md) for verification evidence and the official differential battery. | Partial | Reconcile the complete official higher-rank/skolem corpus, including its library dependencies and separate higher-rank kind requirements; track visible type application and diagnostic agreement. | | FE-19 | Foreign declarations and target-aware external names | Source-declared WIT bindings are resolved for the supported backend path. `foreign import data` is a nominal opaque type with no constructors; a nullary one maps to a WIT resource. THIR and Core keep it as `Constructor(User(id))` plus `opaque_ids`, distinct from `Int` (`lowers_an_opaque_foreign_type_to_core_without_collapsing_it_to_int`). JavaScript FFI is not a frontend target. CC/MIR handle layout is not done. | Partial | Finish target-aware foreign value rules beyond the supported WIT subset. Resource lifetime and handle layout stay in the backend. | -| FE-20 | Warnings, holes, source spans, and official diagnostic codes | Source spans exist and resolution, kind, type, and class `errorCode`s are measured: L1 904/908, L2 72/72, L3 36/48, L4 35/50, L5 53/79. Pattern-binder diagnostics match the annotated duplicate-name cases; warning coverage and complete diagnostic agreement remain open. Non-generalized hole diagnostics remain tracked under FE-17. | Partial | Add the missing class checks (#97) and track warning-code agreement separately from acceptance errors. | +| FE-20 | Warnings, holes, source spans, and official diagnostic codes | Source spans exist and resolution, kind, type, and class `errorCode`s are measured: L1 904/908, L2 72/72 (remeasured 2026-10-07), L3 39/48, L4 39/50, L5 77/97 (remeasured 2026-10-17). L4 per-code agreement is `TypesDoNotUnify` 34/41, `IntOutOfRange` 1/1, `InfiniteType` 2/2, `ExpectedType` 0/2, `EscapedSkolem` 1/2, `CannotApplyExpressionOfTypeOnType` 1/2, and `AmbiguousTypeVariables` 1/1. `MultipleErrors.purs` repeats its annotation, so code-occurrence counts sum to 40/51 while case agreement remains 39/50. Pattern-binder diagnostics match the annotated duplicate-name cases; warning coverage and complete diagnostic agreement remain open. Non-generalized hole diagnostics remain tracked under FE-17. | Partial | Add the missing class checks (#97) and track warning-code agreement separately from acceptance errors. | | FE-21 | Typed Core normalization and CoreFn/optimization compatibility | Typed Core lowering and verification work for the supported subset; official optimize output is not yet a target. | Partial | Add Core optimization passes and an explicit optimize compatibility track. | +| FE-22 | Deriving and newtype-based derivation | Every structural family generates ordinary checked instance members through one identity registry and syntax builder. All mapping families, folds, and traversals consume shared usage/variance analysis, including record fields, given higher-kinded dictionaries, and scoped forall parameters. Source tests cover every deriving diagnostic; differential batteries cover 18 original rules, 17 remaining-family/context/scoped cases, and six diagnostic cases. 25 Wasmtime cases cover all structural classes, Generic tag/field round trips, function Contravariant, polymorphic newtype adapters, applied-variable Eq1/Ord1 dispatch, both fold directions, sequencing, and multi-field traversal. L5 is 77/97, with deriving-specific `InvalidNewtypeInstance` 6/6 and `CannotDeriveInvalidConstructorArg` 7/7. | Partial | Shared declaration checks still mismatch the official orphan and invalid record/synonym instance-head cases, including `3405`, `3510`, and `InvalidDerivedInstance2`. See the [deriving design](frontend/type-system/deriving.md) and [acceptance record](../implementation/frontend/deriving.md). | The frontend landing order is: ```text -FE-01 -> FE-02..FE-07 -> FE-08..FE-13 -> FE-14..FE-17 -> FE-18..FE-21 +FE-01 -> FE-02..FE-07 -> FE-08..FE-13 -> FE-14..FE-17 -> FE-18..FE-22 ``` This is a dependency guide, not a requirement to finish every row in a block @@ -1153,13 +1205,13 @@ Wasm is the target encoding, and WIT/WASI are the platform integration layers. | BE-18 | Generic source-declared WIT imports | Compatible `Int`/`Boolean`/`Number` scalars, handles, and `list`/`string` imports lower through the canonical ABI with signature validation. A WIT `string` is a source `String` and a WIT `list` is `Array Int`, so the two no longer share a source type ([DEC-16](../decision/DEC-16-scalar-strings-and-utf8-storage.md)). Closed, directly flattened WIT records can contain nested `list` fields. | Partial | Add other aggregate WIT values, richer results, and user-library loading. | | BE-19 | WIT aggregate values and resources | Resource handles lower under [DEC-14](../decision/DEC-14-resource-handle-ownership.md): the compiler drops no handle on its own and exposes `resource.drop` to source, so the standard library owns the lifetime discipline; byte lists and closed WIT records with nested byte-list fields are classified and lowered in WIT field order. Indirect parameter tuples are allocated through `cabi_realloc`. Non-byte `list` of scalars, `bool`, `char`, strings, nullary enums, flags, resource handles, and directly flattened records of scalar or string fields is copied between a source GC array and the canonical buffer, with a driver execution test for `list` and synthesized Wasm fixtures for `list`, `list`, and `list` ([ABI-08](../implementation/backend/linear-memory-and-canonical-abi.md) In progress). `option`, `result`, and non-unit `variant` are classified and validated against `Data.Maybe.Maybe`, `Data.Either.Either`, and a source data type, CC derives their variant representation and a concrete payload tree, and MIR branches on each tag and rebuilds the source value recursively for a scalar payload of any width (`s8`..`u64`, `f32`/`f64`), a byte or non-byte list, `flags`, a closed record, and a nested `option`/`result`/`variant`, recursing through record fields and a `list`/`list` element, with synthesized Wasm fixtures ([DEC-13](../decision/DEC-13-wit-to-source-type-mapping.md)); a large aggregate return area is allocated through `cabi_realloc`, a handle in a result is an ordinary value the standard library drops explicitly, and an indirect parameter record carries a mapped aggregate. The aggregate ABI is generated from one normalized canonical type ([compositional canonical ABI lowering](backend/wasm/canonical-abi-compositional.md)); the descriptor types and per-shape plans are removed. `list>`/`list`/`list` elements, nested `list>`, multi-word flags as list elements and in aggregates, non-byte `list`, and `list>` results are classified and lowered, and a unit-success `result<_, E>` maps to `Either E Unit` (the error on `Left`) and sizes its return area from the error payload. | Partial | Add general aggregate layouts beyond the list-and-handle subset. | | BE-20 | Component Model packaging and capability-based imports | `wit-component` lifts the core module to a WASI 0.2 component and prunes unused imports. | Partial | Add component import/export regression cases beyond the CLI path and pass the L6/M7 gate. | -| BE-21 | WASI CLI entry, exit, stdout, and stderr | `wasi:cli/run`, exit codes, console output, and error output work in the component path. A selected `Int` entry returns its value as the exit code. A selected `Effect Unit` entry runs that action once, returns 0 after normal completion, and propagates a trap. Creating an action does not run its deferred operation. Focused Wasmtime tests assert output, status, and trap markers ([WASI-02/03](../implementation/backend/wasi-platform.md) Verified). The official board is 125/413, so this row stays Partial. | Partial | Pass the L6/M7 gate. The 63 files with no selected `main` stay explicit blockers. | +| BE-21 | WASI CLI entry, exit, stdout, and stderr | `wasi:cli/run`, exit codes, console output, and error output work in the component path. A selected `Int` entry returns its value as the exit code. The former compiler-owned `Effect Unit` entry was removed on 2026-10-11. A compiler-selected non-`Int` entry is still rejected. Effect programs use the library `PSRS.Command.run` runner over the accepted native representation. Focused Wasmtime tests assert output and status for `Int` entries. The 2026-10-17 runtime board is 273/413. It has no P10 case with no selected `main`; `Sequence.purs` stops at P7 because its command entry type disagrees with the configured runner. This row stays Partial. | Partial | Pass the L6/M7 gate. | | BE-22 | WASI clocks and randomness | Monotonic time and random bytes are wired through WASI and tested. | Partial | Expose the remaining clock/random library surface and pass the L6/M7 gate. | -| BE-23 | WASI arguments, environment, and filesystem | WIT descriptions are vendored, but the source library and aggregate lowering are not complete ([WASI-07](../implementation/backend/wasi-platform.md) In progress). | Planned | Add module loading and aggregate/list support, then expose these services. | +| BE-23 | WASI arguments, environment, and filesystem | Source wrappers for arguments, environment, and filesystem execute ([WASI-07](../implementation/backend/wasi-platform.md) Verified). This feature row was not remeasured with the 2026-10-16 native-effect fingerprint, and the L6/M7 gate is still open. | Partial | Pass the L6/M7 gate. | | BE-24 | WASI sockets and HTTP | Not part of the current synchronous portable-program target. | Excluded | Revisit as a separate platform scope after the core target is stable. | | BE-25 | WASI 0.3 async streams and futures | The current compiler targets synchronous WASI 0.2. | Planned | Revisit only with an explicit platform decision and async language/library plan. | | BE-26 | Standard library and user module loading | User modules are discovered from the entry files' directories and linked transitively ([WASI-09](../implementation/backend/wasi-platform.md) Verified); the PureScript-facing standard library is loaded from `stdlib/lib` in trusted-prefix order ([WASI-10](../implementation/backend/wasi-platform.md) Verified). | Partial | Pass the L6/M7 module-loading scoreboard. | -| BE-27 | Wasm/WASI execution and official passing-suite runtime coverage | Vertical execution tests pass for the bootstrap slice, and the `l6_runtime_scoreboard` harness compiles, validates, and runs the 413 non-FFI `passing` files; it measures **125/413** on 2026-10-04 after `Data.Functor` (Wasmtime 49.0.2, `purs` 0.15.16). All 125 exit 0. 124 are the files whose previous first blocker was a non-`Int` entry; `passing/3549.purs` is the additional file and was blocked on `Functor`. The first blockers of the other 288 are 53 missing library modules, 63 P10 files with no selected `main`, 80 P3 resolution failures, 49 P5 type errors, 17 P5 kind errors, 16 P8 representation errors, 6 P6 Core-lowering failures, and 4 P0 lexing failures; there are 0 harness-loading blockers and 0 P2 blockers. The library surface this row was waiting on is landed: `Effect`/`Effect.Console` — including `logShow` over the library `show` — and `Test.Assert`, whose failure path is a real guest trap (`Prelude.trap`). The remaining library work is the `Prelude` class and value surface (#94), the unowned `Data.*` modules (#124), and `Test.Assert.assertEqual` (#95), which #137 blocks because a constraint on a variable inside a record type is elaborated against the record. The remaining P10 files have no selected `main` and stay explicit blockers; this row does not emit an empty main. The 26 FFI files are excluded. The row stays Partial because L6 is not complete. | Partial | Land the `Prelude` class surface, then track per-feature runtime cases against the board. | +| BE-27 | Wasm/WASI execution and official passing-suite runtime coverage | The `l6_runtime_scoreboard` compiles, validates, and executes the 413 non-FFI `passing` files under required Wasmtime. The 2026-10-17 measurement is **273/413**, all exiting 0 (Wasmtime 49.0.3, `purs` 0.15.16, compiler `886cd82c`). The other 140 are the L6/M7 split above. The 26 FFI files are excluded. The row stays Partial because L6 is not complete. | Partial | Resolve the remaining compile and runtime blockers, then remeasure the full board. | | BE-28 | JavaScript/Node.js FFI compatibility | Not emitted or executed by this backend. | Excluded | No work planned under this decision. | ### Topic implementation acceptance @@ -1177,14 +1229,15 @@ acceptance result. | Polymorphism and erasure | BE-02, BE-08; FE-09 input | Re-baselined by DEC-10: PE-01..PE-11 are Verified, including GC-string erasure and capture. | [PE-01..PE-11](../implementation/backend/polymorphism-and-erasure.md) | | Scalars and primitives | BE-04; FE-08 input | Re-baselined by DEC-10: SP-01..SP-12 are Verified, including the GC-string representation. | [SP-01..SP-12](../implementation/backend/scalars-and-primitives.md) | | Pattern matching | BE-05, BE-06; supporting BE-08, BE-09 | PM-01..PM-15 have implementation, verifier, and required execution evidence. PM-14 includes source-spanned Boolean redundancy and guarded fallthrough; broader feature rows retain their separate gates. | [PM-01..PM-15](../implementation/backend/pattern-matching.md) | -| Effects | BE-21; supporting BE-02, BE-26 | Trusted Effect identities and checked WIT schemes are passed explicitly. Source `Effect a` stays abstract through Typed Core; P8 lowers it to a generic one-parameter closure. EF-01..EF-13 are Verified, including the `Effect Unit` command adapter and the lexical `runEffect` rule. A type table changed after `lower_effects` returns is not checked again. The official runtime board is 125/413. The 63 files with no selected `main` remain blocked, and BE-21 stays Partial. | [EF-01..EF-13](../implementation/backend/effects.md) | -| Type classes and dictionaries | BE-02, BE-09; FE-14/15 input | Backend acceptance complete from verified Typed Core fixtures: DICT-01..DICT-11 have implementation, verifier, and required execution evidence. Source constrained calls, contextual/imported generic instances, superclasses, fundeps, and ordered instance chains execute; FE-14/15 remain partial for remaining source class/fundep coverage, the constrained instance-member specialization limit, and official-suite acceptance. Class-method local constraints are covered under FE-18; deriving is tracked under FE-16. | [DICT-01..DICT-11](../implementation/backend/type-classes-and-dictionaries.md) | +| Delimited control and generic handlers | BE-02, BE-03, BE-05, BE-06, BE-12; supporting BE-21, BE-26 | Partial: the eight operations implement typed contract v2 with persistent queues and one guest context executor. Mandatory Wasmtime covers clause/return-clause scope, deep/shallow library policies, forwarding, abandonment, aggregate/GC captures, multi-shot replay, replay-local expiry, normal target-completion expiry and long request sequences. Parent-chain probes and post-GC live-heap measurements cover long deep and mixed-forwarding loops while independent non-tail extents remain distinct. Core owns shared checked carrier/callable contracts; immutable CC quotations and live optimized MIR witnesses retain source/target domains and reject incompatible locally proven operands. CC verifies factory operands against those witnesses. Predeclared semantic helper identities reject check/close swaps in CC and MIR calls, including tail calls; mandatory execution also covers 100,000 requests with tail-call capability disabled. Source carrier parameters, results and joins now share the preplanned nominal computation/owner shapes; control entry operands use their declared storage roles without an unconditional payload erasure round trip. Nominal machine storage fields and helper calls reject cross-wired queues, extents and ownership cells in CC/MIR, and CC/MIR check raw capture roles against local closure targets. CC/MIR also check that finish completion closes and restores fields from the same record, rejecting missing close and same-shaped boundary swaps. A predeclared raw-resumption binding now checks the dispatch checkpoint capture, raw capture slots, Resume construction and the saved checkpoint check before replay; MIR requires the check to dominate replay. CC/MIR now also check saved finish validation, original callback sharing and fresh live activation cells within rebuild iterations, including parent/restoration/advancement and terminal replay product identities. Target finishes now preserve one source context and enter its caller; finish copies preserve original extent/parent and append order. Replay entry checks current execution/dispatch ancestry and empty accumulators, with traversal/handoff/callback rebuild preserving active/parent state. Source context constructors now retain current Handle clauses, caller and saved work, with fresh or shared-finish lifetime operands; their transitions enter the inner computation with an empty queue and the new context/extent. Root Run factories now check empty queues, coherent root dispatch/execution extents and normal cleanup, preserving allocation evidence through Start inlining and rejecting sequential extent/status reuse while admitting exclusive initialization. Labelled Control rows over pinned VariantF execute deep forwarding and shallow residual policies. Complete pinned Run/Free dependency sources are imported unchanged; representative pure Run State, Choose and combined Reader/State/Writer/Except programs execute with observed values and log order. CC/MIR now check dispatch coverage and selected-case projections, required scoped clause application results and current operands, and Bind/Resume transition identities; scoped MIR applications must execute on the actual branch path. Complete remaining execution-step, dispatch selection and frame/context provenance, foreign capture, caught-trap cleanup and broader upstream-library API acceptance remain unverified. AE-11 host IO is Verified on 2026-10-16 in that checklist; the provenance items in this cell were not closed by that measurement. | [AE/AC checklist](../implementation/backend/algebraic-effects.md) | +| Effects | BE-21; supporting BE-02, BE-26 | The compiler-owned `Effect` protocol was withdrawn on 2026-10-11. The accepted replacement is library-owned `PSRS.Effect.Native`: NE-01–NE-13 and AE-11 are Verified on 2026-10-16 at fingerprint `fnv1a64-v1:ad19620ada1d2ecf`. `checkThrows`, Console timers, `Effect.Exception`, and `Effect.Aff` stay unimplemented JavaScript foreigns. The runtime board is 273/413 as of 2026-10-17, so BE-21 stays Partial. The independent package commit and lock revision are `4c73aab52226e8f5e7b1f17052f1a19d717a472e`. | [NE-01..NE-13](../implementation/backend/native-effect-and-st.md), [AE-11](../implementation/backend/algebraic-effects.md) | +| Type classes and dictionaries | BE-02, BE-09; FE-14/15 input | Backend acceptance complete from verified Typed Core fixtures: DICT-01..DICT-11 have implementation, verifier, and required execution evidence. Source constrained calls, contextual/imported generic instances, superclasses, fundeps, and ordered instance chains execute; FE-14/15 remain partial for remaining source class/fundep coverage, the constrained instance-member specialization limit, and official-suite acceptance. Class-method local constraints are covered under FE-18; deriving is tracked under FE-22. | [DICT-01..DICT-11](../implementation/backend/type-classes-and-dictionaries.md) | | Generic aggregate erasure | BE-08, BE-09, BE-10; supporting BE-02, BE-03, BE-13, BE-15 | Topic acceptance complete: all GA-01..GA-20 checks have implementation, verifier and required execution evidence. Broader feature rows retain their separate gates. | [Requirements, repair evidence, and validation](../implementation/backend/generic-aggregate-erasure.md) | | Optimization | BE-12 | Topic acceptance complete: OPT-01..OPT-14 have implementation, verifier, and required execution evidence. The official M8-O gate stays on the broader BE-12 row. | [OPT-01..OPT-14](../implementation/backend/optimization.md) | | Wasm encoding, validation, and capability | BE-13, BE-14, BE-16; supporting BE-15 | Topic acceptance complete: ENC-01..ENC-11 have implementation, verifier, and required execution evidence. Broader feature rows retain their separate gates. | [ENC-01..ENC-11](../implementation/backend/wasm-encoding.md) | | Linear memory and canonical ABI | BE-11, BE-17..BE-20 | Re-baselined by DEC-10 and split: LM-02, LM-04, LM-05, ABI-01, ABI-02, ABI-03, ABI-06, and ABI-07 Verified; LM-01 In progress for the one-memory profile; `own`/`borrow` handle drop is lowered; ABI-08's non-byte `list` of scalars, `bool`, `char`, strings, nullary enums, flags, resource handles, and directly flattened records of scalar or string fields is lowered with a driver execution test and synthesized Wasm fixtures. `option`, `result`, and non-unit `variant` are recognized on the resolved-type path ([DEC-13](../decision/DEC-13-wit-to-source-type-mapping.md)) — classification, Core conformance validation, CC's variant representation and payload tree, and MIR recursive tag-branch lowering for a scalar payload of any width (`s8`..`u64`, `f32`/`f64`), a byte or non-byte list, `flags`, a closed record, and a nested `option`/`result`/`variant` are implemented, recursing through record fields and a `list`/`list` element, and a large aggregate return area is allocated through `cabi_realloc`; under [DEC-14](../decision/DEC-14-resource-handle-ownership.md) the compiler drops no handle on its own and exposes `resource.drop` to source — and a tuple maps to a closed record. A wrapper may still pass one of those forms as primitive arguments whose flattening matches; multi-value returns stay unsupported. The allocator, buffer free, and post-return moved to canonical buffer allocation. The aggregate ABI now runs on one normalized canonical type ([compositional canonical ABI lowering](backend/wasm/canonical-abi-compositional.md)); the descriptor vocabulary and per-shape free plans are deleted. ABI-08's remaining list shapes (`list