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..10d3b2f9 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,35 @@ 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 = "equivalent" version = "1.0.2" @@ -79,12 +132,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 +181,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 +237,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 +270,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 +313,8 @@ version = "0.1.0" dependencies = [ "psrs-core", "psrs-hir", + "psrs-linker", + "psrs-runtime", "psrs-span", "psrs-thir", "wasm-encoder", @@ -224,9 +331,13 @@ dependencies = [ "psrs-ast", "psrs-driver", "psrs-hir", + "psrs-linker", "psrs-resolve", "psrs-span", "psrs-syntax", + "serde", + "serde_json", + "wat", ] [[package]] @@ -268,6 +379,8 @@ dependencies = [ "psrs-syntax", "psrs-thir", "psrs-typecheck", + "serde", + "serde_json", ] [[package]] @@ -289,6 +402,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 +426,23 @@ dependencies = [ "psrs-span", ] +[[package]] +name = "psrs-runtime" +version = "0.1.0" +dependencies = [ + "libm", + "ryu-js", + "wasm-encoder", + "wasmparser", + "wasmprinter", +] + [[package]] name = "psrs-span" version = "0.1.0" +dependencies = [ + "stacker", +] [[package]] name = "psrs-syntax" @@ -342,6 +484,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 +524,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", + "serde_derive", ] [[package]] @@ -390,12 +560,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 +650,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 +753,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..9152e6e9 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"] } wasm-encoder.workspace = true wasmparser.workspace = true wasmprinter.workspace = true 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..6e379b18 100644 --- a/crates/psrs-backend/src/abi/mod.rs +++ b/crates/psrs-backend/src/abi/mod.rs @@ -6,11 +6,13 @@ 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; @@ -22,10 +24,13 @@ 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 { @@ -98,11 +103,31 @@ pub(crate) const VALIDATE_STEP_SYMBOL: SymbolId = SymbolId::new(ModuleId::INTRIN /// 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] = [ +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 RESERVED_ABI_SYMBOLS: [SymbolId; 10] = [ REALLOC_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, ]; /// WASI interfaces and functions the backend itself references. The standard @@ -163,10 +188,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 +202,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 +223,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 +316,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 +393,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/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..13af6351 100644 --- a/crates/psrs-backend/src/bindings/mod.rs +++ b/crates/psrs-backend/src/bindings/mod.rs @@ -6,6 +6,9 @@ use psrs_core::{Module as CoreModule, TypeId as CoreTypeId}; use psrs_hir::{ExternalKind, ModuleId, SymbolId}; use std::collections::{HashMap, HashSet}; +mod primitives; +pub(crate) use primitives::lower as lower_primitives; + /// 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)] @@ -118,6 +121,37 @@ impl ExternalBindings { /// cannot accidentally make a target binding disappear by supplying a /// partial table. pub(crate) fn validate_core(&self, module: &CoreModule) -> Result<(), Vec> { + 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() 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..1579fcfd --- /dev/null +++ b/crates/psrs-backend/src/bindings/primitives/mod.rs @@ -0,0 +1,199 @@ +//! 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::>(); + if bindings.is_empty() { + return Ok(()); + } + 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; + let operations = 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 + { + 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..bb5fbdb9 --- /dev/null +++ b/crates/psrs-backend/src/bindings/primitives/tests.rs @@ -0,0 +1,84 @@ +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 { + 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/boundary/mod.rs b/crates/psrs-backend/src/boundary/mod.rs new file mode 100644 index 00000000..c76296c3 --- /dev/null +++ b/crates/psrs-backend/src/boundary/mod.rs @@ -0,0 +1,279 @@ +//! 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; + +/// 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, + /// The owner registered explicit fixed parameters (the Effect runtime + /// token). The payload remains the application's result. + Fixed(Vec), + /// Domains of a transparent newtype's checked callable field. A domain + /// is either independent of constructor arguments or one fixed argument. + Newtype(Vec), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum ProtocolParameter { + Fixed(TypeId), + Argument(usize), +} + +/// 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. + /// Register only domains expressible without inventing source type 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(parameters) = newtypes::parameters(module, *id) { + self.register( + TypeConstructor::User(*id), + RepresentationPolicy::Newtype(parameters), + ); + } + } + } + /// 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::Fixed(parameters) => parameters.clone(), + RepresentationPolicy::Newtype(parameters) => parameters + .iter() + .map(|parameter| match parameter { + ProtocolParameter::Fixed(ty) => Some(*ty), + ProtocolParameter::Argument(index) => arguments.get(*index).copied(), + }) + .collect::>>()?, + }) + } +} + +/// 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) + } + + /// 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, +) -> HashMap { + 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 Some(parameters) = representations + .signature(concrete) + .map(|signature| signature.parameters.clone()) + else { + continue; + }; + let protocol = Signature { + parameters, + result: ValueShape::Reference(Reference { + nullable: false, + heap: RefShape::Erased, + }), + }; + 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); + } + 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.rs b/crates/psrs-backend/src/boundary/newtypes.rs new file mode 100644 index 00000000..70974a67 --- /dev/null +++ b/crates/psrs-backend/src/boundary/newtypes.rs @@ -0,0 +1,120 @@ +//! Callable protocols derived from checked transparent newtype fields. +use super::ProtocolParameter; +use psrs_core::{Module, Type, TypeConstructor, TypeId}; +use psrs_hir::{TypeId as HirTypeId, TypeVariableId}; +use std::collections::{HashMap, HashSet}; + +pub(super) fn parameters(module: &Module, id: HirTypeId) -> Option> { + let constructor = module + .constructors + .iter() + .find(|constructor| constructor.type_id == id)?; + if constructor.field_types.len() != 1 { + return None; + } + let bindings = constructor + .parameters + .iter() + .enumerate() + .map(|(index, variable)| (*variable, ProtocolParameter::Argument(index))) + .collect(); + let mut visiting = HashSet::from([id]); + callable(module, constructor.field_types[0], &bindings, &mut visiting) +} + +fn callable( + module: &Module, + mut ty: TypeId, + bindings: &HashMap, + visiting: &mut HashSet, +) -> Option> { + while let Some((_, body)) = psrs_core::forall_parts(&module.types, ty) { + ty = body; + } + if let Some((parameters, _)) = psrs_core::closure_parts(&module.types, ty) { + return parameters + .iter() + .map(|ty| domain(module, *ty, bindings)) + .collect(); + } + let mut parameters = Vec::new(); + let mut cursor = ty; + while let Some((parameter, result)) = psrs_core::arrow_parts(&module.types, cursor) { + parameters.push(domain(module, parameter, bindings)?); + cursor = result; + if psrs_core::forall_parts(&module.types, cursor).is_some() { + break; + } + } + if !parameters.is_empty() { + return Some(parameters); + } + let (TypeConstructor::User(id), arguments) = module.applied_constructor(ty)? else { + return None; + }; + if !module.newtype_ids.contains(&id) || !visiting.insert(id) { + return None; + } + let constructor = module + .constructors + .iter() + .find(|constructor| constructor.type_id == id)?; + if constructor.field_types.len() != 1 || constructor.parameters.len() != arguments.len() { + return None; + } + let nested = constructor + .parameters + .iter() + .zip(arguments) + .map(|(variable, argument)| Some((*variable, domain(module, argument, bindings)?))) + .collect::>>()?; + let result = callable(module, constructor.field_types[0], &nested, visiting); + visiting.remove(&id); + result +} + +fn domain( + module: &Module, + ty: TypeId, + bindings: &HashMap, +) -> Option { + if let Some(Type::Variable(variable)) = module.types.get(ty.0 as usize) { + return bindings.get(variable).cloned(); + } + closed(module, ty, &HashSet::new(), &mut HashSet::new()).then_some(ProtocolParameter::Fixed(ty)) +} + +fn closed( + module: &Module, + ty: TypeId, + bound: &HashSet, + active: &mut HashSet, +) -> bool { + if !active.insert(ty) { + return false; + } + let result = match module.types.get(ty.0 as usize) { + Some(Type::Variable(variable)) => bound.contains(variable), + Some(Type::Application(function, argument)) => { + closed(module, *function, bound, active) && closed(module, *argument, bound, active) + } + Some(Type::RowExtend { ty, tail, .. }) => { + closed(module, *ty, bound, active) && closed(module, *tail, bound, active) + } + Some(Type::ForAll { variables, body }) => { + let mut bound = bound.clone(); + bound.extend(variables); + closed(module, *body, &bound, active) + } + Some(Type::Closure { parameters, result }) => { + parameters + .iter() + .all(|ty| closed(module, *ty, bound, active)) + && closed(module, *result, bound, active) + } + Some(_) => true, + None => false, + }; + active.remove(&ty); + result +} diff --git a/crates/psrs-backend/src/capability.rs b/crates/psrs-backend/src/capability.rs index 735f45f7..129083e1 100644 --- a/crates/psrs-backend/src/capability.rs +++ b/crates/psrs-backend/src/capability.rs @@ -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; diff --git a/crates/psrs-backend/src/cc/case/coverage/engine.rs b/crates/psrs-backend/src/cc/case/coverage/engine.rs index 2c528092..dc607efc 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, @@ -418,65 +483,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..ce165c86 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)] 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/tests/mod.rs b/crates/psrs-backend/src/cc/case/decision/realize/tests/mod.rs index 06a58810..723be3c0 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,6 +138,7 @@ 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, 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..34251dd6 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,6 +187,7 @@ 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, 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..4505c246 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,6 +127,7 @@ 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, 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..522d8757 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,6 +55,7 @@ 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, 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..482cc45d 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,6 +123,7 @@ 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, 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..10baa07d 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,6 +147,7 @@ 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, diff --git a/crates/psrs-backend/src/cc/layout/aggregate.rs b/crates/psrs-backend/src/cc/layout/aggregate.rs index 29309b4f..936bf260 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) { @@ -43,6 +47,45 @@ pub(super) fn reserve_aggregate_layouts( /// Core IDs select normalization inputs, not runtime layout identities. #[allow(clippy::too_many_arguments)] pub(super) fn normalize_aggregate_layouts( + module: &Module, + enum_types: &HashSet, + aggregate_types: &HashSet, + newtype_ids: &HashSet, + 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, + 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, @@ -100,6 +143,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)) } diff --git a/crates/psrs-backend/src/cc/layout/captures.rs b/crates/psrs-backend/src/cc/layout/captures.rs index 883d63fc..63b20289 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]); @@ -71,7 +75,7 @@ fn expression_has_integer_capture(expression: &Expr, module: &CoreModule) -> boo | ExprKind::Boolean(_) | ExprKind::String(_) | ExprKind::Char(_) => false, - ExprKind::Unit | ExprKind::Trap => false, + ExprKind::Unit | ExprKind::StateToken | ExprKind::Trap => false, } } @@ -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) => { @@ -159,7 +171,7 @@ fn free_integer_local( | ExprKind::Boolean(_) | ExprKind::String(_) | ExprKind::Char(_) => false, - ExprKind::Unit | ExprKind::Trap => false, + ExprKind::Unit | ExprKind::StateToken | ExprKind::Trap => false, } } 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..1b82e8aa 100644 --- a/crates/psrs-backend/src/cc/layout/functions/mod.rs +++ b/crates/psrs-backend/src/cc/layout/functions/mod.rs @@ -9,6 +9,10 @@ mod reachable; use reachable::referenced_types; +pub(super) fn live_type_ids(module: &CoreModule) -> HashSet { + reachable::live_type_ids(module) +} + pub(super) fn append_function_types( module: &CoreModule, enum_types: &HashSet, @@ -19,14 +23,9 @@ pub(super) fn append_function_types( 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() diff --git a/crates/psrs-backend/src/cc/layout/functions/reachable.rs b/crates/psrs-backend/src/cc/layout/functions/reachable.rs index 479de563..79788f60 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); @@ -33,6 +38,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 { @@ -103,7 +117,7 @@ fn record_expr( | ExprKind::Char(_) => {} // Both mention no subexpression, so the type recorded above is all // their layout can refer to. - ExprKind::Unit | ExprKind::Trap => {} + ExprKind::Unit | ExprKind::StateToken | ExprKind::Trap => {} } } @@ -145,14 +159,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 +176,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..38728629 100644 --- a/crates/psrs-backend/src/cc/layout/mod.rs +++ b/crates/psrs-backend/src/cc/layout/mod.rs @@ -2,19 +2,22 @@ use super::VariantCase; use super::{ReprId, Representation, RepresentationTable, Signature, SignatureId, ValueShape}; use crate::BackendError; use psrs_core::{Module as CoreModule, Type, TypeConstructor, TypeId}; -use psrs_hir::{SymbolId, TypeId as HirTypeId}; +use psrs_hir::{SymbolId, TypeId as HirTypeId, TypeVariableId}; use psrs_span::TextRange; use std::collections::{HashMap, HashSet}; mod aggregate; mod captures; +mod declaration; mod functions; +pub(crate) mod protocols; mod scalar; #[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}; @@ -287,7 +290,11 @@ 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); Ok(TypeLayout { + function_slot, + protocols: crate::boundary::payload_erased_protocols(&mut representations, &function_types), representations, array_types, record_types, @@ -299,6 +306,11 @@ pub(super) fn type_layout( } 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, @@ -408,24 +420,44 @@ pub(super) fn array_element_type(module: &CoreModule, id: TypeId) -> Option bool { - fn visit(module: &CoreModule, id: TypeId, visiting: &mut HashSet) -> 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(_)) => true, + Some(Type::Variable(variable)) => !bound.contains_key(variable), Some(Type::Application(function, argument)) => { - visit(module, *function, visiting) || visit(module, *argument, visiting) + 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::ForAll { body, .. }) => visit(module, *body, visiting), Some(Type::RowExtend { ty, tail, .. }) => { - visit(module, *ty, visiting) || visit(module, *tail, visiting) + visit(module, *ty, bound, visiting) || visit(module, *tail, bound, visiting) } Some(Type::Closure { parameters, result }) => { parameters .iter() - .any(|parameter| visit(module, *parameter, visiting)) - || visit(module, *result, visiting) + .any(|parameter| visit(module, *parameter, bound, visiting)) + || visit(module, *result, bound, visiting) } Some(Type::RowEmpty) => false, _ => false, @@ -434,5 +466,5 @@ pub(super) fn depends_on_type_variable(module: &CoreModule, id: TypeId) -> bool result } - visit(module, id, &mut HashSet::new()) + visit(module, id, &mut HashMap::new(), &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..54a6cac0 --- /dev/null +++ b/crates/psrs-backend/src/cc/layout/protocols.rs @@ -0,0 +1,59 @@ +//! 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 + && !table.representations.iter().any(|representation| + matches!(representation, Representation::Array { element } if *element == erased)) + { + let id = table.reserve(); + table.set(id, Representation::Array { element: erased }); + } + 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) + }) +} diff --git a/crates/psrs-backend/src/cc/layout/scalar.rs b/crates/psrs-backend/src/cc/layout/scalar.rs index cb37d31f..064a6e59 100644 --- a/crates/psrs-backend/src/cc/layout/scalar.rs +++ b/crates/psrs-backend/src/cc/layout/scalar.rs @@ -4,7 +4,6 @@ use super::{ }; 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; @@ -21,85 +20,25 @@ pub(crate) fn declaration_shape( 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 parameters = call + .parameters + .into_iter() + .map(|(ty, span)| { + scalar_type( module, - binder.ty, - binder.span, + ty, + span, enum_types, aggregate_types, newtype_ids, 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 is_callable_type(module, ty) { return Ok(Signature { parameters, @@ -181,7 +120,15 @@ 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 enum_types.contains(&type_id) { Ok(Signature { parameters, result: ValueShape::Integer, @@ -328,6 +275,9 @@ 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 enum_types.contains(&type_id) { Ok(ValueShape::Integer) } else if aggregate_types.contains(&type_id) { @@ -393,6 +343,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, diff --git a/crates/psrs-backend/src/cc/layout/tests/mod.rs b/crates/psrs-backend/src/cc/layout/tests/mod.rs index 57c6da6e..0afbae9e 100644 --- a/crates/psrs-backend/src/cc/layout/tests/mod.rs +++ b/crates/psrs-backend/src/cc/layout/tests/mod.rs @@ -5,6 +5,7 @@ use psrs_core::{ }; use psrs_hir::{LocalId, ModuleId, SymbolId, TypeId as HirTypeId, TypeVariableId}; +mod opaque; mod records; fn push_arrow(types: &mut Vec, parameter: TypeId, result: TypeId) -> TypeId { @@ -42,9 +43,31 @@ fn layout_for(module: &Module) -> TypeLayout { type_layout(module, &enums, &aggregates, &newtypes).expect("layout should succeed") } +/// 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 canonical_arrays_key_by_element_shape() { - let module = empty_module(vec![ + let mut module = empty_module(vec![ Type::Variable(TypeVariableId(0)), Type::Constructor(TypeConstructor::Array), Type::Application(TypeId(1), TypeId(0)), @@ -52,6 +75,8 @@ fn canonical_arrays_key_by_element_shape() { 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)]; @@ -85,11 +110,13 @@ fn canonical_arrays_key_by_element_shape() { #[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)]; @@ -122,6 +149,68 @@ 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(), + &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); @@ -369,7 +458,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 +477,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); 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..35410083 --- /dev/null +++ b/crates/psrs-backend/src/cc/layout/tests/opaque.rs @@ -0,0 +1,50 @@ +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(), + ) + .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/lower/array.rs b/crates/psrs-backend/src/cc/lower/array/mod.rs similarity index 99% rename from crates/psrs-backend/src/cc/lower/array.rs rename to crates/psrs-backend/src/cc/lower/array/mod.rs index d44cafb5..8619614b 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; 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..c51fd976 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/array/storage.rs @@ -0,0 +1,69 @@ +//! 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_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_value(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_value(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..7f0e9c54 100644 --- a/crates/psrs-backend/src/cc/lower/call/application.rs +++ b/crates/psrs-backend/src/cc/lower/call/application.rs @@ -5,7 +5,7 @@ use super::helpers::{ 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,18 @@ impl ApplicationLowering for FunctionLowerer<'_> { assignments, ); } + let declaration = self + .module + .declarations + .iter() + .find(|declaration| declaration.symbol == function); + let evidence = declaration.and_then(|declaration| { + self.boundary.checked_instantiation( + declaration.ty, + &declaration.quantified, + head.ty, + ) + }); self.check_call_shape( &signature, arguments.len(), @@ -73,12 +85,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)); } @@ -132,12 +145,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, @@ -175,12 +189,6 @@ impl FunctionLowerer<'_> { 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 +197,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..98caeccb 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 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 62% 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..3c9473f7 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,27 @@ impl FunctionLowerer<'_> { "partial application has an incomplete declaration signature", )]); } + let declaration = self + .module + .declarations + .iter() + .find(|item| item.symbol == function); + let evidence = declaration.and_then(|declaration| { + self.boundary.checked_instantiation( + declaration.ty, + &declaration.quantified, + 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 +68,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 +151,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, @@ -182,15 +195,105 @@ impl FunctionLowerer<'_> { }, 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 +302,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..05fd9860 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/call/partial/mod.rs @@ -0,0 +1,40 @@ +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_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/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..1126df2a --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/conversion/function_slot.rs @@ -0,0 +1,308 @@ +//! 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 = body.fresh(erased_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, + erased_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, + erased_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: erased_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 cast = ValueConversion::RecoverReference { + destination: closure(slot), + evidence: RecoveryEvidence::TypeInstantiation, + }; + if target == slot { + return Ok(cast); + } + 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 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(slot), span, &mut assignments); + let mut result = current; + for (index, parameter_shape) in shape.parameters.iter().enumerate() { + let input = inputs[index]; + let conversion = body.erase_payload(*parameter_shape, span)?; + let argument = body.emit_conversion( + input, + *parameter_shape, + erased_shape(), + conversion, + span, + &mut assignments, + ); + result = body.fresh(erased_shape()); + assignments.push(Assignment { + destination: result, + kind: AssignmentKind::IndirectCall { + function: current, + signature: slot, + arguments: vec![argument], + }, + span, + }); + if index + 1 < shape.parameters.len() { + current = body.emit_conversion( + result, + erased_shape(), + closure(slot), + cast.clone(), + span, + &mut assignments, + ); + } + } + let conversion = body.recover_payload(shape.result, span)?; + let result = body.emit_conversion( + result, + erased_shape(), + 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(slot, 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..6487d467 100644 --- a/crates/psrs-backend/src/cc/lower/conversion/mod.rs +++ b/crates/psrs-backend/src/cc/lower/conversion/mod.rs @@ -3,14 +3,19 @@ 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 scalars; +mod transport; + pub(in crate::cc) struct VariantFieldConversion { pub(in crate::cc) variant: ReprId, pub(in crate::cc) tag: u32, @@ -48,6 +53,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 +79,7 @@ impl FunctionLowerer<'_> { source_shape, destination_shape, span, + instantiation, ) } @@ -65,7 +90,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); } @@ -90,44 +126,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 +191,7 @@ impl FunctionLowerer<'_> { source_element_shape, destination_element_shape, span, + instantiation, )?; return Ok(ValueConversion::ArrayMap { source: source_repr, @@ -247,6 +254,7 @@ impl FunctionLowerer<'_> { self.value_shape(source_field, span)?, self.value_shape(destination_field, span)?, span, + instantiation, )?); } return Ok(ValueConversion::ProductMap { @@ -283,36 +291,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, @@ -359,8 +337,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, @@ -385,6 +367,7 @@ impl FunctionLowerer<'_> { template_shape, target_shape, span, + None, ) } } 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..07661582 --- /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(super) fn erase_payload( + &mut self, + shape: ValueShape, + span: TextRange, + ) -> Result> { + crate::cc::payload::PayloadPlanner::erase_payload(self, shape, span) + } + + pub(super) 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..d852276f 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,6 +45,7 @@ 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, @@ -69,6 +72,7 @@ fn unsupported_typed_boundary_reports_its_source_span() { ValueShape::Integer, ValueShape::Number, span, + None, ) .unwrap_err(); assert!( @@ -90,12 +94,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..920cbce6 --- /dev/null +++ b/crates/psrs-backend/src/cc/lower/conversion/transport.rs @@ -0,0 +1,167 @@ +//! 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), + }); + 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).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", + )); + } + 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..e392e57d 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 = @@ -99,12 +100,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 +188,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..4610897c 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, @@ -147,6 +162,7 @@ impl FunctionLowerer<'_> { &target_shape, span, assignments, + instantiation, ); } // A concrete curried function (the `ado` block's `\x -> \y -> ...`) @@ -168,6 +184,7 @@ impl FunctionLowerer<'_> { &target_shape, span, assignments, + instantiation, ); } return Err(vec![BackendError::new( @@ -221,12 +238,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 +293,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..66abb93e 100644 --- a/crates/psrs-backend/src/cc/lower/global.rs +++ b/crates/psrs-backend/src/cc/lower/global.rs @@ -81,12 +81,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 { @@ -109,21 +124,27 @@ impl GlobalLowering for FunctionLowerer<'_> { if source_shape == result_type { return Ok(destination); } - let source_type = self + let 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( + let source_type = declaration.ty; + let evidence = self.boundary.checked_instantiation( + source_type, + &declaration.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..8ef2fd2f 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( @@ -45,14 +63,14 @@ impl FunctionLowerer<'_> { }); Ok(destination) } - 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 +80,73 @@ 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::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..16f0a4d2 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) => { @@ -90,7 +98,7 @@ pub(in crate::cc::lower) fn collect_captures( | ExprKind::String(_) | ExprKind::Char(_) => {} // Neither a literal unit nor a trap reads a local. - ExprKind::Unit | ExprKind::Trap => {} + ExprKind::Unit | ExprKind::StateToken | ExprKind::Trap => {} } } diff --git a/crates/psrs-backend/src/cc/lower/lambda/mod.rs b/crates/psrs-backend/src/cc/lower/lambda/mod.rs index a5cda1c6..24c5f99d 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, }; @@ -103,8 +105,11 @@ 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, @@ -115,9 +120,26 @@ impl LambdaLowering for FunctionLowerer<'_> { 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 +153,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)); @@ -274,6 +315,7 @@ 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, diff --git a/crates/psrs-backend/src/cc/lower/letrec/tests.rs b/crates/psrs-backend/src/cc/lower/letrec/tests.rs index 582528ef..20962fc0 100644 --- a/crates/psrs-backend/src/cc/lower/letrec/tests.rs +++ b/crates/psrs-backend/src/cc/lower/letrec/tests.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, diff --git a/crates/psrs-backend/src/cc/lower/mod.rs b/crates/psrs-backend/src/cc/lower/mod.rs index 9b564ddc..a6e72836 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}; @@ -22,7 +23,6 @@ mod lambda; mod letrec; mod literals; mod record; -mod scalar; mod string_bytes; mod symbols; use call::ApplicationLowering; @@ -34,6 +34,10 @@ 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, @@ -64,6 +68,7 @@ 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, @@ -203,6 +208,9 @@ 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, @@ -226,6 +234,14 @@ impl FunctionLowerer<'_> { &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, @@ -311,6 +327,15 @@ impl FunctionLowerer<'_> { ty, assignments, )), + // The state token carries no payload for the synchronous effect + // model, so it lowers to a constant scalar. Its type stays opaque, + // so no pass can treat it as an `Int`. + ExprKind::StateToken => Ok(self.lower_literal( + AssignmentKind::Constant(0), + expression.span, + ty, + assignments, + )), // A trap has a type but no value: it is the one expression that // ends the path instead of filling a destination. ExprKind::Trap => self.lower_trap(expression.span, ty, assignments), 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/mod.rs b/crates/psrs-backend/src/cc/mod.rs index 2081c710..6b9ff192 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}; @@ -10,6 +11,7 @@ mod case; mod convert; mod layout; mod lower; +pub(crate) mod payload; mod projection; mod representation; mod scalar; @@ -158,6 +160,12 @@ pub enum AssignmentKind { representation: ReprId, elements: Vec, }, + ArrayFill { + destination: ValueId, + representation: ReprId, + length: ValueId, + value: ValueId, + }, ArrayLen { destination: ValueId, value: ValueId, @@ -178,6 +186,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 +261,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() @@ -335,8 +373,16 @@ pub fn lower_module_with_bindings( (declaration.symbol, wrapper) }) .collect::>(); + 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, diff --git a/crates/psrs-backend/src/cc/payload.rs b/crates/psrs-backend/src/cc/payload.rs new file mode 100644 index 00000000..94d0dad7 --- /dev/null +++ b/crates/psrs-backend/src/cc/payload.rs @@ -0,0 +1,324 @@ +//! 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; + +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")) + } +} + +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/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/verify/helpers.rs b/crates/psrs-backend/src/cc/verify/helpers.rs index 7252dda8..28e8458f 100644 --- a/crates/psrs-backend/src/cc/verify/helpers.rs +++ b/crates/psrs-backend/src/cc/verify/helpers.rs @@ -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..8173a398 100644 --- a/crates/psrs-backend/src/cc/verify/mod.rs +++ b/crates/psrs-backend/src/cc/verify/mod.rs @@ -194,6 +194,7 @@ fn verify_function_inner( representations, functions, function.span, + &function.name, )?; if !available.contains(&function.result) { return Err(vec![BackendError::invalid_ir( 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/mod.rs b/crates/psrs-backend/src/cc/verify/ops/mod.rs index b411b1da..d5371bf6 100644 --- a/crates/psrs-backend/src/cc/verify/ops/mod.rs +++ b/crates/psrs-backend/src/cc/verify/ops/mod.rs @@ -21,6 +21,7 @@ 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,6 +30,7 @@ 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(); @@ -83,6 +85,41 @@ 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); @@ -97,7 +134,19 @@ pub(super) fn verify_assignments( "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 +186,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 +369,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 +412,7 @@ pub(super) fn verify_assignments( table, functions, function_span, + function_name, )?; let mut else_available = available.clone(); verify_assignments( @@ -364,6 +423,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 +457,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/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/mod.rs b/crates/psrs-backend/src/cc/verify/tests/mod.rs index 8e3256ba..cac15013 100644 --- a/crates/psrs-backend/src/cc/verify/tests/mod.rs +++ b/crates/psrs-backend/src/cc/verify/tests/mod.rs @@ -6,6 +6,7 @@ use psrs_span::TextRange; mod adaptation; mod dictionary; mod effects; +mod number_decimal; mod string_eq; mod structure; mod variant; @@ -244,7 +245,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..fea45333 100644 --- a/crates/psrs-backend/src/cc/verify/tests/structure.rs +++ b/crates/psrs-backend/src/cc/verify/tests/structure.rs @@ -157,7 +157,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/effects/mod.rs b/crates/psrs-backend/src/effects/mod.rs index 720c09a1..f1de3058 100644 --- a/crates/psrs-backend/src/effects/mod.rs +++ b/crates/psrs-backend/src/effects/mod.rs @@ -5,20 +5,31 @@ mod entry; mod suspension; mod verify; +use super::boundary::{RepresentationPolicy, RepresentationRegistry}; use crate::BackendError; use crate::bindings::ExternalBindings; -use psrs_core::{Module as CoreModule, effect::EffectCompilation}; +use psrs_core::{Module as CoreModule, TypeConstructor, effect::EffectCompilation}; +use std::collections::HashSet; + +/// Source Core captured before effect applications become closures, plus the +/// representation registry the Effect owner contributes to conversion. +pub(crate) struct EffectPreparation { + pub(crate) source: CoreModule, + pub(crate) registry: RepresentationRegistry, +} pub(crate) fn lower_effects( module: &mut CoreModule, bindings: &mut ExternalBindings, context: &EffectCompilation, -) -> Result<(), Vec> { +) -> Result> { verify::trusted_contract(module, bindings, &context.trusted)?; validate_entry_context(module, context)?; let suspensions = suspension::plan(module, bindings, &context.trusted)?; + let source = module.clone(); let lowering = psrs_core::effect::lower_effects(module, &context.trusted) .map_err(|errors| verify::verification_errors(&errors))?; + let registry = effect_registry(module, &context.trusted, &lowering)?; let applied = suspension::apply(module, bindings, &suspensions)?; if let Some(entry) = context.command_entry { @@ -28,7 +39,7 @@ pub(crate) fn lower_effects( lowering .verify(module) .map_err(|errors| verify::verification_errors(&errors))?; - if let Err(errors) = module.verify() { + if let Err(errors) = module.verify_with_source(&source) { return Err(verify::verification_errors(&errors)); } suspension::verify(module, bindings, &suspensions, &applied)?; @@ -36,7 +47,36 @@ pub(crate) fn lower_effects( bindings .imports .retain(|binding| !synthesized.contains(&binding.symbol)); - Ok(()) + Ok(EffectPreparation { source, registry }) +} + +/// The Effect representation owner registers its constructor's stored calling +/// convention: the runtime token chosen by effect lowering. The payload +/// remains the application's result and is not a hidden parameter. +fn effect_registry( + module: &CoreModule, + trusted: &psrs_core::effect::TrustedEffect, + lowering: &psrs_core::effect::EffectLowering, +) -> Result> { + let mut tokens = HashSet::new(); + for closure in &lowering.closures { + tokens.insert(closure.token); + } + if tokens.len() > 1 { + return Err(vec![verify::effect_error( + module, + module.span, + "Effect lowering produced more than one runtime token", + )]); + } + let mut registry = RepresentationRegistry::new(); + if let Some(token) = tokens.into_iter().next() { + registry.register( + TypeConstructor::User(trusted.effect_type), + RepresentationPolicy::Fixed(vec![token]), + ); + } + Ok(registry) } fn validate_entry_context( diff --git a/crates/psrs-backend/src/lib.rs b/crates/psrs-backend/src/lib.rs index 40629f36..58290b1d 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 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 capability::TargetCapabilities; +pub use trace::*; use psrs_hir::ModuleId; use psrs_span::TextRange; @@ -146,10 +152,14 @@ pub fn lower_cc_with_context( effect_context: Option<&psrs_core::effect::EffectCompilation>, ) -> Result> { let mut external_bindings = ExternalBindings::from_core(&module); + let mut source = None; + let mut registry = boundary::RepresentationRegistry::new(); if let Some(context) = effect_context { - effects::lower_effects(&mut module, &mut external_bindings, context)?; + let prepared = effects::lower_effects(&mut module, &mut external_bindings, context)?; + registry = prepared.registry; + source = Some(prepared.source); } - cc::lower_module_with_bindings(module, external_bindings) + cc::lower_module_with_relations(module, external_bindings, source.as_ref(), registry) } /// The default-profile validator, retained for backend unit tests. @@ -178,6 +188,35 @@ pub struct Stages { 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()) } @@ -195,96 +234,35 @@ pub fn compile_with_context( 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, effect_context, 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, + effect_context: Option, + target: TargetCapabilities, + capture_partial: bool, +) -> TracedCompile { + let (result, trace) = + pipeline::compile_with_context_traced(module, effect_context, 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, + effect_context: Option, + target: TargetCapabilities, +) -> Result { + let mut partial = PartialStages::default(); + pipeline::compile_with_context_inner(module, effect_context, 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..209850c9 --- /dev/null +++ b/crates/psrs-backend/src/linking/mod.rs @@ -0,0 +1,312 @@ +//! 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::{ + ArtifactReference, BindingRequirement, Boundary, CheckedLinkPlan, CoreSignature, CoreType, + MemoryDemand, Provider, RequirementId, ResolvedWorldContext, TargetLinkInput, TargetPolicy, +}; +use psrs_span::TextRange; +use std::collections::{BTreeMap, 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 artifacts: BTreeMap = BTreeMap::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; + // 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)?); + artifacts + .entry(implementation.artifact.id.to_string()) + .or_insert_with(|| implementation.artifact_reference()); + 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, + artifacts: artifacts.into_values().collect(), + 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, + } +} + +/// 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 tests; diff --git a/crates/psrs-backend/src/linking/tests.rs b/crates/psrs-backend/src/linking/tests.rs new file mode 100644 index 00000000..efc249f1 --- /dev/null +++ b/crates/psrs-backend/src/linking/tests.rs @@ -0,0 +1,123 @@ +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(); + 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.result = None; + invalid.push(wrong); + let mut wrong = good.clone(); + wrong.result = Some(ValueType::I64); + invalid.push(wrong); + let mut wrong = good; + wrong.parameters[0] = ValueType::Ref(RefType { + nullable: true, + heap: HeapType::Any, + }); + 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/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/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/erased_storage.rs b/crates/psrs-backend/src/mir/indirect_tests/aggregate/erased_storage.rs new file mode 100644 index 00000000..18bf5d2e --- /dev/null +++ b/crates/psrs-backend/src/mir/indirect_tests/aggregate/erased_storage.rs @@ -0,0 +1,127 @@ +//! 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 { + 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/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/instruction/methods.rs b/crates/psrs-backend/src/mir/instruction/methods.rs index c96d5aab..11c37761 100644 --- a/crates/psrs-backend/src/mir/instruction/methods.rs +++ b/crates/psrs-backend/src/mir/instruction/methods.rs @@ -29,6 +29,7 @@ impl Instruction { | Self::ArrayNew { destination, .. } | Self::ArrayNewDefault { destination, .. } | Self::ArrayNewSized { destination, .. } + | Self::ArrayNewFilled { destination, .. } | Self::ArrayGet { destination, .. } | Self::ArrayGetU { destination, .. } | Self::ArrayClone { destination, .. } @@ -109,6 +110,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], @@ -183,6 +185,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..a7b6a3d1 100644 --- a/crates/psrs-backend/src/mir/instruction/mod.rs +++ b/crates/psrs-backend/src/mir/instruction/mod.rs @@ -190,6 +190,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/mod.rs b/crates/psrs-backend/src/mir/layout/mod.rs index 796736df..011559f2 100644 --- a/crates/psrs-backend/src/mir/layout/mod.rs +++ b/crates/psrs-backend/src/mir/layout/mod.rs @@ -368,9 +368,10 @@ impl PlannedLayout { } } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] pub(crate) enum LayoutError { UnknownRepresentation, + InvalidPayloadConversion(String), UnknownSignature, UnknownField, MissingIntegerBox, @@ -382,6 +383,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 { diff --git a/crates/psrs-backend/src/mir/lower/aggregate/mod.rs b/crates/psrs-backend/src/mir/lower/aggregate/mod.rs index 3f86363a..918f313d 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, 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..af5a534c 100644 --- a/crates/psrs-backend/src/mir/lower/assignment_string/mod.rs +++ b/crates/psrs-backend/src/mir/lower/assignment_string/mod.rs @@ -278,7 +278,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..e9473448 100644 --- a/crates/psrs-backend/src/mir/lower/assignments.rs +++ b/crates/psrs-backend/src/mir/lower/assignments.rs @@ -91,14 +91,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 +207,7 @@ impl FunctionLowerer<'_> { AssignmentKind::ArrayNew { .. } | AssignmentKind::ArrayLen { .. } | AssignmentKind::ArrayAppend { .. } + | AssignmentKind::ArrayFill { .. } | AssignmentKind::StringToBytes { .. } | AssignmentKind::BytesToString { .. } | AssignmentKind::ArrayGet { .. } 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..9ada99e1 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,6 +17,7 @@ mod assignment_array; mod assignment_string; mod assignments; mod conversion_helpers; +mod runtime_call; mod tail; mod variant; pub(super) use conversion_helpers::ConversionHelpers; @@ -28,7 +28,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 +45,6 @@ pub(super) fn lower_function( source: &cc::Function, id: FunctionId, wit_imports: &HashMap, - scalar_helpers: &ScalarHelpers, layout: &PlannedLayout, conversion_helpers: Option<&mut ConversionHelpers>, literals: Option<&mut StringLiterals>, @@ -79,7 +78,6 @@ pub(super) fn lower_function( .max() .map_or(0, |max| max + 1), wit_imports, - scalar_helpers, layout, conversion_helpers, literals, @@ -117,8 +115,7 @@ pub(super) struct FunctionLowerer<'a> { values: Vec, next_value: u32, wit_imports: &'a HashMap, - scalar_helpers: &'a ScalarHelpers, - layout: &'a PlannedLayout, + pub(in crate::mir) layout: &'a PlannedLayout, conversion_helpers: Option<&'a mut ConversionHelpers>, literals: Option<&'a mut StringLiterals>, } 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..6ece156b --- /dev/null +++ b/crates/psrs-backend/src/mir/lower/runtime_call.rs @@ -0,0 +1,127 @@ +//! 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 { + 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 { + 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 { + 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/wit_tests.rs b/crates/psrs-backend/src/mir/lower/wit_tests.rs index d3ad36d6..3d23ab57 100644 --- a/crates/psrs-backend/src/mir/lower/wit_tests.rs +++ b/crates/psrs-backend/src/mir/lower/wit_tests.rs @@ -36,7 +36,6 @@ fn gc_wit_record_projection_uses_the_planned_product_type() { }], next_value: 1, wit_imports: &HashMap::new(), - scalar_helpers: &ScalarHelpers::default(), layout: &layout, literals: None, }; diff --git a/crates/psrs-backend/src/mir/mod.rs b/crates/psrs-backend/src/mir/mod.rs index 2e39decf..043988a4 100644 --- a/crates/psrs-backend/src/mir/mod.rs +++ b/crates/psrs-backend/src/mir/mod.rs @@ -15,14 +15,12 @@ mod numeric; pub mod opt; mod planner; mod reachable; -mod scalar_helpers; 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}; @@ -35,6 +33,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)] @@ -185,6 +185,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, @@ -269,9 +282,7 @@ 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 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 +290,6 @@ fn lower_module_after_binding_validation( function, FunctionId(id as u32), &wit_imports, - &scalar_helpers, &layout, Some(&mut conversion_helpers), Some(&mut literals), @@ -293,14 +303,12 @@ 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, &layout, None, Some(&mut literals), @@ -327,14 +335,18 @@ fn lower_module_after_binding_validation( .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"), + ); + } + for implementation in crate::target_intrinsics::artifacts() { + if used.contains(&implementation.symbol) { + imports.push(implementation.import()); + } } - // 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 + // 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 +363,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..ebc97952 --- /dev/null +++ b/crates/psrs-backend/src/mir/number_format_tests.rs @@ -0,0 +1,176 @@ +//! 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 { + symbol: crate::abi::REALLOC_SYMBOL, + parameters: vec![ValueType::I32; 4], + result: Some(ValueType::I32), + }, + Import { + 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 { + destination: ValueId(2), + function: crate::abi::REALLOC_SYMBOL, + arguments: vec![ValueId(3), ValueId(3), ValueId(4), ValueId(5)], + span: span(), + }, + Instruction::Call { + 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/effects.rs b/crates/psrs-backend/src/mir/opt/effects.rs index bb1065c3..8bfe3d45 100644 --- a/crates/psrs-backend/src/mir/opt/effects.rs +++ b/crates/psrs-backend/src/mir/opt/effects.rs @@ -44,6 +44,7 @@ pub(super) fn classify(instruction: &Instruction) -> InstructionEffects { | I::ArrayNewData { .. } | I::ArrayNewDefault { .. } | I::ArrayNewSized { .. } + | I::ArrayNewFilled { .. } | I::ArrayGet { .. } | I::ArrayGetU { .. } | I::ArrayClone { .. } @@ -66,6 +67,7 @@ pub(super) fn classify(instruction: &Instruction) -> InstructionEffects { | I::ArrayNewData { .. } | I::ArrayNewDefault { .. } | I::ArrayNewSized { .. } + | I::ArrayNewFilled { .. } | I::ArrayClone { .. } ), ..InstructionEffects::default() diff --git a/crates/psrs-backend/src/mir/opt/imports.rs b/crates/psrs-backend/src/mir/opt/imports.rs index 267517c9..072db211 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,12 +82,11 @@ 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(), @@ -109,11 +137,44 @@ 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 { + 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/values.rs b/crates/psrs-backend/src/mir/opt/values.rs index 5154bc90..31a1eb18 100644 --- a/crates/psrs-backend/src/mir/opt/values.rs +++ b/crates/psrs-backend/src/mir/opt/values.rs @@ -224,6 +224,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, diff --git a/crates/psrs-backend/src/mir/reachable/assignments.rs b/crates/psrs-backend/src/mir/reachable/assignments.rs index ffa5dca8..d737bb0b 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, .. } @@ -133,6 +134,7 @@ pub(super) fn add_assignments( | AssignmentKind::StringConstant(_) | AssignmentKind::Primitive { .. } | AssignmentKind::Unary { .. } + | AssignmentKind::RuntimeCall { .. } | AssignmentKind::ArrayLen { .. } | AssignmentKind::Unreachable => {} AssignmentKind::ClosureGetCapture { .. } => { @@ -147,7 +149,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..db5d105c 100644 --- a/crates/psrs-backend/src/mir/reachable/mod.rs +++ b/crates/psrs-backend/src/mir/reachable/mod.rs @@ -1,73 +1,18 @@ //! 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, @@ -142,20 +87,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, - ); + )?; } } } 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..7978090d --- /dev/null +++ b/crates/psrs-backend/src/mir/reachable/projection.rs @@ -0,0 +1,105 @@ +//! Reachability of concrete ABI projections and their storage conversion plans. +use super::*; +use crate::cc::{ + Field, GuestLayout, + payload::{PayloadPlanner, StoragePayloadPlanner}, +}; +use psrs_span::TextRange; + +/// 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/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.rs index 4f4368d4..0d2141b7 100644 --- a/crates/psrs-backend/src/mir/tests.rs +++ b/crates/psrs-backend/src/mir/tests.rs @@ -269,7 +269,13 @@ fn runs_a_mir_gc_struct_under_wasmtime() { span: span(), }; - let wasm = crate::wasm::lower_module(&mir, &mut registry()).expect("lowering to Wasm"); + 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) @@ -283,8 +289,7 @@ fn runs_a_mir_gc_struct_under_wasmtime() { 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 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") diff --git a/crates/psrs-backend/src/mir/verify/capability.rs b/crates/psrs-backend/src/mir/verify/capability.rs index 7076b245..176a8a44 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 { .. } 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/mod.rs b/crates/psrs-backend/src/mir/verify/instruction/mod.rs index 30b59bbc..beddd227 100644 --- a/crates/psrs-backend/src/mir/verify/instruction/mod.rs +++ b/crates/psrs-backend/src/mir/verify/instruction/mod.rs @@ -378,6 +378,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/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/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/wit/aggregate/decode.rs b/crates/psrs-backend/src/mir/wit/aggregate/decode.rs index 0b293582..a40f1824 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)) } @@ -383,23 +386,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..050b5841 100644 --- a/crates/psrs-backend/src/mir/wit/call_lowerer.rs +++ b/crates/psrs-backend/src/mir/wit/call_lowerer.rs @@ -125,6 +125,22 @@ pub(crate) trait WitCallLowerer { None } + /// 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..2344ae2e 100644 --- a/crates/psrs-backend/src/mir/wit/function_lowerer.rs +++ b/crates/psrs-backend/src/mir/wit/function_lowerer.rs @@ -169,6 +169,34 @@ impl WitCallLowerer for FunctionLowerer<'_> { self.resolved_guest_layout(shape) } + 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/mod.rs b/crates/psrs-backend/src/mir/wit/mod.rs index efc251ef..a7cb4e8f 100644 --- a/crates/psrs-backend/src/mir/wit/mod.rs +++ b/crates/psrs-backend/src/mir/wit/mod.rs @@ -18,6 +18,7 @@ mod parameters; pub(super) use bind::BoundFn; pub(super) use call_lowerer::WitCallLowerer; pub(super) use handles::verify_function; +pub(super) use parameters::lower_string; use super::{BlockId, instruction::Instruction}; use crate::BackendError; diff --git a/crates/psrs-backend/src/mir/wit/parameters/mod.rs b/crates/psrs-backend/src/mir/wit/parameters/mod.rs index 378e2b42..dfb45df4 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, diff --git a/crates/psrs-backend/src/pipeline/emission.rs b/crates/psrs-backend/src/pipeline/emission.rs new file mode 100644 index 00000000..88262a8d --- /dev/null +++ b/crates/psrs-backend/src/pipeline/emission.rs @@ -0,0 +1,270 @@ +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()); +} diff --git a/crates/psrs-backend/src/pipeline/mod.rs b/crates/psrs-backend/src/pipeline/mod.rs new file mode 100644 index 00000000..c9242911 --- /dev/null +++ b/crates/psrs-backend/src/pipeline/mod.rs @@ -0,0 +1,394 @@ +use crate::trace::{ + CompileTrace, TraceArtifactId, TraceParameter, TraceRecorder, TraceRepresentation, + TraceValidationCoverage, TraceValidationSpec, +}; +use crate::{ + Artifact, BackendError, CompileFailure, PartialStages, Stages, TargetCapabilities, + annotate_errors, +}; + +use crate::{cc, effects, mir}; + +mod emission; + +pub(crate) fn compile_with_context_inner( + module: psrs_core::Module, + effect_context: Option, + 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 mut 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 mut external_bindings = crate::ExternalBindings::from_core(&module); + let mut binding_id = if let (Some(trace), Some(call)) = (trace.as_deref_mut(), binding_call) { + trace + .complete(call, &[TraceRepresentation::ExternalBindings], &[]) + .first() + .copied() + } else { + None + }; + + let mut effect_source = None; + let mut effect_registry = crate::boundary::RepresentationRegistry::new(); + if let Some(context) = effect_context.as_ref() { + let effect_call = trace.as_deref_mut().map(|trace| { + trace.begin( + "backend.effect.lower", + &[ + core_id.expect("traced optimized Core"), + binding_id.expect("traced external bindings"), + ], + TraceValidationCoverage::Composite, + vec![TraceParameter { + key: "effect_context", + value: "present".into(), + }], + ) + }); + let prepared = match effects::lower_effects(&mut module, &mut external_bindings, context) { + Ok(prepared) => prepared, + Err(errors) => { + if let (Some(trace), Some(call)) = (trace.as_deref_mut(), effect_call) { + trace.reject(call, errors.len()); + } + return Err(errors); + } + }; + effect_registry = prepared.registry; + effect_source = Some(prepared.source); + if let (Some(trace), Some(call)) = (trace.as_deref_mut(), effect_call) { + let outputs = trace.complete( + call, + &[ + TraceRepresentation::Core, + TraceRepresentation::ExternalBindings, + ], + &[TraceValidationSpec::outputs( + "psrs_backend::effects::lower_effects", + &[0, 1], + TraceValidationCoverage::Composite, + )], + ); + core_id = outputs.first().copied(); + binding_id = outputs.get(1).copied(); + } + } else if let Some(trace) = trace.as_deref_mut() { + trace.not_applicable("backend.effect.lower", "no effect context supplied"); + } + + 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, + effect_source.as_ref(), + effect_registry, + ) { + 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, + effect_context, + 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, + effect_context: Option, + target: TargetCapabilities, + capture_partial: bool, +) -> (Result, CompileTrace) { + let mut trace = TraceRecorder::new(); + let mut partial = PartialStages::default(); + let result = compile_with_context_inner( + module, + effect_context, + 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..6a61cfb0 --- /dev/null +++ b/crates/psrs-backend/src/target_intrinsics/generated.rs @@ -0,0 +1,84 @@ +//! 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 { + 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..47b9aaa4 --- /dev/null +++ b/crates/psrs-backend/src/target_intrinsics/mod.rs @@ -0,0 +1,122 @@ +//! 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, + 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::BoolTrue | Intrinsic::BoolFalse | Intrinsic::Unit | Intrinsic::Coerce => { + Elaborated + } + Intrinsic::Undefined => Unsupported, + } +} + +/// 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..b05836b1 --- /dev/null +++ b/crates/psrs-backend/src/target_intrinsics/tests.rs @@ -0,0 +1,49 @@ +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::Generated(_) | Implementation::Elaborated => {} + } + } + assert!(Intrinsic::from_binding("intDiv").is_none()); + assert!(Intrinsic::from_binding("intMod").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), + ] { + 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..feadc792 --- /dev/null +++ b/crates/psrs-backend/src/target_runtime/mod.rs @@ -0,0 +1,164 @@ +//! 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::{ + ArtifactReference, 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, +}; + +/// 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 { + 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 artifact export. + pub fn requirement(&self, id: RequirementId, consumer: CoreSignature) -> BindingRequirement { + let signature = self.signature(); + 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::ArtifactExport { + artifact: self.artifact.id.to_string(), + export: self.abi.export.to_string(), + signature, + }, + } + } + + /// The pinned artifact and typed contract for this implementation. + pub fn artifact_reference(&self) -> ArtifactReference { + ArtifactReference { + contract: psrs_linker::runtime::contract(self.artifact), + bytes: self.artifact.bytes.to_vec(), + } + } +} + +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::ArtifactExport { .. } + )); + 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..b5fb4fbe --- /dev/null +++ b/crates/psrs-backend/src/trace.rs @@ -0,0 +1,453 @@ +//! 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); + } + + pub(crate) fn not_applicable(&mut self, pass_key: &'static str, reason: &'static str) { + let call = self.begin( + pass_key, + &[], + TraceValidationCoverage::NotObserved, + vec![TraceParameter { + key: "reason", + value: reason.into(), + }], + ); + self.trace.executions.push(TraceExecution { + id: call.id, + pass_key: call.pass_key, + contract_version: 1, + inputs: Vec::new(), + outputs: Vec::new(), + status: TracePassStatus::NotApplicable, + diagnostic_indices: Vec::new(), + validation_coverage: call.validation_coverage, + parameters: call.parameters, + }); + } + + #[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/lower/codec/mod.rs b/crates/psrs-backend/src/wasm/lower/codec/mod.rs index 8858c926..39c6d62a 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], - }, + super::generated_signature(abi::STRING_TO_BYTES_SYMBOL, Some(string)), + super::generated_signature(abi::BYTES_TO_STRING_SYMBOL, Some(string)), + super::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, diff --git a/crates/psrs-backend/src/wasm/lower/mod.rs b/crates/psrs-backend/src/wasm/lower/mod.rs index 31f71430..8ebec736 100644 --- a/crates/psrs-backend/src/wasm/lower/mod.rs +++ b/crates/psrs-backend/src/wasm/lower/mod.rs @@ -7,7 +7,7 @@ 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::{DataId, DefinedTypeId, MemoryId, ValueId, ValueType}; use psrs_hir::SymbolId; use psrs_span::TextRange; use std::collections::HashMap; @@ -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 { @@ -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, @@ -233,14 +262,11 @@ pub fn lower_module_with_capabilities( index: ExportIndex::Memory(MemoryIndex(0)), }, ]; - let mut minimum = 1; + let minimum = link.plan.memory().minimum_pages; 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], - }); + types.push(generated_signature(abi::REALLOC_SYMBOL, None)); let index = indices.realloc.expect("a needed realloc has an index"); exports.push(Export { name: "cabi_realloc".into(), @@ -248,9 +274,11 @@ pub fn lower_module_with_capabilities( index: ExportIndex::Function(index), }); // The heap-state segment holds the free-list head (null) and the bump - // break (the first allocatable address). + // break (the first allocatable address). The allocator boundary is the + // one the checked plan reserved, after every artifact region. + let heap_start = link.plan.memory().heap_start; let mut state = 0_u32.to_le_bytes().to_vec(); - state.extend_from_slice(&abi::HEAP_START.to_le_bytes()); + state.extend_from_slice(&heap_start.to_le_bytes()); data.push(DataSegment { id: DataId(data.len() as u32), index: DataIndex(data.len() as u32), @@ -259,16 +287,11 @@ pub fn lower_module_with_capabilities( }, bytes: state, }); - minimum = (u64::from(abi::HEAP_START)).div_ceil(0x10000) + 1; realloc = Some(build_realloc(realloc_type, module.span)); } let 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); @@ -420,3 +443,12 @@ pub(super) fn wasm_error(span: TextRange, message: &'static str) -> Vec) -> 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(val_type).collect(), + results: signature.result.into_iter().map(val_type).collect(), + } +} 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..4c8515d7 --- /dev/null +++ b/crates/psrs-backend/src/wasm/lower/post_return/tests/composition.rs @@ -0,0 +1,45 @@ +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, + artifacts: Vec::new(), + memory: MemoryDemand { + canonical_scratch: (0, crate::abi::SCRATCH_SIZE), + allocator_state: ( + crate::abi::HEAP_STATE, + crate::abi::HEAP_STATE + crate::abi::HEAP_STATE_SIZE, + ), + base_heap_start: crate::abi::HEAP_START, + heap_alignment: crate::abi::MIN_BLOCK, + }, + }, + ) + .map_err(|error| error.to_string())?; + module.memories[0].minimum = plan.memory().minimum_pages; + 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.rs b/crates/psrs-backend/src/wasm/lower/post_return/tests/mod.rs similarity index 89% rename from crates/psrs-backend/src/wasm/lower/post_return/tests.rs rename to crates/psrs-backend/src/wasm/lower/post_return/tests/mod.rs index d19b8a1c..1a3b5b8d 100644 --- a/crates/psrs-backend/src/wasm/lower/post_return/tests.rs +++ b/crates/psrs-backend/src/wasm/lower/post_return/tests/mod.rs @@ -7,7 +7,6 @@ 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}; @@ -17,6 +16,9 @@ fn span() -> TextRange { TextRange::new(0, 1) } +mod composition; +use composition::componentize; + #[test] fn post_return_drops_an_owned_export_handle() { let wit = r#" @@ -24,10 +26,13 @@ fn post_return_drops_an_owned_export_handle() { interface types { resource thing; } + interface api { + use types.{thing}; + take: func() -> thing; + } world guest { import types; - use types.{thing}; - export take: func() -> thing; + export api; } "#; let mut resolve = Resolve::default(); @@ -39,12 +44,20 @@ fn post_return_drops_an_owned_export_handle() { .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:?}"), + let (key, item) = resolve.worlds[world].exports.iter().next().unwrap(); + let WorldItem::Interface { id, .. } = item else { + panic!("interface export"); }; - assert_eq!(export, "take"); - assert_eq!(post_return_name(&export), "cabi_post_take"); + 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); @@ -59,7 +72,7 @@ fn post_return_drops_an_owned_export_handle() { ); assert_eq!(post_signature.parameters, vec![ValType::I32]); assert!(post_signature.results.is_empty()); - assert_eq!(post_export.name, "cabi_post_take"); + assert_eq!(post_export.name, post_return_name(&export)); let module = Module { name: "Handles".into(), @@ -117,8 +130,22 @@ fn post_return_drops_an_owned_export_handle() { 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"); + 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"); @@ -128,7 +155,7 @@ fn post_return_drops_an_owned_export_handle() { "post-return should lower to canon resource.drop: {text}" ); assert!( - text.contains("cabi_post_take") || text.contains("post-return"), + text.contains("cabi_post_fixture:handles/api@0.1.0#take") || text.contains("post-return"), "the export should have a post-return: {text}" ); } @@ -409,10 +436,9 @@ fn buffer_post_return_frees_the_returned_string() { #[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"); + componentize(module, resolve, world, Vec::new()).expect("componentizing the string export"); crate::validator() .validate_all(&component) .expect("the component should validate"); diff --git a/crates/psrs-backend/src/wasm/lower/realloc/block.rs b/crates/psrs-backend/src/wasm/lower/realloc/block.rs index 9d074ed6..435c987b 100644 --- a/crates/psrs-backend/src/wasm/lower/realloc/block.rs +++ b/crates/psrs-backend/src/wasm/lower/realloc/block.rs @@ -45,8 +45,13 @@ pub(super) fn emit_allocate(asm: &mut Asm) { get(asm, L_PNODE); get(asm, L_NODE); asm.leaf(Instruction::I32Sub); + set(asm, L_FRONT); + get(asm, L_FRONT); get(asm, NEW_LEN); asm.leaf(Instruction::I32Add); + set(asm, L_TMP2); + local_trap_if_wrapped(asm, L_TMP2, L_FRONT); + get(asm, L_TMP2); set(asm, L_FRONT); checked_align_up_const(asm, L_FRONT, abi::MIN_BLOCK); // front <= size: this block fits @@ -96,8 +101,6 @@ fn emit_bump(asm: &mut Asm) { 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); @@ -125,6 +128,9 @@ fn emit_bump(asm: &mut Asm) { asm.leaf(Instruction::Unreachable); asm.end(); asm.end(); + // Commit metadata only after the entire block is addressable. + word_store(asm, L_BREAK, 0, L_TMP); + word_store(asm, L_BREAK, 4, NEW_LEN); state_store(asm, 4, L_END); } diff --git a/crates/psrs-backend/src/wasm/lower/realloc/tests.rs b/crates/psrs-backend/src/wasm/lower/realloc/tests.rs index 99bad80f..5fb4c1f9 100644 --- a/crates/psrs-backend/src/wasm/lower/realloc/tests.rs +++ b/crates/psrs-backend/src/wasm/lower/realloc/tests.rs @@ -9,7 +9,7 @@ use psrs_hir::{ModuleId, SymbolId}; use psrs_span::TextRange; use wasm_encoder::{Instruction as I, MemArg, ValType}; -const CHECK_COUNT: u32 = 8; +const CHECK_COUNT: u32 = 10; const REALLOC_INDEX: u32 = CHECK_COUNT; fn mm(align: u32) -> MemArg { @@ -224,6 +224,39 @@ fn check_bounded_growth() -> Body { asm.into_body() } +fn check_page_boundary_growth() -> Body { + let mut asm = Asm::new(); + let length = (65536 - abi::HEAP_START - abi::HEADER_SIZE) as i32; + allocate_into(&mut asm, 0, length, 8); + get(&mut asm, 0); + constant(&mut asm, 123); + asm.leaf(I::I32Store8(mm(0))); + allocate_into(&mut asm, 1, 16, 8); + asm.leaf(I::MemorySize(0)); + constant(&mut asm, 2); + asm.leaf(I::I32Eq); + get(&mut asm, 0); + asm.leaf(I::I32Load8U(mm(0))); + constant(&mut asm, 123); + asm.leaf(I::I32Eq); + asm.leaf(I::I32And); + free_call(&mut asm, 1, 16); + allocate_into(&mut asm, 2, 16, 8); + equal(&mut asm, 1, 2); + asm.leaf(I::I32And); + asm.into_body() +} + +fn free_block_size_overflow() -> Body { + let mut asm = Asm::new(); + allocate_into(&mut asm, 0, 16, 8); + free_call(&mut asm, 0, 16); + realloc_call(&mut asm, 0, 0, 8, -8); + asm.leaf(I::Drop); + constant(&mut asm, 0); + asm.into_body() +} + fn function(name: &str, body: Body, locals: usize) -> Function { Function { symbol: SymbolId::new(ModuleId(0), 0), @@ -250,6 +283,12 @@ fn fixture() -> Module { function("grow_failure", grow_failure(), 0), function("address_overflow", address_overflow(), 0), function("check_bounded_growth", check_bounded_growth(), 2), + function( + "check_page_boundary_growth", + check_page_boundary_growth(), + 3, + ), + function("free_block_size_overflow", free_block_size_overflow(), 1), ]; let realloc_type = TypeIndex(1); functions.push(build_realloc(realloc_type, span())); @@ -358,6 +397,7 @@ fn realloc_reclaims_reuses_coalesces_aligns_and_traps() { "check_reuse_and_coalesce", "check_alignment_and_resize", "check_bounded_growth", + "check_page_boundary_growth", ] { let output = run(export); assert!(output.status.success(), "{export} trapped: {output:?}"); @@ -373,6 +413,7 @@ fn realloc_reclaims_reuses_coalesces_aligns_and_traps() { "mismatched_length", "grow_failure", "address_overflow", + "free_block_size_overflow", ] { let output = run(export); assert!(!output.status.success(), "{export} unexpectedly succeeded"); diff --git a/crates/psrs-backend/src/wasm/lower/structure/instructions.rs b/crates/psrs-backend/src/wasm/lower/structure/instructions.rs index fd7ea514..222269ca 100644 --- a/crates/psrs-backend/src/wasm/lower/structure/instructions.rs +++ b/crates/psrs-backend/src/wasm/lower/structure/instructions.rs @@ -297,6 +297,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/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..370e10f6 100644 --- a/crates/psrs-backend/src/wasm/mod.rs +++ b/crates/psrs-backend/src/wasm/mod.rs @@ -13,6 +13,7 @@ mod verify; mod tests; pub use encode::encode_module; +pub(crate) use lower::lower_module_with_plan; pub use lower::{lower_module, lower_module_with_capabilities}; /// Final index domains assigned by P10. These are deliberately distinct from 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/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..5ab0a39a --- /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 WASI.Random (insecureSeed)\nmain = let seed = runEffect 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/effect/mod.rs b/crates/psrs-core/src/effect/mod.rs index db0657a4..5fb6fcfc 100644 --- a/crates/psrs-core/src/effect/mod.rs +++ b/crates/psrs-core/src/effect/mod.rs @@ -147,7 +147,17 @@ pub fn lower_effects( "trusted Effect identity is missing or is not an opaque type", )]); } - let token = intern(module, Type::Constructor(TypeConstructor::Int)); + match module.callable_parameters(effect) { + Some(1) => {} + Some(_) => { + return Err(vec![verification_error( + module, + "trusted Effect type has an incompatible callable representation", + )]); + } + None => module.callable_types.push((effect, 1)), + } + let token = state_token_type(module); let closures = rewrite_effect_applications(module, effect, token); let synthesized = synthesize_operations(module, token, trusted)?; let lowering = EffectLowering { @@ -259,6 +269,18 @@ fn is_effect_constructor(module: &Module, id: TypeId, effect: HirTypeId) -> bool ) } +/// Interns the compiler-owned opaque state token an `Effect` closure takes. +/// It has no source spelling; marking it opaque gives it a scalar runtime shape +/// without letting any pass treat it as an `Int` +/// ([effects](../../../design/backend/fp/effects.md)). +fn state_token_type(module: &mut Module) -> TypeId { + let token = psrs_hir::TypeId::STATE_TOKEN; + if !module.opaque_ids.contains(&token) { + module.opaque_ids.push(token); + } + intern(module, Type::Constructor(TypeConstructor::User(token))) +} + fn verification_error(module: &Module, message: &'static str) -> VerifyError { VerifyError { module: module.id, diff --git a/crates/psrs-core/src/effect/operations.rs b/crates/psrs-core/src/effect/operations.rs index 7af14f12..d3e805f3 100644 --- a/crates/psrs-core/src/effect/operations.rs +++ b/crates/psrs-core/src/effect/operations.rs @@ -3,7 +3,8 @@ //! `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. +//! runtime state token, and `trap` is the effect that escapes instead of +//! returning. use super::supplies::{LocalSupply, VariableSupply}; use super::{EFFECT_INTERFACE, TrustedEffect, intern}; @@ -225,7 +226,7 @@ fn run_declaration( 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 token_value = expr(ExprKind::StateToken, token, span); let body = application( expr(ExprKind::Local(action), action_ty, span), token_value, diff --git a/crates/psrs-core/src/effect/supplies.rs b/crates/psrs-core/src/effect/supplies.rs index 4a75c508..0387b403 100644 --- a/crates/psrs-core/src/effect/supplies.rs +++ b/crates/psrs-core/src/effect/supplies.rs @@ -76,7 +76,7 @@ fn max_local(expression: &Expr) -> u32 { | ExprKind::Boolean(_) | ExprKind::String(_) | ExprKind::Char(_) => 0, - ExprKind::Unit | ExprKind::Trap => 0, + ExprKind::Unit | ExprKind::StateToken | ExprKind::Trap => 0, } } 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..128e8d28 --- /dev/null +++ b/crates/psrs-core/src/instantiation/local_rows.rs @@ -0,0 +1,359 @@ +//! 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::StateToken + | 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::StateToken + | 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 91% rename from crates/psrs-core/src/opt/specialize/types/substitution.rs rename to crates/psrs-core/src/instantiation/substitution.rs index aa6d1629..9c2de0af 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, @@ -30,6 +30,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 +45,35 @@ 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() { + free_variables( + module, + *id, + &mut HashSet::new(), + &mut HashSet::new(), + &mut replacement_free_variables, + ); + } + 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 +82,7 @@ struct TypeSubstitution<'a> { shadowed: HashSet, renamed: HashMap, active: HashSet, + discharged: HashSet, } impl TypeSubstitution<'_> { @@ -77,6 +108,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 +198,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), @@ -186,6 +228,7 @@ impl TypeSubstitution<'_> { ExprKind::String(value) => ExprKind::String(value.clone()), ExprKind::Char(value) => ExprKind::Char(*value), ExprKind::Unit => ExprKind::Unit, + ExprKind::StateToken => ExprKind::StateToken, ExprKind::Trap => ExprKind::Trap, ExprKind::Array { elements } => ExprKind::Array { elements: elements diff --git a/crates/psrs-core/src/lib.rs b/crates/psrs-core/src/lib.rs index 54d64aec..0f83235f 100644 --- a/crates/psrs-core/src/lib.rs +++ b/crates/psrs-core/src/lib.rs @@ -1,17 +1,23 @@ 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, closure_parts, forall_parts, scheme_parts, +}; use psrs_hir::{ CaseBranchCoverage, ExternalSymbol, Intrinsic, LocalId, ModuleId, SymbolId, @@ -36,8 +42,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 +60,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 +118,7 @@ pub struct Binding { pub span: TextRange, } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Debug, Eq)] pub struct Expr { pub kind: ExprKind, pub ty: TypeId, @@ -137,6 +144,12 @@ pub enum ExprKind { /// constructor application. Its canonical runtime value is the integer `0` /// ([scalars and primitives](../../design/backend/fp/scalars-and-primitives.md)). Unit, + /// The one value of the compiler-owned opaque state token an `Effect` + /// closure takes. Only effect lowering produces it; it is threaded through + /// the chain and never inspected, the `State# RealWorld` analogue of + /// [effects](../../design/backend/fp/effects.md). Its runtime shape is a + /// scalar, but it is not an `Int`. + StateToken, /// An expression that never produces its value: the guest traps. Core /// carries it so the effect interface can supply an `Effect Unit` that /// escapes instead of returning, which is what an uncaught failure is on @@ -191,6 +204,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 +264,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 diff --git a/crates/psrs-core/src/link/mod.rs b/crates/psrs-core/src/link/mod.rs index ca4fdcae..999faaa2 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,108 @@ 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::StateToken + | 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/link/shift.rs b/crates/psrs-core/src/link/shift.rs index 8acbfc66..6f56c577 100644 --- a/crates/psrs-core/src/link/shift.rs +++ b/crates/psrs-core/src/link/shift.rs @@ -24,6 +24,7 @@ pub(super) fn shift_kind(kind: ExprKind, offset: u32, variable_offset: u32) -> E ExprKind::String(value) => ExprKind::String(value), ExprKind::Char(value) => ExprKind::Char(value), ExprKind::Unit => ExprKind::Unit, + ExprKind::StateToken => ExprKind::StateToken, ExprKind::Trap => ExprKind::Trap, ExprKind::Array { elements } => ExprKind::Array { elements: elements diff --git a/crates/psrs-core/src/link/variables.rs b/crates/psrs-core/src/link/variables.rs index 0d327632..05f19190 100644 --- a/crates/psrs-core/src/link/variables.rs +++ b/crates/psrs-core/src/link/variables.rs @@ -97,6 +97,6 @@ fn expression_span(expression: &Expr, maximum: &mut Option) { | ExprKind::Boolean(_) | ExprKind::String(_) | ExprKind::Char(_) => {} - ExprKind::Unit | ExprKind::Trap => {} + ExprKind::Unit | ExprKind::StateToken | ExprKind::Trap => {} } } diff --git a/crates/psrs-core/src/locals/alpha.rs b/crates/psrs-core/src/locals/alpha.rs new file mode 100644 index 00000000..f61bd99a --- /dev/null +++ b/crates/psrs-core/src/locals/alpha.rs @@ -0,0 +1,238 @@ +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::StateToken => ExprKind::StateToken, + 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..39298d26 --- /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::StateToken | 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..22a13ff8 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, @@ -109,6 +113,7 @@ fn eliminate_expr(mut expression: Expr) -> Expr { ExprKind::String(value) => ExprKind::String(value), ExprKind::Char(value) => ExprKind::Char(value), ExprKind::Unit => ExprKind::Unit, + ExprKind::StateToken => ExprKind::StateToken, ExprKind::Trap => ExprKind::Trap, }; expression @@ -159,6 +164,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); @@ -220,6 +229,6 @@ fn collect_refs(expression: &Expr, references: &mut HashSet) { | ExprKind::Boolean(_) | ExprKind::String(_) | ExprKind::Char(_) => {} - ExprKind::Unit | ExprKind::Trap => {} + ExprKind::Unit | ExprKind::StateToken | ExprKind::Trap => {} } } diff --git a/crates/psrs-core/src/opt/effects.rs b/crates/psrs-core/src/opt/effects.rs index 280ca8e2..592e861c 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(_) @@ -31,6 +36,8 @@ pub(super) fn summarize(expression: &Expr) -> Effects { | ExprKind::Char(_) => Effects::default(), // The unit value has no payload and no failure mode. ExprKind::Unit => Effects::default(), + // The state token has no payload and no failure mode. + ExprKind::StateToken => Effects::default(), // A trap is a failure by definition, so no rewrite may drop or move it // as if it were an inert value. ExprKind::Trap => Effects { @@ -43,6 +50,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 +66,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 +84,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 +120,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..2ec48a3c 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; } @@ -110,6 +117,7 @@ fn expr_introduces_type_binders(expression: &Expr, types: &[Type]) -> bool { | ExprKind::String(_) | ExprKind::Char(_) | ExprKind::Unit + | ExprKind::StateToken | ExprKind::Trap => false, } } @@ -154,6 +162,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, .. } @@ -185,11 +197,16 @@ pub(super) fn contains_case(expression: &Expr) -> bool { | ExprKind::String(_) | ExprKind::Char(_) | ExprKind::Unit + | ExprKind::StateToken | ExprKind::Trap => false, } } 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, .. } @@ -249,6 +266,6 @@ pub(super) fn collect_globals(expression: &Expr, out: &mut Vec) { | ExprKind::Boolean(_) | ExprKind::String(_) | ExprKind::Char(_) => {} - ExprKind::Unit | ExprKind::Trap => {} + ExprKind::Unit | ExprKind::StateToken | ExprKind::Trap => {} } } diff --git a/crates/psrs-core/src/opt/inline/global/mod.rs b/crates/psrs-core/src/opt/inline/global/mod.rs index f8f32e7a..1cde4dac 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, @@ -339,6 +362,7 @@ fn inline_expr( ExprKind::String(value) => ExprKind::String(value), ExprKind::Char(value) => ExprKind::Char(value), ExprKind::Unit => ExprKind::Unit, + ExprKind::StateToken => ExprKind::StateToken, ExprKind::Trap => ExprKind::Trap, }; expression diff --git a/crates/psrs-core/src/opt/inline/local.rs b/crates/psrs-core/src/opt/inline/local.rs index 6b042fbc..432ab47a 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(), @@ -159,6 +234,7 @@ fn inline_expr( ExprKind::String(value) => ExprKind::String(value), ExprKind::Char(value) => ExprKind::Char(value), ExprKind::Unit => ExprKind::Unit, + ExprKind::StateToken => ExprKind::StateToken, ExprKind::Trap => ExprKind::Trap, }; expression diff --git a/crates/psrs-core/src/opt/simplify/mod.rs b/crates/psrs-core/src/opt/simplify/mod.rs index cad65d91..5b4477d5 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, @@ -215,7 +219,7 @@ fn simplify_expr(mut expression: Expr, fresh: &mut FreshLocals) -> Expr { ExprKind::Boolean(value) => ExprKind::Boolean(value), ExprKind::String(value) => ExprKind::String(value), ExprKind::Char(value) => ExprKind::Char(value), - kind @ (ExprKind::Unit | ExprKind::Trap) => kind, + kind @ (ExprKind::Unit | ExprKind::StateToken | ExprKind::Trap) => kind, }; expression } @@ -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/calls.rs b/crates/psrs-core/src/opt/specialize/calls.rs index 812b786f..f2aeb3f5 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 { @@ -122,6 +134,7 @@ pub(super) fn rewrite( ExprKind::String(value) => ExprKind::String(value), ExprKind::Char(value) => ExprKind::Char(value), ExprKind::Unit => ExprKind::Unit, + ExprKind::StateToken => ExprKind::StateToken, ExprKind::Trap => ExprKind::Trap, }; expression diff --git a/crates/psrs-core/src/opt/specialize/mod.rs b/crates/psrs-core/src/opt/specialize/mod.rs index 4f3ec83f..dd13c7e6 100644 --- a/crates/psrs-core/src/opt/specialize/mod.rs +++ b/crates/psrs-core/src/opt/specialize/mod.rs @@ -160,6 +160,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, .. } @@ -221,6 +225,6 @@ fn collect_references(expression: &crate::Expr, out: &mut Vec) { | crate::ExprKind::Boolean(_) | crate::ExprKind::String(_) | crate::ExprKind::Char(_) => {} - crate::ExprKind::Unit | crate::ExprKind::Trap => {} + crate::ExprKind::Unit | crate::ExprKind::StateToken | crate::ExprKind::Trap => {} } } 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..dc0a6629 100644 --- a/crates/psrs-core/src/opt/tests/mod.rs +++ b/crates/psrs-core/src/opt/tests/mod.rs @@ -130,7 +130,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 +160,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 +182,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 +201,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 +237,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 +250,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 +268,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..49432fef 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(_) @@ -34,7 +16,7 @@ pub(super) fn count_nodes(expression: &Expr) -> usize { | ExprKind::Boolean(_) | ExprKind::String(_) | ExprKind::Char(_) => 0, - ExprKind::Unit | ExprKind::Trap => 0, + ExprKind::Unit | ExprKind::StateToken | ExprKind::Trap => 0, ExprKind::Constructor { arguments, .. } | ExprKind::IntrinsicCall { arguments, .. } | ExprKind::Array { @@ -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) @@ -131,6 +121,7 @@ fn substitute_inner( ExprKind::String(value) => ExprKind::String(value.clone()), ExprKind::Char(value) => ExprKind::Char(*value), ExprKind::Unit => ExprKind::Unit, + ExprKind::StateToken => ExprKind::StateToken, ExprKind::Trap => ExprKind::Trap, ExprKind::Array { elements } => ExprKind::Array { elements: elements @@ -289,102 +280,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..7f608d28 --- /dev/null +++ b/crates/psrs-core/src/primitive.rs @@ -0,0 +1,192 @@ +//! 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::StateToken + | ExprKind::Trap => {} + } + Ok(()) +} 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 index 9f90da3b..fc8c652a 100644 --- a/crates/psrs-core/src/tests/effects.rs +++ b/crates/psrs-core/src/tests/effects.rs @@ -31,20 +31,21 @@ fn trusted_effect() -> TrustedEffect { /// /// | id | node | /// | --- | --- | -/// | 0 | `Int`, the token representation lowering chose | +/// | 0 | the opaque state token, the 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; + let token = HirTypeId::STATE_TOKEN; Module { id: ModuleId(0), name: "Main".into(), externals: Vec::new(), external_types: Vec::new(), types: vec![ - Type::Constructor(TypeConstructor::Int), + Type::Constructor(TypeConstructor::User(token)), Type::Variable(TypeVariableId(0)), Type::Variable(TypeVariableId(1)), Type::Constructor(TypeConstructor::Function), @@ -54,7 +55,7 @@ fn effect_module() -> Module { Type::Application(EFFECT, ARROW), ], newtype_ids: Vec::new(), - opaque_ids: vec![effect], + opaque_ids: vec![effect, token], callable_types: Vec::new(), constructors: Vec::new(), declarations: Vec::new(), 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/mod.rs b/crates/psrs-core/src/tests/mod.rs index 0ed11b44..d7526094 100644 --- a/crates/psrs-core/src/tests/mod.rs +++ b/crates/psrs-core/src/tests/mod.rs @@ -1,8 +1,10 @@ use super::*; use psrs_hir::{Intrinsic, LocalId, ModuleId, SymbolId}; +mod deep_expr; mod effects; mod external_types; +mod instantiation; mod link; mod patterns; mod rank_n; diff --git a/crates/psrs-core/src/tests/rank_n.rs b/crates/psrs-core/src/tests/rank_n.rs index 766ec086..12535e70 100644 --- a/crates/psrs-core/src/tests/rank_n.rs +++ b/crates/psrs-core/src/tests/rank_n.rs @@ -350,3 +350,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/types.rs b/crates/psrs-core/src/types.rs index c14f48f1..ff63e91e 100644 --- a/crates/psrs-core/src/types.rs +++ b/crates/psrs-core/src/types.rs @@ -113,3 +113,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/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/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..454f90a7 100644 --- a/crates/psrs-core/src/verify/expr/intrinsic.rs +++ b/crates/psrs-core/src/verify/expr/intrinsic.rs @@ -22,7 +22,10 @@ impl Context<'_> { 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 => { @@ -44,7 +47,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..0af086dd 100644 --- a/crates/psrs-core/src/verify/expr/mod.rs +++ b/crates/psrs-core/src/verify/expr/mod.rs @@ -2,7 +2,7 @@ use super::{ Locals, SchemeType, array_element, compatible, error, primitive_type_id, record_field, restore_local, verify_pattern, verify_type, }; -use crate::{Expr, ExprKind, Module, TypeConstructor, TypeId, VerifyError}; +use crate::{Expr, ExprKind, Module, Type, TypeConstructor, TypeId, VerifyError}; use psrs_hir::{ModuleId, SymbolId}; use std::collections::HashMap; @@ -17,14 +17,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 +50,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 +70,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 +86,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, @@ -93,6 +113,21 @@ impl Context<'_> { ExprKind::String(_) => self.shape(expression, TypeConstructor::String), ExprKind::Char(_) => self.shape(expression, TypeConstructor::Char), ExprKind::Unit => self.shape(expression, TypeConstructor::Unit), + // The state token is the one value of the compiler-owned opaque + // token type. Only effect lowering produces it. + ExprKind::StateToken => { + if !matches!( + self.module.types.get(expression.ty.0 as usize), + Some(Type::Constructor(TypeConstructor::User(id))) + if *id == psrs_hir::TypeId::STATE_TOKEN + ) { + self.errors.push(error( + self.owner, + expression.span, + "state token expression does not have the compiler token type", + )); + } + } // A trap produces no value, so its type is only the one its context // wants; the surrounding context check already established that. ExprKind::Trap => {} @@ -148,10 +183,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 +280,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 +304,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 +357,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 +437,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..f9891011 100644 --- a/crates/psrs-core/src/verify/scopes/expr.rs +++ b/crates/psrs-core/src/verify/scopes/expr.rs @@ -8,6 +8,15 @@ pub(super) fn scoped_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, @@ -27,10 +36,16 @@ pub(super) fn scoped_expr( | ExprKind::Char(_) => {} // Both are leaves: their type is already checked by the expression // check, and neither mentions a binder. - ExprKind::Unit | ExprKind::Trap => {} + ExprKind::Unit | ExprKind::StateToken | 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, .. } => { @@ -101,8 +116,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 +138,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 { 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/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..887c1627 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,6 +16,9 @@ use std::collections::{HashMap, HashSet}; mod closure; mod constructors; +mod evidence; +mod invariant; +pub(crate) use evidence::instantiation; mod helpers; mod rows; pub(in crate::verify) use constructors::constructor_fields_match; @@ -29,7 +42,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 +68,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 +102,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 +124,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)) { + 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 +187,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 +199,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 +214,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 +234,55 @@ impl TypeMatcher<'_> { variables: expected_variables, body: expected_body, } = expected_type + && actual_variables.len() == expected_variables.len() { - 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 +309,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 +321,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 +342,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 +421,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..4d971076 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::{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..c77e5258 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,8 @@ pub(super) fn primitive_types(intrinsic: Intrinsic, module: &Module) -> (TypeId, Intrinsic::NumberAdd | Intrinsic::NumberSub | Intrinsic::NumberMul - | Intrinsic::NumberDiv => (Number, Number), + | Intrinsic::NumberDiv + | Intrinsic::NumberAtan2 => (Number, Number), Intrinsic::NumberEq | Intrinsic::NumberNe | Intrinsic::NumberLt @@ -73,13 +72,23 @@ 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 => (Number, Number), 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 +135,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..f360c904 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 diff --git a/crates/psrs-desugar/src/tests.rs b/crates/psrs-desugar/src/tests.rs index 3598370d..fd23b302 100644 --- a/crates/psrs-desugar/src/tests.rs +++ b/crates/psrs-desugar/src/tests.rs @@ -5,14 +5,14 @@ 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 { 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..d31da7c6 100644 --- a/crates/psrs-driver/Cargo.toml +++ b/crates/psrs-driver/Cargo.toml @@ -24,3 +24,5 @@ psrs-span.workspace = true psrs-syntax.workspace = true psrs-thir.workspace = true psrs-typecheck.workspace = true +serde = { version = "1", features = ["derive"] } +serde_json = "1" 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..2ab22ab2 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::{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)] 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 60% rename from crates/psrs-driver/src/prelude.rs rename to crates/psrs-driver/src/prelude/mod.rs index b3ceed55..a6369c1d 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::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,41 @@ 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> { 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(); + 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 +92,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 +113,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 +128,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 +180,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..326d9a78 --- /dev/null +++ b/crates/psrs-driver/src/prelude/package.rs @@ -0,0 +1,269 @@ +//! 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, +} + +#[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() + )); + } + } + // 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, + }) +} + +/// 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 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()); + } +} diff --git a/crates/psrs-driver/src/program/compilation.rs b/crates/psrs-driver/src/program/compilation.rs new file mode 100644 index 00000000..a2f58fd3 --- /dev/null +++ b/crates/psrs-driver/src/program/compilation.rs @@ -0,0 +1,256 @@ +use super::{lower_program_to_core_and_effect_context, 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); + match report.artifact { + Some(artifact) => Ok(artifact), + None => Err(report.diagnostics), + } +} + +pub(crate) fn compile_program_sources_with_trusted_prefix_report( + sources: &[(&str, &str)], + trusted_prefix: usize, +) -> CompilationReport { + compile_attempt(sources, trusted_prefix, true, false) +} + +pub(crate) fn compile_program_sources_with_trusted_prefix_diagnosis( + sources: &[(&str, &str)], + trusted_prefix: usize, + capture_dumps: bool, +) -> CompilationReport { + compile_attempt(sources, trusted_prefix, capture_dumps, true) +} + +fn compile_attempt( + sources: &[(&str, &str)], + trusted_prefix: usize, + capture_dumps: bool, + trace_enabled: bool, +) -> CompilationReport { + let (core, source_warnings, effect_context) = + match lower_program_to_core_and_effect_context(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, + effect_context, + 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, + effect_context, + 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, + effect_context, + 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..f42e7ea9 100644 --- a/crates/psrs-driver/src/program/effects.rs +++ b/crates/psrs-driver/src/program/effects.rs @@ -113,8 +113,8 @@ pub(super) fn trusted_effect( }; let mut operations = Vec::new(); for (name, operation, wit_name) in [ - ("pure", EffectOperation::Pure, "pure"), - ("bind", EffectOperation::Bind, "bind"), + ("effectPure", EffectOperation::Pure, "pure"), + ("effectBind", EffectOperation::Bind, "bind"), ("runEffect", EffectOperation::Run, "run"), ("trap", EffectOperation::Trap, "trap"), ] { @@ -230,7 +230,7 @@ fn collect_runner_references(expression: &Expr, runner: SymbolId, spans: &mut Ve collect_runner_references(element, runner, spans); } } - ExprKind::Record(fields) => { + ExprKind::Record(fields) | ExprKind::MatchProduct(fields) => { for (_, value) in fields { collect_runner_references(value, runner, spans); } @@ -275,7 +275,7 @@ fn collect_runner_references(expression: &Expr, runner: SymbolId, spans: &mut Ve operators, } => { for operator in operators { - if operator.symbol == runner { + if operator.local.is_none() && operator.symbol == runner { spans.push(operator.operator_span); } } 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

CompilationReport { + let (all_sources, trusted_prefix) = match with_prelude(sources) { + Ok(sources) => sources, + Err(errors) => return CompilationReport::failed(errors, Default::default()), + }; + let mut report = + super::compile_program_sources_with_trusted_prefix_report(&all_sources, trusted_prefix); + report.diagnostics = shift(report.diagnostics, trusted_prefix); + report +} + +/// Compiles for `psrs diagnose`, retaining a lightweight pass trace and +/// optionally the IR snapshots used by trace-mode bundles. +pub fn compile_program_sources_with_prelude_diagnosis( + sources: &[(&str, &str)], + capture_dumps: bool, +) -> CompilationReport { + let (all_sources, trusted_prefix) = match with_prelude(sources) { + Ok(sources) => sources, + Err(errors) => return CompilationReport::failed(errors, Default::default()), + }; + let mut report = super::compile_program_sources_with_trusted_prefix_diagnosis( + &all_sources, + trusted_prefix, + capture_dumps, + ); + report.diagnostics = shift(report.diagnostics, trusted_prefix); + report +} + /// Resolves user sources leniently together with the on-disk standard library. /// /// A lenient check tolerates imports whose modules are not provided at all, so a diff --git a/crates/psrs-driver/src/program/mod.rs b/crates/psrs-driver/src/program/mod.rs index e5dcb152..18b2233b 100644 --- a/crates/psrs-driver/src/program/mod.rs +++ b/crates/psrs-driver/src/program/mod.rs @@ -2,19 +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(super) use compilation::{ + compile_program_sources_with_trusted_prefix, + compile_program_sources_with_trusted_prefix_diagnosis, + compile_program_sources_with_trusted_prefix_report, +}; 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; @@ -42,45 +50,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)], @@ -368,6 +337,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 +398,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..78ba5c25 100644 --- a/crates/psrs-driver/src/tests/assertions.rs +++ b/crates/psrs-driver/src/tests/assertions.rs @@ -129,11 +129,9 @@ fn assert_true_and_assert_false_report_the_value_that_did_not_hold() { 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 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..138378bf --- /dev/null +++ b/crates/psrs-driver/src/tests/closure_protocol.rs @@ -0,0 +1,163 @@ +//! 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.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) = run_with_wasmtime(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::lower_cc_with_context( + prepared.core.clone(), + prepared.effect_context.as_ref(), + ) + .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/data_function.rs b/crates/psrs-driver/src/tests/data_function.rs index 2f6d0d42..da911c6b 100644 --- a/crates/psrs-driver/src/tests/data_function.rs +++ b/crates/psrs-driver/src/tests/data_function.rs @@ -7,6 +7,27 @@ 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#" 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..048645da --- /dev/null +++ b/crates/psrs-driver/src/tests/diagnosis_trace.rs @@ -0,0 +1,209 @@ +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 effect_lowering_output_binding_artifact_flows_into_validation_and_cc() { + let source = "module Main where\nimport Prelude\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.effect.lower") + .expect("effect context triggers the lowering pass"); + assert_eq!(effect.status, TracePassStatus::Completed); + let bindings = *effect.outputs.get(1).expect("effect 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("24")); + 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, + prepared.effect_context, + 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/fixtures/methods.rs b/crates/psrs-driver/src/tests/dictionary_audit/fixtures/methods.rs index cc13a951..5cfdd828 100644 --- a/crates/psrs-driver/src/tests/dictionary_audit/fixtures/methods.rs +++ b/crates/psrs-driver/src/tests/dictionary_audit/fixtures/methods.rs @@ -332,8 +332,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/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 index 88480fbf..bc2b3af8 100644 --- a/crates/psrs-driver/src/tests/effect_arity.rs +++ b/crates/psrs-driver/src/tests/effect_arity.rs @@ -29,12 +29,12 @@ fn an_effect_of_a_function_is_not_arity_two_and_log_is_saturated() { "Effect Int is a one-parameter closure" ); assert_eq!( - function("pure").parameters.len(), + function("effectPure").parameters.len(), 1, "pure takes the value and returns the token closure" ); assert_eq!( - function("bind").parameters.len(), + function("effectBind").parameters.len(), 2, "bind takes the effect and the continuation; the token belongs to the result" ); diff --git a/crates/psrs-driver/src/tests/functions.rs b/crates/psrs-driver/src/tests/functions.rs index 24d79242..6fbe2a28 100644 --- a/crates/psrs-driver/src/tests/functions.rs +++ b/crates/psrs-driver/src/tests/functions.rs @@ -197,6 +197,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/generic_aggregate_audit.rs b/crates/psrs-driver/src/tests/generic_aggregate_audit.rs index 1c59130b..e409e8c6 100644 --- a/crates/psrs-driver/src/tests/generic_aggregate_audit.rs +++ b/crates/psrs-driver/src/tests/generic_aggregate_audit.rs @@ -227,7 +227,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 diff --git a/crates/psrs-driver/src/tests/generic_aggregate_fixtures.rs b/crates/psrs-driver/src/tests/generic_aggregate_fixtures.rs index a24268ec..e2d2d8a7 100644 --- a/crates/psrs-driver/src/tests/generic_aggregate_fixtures.rs +++ b/crates/psrs-driver/src/tests/generic_aggregate_fixtures.rs @@ -54,7 +54,7 @@ pub(super) fn clear_array_literals(expression: &mut psrs_core::Expr) -> bool { | ExprKind::Boolean(_) | ExprKind::String(_) | ExprKind::Char(_) => false, - ExprKind::Unit | ExprKind::Trap => false, + ExprKind::Unit | ExprKind::StateToken | ExprKind::Trap => false, } } 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..c949251f 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"; @@ -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..c0ef058d 100644 --- a/crates/psrs-driver/src/tests/integration.rs +++ b/crates/psrs-driver/src/tests/integration.rs @@ -96,7 +96,9 @@ fn compiles_if_expression_through_cfg_to_structured_wasm() { #[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")); } 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/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..2c04c83a 100644 --- a/crates/psrs-driver/src/tests/mod.rs +++ b/crates/psrs-driver/src/tests/mod.rs @@ -5,17 +5,33 @@ use std::sync::atomic::{AtomicU32, Ordering}; static WASM_ARTIFACT_COUNTER: AtomicU32 = AtomicU32::new(0); mod assertions; +mod closure_protocol; mod coercion; mod data_function; mod data_tuple; +mod declaration_calls; mod deriving; +mod diagnosis_trace; mod effect_arity; mod effects; mod foldable; mod functor; mod guard_coverage; +mod intrinsic_contracts; +mod let_constraints; +mod library_foreign; +mod number_abs; +mod number_acos; +mod number_asin; +mod number_atan; +mod number_atan2; +mod number_decimal; +mod number_rounding; +mod number_sqrt; +mod number_trunc; mod operators; mod partial_application; +mod primitive_foreign; mod scalars; mod semigroup; mod show; @@ -93,7 +109,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 +153,7 @@ mod backend; mod declarations; mod integration; mod kinds; +mod long_expression; mod resolution; mod typecheck; mod wasi; @@ -154,3 +178,5 @@ mod module_loader; mod library_types; mod tail_calls; + +mod symbol_reflection; diff --git a/crates/psrs-driver/src/tests/module_loader.rs b/crates/psrs-driver/src/tests/module_loader.rs index 0a814c27..75b2da80 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"); @@ -169,6 +124,34 @@ fn loads_the_standard_library_from_disk_in_trusted_order() { .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 WASI.Console\nimport WASI.Clock\nmain = let stamp = runEffect now in let action = runEffect (log \"ok\") in stamp\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"); 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..99548090 --- /dev/null +++ b/crates/psrs-driver/src/tests/number_atan.rs @@ -0,0 +1,56 @@ +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 +checks = 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_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/mod.rs b/crates/psrs-driver/src/tests/operators/mod.rs index cab83afd..1b1a8a78 100644 --- a/crates/psrs-driver/src/tests/operators/mod.rs +++ b/crates/psrs-driver/src/tests/operators/mod.rs @@ -1,11 +1,12 @@ mod opaque_type; mod prim_type; +mod sections; 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 +457,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/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/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..560e5b31 100644 --- a/crates/psrs-driver/src/tests/pattern_matching_audit.rs +++ b/crates/psrs-driver/src/tests/pattern_matching_audit.rs @@ -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..51128a3a 100644 --- a/crates/psrs-driver/src/tests/polymorphism_erasure_audit.rs +++ b/crates/psrs-driver/src/tests/polymorphism_erasure_audit.rs @@ -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 Prelude\nimport WASI.Console\nmain = let id = (\\x -> x) :: forall a. a -> a in runEffect (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..b1594b44 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,6 +112,16 @@ 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"; 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/show.rs b/crates/psrs-driver/src/tests/show.rs index ad3af3ad..6fe3a588 100644 --- a/crates/psrs-driver/src/tests/show.rs +++ b/crates/psrs-driver/src/tests/show.rs @@ -89,6 +89,124 @@ main = let ignored = runEffect checks in 0 ); } +#[test] +fn formatter_plan_records_the_pinned_artifact_digest() { + 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("1")); + let digests = parameter("artifact_digests").expect("artifact digests are recorded"); + assert!(digests.contains("psrs:runtime-number"), "{digests}"); + assert!( + digests.contains("e295fd40bcb247ee77b8890107b2dea74838ded7b383895ce8db7c428d0e7ac2"), + "{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 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 = runEffect 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\n\nimport Prelude\nimport Effect.Console (log)\n\nmain = let ignored = runEffect (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 +228,24 @@ 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 Prelude +import Effect.Console (log) +import WASI.Random (randomBytes) +main = let retained = show 1.0e21 + first = runEffect (log retained) + large = runEffect (randomBytes 70000) + next = runEffect (log (show 5.0e-324)) + last = runEffect (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/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/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/mod.rs b/crates/psrs-driver/src/tests/wasi/mod.rs index 8ad8306c..ca067969 100644 --- a/crates/psrs-driver/src/tests/wasi/mod.rs +++ b/crates/psrs-driver/src/tests/wasi/mod.rs @@ -2,6 +2,7 @@ use super::*; mod do_notation; mod filesystem; +mod payloads; mod wat; mod where_clause; use wat::*; @@ -427,59 +428,27 @@ 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. +/// Source and command entry share one checked exit import. Stored actions do +/// not invoke their closure; forced actions exit with their own observable code. #[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" - ); + let stored = + "module Main where\nimport WASI.Process\nmain = let action = exitWithCode 99 in 0\n"; + let forced = "module Main where\nimport Prelude\nimport WASI.Process\nmain = let value = runEffect (exitWithCode 99) in 0\n"; + for (source, invokes_action, expected_exit) in [(stored, false, 0), (forced, true, 99)] { + let artifact = compile_source("Main.purs", source).expect("exit action should compile"); + let core = core_main(&artifact.wat); + let entry = exported_func(core, "wasi:cli/run@0.2.12#run"); + let functions = core_functions(core); + let reachable = reachable_by_call(&functions, entry); + assert_eq!(has_call_ref(&functions, &reachable), invokes_action); + // Source and synthesized command exit share one checked import. Values + // distinguish forcing the stored action from normal command completion. + 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..046753ec --- /dev/null +++ b/crates/psrs-driver/src/tests/wasi/payloads.rs @@ -0,0 +1,32 @@ +use super::*; + +#[test] +fn wit_result_bytes_recover_values_and_empty_arrays_when_wasmtime_is_available() { + let source = r#"module Main where +import Prelude +import Data.Either (Either(..)) +import WASI.IO (getStdin, blockingRead) +main = + let stream = runEffect getStdin in + let empty = runEffect (blockingRead stream 0) in + case empty of + Left _ -> 1 + Right zero -> + let result = runEffect (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/wrappers.rs b/crates/psrs-driver/src/tests/wasi/wrappers.rs index 4915c115..2ddb4d28 100644 --- a/crates/psrs-driver/src/tests/wasi/wrappers.rs +++ b/crates/psrs-driver/src/tests/wasi/wrappers.rs @@ -240,10 +240,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:effect` abstract effect + // operations, `psrs:intrinsic` checked primitives). 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(); 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/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..8243421e --- /dev/null +++ b/crates/psrs-hir/src/compiler_interface.rs @@ -0,0 +1,48 @@ +//! 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)], + }, +]; + +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..dcb26a7c --- /dev/null +++ b/crates/psrs-hir/src/intrinsic/effects.rs @@ -0,0 +1,35 @@ +//! 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 => 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, + }, + 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 => Self::default(), + } + } +} diff --git a/crates/psrs-hir/src/intrinsic/mod.rs b/crates/psrs-hir/src/intrinsic/mod.rs index 7127e899..d70c3d79 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,119 @@ 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, } impl Intrinsic { @@ -105,22 +143,32 @@ impl Intrinsic { registry::descriptor(self) } - /// Every variant, in discriminant order. `bootstrap_externals` builds the + /// 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 identities; these slots must never be reused. + pub const RESERVED_IDS: [u32; 2] = [26, 27]; + + /// Every active variant, in discriminant order. `bootstrap_externals` builds the /// compiler-known externals from it; the assertion below keeps it exact. - pub const ALL: [Intrinsic; 60] = [ + pub const ALL: [Intrinsic; 72] = [ 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 +182,6 @@ impl Intrinsic { Intrinsic::IntToBoolean, Intrinsic::CharToInt, Intrinsic::IntToChar, - Intrinsic::IntDiv, - Intrinsic::IntMod, Intrinsic::IntAnd, Intrinsic::IntOr, Intrinsic::IntXor, @@ -168,18 +214,34 @@ 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, ]; } -// `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() + == Intrinsic::NumberAtan2 as u32 as usize + 1, "Intrinsic::ALL is out of date: update it when adding a variant", ); - let mut seen: u128 = 0; + let mut seen: u128 = + (1u128 << Intrinsic::RESERVED_IDS[0]) | (1u128 << Intrinsic::RESERVED_IDS[1]); let mut index = 0; while index < Intrinsic::ALL.len() { let bit = 1u128 << (Intrinsic::ALL[index] as u32); diff --git a/crates/psrs-hir/src/intrinsic/registry.rs b/crates/psrs-hir/src/intrinsic/registry.rs index a252cfa5..b2410650 100644 --- a/crates/psrs-hir/src/intrinsic/registry.rs +++ b/crates/psrs-hir/src/intrinsic/registry.rs @@ -8,7 +8,7 @@ //! through the exhaustive `descriptor` match, so a variant cannot be registered //! silently. -use super::Intrinsic; +use super::{Intrinsic, IntrinsicEffects}; use crate::{BuiltinType, Type, TypeId, TypeKind, TypeParameter}; use psrs_span::TextRange; @@ -18,8 +18,8 @@ use psrs_span::TextRange; pub enum IntrinsicCategory { /// A nullary value: `true`, `false`, `unit`. Nullary, - /// One scalar argument and a scalar result. - UnaryScalar, + /// One language argument and a language result. + Unary, /// Two scalar arguments and a scalar result. BinaryScalar, /// `Array.length`: `forall a. Array a -> Int`. @@ -30,6 +30,10 @@ pub enum IntrinsicCategory { 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`. @@ -46,15 +50,15 @@ pub enum IntrinsicCategory { 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`). + /// 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, } @@ -73,6 +77,7 @@ macro_rules! descriptors { name: $name, arity: $arity, category: IntrinsicCategory::$category, + effects: IntrinsicEffects::for_intrinsic(intrinsic), scheme: $scheme, }, )* @@ -84,32 +89,39 @@ macro_rules! descriptors { 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; + 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, 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; + 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; + 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; @@ -142,6 +154,11 @@ descriptors! { 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; } /// The HIR type schemes. Each returns a fresh [`Type`], so a caller that @@ -273,6 +290,14 @@ mod scheme { 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) } @@ -324,6 +349,13 @@ mod scheme { ) } + 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())) } @@ -349,6 +381,12 @@ mod scheme { 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"))) + } } #[cfg(test)] diff --git a/crates/psrs-hir/src/lib.rs b/crates/psrs-hir/src/lib.rs index c1dfc8da..e876a9de 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,12 @@ 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); + /// The compiler-owned opaque state token an `Effect` closure takes. It has + /// no source spelling and one uninspectable value; it is the `State# + /// RealWorld` analogue of [effects](../design/backend/fp/effects.md), not a + /// source type. Its runtime shape is a scalar, but no source or later pass + /// may treat it as an `Int`. + pub const STATE_TOKEN: Self = Self::new(ModuleId::INTRINSICS, 31); } mod intrinsic; @@ -119,6 +131,20 @@ 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), +} + +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)] 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/mod.rs b/crates/psrs-hir/src/primitives/mod.rs index 8e95729f..9c9f4777 100644 --- a/crates/psrs-hir/src/primitives/mod.rs +++ b/crates/psrs-hir/src/primitives/mod.rs @@ -38,6 +38,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 +64,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/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/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-linker/Cargo.toml b/crates/psrs-linker/Cargo.toml new file mode 100644 index 00000000..3bc06b14 --- /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"] } +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.rs b/crates/psrs-linker/src/application.rs new file mode 100644 index 00000000..83c5009d --- /dev/null +++ b/crates/psrs-linker/src/application.rs @@ -0,0 +1,160 @@ +//! Verify encoded application contracts before executing a checked link plan. +use crate::{CheckedLinkPlan, CoreSignature, CoreType, LinkErrors, LinkStage}; +use wasmparser::{CompositeInnerType, Operator, Parser, Payload, TypeRef, ValType}; + +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 = Vec::new(); + let mut seen = std::collections::BTreeSet::new(); + let mut memory = None; + let mut data = Vec::new(); + for payload in Parser::new(0).parse_all(bytes) { + match payload.map_err(|error| error.to_string())? { + Payload::TypeSection(reader) => { + for group in reader { + for ty in group.map_err(|error| error.to_string())?.into_types() { + types.push(match ty.composite_type.inner { + CompositeInnerType::Func(ty) => Some(ty), + _ => None, + }); + } + } + } + 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()); + }; + 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 ty = types + .get(index as usize) + .and_then(Option::as_ref) + .ok_or("application import has no function signature")?; + let signature = CoreSignature { + parameters: ty + .params() + .iter() + .copied() + .map(scalar) + .collect::>()?, + result: match ty.results() { + [] => None, + [ty] => Some(scalar(*ty)?), + _ => { + return Err( + "application import has unsupported multiple results".into() + ); + } + }, + }; + if signature != binding.signature { + return Err( + "application import signature disagrees with the checked plan".into(), + ); + } + if !seen.insert((binding.module.clone(), binding.field.clone())) { + return Err("application duplicates a planned import".into()); + } + } + } + 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::StartSection { .. } => { + return Err("application start precedes planned shim resolution".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(()) +} + +fn scalar(ty: ValType) -> Result { + match ty { + ValType::I32 => Ok(CoreType::I32), + ValType::I64 => Ok(CoreType::I64), + ValType::F32 => Ok(CoreType::F32), + ValType::F64 => Ok(CoreType::F64), + _ => Err("a target import has an unchecked reference representation".into()), + } +} diff --git a/crates/psrs-linker/src/closure.rs b/crates/psrs-linker/src/closure.rs new file mode 100644 index 00000000..e4411e62 --- /dev/null +++ b/crates/psrs-linker/src/closure.rs @@ -0,0 +1,105 @@ +//! 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().copied().map(scalar), + binding.signature.result.map(scalar), + ); + 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) -> ValType { + match ty { + CoreType::I32 => ValType::I32, + CoreType::I64 => ValType::I64, + CoreType::F32 => ValType::F32, + CoreType::F64 => ValType::F64, + } +} +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..c2b32569 --- /dev/null +++ b/crates/psrs-linker/src/compose.rs @@ -0,0 +1,147 @@ +//! 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 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:#}"), + ) + })?; + } + let output = 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/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/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..85a8ce87 --- /dev/null +++ b/crates/psrs-linker/src/lib.rs @@ -0,0 +1,41 @@ +//! 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 definitions; +mod digest; +mod error; +pub mod guest; +pub mod plan; +pub mod runtime; +mod stack; +mod target; +mod verify; + +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, MemoryPlan, ResolvedBinding, plan}; +pub use stack::{StackBound, measure_stack_bound}; +pub use target::{ + ArtifactContract, ArtifactKind, ArtifactReference, BindingRequirement, Boundary, CoreSignature, + CoreType, DeclaredElement, DeclaredExport, DeclaredGlobal, DeclaredImport, DeclaredTable, + ExportKind, ImportKind, InitializationContract, MemoryDemand, Provider, RequirementId, + 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..1e4895c2 --- /dev/null +++ b/crates/psrs-linker/src/plan.rs @@ -0,0 +1,444 @@ +//! Provider closure, artifact verification, and memory/instantiation planning. + +use crate::definitions::ResolvedWorldContext; +use crate::error::{LinkErrors, LinkStage}; +use crate::target::{ + ArtifactContract, ArtifactKind, Boundary, CoreSignature, Provider, RequirementId, + StorageRegion, TargetLinkInput, +}; +use crate::verify::{VerifiedArtifact, verify_artifact}; +use std::collections::{BTreeMap, BTreeSet}; + +/// 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 checked memory ownership and allocator boundary. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MemoryPlan { + pub heap_start: u32, + pub heap_alignment: u32, + pub minimum_pages: u64, + /// Every owned region, sorted by start address. + pub reservations: Vec, + pub(crate) canonical_scratch: (u32, u32), + pub(crate) allocator_state: (u32, u32), +} + +/// 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, + 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 + } +} + +/// Plans a checked link from a resolved world and target input. +pub fn plan( + context: &ResolvedWorldContext, + input: TargetLinkInput, +) -> Result { + let TargetLinkInput { + requirements, + artifacts: artifact_refs, + memory: memory_demand, + policy, + } = input; + let stage = LinkStage::Requirements; + let mut errors = Vec::new(); + + let mut requirement_ids = BTreeSet::new(); + for requirement in &requirements { + if !requirement_ids.insert(requirement.id) { + errors.push(crate::error::LinkError { + stage, + subject: Some(requirement.origin.clone()), + message: "duplicate requirement identity".into(), + }); + } + } + + let mut artifacts: BTreeMap = BTreeMap::new(); + for artifact in &artifact_refs { + let id = artifact.contract.id.clone(); + if artifacts.insert(id.clone(), artifact.clone()).is_some() { + errors.push(crate::error::LinkError { + stage, + subject: Some(id), + message: "duplicate artifact identity".into(), + }); + } + } + if !errors.is_empty() { + return Err(LinkErrors::new(errors)); + } + + let mut verified: BTreeMap = BTreeMap::new(); + let mut used_artifacts = BTreeSet::new(); + // A core import identity resolves to exactly one provider and signature. + let mut claims = BTreeMap::<(String, String), (CoreSignature, String)>::new(); + let mut bindings = Vec::new(); + + for requirement in &requirements { + match &requirement.provider { + Provider::Generated => {} + Provider::ArtifactExport { + artifact, + export, + signature, + } => { + let reference = artifacts.get(artifact).ok_or_else(|| { + LinkErrors::one( + stage, + &requirement.origin, + format!("absent artifact `{artifact}`"), + ) + })?; + let Boundary::RawCore { module, field } = &requirement.boundary else { + return Err(LinkErrors::one( + stage, + &requirement.origin, + "an artifact export requires a raw-core boundary", + )); + }; + if module != &reference.contract.module_name || field != export { + return Err(LinkErrors::one( + stage, + &requirement.origin, + "artifact boundary does not name the selected export", + )); + } + let declared = artifact_export(&reference.contract, export).ok_or_else(|| { + LinkErrors::one( + stage, + &requirement.origin, + format!("artifact `{artifact}` does not declare export `{export}`"), + ) + })?; + if declared != *signature || requirement.expected.as_ref() != Some(signature) { + return Err(LinkErrors::one( + stage, + &requirement.origin, + "artifact export signature disagrees with the checked requirement", + )); + } + if !verified.contains_key(artifact) { + let value = verify_artifact(&reference.contract, &reference.bytes)?; + verified.insert(artifact.clone(), value); + } + used_artifacts.insert(artifact.clone()); + claim( + &mut claims, + (module.clone(), field.clone()), + signature.clone(), + artifact, + &mut errors, + )?; + bindings.push(ResolvedBinding { + requirement: requirement.id, + origin: requirement.origin.clone(), + module: module.clone(), + field: field.clone(), + signature: signature.clone(), + }); + } + Provider::HostInterface { interface } => { + 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( + &mut claims, + (module.clone(), field.clone()), + expected.clone(), + interface, + &mut errors, + )?; + bindings.push(ResolvedBinding { + requirement: requirement.id, + origin: requirement.origin.clone(), + module, + field, + signature: expected, + }); + } + } + } + if !errors.is_empty() { + return Err(LinkErrors::new(errors)); + } + + let memory = plan_memory(&artifact_refs, &memory_demand, &used_artifacts, &verified)?; + + let mut digests = verified + .values() + .map(|value| value.sha256.clone()) + .collect::>(); + digests.sort(); + digests.dedup(); + + let external = crate::closure::host_imports(context, &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, + artifacts: verified.into_values().collect(), + memory, + external_world: external, + digests, + context: (context.shared_resolve(), context.composition_world()), + }) +} + +fn artifact_export(contract: &ArtifactContract, export: &str) -> Option { + contract.exports.iter().find_map(|declared| { + (declared.name == export && declared.kind == crate::target::ExportKind::Func) + .then(|| declared.signature.clone()) + .flatten() + }) +} + +fn claim( + claims: &mut BTreeMap<(String, String), (CoreSignature, String)>, + key: (String, String), + signature: CoreSignature, + provider: &str, + errors: &mut Vec, +) -> 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())); + let _ = errors; + Ok(()) +} + +fn plan_memory( + artifacts: &[crate::target::ArtifactReference], + demand: &crate::target::MemoryDemand, + used_artifacts: &BTreeSet, + verified: &BTreeMap, +) -> Result { + let stage = LinkStage::Memory; + let mut reservations = vec![ + region("canonical-scratch", demand.canonical_scratch), + 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 id in used_artifacts { + let artifact = verified.get(id).expect("a used artifact is verified"); + let contract = artifacts + .iter() + .find(|reference| &reference.contract.id == id) + .map(|reference| &reference.contract) + .ok_or_else(|| LinkErrors::one(stage, id, "verified artifact has no 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); + let _ = artifact; + } + + 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 > 65536 { + return Err(LinkErrors::plain( + stage, + "memory plan exceeds wasm32 limits", + )); + } + 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 > 65536 { + return Err(LinkErrors::plain( + stage, + "memory plan exceeds wasm32 limits", + )); + } + + Ok(MemoryPlan { + heap_start, + heap_alignment: alignment, + minimum_pages, + reservations, + canonical_scratch: demand.canonical_scratch, + allocator_state: demand.allocator_state, + }) +} + +fn region(owner: &str, range: (u32, u32)) -> StorageRegion { + StorageRegion { + owner: owner.to_string(), + start: range.0, + end: range.1, + } +} diff --git a/crates/psrs-linker/src/runtime.rs b/crates/psrs-linker/src/runtime.rs new file mode 100644 index 00000000..97162a00 --- /dev/null +++ b/crates/psrs-linker/src/runtime.rs @@ -0,0 +1,118 @@ +//! 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. + +use crate::target::{ + ArtifactContract, ArtifactKind, CoreSignature, CoreType, DeclaredElement, DeclaredExport, + DeclaredGlobal, DeclaredImport, DeclaredTable, ExportKind, ImportKind, InitializationContract, + StorageContract, StorageRegion, +}; +use psrs_runtime::{RawType, RuntimeArtifact}; + +/// 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: ImportKind::Memory, + }) + .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, + } +} + +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..3a55970e --- /dev/null +++ b/crates/psrs-linker/src/stack/mod.rs @@ -0,0 +1,284 @@ +//! 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 { + // 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, + )?); + } + 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 => { + if 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; + } + } + 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, +) -> 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 { + 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)?); + } 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..c6c044c6 --- /dev/null +++ b/crates/psrs-linker/src/stack/tests.rs @@ -0,0 +1,215 @@ +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() +} diff --git a/crates/psrs-linker/src/target.rs b/crates/psrs-linker/src/target.rs new file mode 100644 index 00000000..923a473f --- /dev/null +++ b/crates/psrs-linker/src/target.rs @@ -0,0 +1,221 @@ +//! 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 scalar subset of the core-Wasm value model a target signature uses. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CoreType { + I32, + I64, + F32, + F64, +} + +/// 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. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Provider { + /// A compiler-generated local operation or helper; no import results. + Generated, + /// An export of a pinned core-Wasm artifact. + ArtifactExport { + artifact: String, + export: String, + signature: CoreSignature, + }, + /// 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, Copy, Debug, PartialEq, Eq)] +pub enum ImportKind { + Memory, + Function, +} + +/// 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, +} + +/// 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 complete checked input to [`crate::plan::plan`]. +#[derive(Clone, Debug)] +pub struct TargetLinkInput { + pub requirements: Vec, + pub artifacts: 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..a4930845 --- /dev/null +++ b/crates/psrs-linker/src/verify/mod.rs @@ -0,0 +1,112 @@ +//! 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, + 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 measured = crate::measure_stack_bound(bytes, storage.stack_pointer_global) + .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(), + 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..e0aa8199 --- /dev/null +++ b/crates/psrs-linker/src/verify/parse.rs @@ -0,0 +1,411 @@ +//! Parsing a core module and comparing it with a declared contract. + +use crate::error::{LinkErrors, LinkStage}; +use crate::target::{ + ArtifactContract, CoreSignature, CoreType, DeclaredExport, ExportKind, ImportKind, +}; +use wasmparser::{CompositeInnerType, 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 pointer = parsed + .globals + .get(storage.stack_pointer_global as usize) + .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, +} + +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 = Vec::new(); + 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(); + + for payload in Parser::new(0).parse_all(bytes) { + match payload.map_err(|error| error.to_string())? { + Payload::TypeSection(reader) => { + for group in reader { + for ty in group.map_err(|error| error.to_string())?.into_types() { + if let CompositeInnerType::Func(func) = ty.composite_type.inner { + types.push(func); + } else { + return Err("artifact defines a non-function composite type".into()); + } + } + } + } + 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, + }); + } + TypeRef::Func(index) => { + imported_functions.push(index); + imports.push(ParsedImport { + module: import.module.to_string(), + field: import.name.to_string(), + kind: ImportKind::Function, + }); + } + _ => 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()); + } + if !imported_functions.is_empty() { + return Err("artifact function imports are not supported".into()); + } + Ok(ParsedCore { + imports, + exports, + tables, + globals, + data_ranges, + has_start, + elements, + }) +} + +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: &[wasmparser::FuncType], +) -> 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")? + }; + let ty = types + .get(type_index as usize) + .ok_or("artifact function has no type")?; + let convert = |ty: ValType| match ty { + ValType::I32 => Some(CoreType::I32), + ValType::I64 => Some(CoreType::I64), + ValType::F32 => Some(CoreType::F32), + ValType::F64 => Some(CoreType::F64), + _ => None, + }; + let mut parameters = Vec::with_capacity(ty.params().len()); + for param in ty.params() { + parameters.push(convert(*param).ok_or("artifact parameter is not a scalar")?); + } + let result = match ty.results() { + [] => None, + [single] => Some(convert(*single).ok_or("artifact result is not a scalar")?), + _ => return Err("artifact function has multiple results".into()), + }; + Ok(CoreSignature { parameters, result }) +} diff --git a/crates/psrs-linker/src/verify/tests.rs b/crates/psrs-linker/src/verify/tests.rs new file mode 100644 index 00000000..23f97164 --- /dev/null +++ b/crates/psrs-linker/src/verify/tests.rs @@ -0,0 +1,266 @@ +//! 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![27], + }], + 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: [ + "mutable-globals", + "sign-extension", + "bulk-memory", + "reference-types", + ] + .into_iter() + .map(str::to_string) + .collect(), + imports: vec![DeclaredImport { + module: psrs_runtime::MEMORY_MODULE.into(), + field: psrs_runtime::MEMORY_FIELD.into(), + kind: ImportKind::Memory, + }], + 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), + }), + }, + ], + 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(); + wasmparser::Validator::new() + .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/tests/compose.rs b/crates/psrs-linker/tests/compose.rs new file mode 100644 index 00000000..c31aedd4 --- /dev/null +++ b/crates/psrs-linker/tests/compose.rs @@ -0,0 +1,213 @@ +//! Target-only composition tests: the core module is built with `wasm-encoder` +//! and no compiler IR participates. + +use psrs_linker::{ + ArtifactReference, 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::ArtifactExport { + artifact: psrs_runtime::NUMBER_RUNTIME.id.into(), + export: psrs_runtime::NUMBER_EXPORT.into(), + signature: CoreSignature { + parameters: vec![CoreType::F64, CoreType::I32, CoreType::I32], + result: Some(CoreType::I32), + }, + }, + }], + artifacts: vec![ArtifactReference { + contract: psrs_linker::runtime::contract(&psrs_runtime::NUMBER_RUNTIME), + bytes: psrs_runtime::NUMBER_RUNTIME.bytes.to_vec(), + }], + policy: TargetPolicy::default(), + memory: MemoryDemand { + canonical_scratch: (0, 16), + allocator_state: (16, 24), + base_heap_start: 24, + heap_alignment: 8, + }, + } +} + +#[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(), + artifacts: Vec::new(), + policy: TargetPolicy::default(), + memory: MemoryDemand { + canonical_scratch: (0, 16), + allocator_state: (16, 24), + base_heap_start: 24, + heap_alignment: 8, + }, + }, + ) + .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.artifacts.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/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/plan.rs b/crates/psrs-linker/tests/plan.rs new file mode 100644 index 00000000..78b067e3 --- /dev/null +++ b/crates/psrs-linker/tests/plan.rs @@ -0,0 +1,317 @@ +//! Target-only linker tests: no compiler IR is constructed. + +use psrs_linker::{ + ArtifactContract, ArtifactReference, BindingRequirement, Boundary, CoreSignature, CoreType, + MemoryDemand, Provider, RequirementId, 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, + } +} + +fn input( + requirements: Vec, + artifacts: Vec, +) -> TargetLinkInput { + let permitted = resolve_default_definitions() + .map(|context| context.world_imports().to_vec()) + .unwrap_or_default(); + TargetLinkInput { + requirements, + artifacts, + policy: TargetPolicy { + permitted_host_interfaces: permitted, + }, + memory: memory(), + } +} + +fn formatter_artifact() -> ArtifactReference { + ArtifactReference { + contract: psrs_linker::runtime::contract(&psrs_runtime::NUMBER_RUNTIME), + bytes: psrs_runtime::NUMBER_RUNTIME.bytes.to_vec(), + } +} + +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::ArtifactExport { + artifact: psrs_runtime::NUMBER_RUNTIME.id.into(), + export: psrs_runtime::NUMBER_EXPORT.into(), + signature: CoreSignature { + parameters: vec![CoreType::F64, CoreType::I32, CoreType::I32], + result: Some(CoreType::I32), + }, + }, + } +} + +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![formatter_artifact()], + ), + ) + .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)], + artifacts: 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 artifact = formatter_artifact(); + let storage = artifact.contract.storage.as_mut().unwrap(); + storage.static_data.start = 16; + let result = plan( + &context, + input(vec![formatter_requirement()], vec![artifact]), + ); + 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.provider = Provider::ArtifactExport { + artifact: psrs_runtime::NUMBER_RUNTIME.id.into(), + export: psrs_runtime::NUMBER_EXPORT.into(), + signature: 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![formatter_artifact()] + ) + ) + .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 contract: ArtifactContract = + psrs_linker::runtime::contract(&psrs_runtime::NUMBER_RUNTIME); + contract.exports.clear(); + let artifact = ArtifactReference { + contract, + bytes: psrs_runtime::NUMBER_RUNTIME.bytes.to_vec(), + }; + 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 artifact = formatter_artifact(); + artifact.contract.storage.as_mut().unwrap().minimum_pages = u64::MAX; + assert!( + plan( + &context, + input(vec![formatter_requirement()], vec![artifact]) + ) + .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/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..38b16d86 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,45 @@ 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 { + ExternalKind::Wit { + interface: interface.into(), + function: function.into(), + } + } + } }; let Some(signature) = resolver.resolve_type(foreign.annotation.clone()) else { continue; @@ -111,10 +148,7 @@ pub(crate) fn resolve_ast_module( ExternalSymbol { 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 +203,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..f044fe53 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 { @@ -50,8 +72,39 @@ pub(in crate::resolver) fn builtin_type(name: &str) -> Option { }) } +/// `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..47c6a233 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( @@ -48,7 +50,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 +65,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 +85,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.rs index 190bfbf5..bd11a032 100644 --- a/crates/psrs-resolve/src/resolver/type_resolution.rs +++ b/crates/psrs-resolve/src/resolver/type_resolution.rs @@ -1,8 +1,10 @@ -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; @@ -168,8 +170,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 +195,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 +204,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 +222,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 +450,7 @@ impl Resolver { name: name.text, name_span: name.span, kind, + compiler_class: None, parameters, constructors, members, diff --git a/crates/psrs-runtime/Cargo.toml b/crates/psrs-runtime/Cargo.toml new file mode 100644 index 00000000..76225027 --- /dev/null +++ b/crates/psrs-runtime/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "psrs-runtime" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["catalog", "formatter"] +# Immutable host-consumed catalog data: WIT sources, world identity, artifact +# bytes and provenance. Compiles no executable target code. +catalog = [] +# The executable numeric exports. The Wasm artifact build enables only this +# feature; the default native build enables both. +formatter = ["dep:ryu-js", "dep:libm"] + +[dependencies] +ryu-js = { version = "=1.0.2", default-features = false, optional = true } +libm = { version = "=0.2.15", optional = true } + +[dev-dependencies] +wasm-encoder = { workspace = true, features = ["wasmparser"] } +wasmprinter.workspace = true +wasmparser.workspace = true diff --git a/crates/psrs-runtime/artifact/psrs_runtime.wasm b/crates/psrs-runtime/artifact/psrs_runtime.wasm new file mode 100755 index 00000000..f83eadec Binary files /dev/null and b/crates/psrs-runtime/artifact/psrs_runtime.wasm differ diff --git a/crates/psrs-runtime/examples/package.rs b/crates/psrs-runtime/examples/package.rs new file mode 100644 index 00000000..ce3442e4 --- /dev/null +++ b/crates/psrs-runtime/examples/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: 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-runtime/src/acos.rs b/crates/psrs-runtime/src/acos.rs new file mode 100644 index 00000000..58a4ff0f --- /dev/null +++ b/crates/psrs-runtime/src/acos.rs @@ -0,0 +1,44 @@ +//! Inverse cosine of one binary64 value. +//! +//! The pinned `libm` 0.2.15 routine is the fdlibm polynomial. On finite inputs +//! in the closed interval [-1, 1] it matches the official JavaScript +//! `Math.acos` results used by purescript-numbers. Inputs outside that +//! interval, infinities, and NaN produce NaN. The operation does not trap. +//! NaN payloads are not part of the public contract. + +/// Returns the inverse cosine in radians. +fn acos(value: f64) -> f64 { + libm::acos(value) +} + +/// C ABI export of [`acos`]. +/// +/// # 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 { + acos(value) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn 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!(acos(input).to_bits(), bits, "{input}"); + } + for input in [2.0, -2.0, f64::INFINITY, f64::NEG_INFINITY, f64::NAN] { + assert!(acos(input).is_nan(), "{input}"); + } + } +} diff --git a/crates/psrs-runtime/src/asin.rs b/crates/psrs-runtime/src/asin.rs new file mode 100644 index 00000000..9b354b97 --- /dev/null +++ b/crates/psrs-runtime/src/asin.rs @@ -0,0 +1,44 @@ +//! Inverse sine of one binary64 value. +//! +//! The pinned `libm` 0.2.15 routine is the fdlibm polynomial. On finite inputs +//! in the closed interval [-1, 1] it matches the official JavaScript +//! `Math.asin` results used by purescript-numbers, including the sign of zero. +//! Inputs outside that interval, infinities, and NaN produce NaN. The operation +//! does not trap. NaN payloads are not part of the public contract. + +/// Returns the inverse sine in radians. +fn asin(value: f64) -> f64 { + libm::asin(value) +} + +/// C ABI export of [`asin`]. +/// +/// # 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 { + asin(value) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn 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!(asin(input).to_bits(), bits, "{input}"); + } + for input in [2.0, -2.0, f64::INFINITY, f64::NEG_INFINITY, f64::NAN] { + assert!(asin(input).is_nan(), "{input}"); + } + } +} diff --git a/crates/psrs-runtime/src/atan.rs b/crates/psrs-runtime/src/atan.rs new file mode 100644 index 00000000..ccebc89a --- /dev/null +++ b/crates/psrs-runtime/src/atan.rs @@ -0,0 +1,144 @@ +//! Inverse tangent of one binary64 value. +//! +//! The polynomial is the fdlibm routine pinned by libm 0.2.15. On every finite +//! input, and on both infinities, it matches the official JavaScript +//! `Math.atan` results used by purescript-numbers, including the sign of zero. +//! NaN produces NaN. The operation does not trap. NaN payloads are not part of +//! the public contract. +//! +//! The original routine forces an `f32` evaluation on subnormal inputs to raise +//! the host underflow flag. Wasm has no floating-point status flags, and that +//! evaluation writes below the stack pointer without reserving the frame. This +//! copy returns the same bits and does not touch the stack pointer. + +/// Returns the inverse tangent in radians. +/// +/// The decimal coefficients are copied from libm 0.2.15. Their extra digits +/// and the split pi/2 terms belong to that polynomial. +#[allow(clippy::excessive_precision, clippy::approx_constant)] +pub(crate) fn atan(value: f64) -> f64 { + const ATANHI: [f64; 4] = [ + 4.63647609000806093515e-01, + 7.85398163397448278999e-01, + 9.82793723247329054082e-01, + 1.57079632679489655800e+00, + ]; + const ATANLO: [f64; 4] = [ + 2.26987774529616870924e-17, + 3.06161699786838301793e-17, + 1.39033110312309984516e-17, + 6.12323399573676603587e-17, + ]; + const AT: [f64; 11] = [ + 3.33333333333329318027e-01, + -1.99999999998764832476e-01, + 1.42857142725034663711e-01, + -1.11111104054623557880e-01, + 9.09088713343650656196e-02, + -7.69187620504482999495e-02, + 6.66107313738753120669e-02, + -5.83357013379057348645e-02, + 4.97687799461593236017e-02, + -3.65315727442169155270e-02, + 1.62858201153657823623e-02, + ]; + + let mut x = value; + let mut ix = (x.to_bits() >> 32) as u32; + let sign = ix >> 31; + ix &= 0x7fff_ffff; + if ix >= 0x4410_0000 { + if x.is_nan() { + return x; + } + let z = ATANHI[3] + f64::from_bits(0x0380_0000); + return if sign != 0 { -z } else { z }; + } + let id = if ix < 0x3fdc_0000 { + if ix < 0x3e40_0000 { + return x; + } + -1 + } else { + x = x.abs(); + if ix < 0x3ff3_0000 { + if ix < 0x3fe6_0000 { + x = (2. * x - 1.) / (2. + x); + 0 + } else { + x = (x - 1.) / (x + 1.); + 1 + } + } else if ix < 0x4003_8000 { + x = (x - 1.5) / (1. + 1.5 * x); + 2 + } else { + x = -1. / x; + 3 + } + }; + let z = x * x; + let w = z * z; + let s1 = z * (AT[0] + w * (AT[2] + w * (AT[4] + w * (AT[6] + w * (AT[8] + w * AT[10]))))); + let s2 = w * (AT[1] + w * (AT[3] + w * (AT[5] + w * (AT[7] + w * AT[9])))); + if id < 0 { + return x - x * (s1 + s2); + } + let z = ATANHI[id as usize] - (x * (s1 + s2) - ATANLO[id as usize] - x); + if sign != 0 { -z } else { z } +} + +/// C ABI export of [`atan`]. +/// +/// # 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 { + atan(value) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn matches_zero_signs_unit_slopes_and_infinities() { + for (input, bits) in [ + (0.0, 0), + (-0.0, 0x8000000000000000), + (1.0, 0x3fe921fb54442d18), + (-1.0, 0xbfe921fb54442d18), + (f64::INFINITY, 0x3ff921fb54442d18), + (f64::NEG_INFINITY, 0xbff921fb54442d18), + ] { + assert_eq!(atan(input).to_bits(), bits, "{input}"); + } + assert!(atan(f64::NAN).is_nan()); + } + + #[test] + fn matches_libm_bits_including_subnormals() { + for input in [ + f64::from_bits(1), + f64::from_bits(0x000f_ffff_ffff_ffff), + f64::from_bits(0x0010_0000_0000_0000), + 1e-20, + -1e-20, + 0.4375, + -0.4375, + 0.7, + 1.1875, + 2.4375, + 1e20, + f64::MAX, + -f64::MAX, + ] { + assert_eq!( + atan(input).to_bits(), + libm::atan(input).to_bits(), + "{input}" + ); + } + } +} diff --git a/crates/psrs-runtime/src/atan2.rs b/crates/psrs-runtime/src/atan2.rs new file mode 100644 index 00000000..2e7910f3 --- /dev/null +++ b/crates/psrs-runtime/src/atan2.rs @@ -0,0 +1,202 @@ +//! Four-quadrant inverse tangent of two binary64 values. +//! +//! The reduction follows the fdlibm cutoff: an exponent gap above 60 uses a +//! signed half-pi, and a negative `x` with an exponent gap below -60 uses a +//! zero before the pi adjustment. Moderate ratios call this crate's inverse +//! tangent, which matches libm `atan` and does not touch the stack pointer. +//! libm 0.2.15's own `atan2` uses a wider gap and differs from official +//! `Math.atan2` by one ulp on some of those large ratios. +//! +//! The argument order is `(y, x)`, matching official `Math.atan2` and +//! `Data.Number.atan2`. Either NaN produces NaN. The operation does not trap. +//! NaN payloads are not part of the public contract. + +use super::atan::atan; + +/// Returns the angle from the positive x axis to `(x, y)`, in radians. +/// +/// The split pi terms are the fdlibm constants. Their extra digits belong to +/// that reduction. +#[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 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 { + 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 matches_moderate_ratios_with_libm_and_large_ratios_with_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}"); + } + let mut samples = vec![ + (0.0, 1.0), + (-0.0, -1.0), + (f64::from_bits(1), 1.0), + (1.0, f64::from_bits(1)), + (f64::from_bits(1), f64::from_bits(1)), + (f64::from_bits(0x000f_ffff_ffff_ffff), 1.0), + (1e-20, -1.0), + (1.5, 1.0), + (3.0, 2.0), + (2.0, -1.0), + (-2.0, -1.0), + ]; + let mut state = 0x5eed_5a17u32; + for _ in 0..256 { + let y = f64::from_bits(next_bits(&mut state)); + let x = f64::from_bits(next_bits(&mut state)); + if exponent_gap(y, x).abs() <= 60 && y.is_finite() && x.is_finite() && x != 0.0 { + samples.push((y, x)); + } + } + for (y, x) in samples { + assert_eq!( + atan2(y, x).to_bits(), + libm::atan2(y, x).to_bits(), + "{y}, {x}" + ); + } + } + + fn exponent_gap(y: f64, x: f64) -> i32 { + let ix = (x.to_bits() >> 32) as u32 & 0x7fff_ffff; + let iy = (y.to_bits() >> 32) as u32 & 0x7fff_ffff; + (iy as i32).wrapping_sub(ix as i32) >> 20 + } + + fn next_bits(state: &mut u32) -> u64 { + let low = step(state); + let high = step(state); + u64::from(low) | (u64::from(high) << 32) + } + + fn step(state: &mut u32) -> u32 { + *state ^= state.wrapping_shl(13); + *state ^= state.wrapping_shr(17); + *state ^= state.wrapping_shl(5); + *state + } +} diff --git a/crates/psrs-runtime/src/catalog.rs b/crates/psrs-runtime/src/catalog.rs new file mode 100644 index 00000000..2b318732 --- /dev/null +++ b/crates/psrs-runtime/src/catalog.rs @@ -0,0 +1,238 @@ +//! Immutable target catalog consumed by the linker and backend. +//! +//! The catalog returns plain data: WIT source paths and bytes, a world +//! identity, and a pinned artifact's bytes plus declared raw contract and +//! provenance. It never returns a `wit_parser::Resolve`, compiler IR, or +//! link-plan value, and it compiles no executable target code, so a native +//! consumer can read it without linking the formatter. + +use crate::RawType; + +/// 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"), +}; + +/// 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, +} + +/// An import the artifact declares. +#[derive(Clone, Copy, Debug)] +pub struct RawImport { + pub module: &'static str, + pub field: &'static str, +} + +/// 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, + /// The mutable stack-pointer global the artifact uses. + 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, +} + +/// Numeric formatting and decimal conversion, produced by `tools/build.sh`. +pub const NUMBER_RUNTIME: RuntimeArtifact = RuntimeArtifact { + id: "psrs:runtime-number", + module_name: crate::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: --import-memory --global-base=65536 \ + -zstack-size=65536 --export=__heap_base, then package", + sha256: "e295fd40bcb247ee77b8890107b2dea74838ded7b383895ce8db7c428d0e7ac2", + }, + required_features: &[ + "mutable-globals", + "sign-extension", + "saturating-float-to-int", + "multi-value", + "bulk-memory", + "reference-types", + ], + imports: &[RawImport { + module: crate::MEMORY_MODULE, + field: crate::MEMORY_FIELD, + }], + function_exports: &[ + RawExport { + name: crate::NUMBER_EXPORT, + parameters: &[RawType::F64, RawType::I32, RawType::I32], + result: Some(RawType::I32), + }, + RawExport { + name: crate::DECIMAL_EXPORT, + parameters: &[RawType::I32, RawType::I32], + result: Some(RawType::F64), + }, + RawExport { + name: crate::ACOS_EXPORT, + parameters: &[RawType::F64], + result: Some(RawType::F64), + }, + RawExport { + name: crate::ASIN_EXPORT, + parameters: &[RawType::F64], + result: Some(RawType::F64), + }, + RawExport { + name: crate::ATAN_EXPORT, + parameters: &[RawType::F64], + result: Some(RawType::F64), + }, + RawExport { + name: crate::ATAN2_EXPORT, + parameters: &[RawType::F64, RawType::F64], + result: Some(RawType::F64), + }, + ], + global_exports: &[crate::HEAP_BASE_EXPORT], + tables: &[RawTable { + element: "funcref", + minimum: 2, + maximum: Some(2), + }], + elements: &[RawElement { + table: 0, + offset: 1, + functions: &[27], + }], + globals: &[(true, crate::HEAP_START), (false, crate::HEAP_START)], + storage: RawStorage { + static_data: (crate::RESERVED_START, crate::STACK_BOTTOM), + stack: (crate::STACK_BOTTOM, crate::HEAP_START), + heap_start: crate::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::RESERVED_START, crate::STACK_BOTTOM), + instantiate_after_shims: false, +}; diff --git a/crates/psrs-runtime/src/decimal.rs b/crates/psrs-runtime/src/decimal.rs new file mode 100644 index 00000000..a9e0fae5 --- /dev/null +++ b/crates/psrs-runtime/src/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/formatter.rs b/crates/psrs-runtime/src/formatter.rs new file mode 100644 index 00000000..7aff0f39 --- /dev/null +++ b/crates/psrs-runtime/src/formatter.rs @@ -0,0 +1,59 @@ +//! 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::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(target_arch = "wasm32")] +#[panic_handler] +fn panic(_: &core::panic::PanicInfo<'_>) -> ! { + core::arch::wasm32::unreachable() +} + +#[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::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/lib.rs b/crates/psrs-runtime/src/lib.rs new file mode 100644 index 00000000..4f9af00b --- /dev/null +++ b/crates/psrs-runtime/src/lib.rs @@ -0,0 +1,149 @@ +//! The target runtime package. +//! +//! This crate has two distinct build entry points: +//! +//! - The [`catalog`] module (feature `catalog`) exposes immutable, host-consumed +//! metadata: pinned WIT source bytes, the default command-world identity, and +//! the compiler-owned formatter artifact with its provenance and storage +//! contract. It links no executable target code. +//! - Feature `formatter` compiles the numeric formatting, complete-decimal +//! conversion, and inverse trigonometric exports, built for `wasm32-unknown-unknown` +//! and embedded as the pinned artifact. It embeds neither WIT text nor the +//! catalog. +//! +//! The compiler depends on this crate with `default-features = false, +//! features = ["catalog"]`; the Wasm artifact build uses +//! `--no-default-features --features formatter`. + +#![cfg_attr(all(feature = "formatter", target_arch = "wasm32"), no_std)] + +#[cfg(feature = "catalog")] +pub mod catalog; +#[cfg(feature = "catalog")] +pub use catalog::*; + +#[cfg(feature = "formatter")] +mod acos; +#[cfg(feature = "formatter")] +mod asin; +#[cfg(feature = "formatter")] +mod atan; +#[cfg(feature = "formatter")] +mod atan2; +#[cfg(feature = "formatter")] +mod decimal; +#[cfg(feature = "formatter")] +mod formatter; +#[cfg(feature = "formatter")] +pub use acos::number_acos; +#[cfg(feature = "formatter")] +pub use asin::number_asin; +#[cfg(feature = "formatter")] +pub use atan::number_atan; +#[cfg(feature = "formatter")] +pub use atan2::number_atan2; +#[cfg(feature = "formatter")] +pub use decimal::number_from_decimal; +#[cfg(feature = "formatter")] +pub use formatter::number_to_string; + +/// 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"; +/// 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 imports the application's canonical memory. +pub const MEMORY_MODULE: &str = "env"; +/// The runtime's memory import field. +pub const MEMORY_FIELD: &str = "memory"; +/// The runtime exports its heap boundary for the preparer to relocate. +pub const HEAP_BASE_EXPORT: &str = "__heap_base"; + +/// 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, +} + +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, +}; diff --git a/crates/psrs-runtime/tools/build.sh b/crates/psrs-runtime/tools/build.sh new file mode 100644 index 00000000..0134925c --- /dev/null +++ b/crates/psrs-runtime/tools/build.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -eu +cd "$(dirname "$0")/../../.." +cargo rustc --locked -p psrs-runtime --lib --no-default-features --features formatter \ + --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 --example package -- \ + target/wasm32-unknown-unknown/target-runtime/psrs_runtime.wasm \ + 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..4239f1a9 --- /dev/null +++ b/crates/psrs-runtime/tools/check-reproducible.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# Rebuilds the runtime artifact and checks that it reproduces the committed +# bytes. A mismatch means the reviewed provenance digest must be updated +# explicitly; this check never rewrites the artifact. +set -eu +cd "$(dirname "$0")/../../.." +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +cargo rustc --locked -p psrs-runtime --lib --no-default-features --features formatter \ + --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 --example package -- \ + target/wasm32-unknown-unknown/target-runtime/psrs_runtime.wasm \ + "$tmp/psrs_runtime.wasm" +if cmp -s "$tmp/psrs_runtime.wasm" crates/psrs-runtime/artifact/psrs_runtime.wasm; then + echo "runtime artifact reproduces byte-for-byte" +else + echo "runtime artifact differs from the committed bytes; update the reviewed provenance" >&2 + exit 1 +fi 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..2cf9017a 100644 --- a/crates/psrs-syntax/src/parser/expr/pattern.rs +++ b/crates/psrs-syntax/src/parser/expr/pattern.rs @@ -224,10 +224,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.rs index b61d7960..5b5a9316 100644 --- a/crates/psrs-syntax/src/parser/tests.rs +++ b/crates/psrs-syntax/src/parser/tests.rs @@ -3,6 +3,23 @@ use crate::{add_layout, lex}; use psrs_cst::{Declaration, ExprKind, PatternKind, TypeExprKind, ValueRhs}; use psrs_span::SourceFile; +#[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 +93,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 +156,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 +265,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 +442,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/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..c87a416c 100644 --- a/crates/psrs-thir/src/external_type_tests.rs +++ b/crates/psrs-thir/src/external_type_tests.rs @@ -50,6 +50,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..f4770ecb 100644 --- a/crates/psrs-typecheck/src/typecheck/classes/evidence/mod.rs +++ b/crates/psrs-typecheck/src/typecheck/classes/evidence/mod.rs @@ -51,6 +51,8 @@ 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) } @@ -95,7 +97,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 88% rename from crates/psrs-typecheck/src/typecheck/classes/fundeps.rs rename to crates/psrs-typecheck/src/typecheck/classes/fundeps/mod.rs index 1b302772..b5cbfef9 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( @@ -230,6 +239,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 +257,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 +282,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 +427,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..09c7a3fc 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. @@ -49,8 +42,13 @@ impl Checker { } 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 +71,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 +136,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 +187,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 +204,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 +244,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..40b4ab0f 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,7 +176,11 @@ 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; } 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..8ce1636a --- /dev/null +++ b/crates/psrs-typecheck/src/typecheck/infer/let_expr.rs @@ -0,0 +1,59 @@ +//! 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 { + let scheme = Scheme::monomorphic(checker.fresh()); + 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 + } +} diff --git a/crates/psrs-typecheck/src/typecheck/infer/mod.rs b/crates/psrs-typecheck/src/typecheck/infer/mod.rs index 0b50ca88..0dcf3767 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,24 @@ 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::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 +179,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 +247,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 +362,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..168e9ac4 100644 --- a/crates/psrs-typecheck/src/typecheck/signature.rs +++ b/crates/psrs-typecheck/src/typecheck/signature.rs @@ -171,15 +171,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-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/design/D-04-suite-roadmap.md b/docs/design/D-04-suite-roadmap.md index d47cdbfc..3ba14fb9 100644 --- a/docs/design/D-04-suite-roadmap.md +++ b/docs/design/D-04-suite-roadmap.md @@ -390,6 +390,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 +431,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 +475,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 +507,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 +582,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-06, deriving follow-up):** **75/97** +failing cases agree. Per-code agreement is `OverlappingInstances` 8/8, +`NoInstanceFound` 46/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 previous 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 +730,28 @@ 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 by `runtime::l6_runtime_scoreboard`):** **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 +761,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 +878,7 @@ 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 164/413 table above. 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 +1087,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 | 75/97 failing cases: `OverlappingInstances` 8/8, `NoInstanceFound` 46/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 on 2026-10-06. | 100% agreement for the mapped class cases. | +| L6/M7 | Runtime and standard library | **164/413** non-FFI passing files compile, validate, and run, all with exit code 0. The other 249 do not agree: 46 stop at P10 with no selected `main`, 30 at P8 CC verification, 68 at P5 typecheck, 23 at P3, 16 at P5 kind checking, 16 at P8 closure conversion, 4 at P0, 1 at P7, 26 during harness loading, and 19 trap at runtime. Remeasured with `PSRS_REQUIRE_WASMTIME=1 PSRS_ORACLE=annotations` on 2026-10-04 (Wasmtime 49.0.2, `purs` 0.15.16). | 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. +The gate rows above are the 2026-10-04 remeasurement after vendoring the core libraries, except L4/L5, remeasured on 2026-10-06. 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 +1113,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 +1133,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 +1145,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 75/97. 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 75/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 +1192,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. 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 latest official runtime board is 164/413; 46 files have no selected `main`, and this row stays Partial. | Partial | Pass the L6/M7 gate. The 46 files with no selected `main` stay explicit blockers. | | 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-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-04 full-board measurement is **164/413**, all exiting 0 (Wasmtime 49.0.2, `purs` 0.15.16). Of the other 249 cases, 230 stop before runtime and 19 trap. First blockers are P5 typecheck 68, P10 no selected `main` 46, P8 CC verification 30, P3 resolution 23, P8 closure conversion 16, P5 kind check 16, P0 lexing 4, P7 Core verification 1, and harness loading 26; no case stops at P2. 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,8 +1216,8 @@ 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) | +| 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 164/413; 46 files with no selected `main` remain blocked, and BE-21 stays Partial. Focused effect tests still expose CC verification and runtime traps, so their source-level integration remains open. | [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-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) | diff --git a/docs/design/D-15-compiler-builtins.md b/docs/design/D-15-compiler-builtins.md index 564e21b0..fbfe9fdf 100644 --- a/docs/design/D-15-compiler-builtins.md +++ b/docs/design/D-15-compiler-builtins.md @@ -72,27 +72,18 @@ knows": ### Intrinsics on the Wasm target -For the Wasm target the intrinsics are the compiler's curated vocabulary of -primitive operations at the machine layer — the operations the standard library -is allowed to rely on directly. Each is realized by the backend from one or more -Wasm instructions (scalar, GC `array`/`struct`/`ref`, or bulk memory), from a -short helper sequence, or from no instruction at all. The correspondence is -many-to-many, so the set is not literally a subset of the instruction list: - -- One instruction, one intrinsic: `i32.add` for `i32Add`. -- One instruction, several intrinsics: `i32.lt_s` serves `i32LtS` and `charLt`. -- One intrinsic, several instructions: `arrayAppend` is `array.new_default` plus - two element-copy loops; `intDiv` is `i32.div_s` with a sign-correction helper. -- No instruction: `charToInt` and `intToChar` are the identity; `true`, `false`, - and `unit` are constants; `Coerce` has no runtime form. - -The set is a project decision, not a projection of the instruction list. It holds -exactly the operations the standard library needs that the source language cannot -express and no host capability provides. That places it at the machine layer and -below the Canonical ABI, which is why it cannot be a WIT call; the encoding of -each operation belongs to the backend -([Wasm encoding and structuring](backend/wasm/encoding-and-structuring.md)), not -to this registry. +The registry describes checked language operations, not Wasm opcode names. +`IntAdd`, `IntQuot`, and `NumberAbs` have language schemes; MIR selects +`I32Add`, `I32DivS`, and `F64Abs`. A primitive may require one instruction, +a sequence, a generated representation operation, or a pinned artifact export. +Constants and checked coercions can have no runtime instruction. + +The backend's exhaustive implementation catalog owns that selection; runtime +metadata owns raw export signatures and value protocols. See +[intrinsic implementations](backend/fp/intrinsic-implementations.md). Library +algorithms remain ordinary target library functions. In particular, public +Euclidean division/modulo are library policy over truncating primitives; +retired floor intrinsic IDs 26 and 27 are never reused. ### Why intrinsics are compiler-owned here @@ -159,13 +150,14 @@ pub struct IntrinsicDescriptor { pub name: &'static str, pub arity: u8, pub category: IntrinsicCategory, + pub effects: IntrinsicEffects, // conservative traps and mutation /// A non-capturing constructor, so the table can be `const`. pub scheme: fn() -> Type, // an HIR Type } pub enum IntrinsicCategory { Nullary, // true, false, unit - UnaryScalar, + Unary, // one language argument; not necessarily a scalar BinaryScalar, ArrayLength, ArrayIndex, @@ -207,12 +199,12 @@ registry and the registry on `core`. | P3 Resolve | `name`, `scheme` | `ExternalSymbol { signature: Some(scheme) }` | | P5 Type check | `scheme`, instantiated | the intrinsic's `InferType`; `Coercion` takes a special path | | P6 Core lowering | `intrinsic`, `arity`, `category` | one `ExprKind::IntrinsicCall` | -| P8/P9 Backend | its own tables only | CC `AssignmentKind` and MIR instructions | +| P8/P9 Backend | checked identity and scheme; target implementation catalog | CC `AssignmentKind` and MIR instructions | -The backend does not consult the registry: by the time it runs, Core has already -expressed the operation in its own terms, and the backend lowers those with its -own representation tables. The registry tells the front-end stages *what* the -operation is. +The registry defines the language contract. The backend reads that contract to +verify artifact-call values before ABI erasure, and its implementation catalog +selects how to realize it. HIR contains no target lowering or raw ABI dependency. +Core optimizations consume the registry's conservative trap and mutation effects. ### Why lowering is not in the registry @@ -381,22 +373,31 @@ its schemes, and each representation's intrinsic module owns the per-operation verification, folding, and lowering. Core has no per-operation expression node and no scalar `Primitive`/`UnaryPrimitive` enum. +`numberTrunc`, `numberFloor` and `numberCeil` are unary scalar primitives +with the checked scheme `Number -> Number`. Core and each backend representation +check their operand/result contracts before lowering to Wasm `f64.trunc`, +`f64.floor` and `f64.ceil`, respectively. They retain Number range, signed zeros +and nonfinite behavior; +integer saturation and library clamping remain separate operations. + The surface operators `+`, `*`, `==`, `/=`, `<`, `<=`, `>`, and `>=` are now library classes — `Data.Semiring`, `Data.Eq`, `Data.Ord` — over internal primitives, and `Prelude` re-exports them, matching official PureScript. A source that uses them imports `Prelude`. Each instance eta-expands its intrinsic -(`eq x y = intEq x y`) because a first-class intrinsic reference is not lowerable. - -The remaining surface operators `-`, `/`, and `%` are still bound to the `Int` -intrinsics directly. `Data.Ring` and the Euclidean division class are the -follow-up. - -`Show` is a library class in `Data.Show`, re-exported from `Prelude`, over the -same primitives. It does not add an intrinsic: integer, character, and string -rendering are written in the source language, and `Number` rendering is too. -That `Number` spelling is not a correctly rounded ECMAScript conversion. A pure -numeric formatter with canonical inputs and outputs is the open capability -question above, not a new `Intrinsic`. +(`eq x y = intEq x y`); unsaturated intrinsic uses also elaborate through +checked eta expansion before backend lowering. + +`-` and `/` are the library `Data.Ring` and `Data.EuclideanRing` operators. +The target library implements the official Int Euclidean policy using `intSub`, +`intQuot`, and truncating `%`. Compiler `intDiv/intMod` are retired because their +floor policy differed for negative divisors and zero. `mod` remains the public +nonnegative Euclidean remainder, rather than an alias for `%`. + +`Show` remains a library class. Integer, character, and string wrappers stay +library-owned; Number conversion uses the checked `NumberToString` primitive +whose target implementation is the pinned correctly rounded formatter artifact. +The parser likewise exposes a small conversion leaf while public validation and +`Maybe` construction remain in the unchanged official wrapper. ## References diff --git a/docs/design/D-16-compile-diagnosis.md b/docs/design/D-16-compile-diagnosis.md new file mode 100644 index 00000000..8bfd3af6 --- /dev/null +++ b/docs/design/D-16-compile-diagnosis.md @@ -0,0 +1,214 @@ +# D-16 — Compile Diagnosis + +**Status:** Stable (design) + +**Implements:** [F-04 — Compile Diagnosis](../feature/F-04-compile-diagnosis.md) + +## Purpose and scope + +`psrs diagnose` records what the compiler actually did for each selected case. +It groups compile outcomes by their first reported blocker, records pass and +artifact boundaries, and compares compatible runs. A trace may retain selected +IR dumps and node lineage for focused investigations. Compile acceptance, +compile lineage, and Wasmtime runtime evidence are separate records with +separate claims. + +The tool does not infer root causes from matching diagnostics, reconstruct +lineage from text dumps, alter compiler semantics, or treat compilation as +runtime validation. Official suite scoreboards remain the acceptance evidence +for roadmap gates. + +## Commands + +```sh +cargo run -p psrs-cli -- diagnose path/to/Main.purs --out /tmp/main.json +cargo run -p psrs-cli -- diagnose path/to/Main.purs --trace --out /tmp/main.json +cargo run -p psrs-cli -- diagnose path/to/Main.purs \ + --input path/to/Imported.purs --trace --out /tmp/explicit-inputs.json +cargo run -p psrs-cli -- diagnose --corpus passing --filter Functor --limit 20 \ + --out /tmp/functor.json --timeout 20 +cargo run -p psrs-cli -- diagnose --compare /tmp/before.json /tmp/after.json +``` + +Without `--trace`, diagnosis records the run manifest, pass executions, +artifact identities, explicit input/output edges, diagnostics, and compact +canonical summaries only for representations that implement a versioned +summary. It does not clone or format complete IR values solely to make dumps. +With `--trace`, the compiler retains requested IR dumps on both accepted and +rejected cases and may attach node-level lineage when the producing pass +supports it. A replay bundle records the selected trace mode and forwards it +when replaying. Repeatable `--input FILE` adds explicitly ordered sources after +the primary file; this mode compiles exactly that list without rediscovering +imports. It is available only for file diagnosis, cannot be combined with +`--corpus`, and rejects duplicate canonical paths. The replay script first +runs a new diagnosis with the saved entry-first input list, trace mode, and a +separate `replay-trace.json`, then invokes the recorded `build` command on the +same files. These are separate compile attempts; the build command preserves +the original compile-failure exit status. Unsupported summary and lineage coverage is represented as +`unavailable` or `untracked`, never approximated with a Debug-text hash or +source-span overlap. + +The corpus command uses `tests/upstream` unless `PURESCRIPT_REPO` selects an +official checkout. It applies the path filter before the limit. Cases with +foreign imports or adjacent JavaScript FFI files are recorded as `excluded`; +they do not count as passes. Each selected case runs in its own child process. +`--timeout` is a per-case deadline in seconds. A timeout or crash is captured +as a case result, and the batch continues. Progress and elapsed time are +printed to stderr. Compiler rejections are snapshot data and do not make the +diagnosis command itself fail; invalid options, missing files, or report I/O +errors do. + +Querying a saved trace by case, symbol, or source span is part of the design. +Symbol queries resolve to declaration identities; span queries return every +linked node and its relation. If the saved coverage cannot answer a query, the +result says so. Re-capturing missing evidence creates a new linked run rather +than mutating an existing snapshot. The query command is not implemented by the +current first slice. + +## Run and trace schema + +The serialized format is versioned independently of compiler and pass +versions. A run manifest records: + +- the selected case cohort and whether each case's complete input set was + observed; +- ordered logical source identities and content fingerprints, including + trusted-library inputs and their fingerprint, plus logical-to-physical path + mappings when replay needs them; +- compiler revision, working-tree and executable fingerprints when available; +- target capabilities, optimization settings, relevant tool versions, and + environment inputs when the runner can observe them; +- the trace mode, schema version, per-pass contract versions, summary versions, + and explicit coverage gaps. + +Unavailable environment facts stay absent or explicitly unavailable. A run +must not claim compatibility based on guessed defaults. + +An artifact ID is unique within one run and identifies one produced snapshot; +it is not a content digest and equal content does not merge identities. Each +artifact record names its representation and format version, producer, storage +reference if retained, summary and digest state, and whether the artifact is +complete, partial, invalid, or unavailable. Production and validation are +different facts: validation records refer to artifact IDs and name the +validator, result, and observed coverage. A failed or skipped validator never +turns an artifact into a verified artifact. + +A pass execution records a stable machine key, pass contract version, ordered +input and output artifact IDs, status (`completed`, `rejected`, `crashed`, +`timed_out`, or `not_run`), diagnostics, and observed validation references. +One execution may consume and produce multiple artifacts. Explicit edges tie +each artifact to the pass execution and its input or output role. In-place +optimization still creates a new artifact identity. Configuration, target +capabilities, effect context, and external bindings that affect a pass are +recorded as metadata inputs where available; the manifest marks any +unobserved dependency. + +Diagnostics retain source, trusted-library, or program origin. When the +compiler can identify a trusted-library module, the diagnostic also records +that logical source identity and span; the broad `trusted_library` class alone +does not identify the file. + +The complete backend pass sequence is represented at actual call boundaries: + +```text +linked Core + -> Core optimization + -> Effect lowering and binding conformance + -> closure conversion and CC verification + -> MIR lowering and verification + -> MIR optimization + -> Wasm structuring and encoding + -> component assembly and validation + -> WAT printing +``` + +Nested verifier calls may initially have coarser coverage than their enclosing +pass. The trace states that granularity instead of synthesizing a separate +verifier event from a final success flag. Driver diagnostics and artifact +records use backend-owned pass/artifact events; serialization belongs to the +CLI layer, and lower-level IR crates do not depend on the CLI or its JSON +format. + +## Source identity and lineage + +Lineage is a many-to-many derivation relation. It is produced by the pass that +creates or transforms nodes; query code may index it but may not infer it from +formatted output. + +- `SourceRef` identifies a logical source file by stable logical name and + content fingerprint, with a byte span and an origin class (`user`, + `trusted_library`, or `generated`). A run may separately map this identity to + a physical path used for replay. A span locates source text; it is not a + semantic identity. +- `DeclarationRef` identifies a module declaration by namespace and a stable + cross-run key. Compiler-local numeric IDs are retained for within-run joins + but are not treated as stable across runs. +- `NodeRef` is the pair of an artifact ID and a node ID local to that artifact. +- A derivation record names the producing execution, zero or more input nodes, + zero or more output nodes, a relation role, and source/declaration origins. + It has at least one input or output; elimination has one or more inputs and + no outputs. + +This shape expresses one-to-many expansion, many-to-one combination, and +elimination. For example, lowering one lambda may produce a lifted function, +an environment layout, and a closure construction; inlining may combine +several input nodes into several output nodes. Generated adapters record their +actual generation role and the input value, instantiation site, and owning +declaration when the pass has that evidence. Shared spans are query hints, not +proof of a derivation. Uninstrumented nodes are marked `untracked`. + +## Comparison + +`--compare` first checks compatibility of the selected case set, complete +source/import closure and load order, trusted-library contents, target and +relevant tool configuration, schema, pass contracts, and summary versions. +Compiler revision and executable fingerprints may differ because revisions +are the subject of comparison; both are shown. Incomplete source capture, +changed environment, unsupported versions, or coverage gaps are reported and +limit the claims that can be made. + +For each matched case, comparison preserves compile status, blocker stage and +category, diagnostic details, and input-change semantics. Where compatible +canonical summaries exist, artifacts and pass executions are aligned by their +dependency edges and roles. The report names the earliest observed boundary +whose output differs. Independent branches may produce several incomparable +earliest differences. A missing artifact, pass, summary, or lineage record is +reported as an observation gap; it is not treated as an unchanged artifact. +Cross-run node IDs are never compared directly. + +The initial trace implementation does not yet produce canonical IR summaries, +so artifact-content and earliest-artifact-difference comparison must report +`unavailable`. It may still compare the existing case statuses, blockers, +diagnostics, cohort, and source fingerprints. + +## Runtime evidence + +Compile traces end at produced Wasm/component artifacts. Runtime validation is +a separate `RuntimeRun` that references the exact Wasm artifact digest and +records the Wasmtime version and configuration, exit or trap result, and +value-sensitive assertions. A successful compile is never a runtime pass. +Runtime events do not become compiler pass executions. If a runtime provides a +Wasm function/type/instruction location, it may be joined to the Wasm artifact; +otherwise the raw trap is retained with the location marked unavailable. + +## Implementation coverage + +The backend and driver provide a versioned run-local artifact/pass model, +actual top-level backend pass IO boundaries, and an opt-in diagnosis API that +avoids dump capture by default. The CLI writes schema-v2 manifests, captures +selected dumps for accepted and rejected cases with `--trace`, replays explicit +ordered inputs, and preserves v1 status/blocker/input comparison. Its pass +comparison reports only the first observed execution-sequence difference +after checking cohort and complete per-case input compatibility. It is not an +artifact-content or DAG comparison, and it does not identify a root cause. +Observed host and installed `rustc`/`cargo` versions are reported separately; +the compiler build toolchain cannot be established from those observations and +remains unavailable. + +The frontend trace is one coarse lowering boundary, and backend trace +validation is recorded only at the granularity the backend observes. Canonical +artifact summaries, source/declaration/node lineage, query commands, earliest +artifact-DAG diffing, and Wasmtime runtime records remain planned. Diagnosis +does not fix backend defects; in particular, an observed closure-signature +trap remains runtime evidence until a focused runtime test and compiler +investigation establish the cause and repair. diff --git a/docs/design/D-17-stdlib-and-conformance-boundaries.md b/docs/design/D-17-stdlib-and-conformance-boundaries.md new file mode 100644 index 00000000..d85de9ca --- /dev/null +++ b/docs/design/D-17-stdlib-and-conformance-boundaries.md @@ -0,0 +1,92 @@ +# Standard-library package and conformance boundaries + +Matching features: [F-02](../feature/F-02-portable-programs.md) and +[F-04](../feature/F-04-compile-diagnosis.md). + +## Ownership + +`psrs-stdlib` is a separate source package and local Git repository. It owns +official library sources, exact upstream package pins and SHA-256 source hashes, +target adaptations, and library behavior cases. Its initial history is the +compiler's `stdlib/` subtree history; the move preserves source bytes. + +The compiler owns language semantics, checked foreign binding identity and type +evidence, supported binding protocols, lowering, and runtime representation. +The library owns array algorithms over small runtime/storage primitives; +checked primitive calls and allocation/write semantics belong to the compiler. +Whole stdlib functions are not automatically intrinsic candidates. +Library source changes cannot compensate for compiler defects. Follow the +[source-fidelity contract](../workflow/stdlib-vendoring.md). + +Executable Core linking retains static library foreign declarations and their +checked signatures only when reached from the entry, just as it retains ordinary +library values. A reached unimplemented binding remains a linking error. +Explicit primitive and WIT declarations retain source-wide protocol validation, +including unused declarations. This executable reachability contract does not +establish library support: inventories and public API execution cases must expose +missing implementations independently of dead-code removal. + +`psrs-stdlib/tools/conformance.mjs` is a library-owned Node component. It compares +source inventories, evaluates pinned upstream JavaScript implementations, and +runs a compiler executable and Wasmtime. Inputs are package paths, manifests, +case data, and executable paths. It uses Node built-ins, requires no npm +installation, and has no compiler-internal Rust dependency. Its implementation +and tests evolve with the library; the compiler retains package locking and +Rust integration tests. +A future cargo xtask may invoke these commands without becoming their owner. + +## Package selection + +`stdlib.lock.json` records a package revision, relative local checkout path, +and portable content fingerprint. The development default is `../psrs-stdlib`; +no remote publishing or download mechanism is implied. A fresh checkout or CI +worker must provision the separate package before library-dependent checks. +Automatic CI acquisition remains pending a published source location. The default loader +rejects a content mismatch. The revision records provenance; archives do not +require Git. Content identity, rather than an unchecked Git HEAD, governs load. + +`PSRS_STDLIB_ROOT` explicitly selects an unlocked development package. An invalid +selection fails without falling back. Installed executables currently require +this variable when their build-time checkout and lock are unavailable. +The driver exposes the selected package metadata and caches the parsed sources +for the process lifetime. Changes during initial loading are rejected; restart +the process after editing a development package. + +The manifest declares binding protocol version 1, Unicode scalar/UTF-8 strings, +and signed i32 integers. Unsupported or missing protocol fields are errors. +The existing import-closure selection and compiler-provided module ownership +remain in the driver and resolver respectively. + +## Reproducible content identity + +The `fnv1a64-v1:` identifier is a reproducibility fingerprint, not a cryptographic +integrity check. Starting at FNV-1a's 64-bit offset basis, hash the bytes +`psrs-stdlib-content-v1` followed by NUL. Sort relative paths lexicographically +by UTF-8 path components from `lib/`, `conformance/`, `manifest.json`, and `upstream-lock.json`. For each, +hash its UTF-8 path, NUL, file length as unsigned 64-bit little-endian bytes, +and the exact file bytes. FNV multiplication wraps at 64 bits. Package symlinks +and special files are rejected. The upstream lock retains cryptographic hashes. +Tooling and docs are outside this package content fingerprint; record the library tool revision with evidence. +Moving tools does not change the source/case fingerprint. + +The CLI obtains the fingerprint from the driver's selected package, including +an override. This changes the diagnosis cohort identity from the previous +absolute-path fingerprint; old and new snapshots are not the same cohort. + +## Evidence and remaining work + +Audit reports must compare the complete package set to the package's pinned +upstreams. Scalar oracle generation reads case data from the library package, +checks upstream revisions and clean checkouts, retains raw official results, +and records explicit target representation differences. + +Runtime reports identify compiler and Wasmtime binaries, input sources, package +content, generated Wasm, exact commands, timeouts, exit codes, and stdout/stderr +bytes. Compile failure, launch failure, timeout, and an observation mismatch +cannot pass. Missing Wasmtime is an error. Scalar observations cover only the +implemented bindings; they do not establish whole-library FFI correctness. + +The package manifest remains `incomplete`. A separate repository does not imply +release readiness, full compile acceptance, or complete runtime support. Future +publishing must add acquisition, license packaging, release compatibility, and +broader value-sensitive conformance evidence before claiming those properties. diff --git a/docs/design/backend/00-ir-boundaries.md b/docs/design/backend/00-ir-boundaries.md index 4f0791b2..fe5975be 100644 --- a/docs/design/backend/00-ir-boundaries.md +++ b/docs/design/backend/00-ir-boundaries.md @@ -21,7 +21,9 @@ the boundary-verification philosophy, and the responsibilities of the thin Wasm encoding (P10) and artifact production (P11). It is the cross-cutting contract between the two backend concerns. -It does not own the individual representations or topics. The typed core +It does not own the individual representations or topics. The shared +representation model and checked-boundary conversion contract is +[representation and evidence](fp/representation-and-evidence.md); the typed core calculus is [functional core](../frontend/semantics/functional-core.md); ANF and closure conversion are [CC IR](fp/cc-ir.md); SSA/CFG and representation planning are [MIR](fp/mir.md); control-flow structuring and tail calls are @@ -125,6 +127,14 @@ the selected target. P10 may optimize while preserving MIR types and the order and multiplicity of potentially effectful calls. It structures control flow and assigns final indices mechanically; it must not choose a representation. +The draft [linking and runtime](wasm/linking-and-runtime.md) contract adds +checked target requirements alongside MIR and a link plan before Wasm emission. +The plan is metadata, not another language IR. It joins implementation/provider +selection, memory ownership, and component composition without moving WIT names +into CC or reinterpreting source types after ABI lowering. P10 and P11 consume +the same checked plan for the formatter slice; general guest providers are not +yet implemented. + ## Model ### Boundary @@ -318,7 +328,8 @@ lib.rs crate surface: `compile` / `compile_with_target`, `ExternalBin capability.rs `TargetCapabilities` and validator feature mapping types.rs shared Wasm value/type model abi/ WIT registry, canonical ABI classification, checked source signatures -component.rs component packaging +linking/ checked target requirements and plan/diagnostic mapping +target_runtime.rs intrinsic-to-artifact implementation catalog cc/ CC IR (functional) mod.rs `Module`, `Function`, `Assignment`, `AssignmentKind`, lowering entry representation.rs `ReprId`, `ValueShape`, `Reference`, `RefShape`, `Representation`, `RepresentationTable` @@ -335,7 +346,7 @@ mir/ MIR verify/ MIR verifier wit/ canonical ABI adaptation for WIT calls reachable.rs reachability of representation requirements - scalar_helpers.rs, numeric.rs scalar operations and helpers + numeric.rs concrete scalar operations wasm/ thin structured Wasm target mod.rs Wasm IR: `Module`, `Op`, `Function`, `Export`, `DataSegment` encode.rs binary encoding @@ -450,7 +461,10 @@ and P10/P11 would name it from the ABI registry; CC would be unchanged. ([D-01](../D-01-frontend-and-ir-boundaries.md), [Wasm encoding](wasm/encoding-and-structuring.md)). - **P7 to P8.** Verified Core plus explicit trusted-effect and selected-entry - metadata. P8 builds WIT bindings from Core's checked `ExternalType` + metadata, plus the boundary side table that carries checked instantiation + evidence and representation policies + ([representation and evidence](fp/representation-and-evidence.md)). P8 builds + WIT bindings from Core's checked `ExternalType` schemes before erasure. See [functional core](../frontend/semantics/functional-core.md) and [CC IR](fp/cc-ir.md). diff --git a/docs/design/backend/README.md b/docs/design/backend/README.md index f0d63cf1..26e84602 100644 --- a/docs/design/backend/README.md +++ b/docs/design/backend/README.md @@ -45,6 +45,7 @@ is [Functional Core](../frontend/semantics/functional-core.md). | Document | Owns | Depends on | | --- | --- | --- | +| [representation-and-evidence.md](fp/representation-and-evidence.md) | The shared RuntimeRep model, generic aggregate normalization, and the checked-boundary conversion contract every adaptation uses | frontend classes and evidence; functional core | | [cc-ir.md](fp/cc-ir.md) | ANF, closure conversion, CC operations and verifier | frontend Functional Core | | [mir.md](fp/mir.md) | SSA/CFG model, representation planning, MIR verifier | cc-ir | | [polymorphism-and-erasure.md](fp/polymorphism-and-erasure.md) | Rank-1 polymorphism, erased representation, adapters | mir | @@ -62,6 +63,7 @@ is [Functional Core](../frontend/semantics/functional-core.md). | [encoding-and-structuring.md](wasm/encoding-and-structuring.md) | Structured Wasm encoding and binary emission | 00-ir-boundaries | | [capability-profile.md](wasm/capability-profile.md) | Target capability profile and gating | encoding-and-structuring | | [canonical-abi-and-wit.md](wasm/canonical-abi-and-wit.md) | WIT bindings and canonical ABI adaptation | encoding-and-structuring | +| [linking-and-runtime.md](wasm/linking-and-runtime.md) | Intrinsic implementations, WIT providers, checked target link plans, and artifact composition (Draft) | primitive FFI, canonical-abi-and-wit | | [linear-memory-and-canonical-abi-boundary.md](wasm/linear-memory-and-canonical-abi-boundary.md) | Linear memory as the ABI boundary | canonical-abi-and-wit | | [wasi-platform-library.md](wasm/wasi-platform-library.md) | Component packaging and WASI services | canonical-abi-and-wit | @@ -77,11 +79,12 @@ rules. ## Ordering -Read the functional concern bottom-up: functional core, then CC and MIR, then -representation topics (erasure, scalars, data, patterns, control flow), then -dictionaries and effects. Read P7 optimization after Core and P10 optimization -after MIR and the target capability profile. Wasm/WASI encoding consumes the -optimized MIR. +Read the functional concern bottom-up: functional core, then the shared +[representation and evidence](fp/representation-and-evidence.md) model, then CC +and MIR, then representation topics (erasure, scalars, data, patterns, control +flow), then dictionaries and effects. Read P7 optimization after Core and P10 +optimization after MIR and the target capability profile. Wasm/WASI encoding +consumes the optimized MIR. ## Writing diff --git a/docs/design/backend/fp/cc-ir.md b/docs/design/backend/fp/cc-ir.md index 7fa9c3ce..9c063da5 100644 --- a/docs/design/backend/fp/cc-ir.md +++ b/docs/design/backend/fp/cc-ir.md @@ -20,11 +20,13 @@ operation families, the external-binding boundary, and the CC verifier. It specifies what CC must express and must never contain. It does not own the Core terms it lowers (see [functional core](../../frontend/semantics/functional-core.md)), -the concrete runtime layout or Wasm type table (see [MIR](mir.md)), the pattern +the shared representation model and checked-boundary contract (see +[representation and evidence](representation-and-evidence.md)), the concrete +runtime layout or Wasm type table (see [MIR](mir.md)), the pattern decision algorithm (see [pattern matching](pattern-matching.md)), the erased representation protocol (see [polymorphism and erasure](polymorphism-and-erasure.md)), or canonical generic aggregate layouts and conversion semantics (see -[generic aggregate erasure](generic-aggregate-erasure.md)), +[representation and evidence](representation-and-evidence.md)), the scalar operator definitions (see [scalars and primitives](scalars-and-primitives.md)), or the WIT/canonical-ABI binding rules (see [canonical ABI and WIT](../wasm/canonical-abi-and-wit.md)). Dictionary @@ -142,7 +144,7 @@ canonical aggregate representation with recursively normalized elements or fields. CC records a target-neutral aggregate conversion plan when a typed boundary must reconstruct a different aggregate shape; it never encodes that work as a nominal reference cast. See -[generic aggregate erasure](generic-aggregate-erasure.md) for the canonical +[representation and evidence](representation-and-evidence.md) for the canonical keys and conversion rules. `RepresentationTable::reserve` allocates a stable `ReprId` before its @@ -400,16 +402,27 @@ function value without a separate calling convention. ### Partial application and erased adapters -When a `Global` is applied to fewer source-arrow arguments than its signature -has, P8 generates a wrapper closure that captures the supplied arguments and -calls the original function with the remaining source parameters appended. +Partial application is under-application of a source arrow: a call that +supplies fewer arguments than the callee's arrow arity. This is the ordinary +partial-application closure of functional-language runtimes (GHC's `PAP` +objects, OCaml's `caml_apply`), so P8 handles it uniformly for every callee +kind, not only `Global`: + +- a `Global` applied to fewer source-arrow arguments generates a wrapper + closure that captures the supplied arguments and calls the declaration with + the remaining source parameters appended; and +- a callee value that is not a declaration — a local closure or a class method + reached through a dictionary field — generates a closure that captures the + callee and the supplied arguments, exposes the remaining parameters, and + calls the captured callee indirectly. + `log "message"` for `log :: String -> Effect Unit` is a saturated source call; the effect token is not a remaining parameter of `log`. The token belongs to -the representation closure that the call returns -([effects](effects.md)). When a concrete function value crosses a polymorphic -function boundary, `adapt_erased_function_value` builds an adapter closure with -the erased signature that captures the original, boxes/unboxes each parameter, -calls the concrete closure, and boxes/unboxes the result +the representation closure that the call returns ([effects](effects.md)). When +a concrete function value crosses a polymorphic function boundary, +`adapt_erased_function_value` builds a reabstraction thunk with the erased +signature that captures the original, boxes/unboxes each parameter, calls the +concrete closure, and boxes/unboxes the result ([polymorphism and erasure](polymorphism-and-erasure.md)). ### Pattern decision and case lowering @@ -578,8 +591,12 @@ inside `If` assignments. See [MIR's worked example](mir.md) for the SSA form. the layout, builds the Wasm type table, lowers aggregate maps, and converts structured `If` into a CFG; it must not invent a requirement CC did not state. -- **From Core.** Core types and names are consumed here; nothing below CC - depends on Core `TypeId`s except lowering-only diagnostic side data. +- **From Core.** Core types and names are consumed here. A representation + decision must not read the Core type arena ambiently: checked instantiation + evidence and each erased value's representation policy travel with the input + as an explicit side table, the same way `ExternalBindings` carries the WIT + binding boundary. Nothing below CC depends on Core `TypeId`s except that + side table and lowering-only diagnostic data. ## Open questions and future work @@ -595,7 +612,7 @@ inside `If` assignments. See [MIR's worked example](mir.md) for the SSA form. - **Open rows.** Closed records only; row polymorphism needs a separate representation contract. Canonical closed generic aggregates and explicit conversion plans are specified in - [generic aggregate erasure](generic-aggregate-erasure.md). + [representation and evidence](representation-and-evidence.md). ## Implementation notes diff --git a/docs/design/backend/fp/data-representation.md b/docs/design/backend/fp/data-representation.md index f44cdcbc..bbbcf6b7 100644 --- a/docs/design/backend/fp/data-representation.md +++ b/docs/design/backend/fp/data-representation.md @@ -4,7 +4,7 @@ **Status:** Stable (design) **Prerequisites:** [CC IR](cc-ir.md), [MIR](mir.md), [polymorphism and erasure](polymorphism-and-erasure.md), and -[generic aggregate erasure](generic-aggregate-erasure.md); the WebAssembly 3.0 +[representation and evidence](representation-and-evidence.md); the WebAssembly 3.0 type system (structs, arrays, subtyping, `ref.test`/`ref.cast`, `i31`, and typed function references). Read [IR boundaries](../00-ir-boundaries.md) first. @@ -13,21 +13,28 @@ entirely by the P9 planner: sums become a tag-carrying abstract supertype with one final subtype per case (or an immediate `i32` tag when every case is nullary), products and records become structs, arrays become mutable GC arrays, and closures become `{ funref, capture-array }` structs whose captures are -stored in one uniform `eqref` array. This document fixes those layouts, the -operation lowering, and the execution-evidence expectations that promote a -backend capability. +stored in one uniform `eqref` array. This document is the Wasm GC half of the +runtime representation model: it realizes the `heap_layout : RuntimeRep -> +HeapLayout` mapping of +[representation and evidence](representation-and-evidence.md). It fixes those +layouts, the operation lowering, and the execution-evidence expectations that +promote a backend capability. ## Scope This document owns the concrete GC heap layouts that the P9 GC planner builds from CC requirements, the Wasm operations that construct and observe them, and -the execution-evidence expectations recorded for each capability. It does not +the execution-evidence expectations recorded for each capability. It realizes +the target-specific `Layout` of every `RuntimeRep` the Wasm GC profile supports, +so the runtime representation model is complete on this target. It does not own the planner contract (see [IR boundaries](../00-ir-boundaries.md)), the +shared representation model and checked-boundary contract (see +[representation and evidence](representation-and-evidence.md)), the target-neutral `Variant` model (see [CC IR](cc-ir.md)), the erased protocol for polymorphic values (see [polymorphism and erasure](polymorphism-and-erasure.md)), the normalization and conversion between generic and concrete aggregate layouts (see -[generic aggregate erasure](generic-aggregate-erasure.md)), scalar semantics (see +[representation and evidence](representation-and-evidence.md)), scalar semantics (see [scalars and primitives](scalars-and-primitives.md)), or the byte-oriented string and ABI boundary (see [linear memory and the canonical ABI @@ -62,7 +69,7 @@ fields retain canonical array, product, or closure references. Captures use a separate uniform reference-array protocol. A generic array or closed record has its own canonical aggregate layout; converting to or from a specialized concrete layout requires reconstruction, -as specified by [generic aggregate erasure](generic-aggregate-erasure.md). +as specified by [representation and evidence](representation-and-evidence.md). **Target-neutral variants.** [CC IR](cc-ir.md) makes the sum encoding a P9 decision: CC states one `Variant` requirement per @@ -73,6 +80,10 @@ chooses the object layout. ### Value types +A CC `ValueShape` is the materialization of a +[`RuntimeRep`](representation-and-evidence.md), and the MIR `ValueType` is its +`HeapLayout` on this target. The mapping is total on the supported profile: + | CC `ValueShape` | MIR `ValueType` | | --- | --- | | `Integer` | `I32` | @@ -138,7 +149,7 @@ for `Array a` is `Array(Erased)` with nullable `eqref` storage; other generic array layouts are keyed by their recursively normalized element shape. Closed generic records likewise use canonical products of normalized field shapes. Their conversions are explicit aggregate reconstruction, not `ref.cast` -between nominal layouts. See [generic aggregate erasure](generic-aggregate-erasure.md). +between nominal layouts. See [representation and evidence](representation-and-evidence.md). ### Type-table invariants @@ -201,7 +212,7 @@ shapes, and dependent closed records use canonical record keys containing sorted labels and normalized field shapes. P9 assigns one `DefinedTypeId` per reachable `ReprId`; concrete `Array Int` and canonical `Array(Erased)` remain distinct nominal types. An aggregate conversion plan that names distinct layouts is lowered to -reconstruction by [generic aggregate erasure](generic-aggregate-erasure.md), +reconstruction by [representation and evidence](representation-and-evidence.md), never to an array or struct `ref.cast`. A profile without `gc`, `reference_types`, or (for closures) `function_references` @@ -240,7 +251,7 @@ Three sequences are worth spelling out: - **Converting aggregate layouts.** A generic/concrete array boundary lowers to a fresh array and a loop that converts each element; a closed record boundary reads, converts, and rebuilds its fields. The aggregate conversion - plan is defined in [generic aggregate erasure](generic-aggregate-erasure.md). + plan is defined in [representation and evidence](representation-and-evidence.md). No conversion uses a cast between distinct nominal array or struct types. - **Closure call.** The closure is loaded, the arguments are loaded, and the closure is loaded again, cast to the closure struct, and its field 0 code @@ -557,7 +568,7 @@ Recovery to scalar fields uses typed GC boxes; conversion between different nominal array or closed-record layouts uses explicit reconstruction between `ReprId`s, and function signatures change through closure adapters. Canonical layouts and conversion plans are implemented as specified in -[generic aggregate erasure](generic-aggregate-erasure.md). The +[representation and evidence](representation-and-evidence.md). The [acceptance record](../../../implementation/backend/generic-aggregate-erasure.md) includes source-to-component execution and verified Typed Core fixtures for backend inputs that source lowering does not yet produce. It also records the diff --git a/docs/design/backend/fp/effects.md b/docs/design/backend/fp/effects.md index 6687b8d6..6ac01640 100644 --- a/docs/design/backend/fp/effects.md +++ b/docs/design/backend/fp/effects.md @@ -17,7 +17,11 @@ effectful entry once and returns zero after normal completion. This document owns the representation of `Effect a` and the meaning of `pure`, `bind`, and `runEffect`, including the execution token and effect sequencing. It -does not own the WASI services an effect may call +is the `Effect` representation owner in the shared model of +[representation and evidence](representation-and-evidence.md): `Effect a` is a +`Callable` policy with a fixed state-token parameter and the payload as result, +so it uses the ordinary closure and conversion contract rather than a private +path. It does not own the WASI services an effect may call ([WASI platform library](../wasm/wasi-platform-library.md)), the do/ado desugaring that produces `bind` (frontend; `FE-05` in [DEC-04](../../../decision/DEC-04-official-test-suite-roadmap.md)), or the @@ -174,9 +178,14 @@ flag, private constructor, or representation mode. - No stage stores an "effect" flag on an expression or adds a dedicated Effect IR node, closure kind, or runtime object. After representation lowering, CC and MIR see generic closures and calls. -- The synchronous token may be the integer zero. It carries no scheduling or - ordering guarantee. Call order and multiplicity are preserved by the - evaluation and optimizer contracts. +- The runtime token is the state token of a strict IO-like effect, the + `State# RealWorld` analogue in GHC. It is the compiler-owned opaque + `TypeId::STATE_TOKEN`, threaded through the chain and neither duplicated nor + observed; ordering comes from the calls and strict evaluation order, not from + the token's bits. For the current synchronous, single-threaded `Effect` the + token carries no payload, so its runtime shape is a scalar constant; the type + stays opaque, so no pass treats it as an `Int`. Call order and multiplicity + are preserved by the evaluation and optimizer contracts. - Entry selection produces one resolved command-entry `SymbolId`: use `Main.main` when present, otherwise require one unique top-level `main`. The lexical `runEffect` reference check and generated entry wrapper use that @@ -228,10 +237,15 @@ becomes a record/closure over its operation implementations and `runEffect` interprets it. That is a change of the operation set, not of the lowering mechanism, and it still introduces no dedicated CC/MIR node. -The synchronous token may be the constant `i32` value `0`. The calls themselves -are observable and cannot be merged or removed; distinct token bits are not a -substitute for that optimizer rule. A later runtime may pass state or resource -handles without exposing the token to source programs. +The token is a single abstract state value, the `State# RealWorld` analogue: +the lowering passes it along and never inspects or copies it. The compiler +interns it as the opaque `TypeId::STATE_TOKEN`, which has no source spelling; +its one value is the `StateToken` expression the effect runner supplies. Its +runtime shape is a scalar constant because the synchronous effect carries no +payload — distinct token bits carry no meaning, and the calls themselves are +observable and cannot be merged or removed. A later runtime may pass a state or +resource handle through the same parameter without exposing it to source +programs. ### Partial application @@ -374,7 +388,10 @@ rewrites to `bind` before Core, so the backend sees only `pure`, `bind`, - **`bind` with a continuation that ignores its argument.** Still sequenced; the first computation runs before the continuation. - **Polymorphic effect.** `Effect a` with an erased `a` uses the erased - protocol; `runEffect`'s consumer knows the concrete type + protocol; `runEffect`'s consumer knows the concrete type. Crossing an abstract + constructor boundary (`f a`, including `f Unit`) uses the common checked + representation conversion contract, including the producer's stored closure + signature; source instantiation alone does not authorize a signature cast ([polymorphism and erasure](polymorphism-and-erasure.md)). - **A future richer token.** Changing the token to a stateful value changes the representation lowering and the runtime, not the source API or the CC/MIR @@ -439,14 +456,30 @@ Responsibilities and required types: - The wrapper and rewritten Core are structurally verified before CC. This checks binding and type-shape contracts; it does not add a Core or CC Effect node or prove runtime behavior. +- Authoritative source Core remains available until checked boundary relations + and representation conversion plans have been consumed. The application-to- + closure mapping is explicit. An internal rewritten Core-shaped module is a + separate physical view, verified against its own complete structural contract; + it is not passed to the source matcher as though `Effect a` were a source + function. Arity-based reconstruction of the erased constructor is forbidden. +- The Effect representation owner contributes its trusted constructor mapping, + token parameter, result representation and representation policy to the common + conversion planner. It owns operation synthesis, suspension and command-entry + behavior. Generic calls, dictionary fields, captures and adapter construction + consume ordinary checked boundary evidence and representation policies; they + do not select an Effect-specific conversion path. A representation-only + canonical closure receives its authority from the plan that creates it, + without a fabricated source `Effect` type or closure-origin field. - CC and MIR lower the resulting generic closures through `FunctionRef` and direct or indirect calls. Curried-arrow flattening reads source `Function` - spines only. Partial application - (`lower_partial_global_application`) applies to under-applied source arrows, - not to the token of an effect. -- The representation may later grow into dictionary passing - ([type classes and dictionaries](type-classes-and-dictionaries.md)). The token - stays inside effect lowering; its type is chosen there. + spines only. Partial application applies to under-applied source arrows, + whether the callee is a declaration or an indirect value, and never to the + token of an effect. +- Dictionary passing uses the ordinary product and closure representation + ([type classes and dictionaries](type-classes-and-dictionaries.md)). A chosen + `Bind Effect` instance still crosses the definition ABI of a shared generic + method; instance selection does not specialize that ABI automatically. The + token stays inside effect lowering; its type is chosen there. - WASI operations are owned by the [WASI platform library](../wasm/wasi-platform-library.md) and the [canonical ABI and WIT](../wasm/canonical-abi-and-wit.md). A host call is @@ -561,6 +594,8 @@ the behavioral guarantees above. - Wadler, P., *Monads for Functional Programming* (1992/1995). - Levy, P. B., *Call-by-Push-Value: A Subsuming Paradigm* (1999) and *Call-by-Push-Value* (2004). +- GHC's `IO` as `State# RealWorld` (Launchbury and Peyton Jones, *State in + Haskell*, 1994): the state-token representation of a strict IO effect. - [CC IR](cc-ir.md): closures, captures, and partial application. - [type classes and dictionaries](type-classes-and-dictionaries.md): the dictionary form the effect representation may grow into. diff --git a/docs/design/backend/fp/generic-aggregate-erasure.md b/docs/design/backend/fp/generic-aggregate-erasure.md deleted file mode 100644 index c7af47e8..00000000 --- a/docs/design/backend/fp/generic-aggregate-erasure.md +++ /dev/null @@ -1,575 +0,0 @@ -# Generic Aggregate Erasure - -**Feature:** F-02 -**Status:** Stable (design) -**Prerequisites:** [CC IR](cc-ir.md), [MIR](mir.md), [data representation](data-representation.md), and [polymorphism and erasure](polymorphism-and-erasure.md); WebAssembly GC's nominal struct and array types. Read [IR boundaries](../00-ir-boundaries.md) first. -**Summary:** This document defines how generic arrays and closed records cross between concrete, specialized layouts and canonical layouts that are independent of type arguments. It keeps DEC-07's runtime erasure for parameterized ADTs and adds explicit, typed conversions wherever an aggregate's nominal GC layout changes. Generic arrays and closed records use canonical erased layouts inside polymorphic code; concrete instantiations keep their specialized layouts. - -## Scope - -This document owns representation normalization and conversion for generic -arrays and closed records, including their use as fields of parameterized ADTs. -It defines the CC conversion contract and its lowering and verifier obligations -through MIR. It refines field normalization while preserving the single-layout -invariant in [DEC-07](../../../decision/DEC-07-runtime-representation-for-parameterized-adts.md), -and does not define open-row records, change source type inference, or define -independent Wasm artifact linking. Scalar boxing and polymorphic function adapters remain -specified by [polymorphism and erasure](polymorphism-and-erasure.md); concrete -GC objects and pure array update remain specified by -[data representation](data-representation.md). - -Implementation acceptance is tracked in the -[topic execution checklist](../../../implementation/backend/generic-aggregate-erasure.md). -Existing code and regression tests are evidence candidates; this stable design -status does not claim that every requirement has been implemented or verified. - -## Background - -Wasm GC struct and array types are nominal. Mutable array element types are -invariant: `(array (mut i32))` and `(array (mut eqref))` are distinct defined -types, and `ref.cast` cannot transform one array's elements into another -layout. A reference cast checks object identity and declared subtyping; it does -not walk an array or rebuild a struct. - -DEC-07 selects one erased runtime representation for parameterized ADTs and -does not specialize a polymorphic function for every call. Its erased `eqref` -slot can hold a reference, but that fact does not make a concrete aggregate -object have the layout expected by a generic body. For example, an -`Array Int` reference may be upcast to `eqref`, but it is not an instance of the -canonical array type needed to implement `Array a` for arbitrary `a`. - -The design therefore distinguishes **erasing a reference** from **converting an -aggregate layout**. Erasing a reference is a reference upcast and preserves the -object. Converting a layout allocates a new aggregate and converts its elements -or fields according to the typed source and destination. Scalar values use the -existing typed boxes when they cross an `Erased` boundary. - -## Model - -### Runtime shape normalization - -P8 derives a runtime shape from a typed Core type and its type-variable scope. -It distinguishes a declaration template from an instantiated actual type; this -keeps a generic layout canonical at a concrete call site. Normalization is -structural and cycle-safe, and produces CC requirements, never Wasm types: - -- `Template(T, Q)` normalizes a type in a polymorphic declaration while - treating that declaration's bound variables `Q` as abstract. It does not - apply a call-site substitution to those variables. -- `Actual(T, S)` first applies boundary substitution `S` to the source - expression's type. A closed result receives its specialized shape; variables - that `S` does not resolve remain abstract and use template normalization. - -Formal parameters, function results, and declared ADT field templates are -normalized with `Template`. Values at calls and constructors are normalized -with `Actual`. P8 constructs a conversion plan between those two shapes. - -In the CC plan, `TypeRole` records only which normalization rule P8 used: -`Template` or `Actual`. It carries no Core `TypeId` into MIR. `TemplateContext` -identifies the bound variables that remain abstract; `Substitution` resolves -the variables known at an actual call or construction boundary. - -| Typed Core type | Normalized runtime value shape | -| --- | --- | -| A bare type variable `a` | `Erased` | -| An application `f a` headed by a type variable | `Erased`, because its storage constructor is unknown | -| A concrete scalar such as `Int` | Its scalar CC shape | -| A concrete `Array Int` | `Reference(Repr(Array(Integer)))` | -| A type-dependent `Array a` | `Reference(Repr(Array(Erased)))`, the canonical generic array | -| A type-dependent `Array (Array a)` | An array of references to canonical `Array(Erased)` values | -| A closed record `{ x :: Int }` | A product with the concrete `Integer` field | -| A dependent closed record `{ x :: a }` | A canonical product with an `Erased` field | -| A dependent closed record `{ xs :: Array a }` | A canonical product with an `Array(Erased)` reference field | -| A function `a -> Int` | `Reference(Closure(Signature([Erased], Integer)))` | -| A parameterized ADT | Its nominal variant representation, independent of type arguments | - -For a type-dependent `Array T`, P8 recursively normalizes `T` and interns the -array by that normalized element shape. Thus the canonical `Array a` layout is -`Array(Erased)`, a GC array whose physical element storage is nullable `eqref`. -Its elements are logically non-null language values; nullable storage exists -for default allocation and private construction only. `Array (Array a)` stores -references to the canonical inner-array layout in its slots. This recursive -rule preserves useful reference shapes while ensuring that `Array a` has one -layout independent of the instantiation of `a`. - -A closed generic record uses a canonical product shape. Each field is -normalized recursively, and the physical field order is the canonical field -order. Thus `{ value :: a, items :: Array a }` has the field shapes -`[Erased, Reference(Repr(Array(Erased)))]`. A bare variable field is erased; -aggregate fields retain their canonical aggregate reference shape. Open rows -have no canonical product in this design and remain unsupported. - -An ADT's variant layout continues to be keyed by its resolved declaration. A -constructor field is stored in the normalized shape of its declared template, -independent of concrete substitutions. A field declared `Array a` directly -stores its canonical `Array(Erased)` reference; closed records and function -fields likewise retain canonical product and closure shapes. A field declared -just `a` uses ordinary erased storage and does not force an aggregate copy. - -### Layout identity and conversion plans - -`CoreTypeId` and a concrete type substitution are inputs to normalization, not -runtime layout identities. CC interns requirements by canonical shape: - -- generic arrays share a representation keyed by normalized element shape; - every `Array a` specifically uses `Array(Erased)`; -- concrete arrays are keyed by their element storage shape; -- closed record keys contain canonical `(label, normalized field shape)` pairs; - ordinary positional products have their own key. `Representation::Product` - still contains only physical field shapes, while P8 keeps each record's - label-to-index mapping for lowering; and -- ADT variants are keyed by resolved declaration identity, with one set of - normalized template fields for all type arguments. - -P8 reserves a representation handle before recursively normalizing its fields, -so recursive references are cycle-safe. Equal canonical keys share a CC -`ReprId`; distinct concrete instantiations may have different IDs. P9 assigns -one `DefinedTypeId` to each reachable `ReprId` according to the existing -planner contract. P9 does not merge IDs by physical shape: ADT identity remains -declaration-based, and record/positional-product identity follows its P8 key. -The current driver links source modules into one Core program before P8, so -they share normalized keys and the resulting layout table. Linking -independently compiled Wasm artifacts while sharing GC nominal types is -outside this design. - -P8 records a target-neutral conversion plan when typed values with different -normalized shapes cross a semantic boundary: - -```text -ValueConversion = Identity - | BoxScalar(BoxKind) - | UnboxScalar(BoxKind) - | EraseReference - | RecoverReference { destination: ValueShape, - evidence: RecoveryEvidence } - | Sequence([ValueConversion]) - | ArrayMap { source: ReprId, target: ReprId, - element: ValueConversion } - | ProductMap { source: ReprId, target: ReprId, - fields: [ValueConversion] } - | FunctionAdapter { function: SymbolId, source: ValueShape, - destination: ValueShape } - -RecoveryEvidence = TypeInstantiation - | ErasedVariantField { variant: ReprId, tag: u32, - field: u32, template: ValueShape } - -AggregateConvert = { value: ValueId, source: ValueShape, - destination: ValueShape, plan: ValueConversion } -``` - -`FunctionAdapter` names a P8-generated factory with one parameter of the -source shape and a result of the destination closure shape. The factory -captures the original closure once and returns an adapter; the adapter converts -arguments and results when called. This leaf participates recursively in both -`ArrayMap` and `ProductMap`, in either conversion direction. A field declared -as a function retains its normalized template closure signature; instantiation -never casts it directly to a different call signature. A bare variable -instantiated as a function can recover the already-stored concrete closure. - -The exact Rust names may differ, but CC must state the source and destination -shapes and the recursive work. The plan contains no Wasm type index, physical -field offset, or unverified type-variable cast. `ArrayMap` and `ProductMap` -mean element-wise or field-wise reconstruction; they are not reference casts. - -## Design - -### Canonical layouts and concrete layouts - -Concrete, variable-free arrays and records keep their specialized layouts. For -example, `Array Int` remains `(array (mut i32))`, and `{ x :: Int }` remains a -struct with an `i32` field. This preserves the current concrete data path. - -Within a polymorphic function, `Array a` uses the canonical -`(array (mut (ref null eq)))` layout. Its slots contain erased elements: `Int`, -`Boolean`, and `String` values are boxed in the integer box, `Number` values -use the number box, and references are upcast to `eqref`. Other -generic arrays use an array of their recursively normalized element shape; for -example, `Array (Array a)` stores references to the canonical `Array a` -layout. A dependent closed record similarly uses one canonical product whose -fields are the normalized shapes of its declared fields. Such a product may -contain scalar fields, erased fields, canonical generic arrays, or nested -canonical records. - -When values cross between these layouts, P8 emits a conversion plan and P9 -lowers it to explicit work. If the source and destination shapes agree, the -conversion is identity. If the shapes differ only because a concrete scalar -crosses an erased slot, the conversion boxes or unboxes. If a nominal aggregate -layout differs, the conversion allocates and reconstructs it. A nominal -`ref.cast` is used only to recover a reference whose canonical or concrete -layout is guaranteed by the typed construction path; it never substitutes for -reconstruction. - -### Where conversions occur - -Conversions are inserted at every typed boundary where the caller's actual -representation and the callee or storage representation differ: - -- **Parameterized ADT construction and projection.** Convert the field value - to the normalized shape of its declared field template and store that shape - directly. Projection converts from the same template shape to the consumer's - concrete shape if needed. Only bare-variable slots require erased recovery. -- **Record construction, access, and update.** Construct the product required - by the record type at that boundary. Access reads the current record layout - and converts its field to the typed result shape. Update converts assigned - fields to the layout of the new record and returns a fresh product. -- **Array construction, read, and update.** A literal converts each element to - the chosen array's logical element representation before storage. Reads - convert from the physical slot shape to the statically known result shape. - A pure update clones the source layout and converts the replacement element - before writing; it never mutates the source. -- **Direct calls and returns.** Arguments convert from the caller's actual - shapes to the callee signature's normalized shapes. Results convert from the - callee result shape to the caller's instantiated result shape. Reading a - top-level value invokes its zero-argument producer and uses this same result - conversion, including generic dictionary records. -- **Higher-order calls.** The existing erased function adapter includes these - argument and result conversions in its body, alongside scalar box/unbox and - closure-signature adaptation. -- **Captures.** A captured generic aggregate is stored using its canonical - aggregate layout and then placed in the uniform capture array as an - `eqref`. A concrete aggregate captured by concrete code keeps its specialized - layout. Any conversion required by the lifted function's signature is - performed before capture or when the capture is read. -- **Linked source modules.** Since modules are combined into one Core program - before P8, the call boundary uses the same canonical `ReprId` and requires - only the type-directed conversion above, not a separate module ABI adapter. - -### Reconstruction semantics - -`ArrayMap` allocates a fresh destination array of the target layout, iterates -from zero to the source length, reads each source element, applies the nested -element conversion, and writes the converted element into the private target. -For reference element storage, physical slots are nullable references of the -target heap type; for `Array(Erased)` they are nullable `eqref`. All logical -values are converted to non-null references before the destination is exposed. -No source array is changed. Nested array and record values recursively use -their own conversion plans. A conversion whose source and target layouts are -identical does not allocate. - -`ProductMap` reads fields in canonical logical order, converts each field, and -allocates one fresh destination product only after the field values are ready. -Record updates follow the existing pure semantics: they construct a fresh -product with unchanged fields copied and changed fields converted. They do not -mutate an aliased record. Because arrays and records have no observable pointer -identity or mutation, a conversion may duplicate physical sharing while -preserving values. If the language later exposes pointer identity, mutable -aggregates, or cyclic aggregate graphs, the conversion design must add an -identity-preserving memo before those features are enabled. - -### Rejected alternatives - -- **Blind `ref.cast` from concrete to generic aggregate.** Rejected because - nominal GC types and mutable array element types differ; the cast does not - transform slots and may trap. -- **Erase every aggregate pointer and cast it back at each use.** Rejected for - the same nominal-layout reason. A cast from `eqref` to `Array(Erased)` is - valid only after the compiler has established that canonical layout. -- **Give every erased value a runtime type tag or dictionary.** Rejected by - DEC-07's uniform erased protocol; aggregate type reflection is not needed for - parametric source operations. -- **Use erased layouts for all concrete arrays and records.** Rejected because - it would discard specialized concrete layouts and force boxing and mapping - on programs that do not cross a polymorphic boundary. -- **Monomorphize every generic function.** Rejected by DEC-07; it changes the - selected runtime model and does not define behavior for higher-order values - or separate linking. - -## Algorithms - -### Normalization and plan construction - -P8 walks each typed value shape with a cycle guard and the active substitution. -It preserves concrete shapes when the type is closed. When a variable occurs -inside a generic aggregate, it creates or reuses the canonical aggregate -representation and records the logical element or field conversions at the -operation that crosses into or out of it. Open record rows and unknown foreign -aggregate layouts produce a named, source-spanned backend diagnostic. - -At a typed boundary, plan construction follows this procedure: - -```text -convert(source_type, source_role, destination_type, destination_role, substitution): - source_shape = normalize(source_type, source_role, substitution) - destination_shape = normalize(destination_type, destination_role, substitution) - require Core proves the typed boundary is valid under substitution - if source_shape == destination_shape: - return Identity - if destination_role is Template and destination_type is a bare variable: - if source_shape is scalar: return BoxScalar(box_kind(source_shape)) - if source_shape is reference: return EraseReference - if source_role is Template of bare variable and destination_role is Actual: - if destination_shape is scalar: return UnboxScalar(box_kind(destination_shape)) - if destination_shape is reference: - return RecoverReference(destination_shape, TypeInstantiation) - if both types are arrays and their element types correspond under substitution: - return ArrayMap(source_repr, destination_repr, - convert(source_element_type, source_role, - destination_element_type, destination_role, - substitution)) - if both types are closed records with the same label set and corresponding - field types under substitution: - return ProductMap(source_repr, destination_repr, - convert each matching field in canonical order with - the same source and destination roles) - otherwise: - report UnsupportedAggregateConversion at the boundary's source span -``` - -ADT construction and projection use the declared field template as the storage -contract. The variant slot must equal `Template(field_type)`; a mismatch is a -compiler IR error. A concrete substitution never changes that slot's layout. -For `Array a`, construction maps `Array Int` to canonical `Array(Erased)` and -stores that reference directly. A polymorphic projection reads the canonical -reference directly; a concrete projection maps its elements to `Array Int`. -For a bare `a`, construction erases the actual value without canonicalizing its -internal structure, and projection boxes/unboxes or recovers that actual shape. - -```text -construct_field(actual_value, field_template, substitution, stored_shape): - template_shape = Template(field_template) # never substitute first - assert stored_shape == template_shape - actual_shape = Actual(actual_value.ty, substitution) - return plan_conversion(actual_value.ty, Actual, field_template, Template, - substitution, actual_shape, template_shape) - -project_field(stored_value, field_template, substitution, stored_shape): - template_shape = Template(field_template) - assert stored_shape == template_shape - actual_type = substitute(field_template, substitution) - actual_shape = normalize_actual(actual_type, substitution) - if field_template is a bare variable and actual_shape is a concrete reference: - return RecoverReference(actual_shape, - ErasedVariantField(variant, tag, field, actual_shape)) - return plan_conversion(field_template, Template, actual_type, Actual, - substitution, template_shape, actual_shape) -``` - -`ErasedVariantField` evidence is produced only for an erased slot's reference -recovery. CC verifies that the named slot is physically `Erased` and the target -agrees with the token. Construction proves that a bare-variable slot received -the actual reference without changing its layout. Scalar recovery uses typed -box plans. Composite template fields require no outer erased recovery, and -function signature changes require adapters rather than reference casts. - -### Lowering conversion plans - -P9 lowers `Identity`, scalar boxing, scalar unboxing, reference erasure, and -reference recovery with the existing shape-checked operations. It lowers -`ProductMap` by `StructGet` on each source field, recursively converting the -fields, then `StructNew` on the target product. It lowers `ArrayMap` to MIR -control flow: read the source length, allocate a private destination with -defaultable storage, loop over elements, convert each element, and store it. -MIR includes an explicit `ArrayNewDefault` operation for this construction; -the verifier proves that the target element storage is defaultable and that -every slot is initialized before the array escapes. The Wasm encoder maps this -operation to `array.new_default` and emits the already-verified loop. - -P9 lowers a `FunctionAdapter` leaf to a direct call to its generated factory, -including inside an array loop or a product conversion. CC module verification -checks the factory's actual parameter and result against the plan's endpoints; -reachability includes the factory and its captured closure implementation. -P9 introduces no source-type reasoning or adapter-generation policy. - -Conversion helpers are interned by their complete source shape, target shape, -and nested conversion plan. This shares identical work without conflating -different type substitutions. The helper receives and returns the exact MIR -types in its key. A plan with an unsupported source or target fails in P8 with -a named diagnostic at the source operation; malformed internal plans are -rejected by the CC or MIR verifier as compiler errors. - -## Code map - -The design keeps normalization in P8, abstract conversion intent in CC, and -physical reconstruction in P9/MIR. Wasm encoding remains a mechanical mapping. - -```text -cc/ - layout/normalize.rs # typed Core shape normalization and canonical keys - convert.rs # target-neutral conversion plan construction - representation.rs # interned ReprId and ValueShape requirements -mir/ - layout/ # canonical and concrete GC layout planning - lower/aggregate.rs # ProductMap and ArrayMap helper/CFG lowering - lower/erased.rs # scalar boxes and checked erased-reference recovery - verify/conversion.rs # conversion-plan endpoints and initialization proof -wasm/lower/structure/ - arrays.rs # verified array.new_default and array operations -``` - -The entry points must preserve the IR boundary: - -```rust -fn normalize_template(core: &TypedCore, ty: TypeId, context: &TemplateContext) - -> Result; -fn normalize_actual(core: &TypedCore, ty: TypeId, substitution: &Substitution, - residual_template: &TemplateContext) - -> Result; -enum TypeRole { Template(TemplateContext), Actual(Substitution) } -fn plan_conversion(core: &TypedCore, source_type: TypeId, source_role: TypeRole, - destination_type: TypeId, destination_role: TypeRole, - substitution: &Substitution, - source_shape: ValueShape, destination_shape: ValueShape, - span: TextRange) -> Result; -fn lower_conversion(plan: &ValueConversion, value: ValueId, span: TextRange) - -> Result; -``` - -P8 owns type substitutions, source spans, and the proof that source and -destination types are compatible. CC carries only interned shapes and -target-neutral conversion plans. P9 resolves those shapes to `DefinedTypeId`s -and emits MIR instructions and CFG. MIR carries no Core `TypeId` or type -variable. The Wasm encoder receives verified MIR and chooses no layout. -`TemplateContext`, `Substitution`, and `TypeRole` are P8-only inputs; emitted CC -plans carry only `ValueShape`s, representation handles, and recovery evidence. - -## Invariants and verification - -- Every canonical key is deterministic and cycle-safe; `CoreTypeId` and - concrete type substitutions do not become runtime tags. -- Every value crossing a boundary has a conversion plan whose source and - destination match the typed Core types and CC shapes recorded at that - operation. -- A reference cast from `Erased` to an aggregate layout is allowed only when - the producing operation establishes that exact canonical or concrete - layout. It never converts between two different nominal aggregate layouts. -- Every `RecoverReference` includes P8 evidence. `TypeInstantiation` is emitted - only for a value whose typed source is an abstract variable instantiated at - that boundary. `ErasedVariantField` identifies a variant case field whose - stored shape is `Erased` and whose declared template normalizes to the target - shape. -- `ArrayMap` source and target handles resolve to array representations; its - nested plan matches their logical element conversion. The source is not - mutated, every target slot is initialized before exposure, and the result has - the target array shape. -- `ProductMap` source and target handles resolve to closed products with the - same field-label set. Corresponding field types agree under the boundary - substitution; field plans match labels and normalized shapes and the result - has the target product shape. -- Every ADT field's storage equals its normalized declared template. Bare - variables are `Erased`; composite references retain canonical aggregate or - closure shapes. Construction and projection convert between actual and - template shapes without changing the variant layout. -- Pure array and record updates produce fresh values. Conversion cannot mutate - its input or change any language-observable alias. -- CC verification rejects plans with unresolved handles, incompatible source - and destination shapes, wrong field arity, or an invalid nested conversion. - P9/MIR verification rejects mismatched nominal types, uninitialized - reference slots, invalid casts, and incorrect array element/index types. -- A source-level conversion that cannot be represented is a named, - source-spanned P8 diagnostic. A malformed internal plan is a compiler error; - it must never be emitted as a runtime cast that can trap on a well-typed - program. - -## Worked example - -Consider a parameterized constructor whose field is an array: - -```purescript -data Wrap a = Wrap (Array a) - -wrap :: Array Int -> Wrap Int -wrap xs = Wrap xs - -unwrap :: forall a. Wrap a -> Array a -unwrap (Wrap xs) = xs - -main :: Int -main = arrayIndex (unwrap (wrap [40, 42])) 1 -``` - -The concrete literal and `wrap` argument use `Array Int`, whose runtime layout -is `(array (mut i32))`. The declared field template is `Array a`, so its -canonical shape is `Array(Erased)`. At construction P8 records `ArrayMap` with -an `Integer -> Erased` element conversion; P9 allocates the canonical array, -boxes each integer, then stores the canonical array reference directly in the -ADT's canonical array field: - -```text -ArrayInt([40, 42]) - -> ArrayMap(BoxInteger) - -> ArrayErased([box(40), box(42)]) - -> Canonical array variant field -``` - -`unwrap` is compiled once. Its pattern projection directly reads the canonical -`Array(Erased)` layout established by the constructor path, and its generic -result uses that same layout. At the concrete call site, the result boundary -maps the canonical array back to `Array Int`, unboxing each element before -`arrayIndex` returns an `Int`. No runtime type tag is needed, and no nominal -array cast is used as a conversion. - -A closed generic record follows the same rule: - -```purescript -copy :: forall a. { values :: Array a } -> Array a -copy record = record.values - -main = arrayIndex (copy { values: [40, 42] }) 1 -``` - -The concrete record contains an `Array Int` reference. The generic record -layout contains a canonical `Array(Erased)` reference. The call boundary -converts the nested array and builds the canonical product. `copy` reads that -field using its canonical product layout and returns the canonical array. The -concrete caller converts the result back to `Array Int`. A generic update -builds a fresh canonical product and adapts the new `values` field by the same -rule; it does not mutate the input record. - -By contrast, for `data Hold a = Hold a`, `Hold (Array Int)` stores the concrete -array reference directly in the erased `a` slot. A polymorphic function may -return it as `a` without inspecting its shape; a concrete caller at -`Hold (Array Int)` recovers the same `Array Int` representation. This case -does not need an array map because the declared template is the bare variable -`a`, not `Array a`. - -## Boundaries and interfaces - -- **Typed Core to P8:** Core supplies the actual type substitution and source - span. P8 determines whether a boundary is identity, box/unbox, reference - erasure/recovery, or aggregate reconstruction. -- **P8 to CC:** CC receives target-neutral shapes and conversion plans. It does - not receive Core type variables, Wasm heap types, or physical field indices. -- **CC to P9/MIR:** P9 resolves canonical `ReprId`s to nominal layouts and - lowers `ArrayMap`/`ProductMap` to verified instructions and CFG. `ArrayNewDefault` - is legal only for defaultable storage and only while the result is private - until initialized. -- **MIR to Wasm:** the encoder emits GC operations and structured loops from - verified MIR; it performs no layout conversion itself. -- **Source modules:** linked Core modules share normalization and layout keys. - Independently compiled Wasm artifacts that must share nominal GC type - definitions need a separate artifact-linking design and are out of scope. -- **Failure:** unsupported open rows, unknown foreign generic aggregates, or - inconsistent source/target types produce source-spanned diagnostics before - Wasm emission. A Wasm allocation failure may still trap as specified by the - runtime. - -## Open questions and future work - -- Define a safe conversion protocol for open-row records if row-polymorphic - values become a backend feature. -- If pointer identity, mutation, or cyclic aggregate graphs become observable, - determine whether reconstruction must preserve sharing and cycles. -- Independent compiled Wasm module linking must define how canonical GC types - are shared or adapted; source-module linking through shared Core does not - answer that ABI question. - -## References - -- [DEC-07: Runtime Representation for Parameterized ADTs](../../../decision/DEC-07-runtime-representation-for-parameterized-adts.md). -- [Polymorphism and erasure](polymorphism-and-erasure.md). -- [Data representation](data-representation.md). -- [CC IR](cc-ir.md) and [MIR](mir.md). -- WebAssembly 3.0 specification: GC struct and array types, subtyping, - `array.new_default`, `array.get`, `array.set`, and reference casts. - -## Implementation notes - -The [implementation acceptance checklist](../../../implementation/backend/generic-aggregate-erasure.md) -records the GA-01 through GA-20 evidence and the independent review repairs. -P9 now interns complete conversion plans into shared MIR helpers, and MIR -verification rejects nullable array loads declared non-null and aggregate -conversion paths that can expose incompletely initialized arrays. Optimized -components execute the array, record, ADT, adapter, and capture cases under -required Wasmtime. Empty array backend coverage uses a Typed Core fixture; -source empty literals remain unsupported at P5. diff --git a/docs/design/backend/fp/intrinsic-implementations.md b/docs/design/backend/fp/intrinsic-implementations.md new file mode 100644 index 00000000..3985e16e --- /dev/null +++ b/docs/design/backend/fp/intrinsic-implementations.md @@ -0,0 +1,83 @@ +# Intrinsic semantic and target implementation contracts + +**Feature:** [F-02](../../../feature/F-02-portable-programs.md) + +**Related design:** [IR boundaries](../../D-01-frontend-and-ir-boundaries.md), +[scalars](scalars-and-primitives.md), and +[target linking](../wasm/linking-and-runtime.md). + +## Semantic identity + +HIR owns stable intrinsic identities, language schemes, arity, and conservative +semantic effects. Names describe language values: IntAdd, IntQuot, IntRem, +NumberAbs, and StringToBytes. Machine widths and signed opcodes belong to MIR, +where IntAdd selects I32Add and NumberAbs selects F64Abs. Renaming a language +operation must preserve its symbol ID. Retired IDs remain reserved and cannot +be reused or bootstrapped as callable operations. + +An intrinsic need not correspond to one machine opcode. Opaque representation +operations and correctly rounded numerical conversion are valid primitive +boundaries. Library algorithms and policies remain ordinary library functions. +Integer Euclidean div/mod, including negative-divisor and zero-divisor policy, +belong to psrs-stdlib over checked truncating IntQuot/IntRem. The former compiler +IntDiv/IntMod floor algorithms are retired, not aliases for the official API. +Their IDs are reserved. The official source functions and bindings remain intact. + +The vocabulary also includes values and compile-time operations. Checked +coercions and unsupported partial values must be classified explicitly; they +must not fall through to a scalar emitter or acquire a fabricated runtime body. + +## Target selection + +The backend owns one exhaustive implementation selection keyed by intrinsic +identity. A supported runtime operation selects one of: + +- Direct: a CC scalar operation selecting a MIR opcode or instruction sequence. +- Generated: a representation operation lowered to checked local IR, including + storage operations, codecs and explicit conversion plans. +- Artifact: a pinned executable export with a raw signature and value protocol. + +Compile-time operations and unsupported values have separate explicit cases. +HIR must not depend on the backend, runtime code, or raw target ABI. Runtime +metadata must not depend on compiler IR. The backend performs the conversion +between language contracts, CC shapes, MIR values and the runtime catalog. + +Adding a supported intrinsic requires an implementation case. Missing cases +must fail compilation or report unsupported behavior, never panic in a generic +scalar fallback. Operation effects have one semantic owner; optimization must +retain possible traps and mutations independently of the selected provider. + +## Artifact calls + +CC retains an intrinsic identity and checked arguments for an artifact call. +Before erasure, the verifier checks operand and result shapes against the +intrinsic's closed language scheme. MIR consumes the selected value protocol: +raw scalars, a borrowed canonical UTF-8 input buffer, or caller-owned bounded +UTF-8 output. Protocol metadata records capacity and normal-return release. +Existing reviewed codecs implement representation conversion; the linker does +not reconstruct language layouts. Unsupported schemes or protocols are errors. + +The import signature in actual MIR is the consumer's contract. Target planning +must compare it with the selected export ABI, then verify the export against +real artifact bytes. Setting both expected and provided signatures from the +catalog does not establish compatibility with the consumer. Arity, operand +width, result type, and GC references require rejection tests at this boundary. + +Generated bindings must likewise satisfy the signature of the generated body; +reserved symbols identify a provider, not evidence that an arbitrary signature +is correct. Wasm emission consumes the checked selection and memory plan. + +Artifact provenance, digest, initialization, private storage, memory ownership, +and reachable stack bounds remain governed by the target-linking design. A trap +aborts the command; successful raw calls release transient buffers and retain +no pointer. A direct implementation contributes no artifact dependency. + +## Acceptance + +Verify stable IDs and reserved slots, exhaustive target selection, checked +source shapes, malformed MIR imports, and actual export contracts. Execute +direct operations and artifact calls, including unsaturated source uses, +signed zeros, nonfinite values, retained strings and buffer reuse. Compare +public integer div/mod against pinned official JS on all sign combinations, +zero divisors and representable bounds. Unrepresentable quotient overflow +remains an explicit Wasm trap. Retired floor bindings must be unavailable. diff --git a/docs/design/backend/fp/mir.md b/docs/design/backend/fp/mir.md index 4a614cc3..a39d08a8 100644 --- a/docs/design/backend/fp/mir.md +++ b/docs/design/backend/fp/mir.md @@ -4,7 +4,7 @@ **Status:** Stable (design) **Prerequisites:** [functional core](../../frontend/semantics/functional-core.md), [CC IR](cc-ir.md), [data representation](data-representation.md), and -[generic aggregate erasure](generic-aggregate-erasure.md); +[representation and evidence](representation-and-evidence.md); the WebAssembly type system (GC structs and arrays, typed function references) and the basics of SSA form and dominators. Read [IR boundaries](../00-ir-boundaries.md) first. @@ -27,7 +27,7 @@ in [scalars and primitives](scalars-and-primitives.md); concrete GC layouts in [data representation](data-representation.md); erased values in [polymorphism and erasure](polymorphism-and-erasure.md); generic aggregate normalization and conversion in -[generic aggregate erasure](generic-aggregate-erasure.md). +[representation and evidence](representation-and-evidence.md). ## Background @@ -214,7 +214,7 @@ generic array layout, and other dependent arrays use an array of recursively normalized element shapes. Dependent closed records likewise use canonical product layouts. P9 converts between these nominal layouts by fresh allocation and recursive reconstruction, following -[generic aggregate erasure](generic-aggregate-erasure.md). +[representation and evidence](representation-and-evidence.md). ### Aggregate conversion lowering @@ -298,8 +298,8 @@ lower_if(cond, then_assignments, then_value, else_assignments, else_value): ``` A direct call becomes `Call`/`CallVoid`; a closure call becomes `ClosureCall`. -The scalar helpers of `scalar_helpers` are appended as extra functions when the -module uses floor division or modulo. +Artifact calls use the shared raw-scalar or UTF-8 transport protocol in +`lower/runtime_call.rs`. Public division/modulo remain ordinary library code. ### Dominance @@ -339,7 +339,7 @@ mir/ verify/ MIR verifier wit/ canonical ABI adaptation for WIT calls reachable.rs reachability of representation requirements - scalar_helpers.rs, numeric.rs scalar operations and helpers + numeric.rs concrete scalar operations ``` **Required types.** `mir/mod.rs` MUST define `Module`, `Function`, @@ -357,7 +357,7 @@ pub enum Terminator { Return, Jump, Branch, Switch, ReturnCall, ReturnCallRef } `mir/instruction.rs` MUST define `Instruction` so every variant carries a destination `ValueId`, its operand `ValueId`s, and a source span. The scalar -vocabularies live in `mir/numeric.rs` and `mir/scalar_helpers.rs` +vocabularies live in `mir/numeric.rs` ([scalars and primitives](scalars-and-primitives.md)); `mir/layout/` owns `PlannedLayout` and every concrete GC layout ([data representation](data-representation.md)); `mir/wit/` owns canonical ABI @@ -502,7 +502,7 @@ dominated by the block, since `B3`'s parameter is defined at its entry. meantime. - **Generic aggregate conversion.** Canonical generic arrays, closed records, and their explicit reconstruction helpers are specified in - [generic aggregate erasure](generic-aggregate-erasure.md). Open rows and + [representation and evidence](representation-and-evidence.md). Open rows and unknown foreign aggregate layouts require separate contracts. - **Optimization.** [MIR optimization](../opt/mir.md) specifies P10 passes. Scalar unboxing across call boundaries remains a P9 representation decision. diff --git a/docs/design/backend/fp/pattern-matching.md b/docs/design/backend/fp/pattern-matching.md index a9face55..cf2ec8c7 100644 --- a/docs/design/backend/fp/pattern-matching.md +++ b/docs/design/backend/fp/pattern-matching.md @@ -216,6 +216,16 @@ frontend diagnostics; the compiler never silently emits a partial decision. Coverage is checked where the pattern matrix is built, so a rejected program keeps its source span. +For an irrefutable query over a finite signature, check the default matrix first +when the matrix does not observe every constructor, then construct a finite +inhabitant of a missing constructor. A complete signature specializes every +constructor; if all missing constructors are uninhabited, specialization still +checks the observed constructors for uncovered fields. Witness construction tracks types +along the current field path, so recursive products cannot expand the query +indefinitely. A recursive type without a finite constructor inhabitant does not +justify an uncovered witness. Constructor enumeration order must not affect +termination or first-match behavior. + ### Rejected alternatives - **Ordered backtracking matcher (nested if-chain).** The current lowering @@ -305,8 +315,13 @@ useful(matrix, query): return useful(specialize(matrix, 0, exact_length(length(elements))), elements ++ query[1..]) Irrefutable: - if matrix has a full first-column signature S: - // Boolean and closed ADT signatures enumerate every head. + if column has a finite signature S: + if matrix does not observe all of S: + if not useful(default(matrix, 0), query[1..]): + return false + if some missing shape in S has a finite inhabitant: + return true + // Missing empty constructors cannot hide observed gaps. for shape in S: if useful(specialize(matrix, 0, shape), wildcards(field_count(shape)) ++ query[1..]): diff --git a/docs/design/backend/fp/polymorphism-and-erasure.md b/docs/design/backend/fp/polymorphism-and-erasure.md index 3a4000a3..54e33483 100644 --- a/docs/design/backend/fp/polymorphism-and-erasure.md +++ b/docs/design/backend/fp/polymorphism-and-erasure.md @@ -17,7 +17,7 @@ boxing, unboxing, casts, and generated function adapters. This document specifies that erased representation, the concrete-versus-erased boundary, and the adaptation operations. Generic arrays and closed records use the recursive layout and conversion rules in -[generic aggregate erasure](generic-aggregate-erasure.md). +[representation and evidence](representation-and-evidence.md). ## Scope @@ -25,7 +25,8 @@ This document owns the erased representation for polymorphic values, the distinction between concrete and erased representation requirements, the semantics of the adaptation operations, the generated function adapters used at higher-order boundaries, and the closure capture rules that follow from -erasure. It does not own concrete scalar and GC layouts (see +erasure. This includes applications of abstract constructors (`f a`) and +methods transported through ordinary class dictionaries. It does not own concrete scalar and GC layouts (see [data representation](data-representation.md)), the MIR type model and verifier (see [mir](mir.md)), type-class elaboration and dictionary construction (see [type classes and dictionaries](type-classes-and-dictionaries.md)), scalar @@ -73,7 +74,7 @@ code signature is the erased one. **Terminology.** An *erased value* is the value of an abstract type variable, whose runtime representation is the uniform reference. A type such as `Array a` contains a variable but is itself a generic aggregate; see -[generic aggregate erasure](generic-aggregate-erasure.md). *Boxing* +[representation and evidence](representation-and-evidence.md). *Boxing* allocates a wrapper for a scalar so it can inhabit the erased representation; *unboxing* projects it back. A *concrete* value is one whose source type is variable-free and therefore has a specialized representation. An *adapter* is a @@ -101,7 +102,7 @@ containing a type variable does not by itself make an entire aggregate value an fields, and function signatures. In particular, `Array a` uses the canonical generic array shape and a dependent closed record uses a canonical product; their element or field values may use `Erased`. See -[generic aggregate erasure](generic-aggregate-erasure.md) for the normalization +[representation and evidence](representation-and-evidence.md) for the normalization and layout conversions. A function value uses `Closure(SignatureId)`, whose `Signature` records the normalized `ValueShape` for each parameter and result. @@ -174,6 +175,32 @@ same kind of boundary: `Effect (a -> b)` lowers to a closure that takes the runtime token and returns a function, and flattening that function into the effect closure is forbidden ([effects](effects.md)). +### The erased instance + +The shared representation model, the definition of a representation policy, its +ownership, and the one conversion planner are in +[representation and evidence](representation-and-evidence.md). This document +owns the erased instance of that model: a bare type variable has representation +`Erased`, and the policy its producer recorded is one of `Boxed`, `Nominal`, +`AggregateLayout`, or `Callable`. + +Three consequences are specific to erasure: + +- **A bare variable transports an existing object.** Its recovery policy is the + one established when the value entered the slot. +- **An abstract constructor hides its constructor.** A generic method may + produce a new value of `f a` (including `f Unit`), so recovery uses the + constructor's policy, shared by the method and its generic consumers, never a + guess from the consumer's concrete type. +- **An aggregate containing a variable is not itself `Erased`.** `Array a` and a + dependent closed record keep a canonical aggregate layout; only their bare + variable slots are erased + ([representation and evidence](representation-and-evidence.md)). + +The boundary, evidence, and `plan`/`emit` definitions and the edge cases are +specified once in [representation and evidence](representation-and-evidence.md) +and are not restated here. + ### Erased values and boxes The erased representation is `eqref`, a non-null reference. Concrete values @@ -198,8 +225,9 @@ an `eqref` cast alone is not that conversion. turning a string into an integer. The i31 shorthand is used only for closure *captures*, not for the general erased protocol (see [data representation](data-representation.md)). -The empty-erasure case (an erased value used where an erased value is expected) -is an identity, so nested polymorphic boundaries add no work. +The empty-erasure case is an identity when both endpoints share the same +representation policy. Equal `Erased` shapes alone do not prove that two hidden +closure or aggregate policies agree. ### Dictionaries @@ -236,7 +264,7 @@ type stay specialized. Aggregate types are normalized recursively, so a generic function's `a` parameter uses `Erased`, while its `Array a` parameter uses the canonical generic array. A call site with a concrete instantiation converts between that canonical shape and its specialized shape as specified -in [generic aggregate erasure](generic-aggregate-erasure.md). There is exactly +in [representation and evidence](representation-and-evidence.md). There is exactly one erased representation for abstract values, and it carries no source type identity. @@ -330,36 +358,50 @@ normalize(ty, substitution): ``` The array and record cases are defined by -[generic aggregate erasure](generic-aggregate-erasure.md), including their +[representation and evidence](representation-and-evidence.md), including their conversion plans. An application headed by an abstract constructor, such as `f a`, has no known aggregate layout and uses the erased value protocol; known constructors such as `Array a` still retain their canonical layouts. A type variable nested in an ADT field continues to follow [DEC-07](../../../decision/DEC-07-runtime-representation-for-parameterized-adts.md). +### Planning is shared + +Conversion planning is defined once in +[representation and evidence](representation-and-evidence.md#algorithms) and is +not restated here. Every boundary in this topic — a direct or indirect call, a +partial application, a returned function, a dictionary field, an aggregate +element, or a lifted capture — is an instance of that one planner. Emission +consumes the completed plan: it does not rerun source matching for +representation-only adapter types, search a module for a plausible signature, +or recover directly to the desired consumer signature. + ### Boxing and unboxing ```text -adapt(value, source_type, destination_type): - if destination_type is a bare type variable: +adapt(value, checked_boundary, producer_policy, consumer_requirement): + validate the checked plan and its endpoint contracts + if consumer is a bare type-variable slot: scalar -> allocate its existing erased box reference -> RepresentationCast(value, Erased) - else if source_type and destination_type are aggregate types: - AggregateConvert(value, plan_for(source_type, destination_type)) - else if source_type is erased and destination_type is a concrete scalar: - project the typed box and unbox - else if source and destination reference shapes agree: - use identity or the compatible erased reference cast + else if producer and consumer policies require aggregate conversion: + AggregateConvert(value, checked recursive plan) + else if value is erased: + recover the box, layout or callable signature established by storage + apply the remaining plan to reach the consumer requirement + else if callable signatures differ: + generate the checked function adapter + else if physical shapes and representation policies agree: + Identity otherwise: report a source-spanned unsupported conversion ``` -Supplying a value to a bare erased parameter may erase an aggregate reference -without copying it, as in `Hold a`. Supplying it to a generic aggregate such -as `Array a` may need `AggregateConvert` first. Recovery from an erased ADT -field likewise depends on the declared field template: `a` can recover a -concrete reference directly, while `Array a` first recovers its canonical -array and then maps to a concrete array when required. Aggregate conversions +Supplying arrays and ordinary closed records to a bare erased parameter uses +their owner's canonical element or field protocol. Generic aggregate parameters +and erased ADT fields use the same recursive conversions. Recovery reads the +canonical storage before constructing the consumer's concrete layout. Nominal +references retain the identity policy established by their producer. Aggregate conversions are never implemented as `RepresentationCast`s between distinct nominal layouts. @@ -375,26 +417,26 @@ payload. Unwrapping a newtype never allocates a separate wrapper object. ### Adapter generation ```text -adapt(value, concrete_type, generic_type): - source = function_signature(concrete_type) - target = function_signature(generic_type) +adapt(value, checked_callable_plan): + source = checked_callable_plan.producer_signature + target = checked_callable_plan.consumer_signature require source and target have equal arity adapter: captured = ClosureGetCapture(closure = adapter_closure, index = 0) - concrete = RepresentationCast(captured, Closure(signature(concrete_type))) - args' = for each (arg, source_param, target_param): - source erased and target concrete -> box(arg) - source concrete and target erased -> unbox(arg, source_param) - otherwise -> arg - result = IndirectCall(concrete, signature(concrete_type), args') - return target erased and source concrete -> box(result) - target concrete and source erased -> unbox(result, target) - otherwise -> result - - emit FunctionRef(adapter, signature(generic_type), captures = [value]) + producer = RepresentationCast(captured, Closure(source)) + args' = emit each checked target-parameter -> source-parameter plan + result = IndirectCall(producer, source, args') + return emit the checked source-result -> target-result plan + + emit FunctionRef(adapter, target, captures = [value]) ``` +This is the equal-arity branch. Curried and eta-expanded adapters segment the +call at quantifier and representation-closure boundaries and use the same +checked plans for each segment; they do not flatten a returned closure into +the producer's own parameters. + The original `value` is named once and captured; the adapter body is verified against the CC signatures and representations exactly like a source function before it is added to the module. @@ -434,6 +476,13 @@ The erased requirement itself is produced by the CC representation model as non-null `eqref` reference (`RefType { nullable: false, heap: Eq }`). No module may attach a runtime type tag to an erased value. +The source Core checking owner supplies scoped instantiation and compatibility +evidence. P8 representation lowering supplies source-to-physical mappings and +storage protocols. `cc/lower/conversion/` combines those inputs into the common +plan; `cc/lower/erased/` emits its callable leaves. Extracting evidence must +preserve the source checker's acceptance contract. Missing evidence is a +reported limitation, not a reason to silently strengthen or weaken subsumption. + **Required types and helpers.** CC owns representation-directed conversion and adapter generation. Its callable-shape and adapter entry points are: @@ -474,11 +523,14 @@ signatures map to one `SignatureId` and one MIR func type, so one The CC verifier: +- validates stored representation policies and checked conversion-plan + endpoints before semantic evidence is discharged; equal erased shapes do + not authorize recovery to an arbitrary signature; - accepts `RepresentationTest`/`RepresentationCast` only when the source value is erased or the destination requirement is erased (`cc/verify/adaptation.rs`); - verifies `AggregateConvert` endpoints and nested plans as specified by - [generic aggregate erasure](generic-aggregate-erasure.md); and + [representation and evidence](representation-and-evidence.md); and - checks that direct-call arguments and results exactly match the callee `Signature`, closure-call arguments match the closure `SignatureId`, and capture count, order, and representations match the lifted function; and @@ -497,6 +549,13 @@ The MIR verifier then checks the concrete side: Failure is a compiler bug or an unsupported program, reported with the operation's source span. +If P8 uses an internal Core-shaped materialization, it is a distinct lowered +representation with a complete structural verifier. Checking source Core and +selected rewritten types alone does not verify its expressions, generated +declarations, suspension wrappers or entry adapter. Removing source verification +from a physical view requires replacing it with these target contracts; it +does not remove the verification obligation. + ## Worked example ### Polymorphic identity @@ -555,13 +614,28 @@ adapter is invoked, and each adapter call unboxes its argument exactly once. reach the ABI boundary; ABI adaptation happens on concrete canonical signatures only. +### Closed local row instantiations + +Before CC layout, checked nonrecursive local lambdas with row quantifiers may +be expanded at finite closed uses. The Core checking relation owns each row +solution, including residual fields with no existing type-arena node. Explicit +materialization appends row nodes without changing the source arena's existing +identities; capture-avoiding substitution discharges the scheme's quantifiers +and freshens local identities. This preparation is independent of optimization +budgets and preserves evaluation of captured computations. + +An unresolved use, recursive binding, or scheme with additional unresolved +quantifiers retains its full source scheme. Open runtime rows remain an explicit +unsupported layout; this preparation does not define an open-row ABI or narrow +the source model. Input and output Core verification remain mandatory. + ## Open questions and future work - **Dictionaries end to end.** Type-class elaboration must produce the dictionary values this design assumes and feed them through the normal aggregate path. - **Open-row aggregates.** Canonical generic arrays and closed records use the - conversion contract in [generic aggregate erasure](generic-aggregate-erasure.md). + conversion contract in [representation and evidence](representation-and-evidence.md). Open-row records still need a separate representation and conversion contract. - **Higher-order acceptance breadth.** Direct generic calls, concrete arguments to generic parameters, and returned polymorphic functions are the remaining @@ -586,6 +660,15 @@ conversion plans, including recursive function-adapter leaves. The distinguishes source programs from verified Typed Core backend fixtures and records source coverage and remaining obligations. +Checked instantiation evidence and each constructor's representation policy +travel to P8 through the Core-to-CC side table +(`psrs-backend/src/boundary.rs`), beside CC the way `ExternalBindings` carries +the WIT boundary. The `RepresentationRegistry` registers `Function` (its checked +instantiation arguments are the fixed protocol parameters) and the trusted +`Effect` (its runtime token); an unregistered constructor is reported rather +than resolved by arity or a signature search, and the payload-erased protocol +signature is interned once per registered callable constructor. + ## References - Reynolds, *Types, Abstraction and Parametric Polymorphism* (1983). @@ -594,6 +677,12 @@ records source coverage and remaining obligations. - Wadler and Blott, *How to Make ad-hoc Polymorphism Less ad hoc* (1989). - Peyton Jones, Jones, and Meijer, *Type Classes: an exploration of the design space* (1997). +- Eisenberg and Peyton Jones, *Levity Polymorphism* (2017); GHC's + `RuntimeRep` and `Any`, and `unsafeCoerce#` as the representation coercion. +- Swift's function calling conventions (`@convention(thin)`/`thick`) and + *reabstraction thunks*; witness tables for dictionary evidence. +- Java type erasure and *bridge methods* (JLS/JVMS), as the erased-call + compatibility analogue. - WebAssembly 3.0: garbage collection and typed function references. - [DEC-07](../../../decision/DEC-07-runtime-representation-for-parameterized-adts.md), [CC IR](cc-ir.md), diff --git a/docs/design/backend/fp/representation-and-evidence.md b/docs/design/backend/fp/representation-and-evidence.md new file mode 100644 index 00000000..c96c9912 --- /dev/null +++ b/docs/design/backend/fp/representation-and-evidence.md @@ -0,0 +1,594 @@ +# Runtime Representation and Checked Boundaries + +**Feature:** F-02 +**Status:** Stable (design) +**Prerequisites:** [functional core](../../frontend/semantics/functional-core.md), +[classes and evidence](../../frontend/type-system/classes-and-evidence.md), +[CC IR](cc-ir.md), [MIR](mir.md); type-erasure semantics and dictionary +passing. Read [IR boundaries](../00-ir-boundaries.md) first. +**Summary:** A source type does not determine a runtime representation. One +representation model and one checked-boundary contract govern every place the +backend changes or recovers a representation: scalar boxing and unboxing under +erasure, reference cast and recovery, canonical aggregate conversion, callable +adaptation, dictionary projection, effect token threading, and coercions. +Every change is a **conversion plan** built from checked **evidence** and the +producer's **representation policy**, then emitted as explicit CC operations. +A `RuntimeRep` and its target `Layout` are the two levels of one model, joined +by a total mapping per target. This document owns the target-neutral `RuntimeRep` +model and the layout-mapping contract; each topic document owns the concrete +operations that instantiate it, and each target document owns its `Layout`. + +## Scope + +This document owns the shared representation model (`RuntimeRep`, +`RepresentationPolicy`), the checked-boundary contract (`Boundary`, `Evidence`, +`ConversionPlan`), the ownership rules for who produces and who consumes them, +generic aggregate normalization and the aggregate conversion plan, and the +single conversion-planning algorithm. It also defines the layout obligations a +conversion depends on; it does not own the concrete layout that realizes a +representation. It exists so that no feature introduces a private rule for "how +this value is adapted". + +It does not own the concrete Wasm GC layouts (see +[data representation](data-representation.md)); the CC requirement table and +verifier (see [CC IR](cc-ir.md)); the erased protocol for bare variables and +abstract constructors (see [polymorphism and erasure](polymorphism-and-erasure.md)); +class validation, +entailment, functional dependencies, and evidence elaboration (see +[classes and evidence](../../frontend/type-system/classes-and-evidence.md)) or +its runtime product form (see +[type classes and dictionaries](type-classes-and-dictionaries.md)); effect +semantics (see [effects](effects.md)); or scalar operator semantics (see +[scalars and primitives](scalars-and-primitives.md)). + +## Background + +**A `RuntimeRep` is not a function of the source type alone.** The source type +is representation-polymorphic wherever a type variable, an abstract +constructor, a class dictionary, an effect, or a newtype is involved. A +`forall a. a -> a` applied to an `Int`, a value of type `f a` whose `f` is +`Effect`, and a `newtype` over `Int` all have a source type that cannot by +itself say how the value is laid out at a given boundary. + +**Industry practice names the pieces separately.** GHC represents a value of +kind `TYPE r` through its `RuntimeRep` `r`; a coercion site carries that +representation, `Any` is the uniform boxed inhabitant, dictionaries are +ordinary values, `IO` is a state token, and `unsafeCoerce#` is the primitive +representation coercion. Swift gives a function value a calling convention and +generates a *reabstraction thunk* when the convention changes, with a witness +table for dictionaries. Java erases generic type arguments and restores call +compatibility with bridge methods. Koka elaborates type-class and effect +operations to explicit *evidence*. These are the same idea: choose a +representation when a value is produced, keep the choice as compiler evidence, +and adapt explicitly where the requirement changes. + +**This repository already has the two halves and must join them.** The frontend +produces checked evidence +([classes and evidence](../../frontend/type-system/classes-and-evidence.md), +[type inference](../../frontend/type-system/type-inference.md)); the backend +owns representation ([CC IR](cc-ir.md), [MIR](mir.md)). What was missing is a +single contract for how the two meet, which is why per-feature rules appeared. + +## Model + +```text +RuntimeRep = -- the target-neutral runtime representation + Scalar(Int | Number | Boolean | Char | Unit) + | Box(Scalar) -- one-field struct that lets a scalar inhabit Erased + | Reference(NominalRef) -- GC struct/array of a known nominal shape + | Aggregate(CanonicalKey) -- canonical array/product layout + | Closure(Signature) -- callable value: code ref + capture array + | Erased -- the uniform non-null anyref slot + +RepresentationPolicy = -- the RuntimeRep a producer chose for an erased value + Boxed(Scalar) + | Nominal(NominalRef) + | AggregateLayout(CanonicalKey) + | Callable(Signature) -- includes Function, dictionary methods, effects + +Boundary = Call | Return | Field | Element | Capture | Slot +Evidence = Instantiation { scheme, use, substitutions } + | Subsumption { actual, expected } + | Dictionary { selected_instance | given } + | Coercion { proof } + | EffectPlan { import, payload } +ConversionPlan = { boundary, producer, consumer, operations } +Adaptation = Identity | Box | Unbox | Cast | AggregateConvert | Thunk | Coerce + +-- Layout is the target-specific realization of a RuntimeRep. One total mapping +-- per target; a RuntimeRep used by a target must have a layout. +HeapLayout = Wasm GC heap layout -- the language heap, owned by P9 +AbiLayout = Canonical ABI byte layout -- the boundary, owned by the ABI docs +heap_layout : RuntimeRep -> HeapLayout +abi_layout : RuntimeRep -> AbiLayout +``` + +`RuntimeRep` is this compiler's analogue of GHC's `RuntimeRep`. GHC's is a kind +because it has representation polymorphism (`TYPE r`) in the source; this +compiler has none, so `RuntimeRep` is an internal descriptor used by lowering, +not a source or type-system kind. It is target-neutral; a `Layout` is its +target-specific realization. They are two levels of one model, joined by a +total mapping per target. + +`Erased` is a *representation*, not a loss of information: it is the uniform +anyref slot. The information the producer had before the value entered that slot +is its `RepresentationPolicy`, carried as evidence. Recovery is valid only +against the policy that stored the value; the consumer's concrete type does not +establish it. + +## Design + +### Four facts, four owners + +1. **Source relation.** Whether a use is a legal instantiation of a scheme, a + subsumption, a selected dictionary, a `Coercible` proof, or a classified + effectful import. Owned by checking (P5/P6); it is `Evidence`. +2. **Producer policy.** The representation the value's producer chose. Owned by + the definition and, for a constructor, by that constructor's **representation + owner**. It is fixed wherever the value is produced, not at its use. +3. **Consumer representation.** The representation the use requires. Owned by + the checked type at the boundary. +4. **Layout.** The target-specific realization of a `RuntimeRep`, produced + by the one representation-to-layout mapping. Owned by P9 + ([MIR](mir.md), [data representation](data-representation.md)). + +No stage may substitute for a fact it does not own. In particular, a consumer +never infers the producer policy from its own type, and a representation owner +never inspects a consumer. + +### Representation owners + +A **representation owner** maps a source type constructor plus its checked +instantiation arguments to a `RepresentationPolicy`. The owners form one +registry, not a set of private special cases: + +| Constructor | Policy | +| --- | --- | +| `Function` | `Callable(arrow parameters, result)` | +| `Array` | `AggregateLayout(canonical array key)` | +| a data type | `Nominal(variant)` with declared field templates | +| a newtype | the policy of its declared field template | +| the trusted `Effect` | `Callable([state token], payload)` | +| a bare type variable | `Erased`; the value keeps whatever policy it entered with | + +The registry belongs to the Core-to-CC boundary (below). CC itself does not know +a source constructor, key behavior on a HIR identity, or derive a protocol by +searching signatures. + +### Generic aggregate RuntimeReps + +`Array` and a closed record are representation owners too, but a type argument +can appear inside them. Their `RuntimeRep` is **canonical**: it does not depend +on the instantiation of that argument. A bare variable `a` is `Erased`; an +application `f a` headed by an unresolved variable is `Erased`, because its +storage constructor is unknown. + +| Source type | Canonical `RuntimeRep` | +| --- | --- | +| `a` | `Erased` | +| `f a` (head a variable) | `Erased` | +| concrete `Array Int` | the specialized `Aggregate([Integer])` | +| `Array a` | `Aggregate([Erased])`, the canonical generic array | +| `Array (Array a)` | an aggregate whose element references the canonical `Array a` | +| concrete `{ x :: Int }` | the specialized `Aggregate` product | +| `{ x :: a }` | a canonical product with an `Erased` field | +| a parameterized data type | one nominal variant, each field in its declared template's normalized shape | + +Normalization is structural and cycle-safe, and distinguishes a declaration +**template** (its bound variables abstract) from an **actual** type (the +boundary substitution applied). A closed actual type is specialized; a variable +the substitution does not resolve stays abstract and uses the template rule. +Canonical keys are: arrays by normalized element shape, closed records by sorted +`(label, normalized field shape)` pairs, and variants by declaration identity. +Equal keys intern to one representation; P9 assigns one `DefinedTypeId` per +reachable representation and does not merge by physical shape. This preserves +[DEC-07](../../../decision/DEC-07-runtime-representation-for-parameterized-adts.md)'s +single erased layout for a parameterized data type. Open rows have no canonical +product and remain unsupported. + +### Aggregates in bare polymorphic slots + +The Array owner normalizes an array entering a bare variable or abstract Array +constructor slot to an array of non-null erased elements. The closed-record +owner normalizes a record to a product with the same ordered logical labels +and one non-null erased field per label. These protocols apply recursively to +nested arrays, records, and callable fields. Recovery first reads the owner's +canonical storage, then converts each element or field to the checked use +representation. A direct cast to the consumer's specialized aggregate layout +cannot establish this contract. + +Canonical protocols are registered by the layout owner for the aggregate +layouts a module contains before conversion planning; a module with no array or +record layout gains no protocol representation. Arrays and ordinary records have +no observable identity, so these conversions may construct new aggregates. +Mutable cells and other nominal references keep their owner's identity protocol; +they are not reconstructed as records. Checked storage primitives consume their +checked use representations before ABI erasure, preserving writes to the owning +storage object. + +Canonical ABI adapters follow the same owner protocols for aggregates in +bare erased fields. Decoding first constructs the checked concrete value, then +normalizes it before storing the field; parameter lowering recovers the checked +concrete value before flattening it. CC conversion and ABI lowering share the +recursive storage conversion planner. Layout reachability traverses these ABI +conversion plans before assigning target types, retaining their protocol +representations and scalar boxes. Missing owner protocols are errors. + +### Bare polymorphic function slots + +A bare type variable stores functions using one registered unary protocol: +`Erased -> Erased`. Erasing a concrete flattened function builds one segment +per parameter. Each segment captures the producer and the already recovered +arguments; the final segment calls the producer and erases its result. A +function result is recursively placed in the same protocol. Recovery builds +an adapter that supplies the consumer's arguments to those segments and +recovers its result. This permits an instantiation to change flattened arity +without treating a cast as a calling-convention conversion. + +This protocol belongs to bare polymorphic value storage. Abstract constructor +transport retains the constructor owner's registered fixed parameters and +payload protocol, including the Function domain and Effect state token. +Variant-field recovery must use the bare-slot adapter for callable targets; +the reference-only recovery shortcut cannot cast an erased function straight +to the consumer signature. Generated adapter parameters precede all local +values and each generated function passes the CC verifier. + +### Aggregate conversion + +Canonical products and callable signatures are normalized together to a fixed +point: record fields may contain closures and closure signatures may contain +record layouts. Every iteration remaps aggregate handles in representations, +layouts, and signatures before signature interning. Convergence is bounded by +the representation and signature graph; failure to converge is invalid IR. +A single normalization pass cannot publish canonical identities for this graph. + +When two normalized shapes meet at a typed boundary, the planner builds one +`ConversionPlan`; the concrete layouts it produces are owned by the target +(see [data representation](data-representation.md)). The aggregate leaves are a +`ProductMap` (field-wise reconstruction) and an `ArrayMap` (element-wise +reconstruction), each recursing into its fields or elements. Reconstruction +allocates a fresh destination and converts each field or element: it never +mutates the source and never uses a nominal `ref.cast` between two different +aggregate layouts. A reference `Cast` recovers an object only to the layout its +producer established. + +Conversions are inserted at every boundary where the actual and required shapes +differ: parameterized ADT construction and projection, record construction, +access and update, array construction, read and update, direct-call arguments +and returns, higher-order adapters, and captures. Linked source modules are +merged into one Core program before P8, so they share canonical keys and need no +module ABI adapter. Because arrays and records have no observable identity or +mutation, a conversion may duplicate physical sharing while preserving values; +if the language later exposes pointer identity, mutation, or cyclic aggregates, +the plan must add an identity-preserving memo before those features are enabled. + +### One model, two levels: RuntimeRep and Layout + +This is one model, not two. It has a **target-neutral level** — the `RuntimeRep` +a value has at a boundary, and the conversion plan between two `RuntimeRep`s — +and a **target-specific level** — the `Layout` that realizes a `RuntimeRep` on a +target. The levels are joined by a **total mapping per target**: every +`RuntimeRep` a target uses has exactly one layout. P9 owns the Wasm GC +`heap_layout`; the ABI documents own the canonical `abi_layout`. A complete +functional backend is the target-neutral model plus a total, verified mapping to +each target it emits. + +A `RuntimeRep` classifies how a value is used and converted at a boundary; a +`Layout` fixes the concrete types and bytes that realize it. The target has +exactly two layout domains: + +- the **language heap** is Wasm GC. Its concrete types, field offsets and + mutability, tag encoding, box structs, the closure struct and its capture + array, and the defined-type table are planned by P9 and fixed in + [MIR](mir.md) and [data representation](data-representation.md). This is + **MIR layout**: the `ValueType`/`RefType`/`DefinedType` table and the GC + layouts built from CC requirements. +- the **Canonical ABI boundary** is linear memory. Its byte layout — computed + offsets, alignment and padding, `(pointer, length)` pairs, the scratch return + area, and `cabi_realloc` — is owned by + [canonical ABI and WIT](../wasm/canonical-abi-and-wit.md) and + [linear memory and the canonical ABI boundary](../wasm/linear-memory-and-canonical-abi-boundary.md). + +A concrete value converted for a WIT call is an instance of this model at the +ABI boundary: the consumer representation is the canonical exchange shape and +the adaptation is the canonical conversion; the ABI documents own the bytes. +Erased values and generic aggregates never cross that boundary — they are +recovered to a concrete representation first. + +The layout obligations the model relies on are what let it plan a conversion +without owning a layout: + +- the same requirements name the same runtime shape: representation is + deterministic and independent of source identity; +- aggregates are canonically keyed, so structurally equal aggregates share one + representation while physically equal but nominally distinct layouts stay + distinct (see [Generic aggregate RuntimeReps](#generic-aggregate-runtime-reps)); +- a `Box` is one field, and `Erased` is a non-null, untagged reference; +- a `Closure` is `{ funref, capture-array }` with one uniform nullable-reference + capture array, so its type does not depend on its captures; and +- a variant is one representation per source sum, with stable tags. + +Offsets, type indices, sizes, alignments, and addresses are layouts, not +representations, and never appear in the model. + +### One planner + +Every boundary uses one planning operation and one emission step; the model is +defined here and its algorithm is specified under [Algorithms](#algorithms). +Planning chooses an adaptation from the checked relation and the two +representations; it never searches for one. + +### Everything is an instance + +The contract is one, and each functional topic is one instance of it: + +- **Erasure** ([polymorphism and erasure](polymorphism-and-erasure.md)): bare + variables are `Erased`; a policy is `Boxed`, `Nominal`, `AggregateLayout`, or + `Callable`. Box, Unbox, Cast, and Thunk implement it. +- **Aggregates** (see [Generic aggregate RuntimeReps](#generic-aggregate-runtime-reps)): + a canonical array or product; a changed shape is an `AggregateConvert` with a + recursive `ArrayMap`/`ProductMap` plan. +- **Callables** ([CC IR](cc-ir.md)): a `Closure(Signature)`; a changed + signature is a `Thunk`. This is Swift's reabstraction thunk. +- **Dictionaries** ([type classes and dictionaries](type-classes-and-dictionaries.md)): + a `Nominal` product of method closures; method use is `ProductGet`, and the + `Dictionary` evidence names the selected instance. There is no dictionary + adaptation because the dictionary is already an ordinary value. +- **Effects** ([effects](effects.md)): the `Effect` representation owner + supplies `Callable([state token], payload)`; `pure`, `bind`, `runEffect`, and + `trap` are its operations. There is no Effect-specific conversion path. +- **Coercion and newtypes** ([roles and coercions](../../../implementation/frontend/roles-and-coercions.md)): + a `Coercion` proof justifies `Identity` or a newtype's field policy. The + checked coercion is the intrinsic `Safe.Coerce.coerce`; the unchecked cast is + the primitive `Unsafe.Coerce.unsafeCoerce`, the `unsafeCoerce#` analogue. +- **Partial application** ([CC IR](cc-ir.md#partial-application-and-erased-adapters)): + a `Callable` value applied to fewer arguments produces a new `Callable` value + under the same policy; it is the standard partial-application closure. +- **Canonical ABI** ([canonical ABI and WIT](../wasm/canonical-abi-and-wit.md), + [linear memory](../wasm/linear-memory-and-canonical-abi-boundary.md)): a + concrete value converted for a WIT call takes the canonical exchange + representation; the ABI documents own its byte layout. + +### Evidence and policies travel as a side table + +Checked evidence and representation policies reach P8 through an explicit +Core-to-CC **side table**, the same boundary discipline +[`ExternalBindings`](../00-ir-boundaries.md) uses for WIT bindings. They are not +read ambiently from the Core type arena, and they are not keyed by a HIR +identity inside CC. + +- **Produced by** checking and the representation owners. +- **Consumed by** P8's planner, which emits explicit operations and signatures. +- **Never reconstructed** from a consumer type, a declaration arity, or a search + over signatures. Missing evidence is a reported unsupported boundary. + +### What this forbids + +- runtime type tags on erased values (dictionary and dictionary-selection + evidence are compile-time); +- a dedicated IR node, closure kind, or runtime object per feature + (dictionaries and effects are ordinary products and closures); +- two mechanisms for the same constructor (for example a token table *and* a + signature-prefix derivation for callables); +- deriving a producer policy from arity or from the consumer's concrete type; +- using a HIR/Core identity as a semantic key inside CC; and +- treating representation equality as nominal identity — structurally equal + signatures share one `SignatureId`. + +## Algorithms + +### Planning and emission + +```text +plan(evidence, producer_policy, consumer_representation): + validate boundary ownership, scope, direction and endpoint positions + if producer_policy and consumer_representation agree: + Identity + else: + recursively plan scalar, reference, callable and aggregate conversions + reject a recovery whose producer policy cannot be established + +emit(plan, value): + emit the plan's exact source/target representations and signatures + verify generated adapter bodies, captures and calls +``` + +### Choosing the adaptation + +```text +adapt(value, plan): + bare variable slot, scalar -> Box + bare variable slot, reference -> Cast(Erased) + aggregate layouts differ -> AggregateConvert(plan) + value is Erased -> recover the box, layout or callable + signature the policy names, then adapt + callable signatures differ -> Thunk(plan) -- reabstraction + representation-preserving proof -> Identity | Coerce + otherwise -> source-spanned unsupported boundary +``` + +### Aggregate conversion plans + +```text +convert(source, destination): + normalize both with the boundary's template/actual role + require the checked boundary to prove them compatible + if shapes agree: Identity + if the destination is a bare variable: Box a scalar, or Cast a reference + if the source is a bare variable: Unbox a scalar, or recover the reference + if both are arrays with corresponding elements: + ArrayMap(source, target, convert(elements)) + if both are closed records with the same labels: + ProductMap(source, target, convert(each field in canonical order)) + otherwise: report an unsupported aggregate conversion at the source span +``` + +The plan is target-neutral and carries no Wasm type, offset, or unverified +cast. The concrete allocation, loop, and initialization it lowers to are owned +by [data representation](data-representation.md) and [MIR](mir.md). + +### Edge cases + +- **Direction.** A parameter position is contravariant: the producer plan and + the consumer plan reverse relative to a result position. Evidence preserves + direction; it is never reused for the mirrored pair. +- **Nested boundaries.** A plan inside an `ArrayMap` or `ProductMap` retains its + element or field position and its own scoped evidence. +- **Fixed payloads.** `f Unit` does not make the abstract constructor concrete; + the producer policy of the value returned through the generic method still + governs recovery. +- **Missing evidence.** An abstract boundary without a producer policy is + reported with its source span, never resolved by arity or by a signature + search. + +## Code map + +This document is the shared contract; it has no runtime code of its own. The +implementation target is split across the topic owners: + +```text +cc/lower/conversion/ the planner and emission: scalar, reference, callable and + aggregate leaves, and the generated adapters +cc/payload.rs bare-slot storage plans shared by CC and canonical ABI +mir/reachable/ projection and storage-plan layout requirements +cc/layout/ constructor policies for the local constructors + (Function, Array, data, newtype) and signature interning +cc/verify/ conversion-plan endpoint, capture and call-signature checks +mir/layout/ the physical layout each RuntimeRep maps to +mir/lower/ lowering of the emitted adaptations +abi/ and wit/ canonical-ABI adaptation on concrete signatures only +``` + +The Core-to-CC **side table** that carries `Evidence` and `RepresentationPolicy` +is a boundary input, carried beside CC like `ExternalBindings`; it is not a +field of the CC model and never appears in an emitted operation. The frontend +owners of `Evidence` are +[classes and evidence](../../frontend/type-system/classes-and-evidence.md) and +[type inference](../../frontend/type-system/type-inference.md). + +## Invariants and verification + +- Every value has exactly one `RuntimeRep` at its definition and at each + boundary; the planner is the only place a representation changes. +- An `Erased` value is recovered only to a policy recorded by its producer. +- Every adaptation is justified by `Evidence` and checked at both endpoints. +- A representation owner maps a constructor to exactly one policy; there is no + fallback obtained by searching signatures or by arity. +- CC and MIR contain no target type above P9 and no runtime type tag anywhere. +- The verifiers of [CC IR](cc-ir.md) and [MIR](mir.md) check the emitted + operations; the boundary side table is validated in both directions, as + `ExternalBindings` is. + +## Worked examples + +### A bare variable at a concrete use + +`identity :: forall a. a -> a` used at `Int`. Evidence is the instantiation +`a := Int`; the producer policy at `Int` is `Boxed(Int)`. The plan boxes the +argument, `Cast`s to `Erased`, calls the `Callable([Erased], Erased)` body, then +casts to the box and unboxes the result. This is +[polymorphism and erasure](polymorphism-and-erasure.md)'s identity fixture; the +same symbol serves every instantiation. + +### An abstract constructor with a fixed payload + +`again :: forall f. Bind f => f Unit -> f Unit` used at `Effect`. Evidence names +the selected `Bind Effect` dictionary and the instantiation `f := Effect`; the +`Effect` owner's policy is `Callable([state token], Unit)`. The generic body +still stores that callable value; the consumer recovers the policy and runs it. +No cast onto the consumer's signature is used, which is what the fixed-payload +case verifies. + +### A representation-preserving coercion + +`coerce (UserId (Raw 47)) :: Int` over two visible newtypes. The `Coercion` +proof establishes representational equality; the planner lowers the chain of +newtype field policies to `Identity`, so no wrapper object is allocated and no +cast is needed. + +### A generic aggregate across a concrete boundary + +`data Wrap a = Wrap (Array a)`, `wrap :: Array Int -> Wrap Int`, and +`unwrap :: forall a. Wrap a -> Array a`, with +`main = arrayIndex (unwrap (wrap [40, 42])) 1`. The concrete literal is +`Array Int`; the declared field template `Array a` has the canonical +`Array(Erased)` RuntimeRep. Construction plans an `ArrayMap` with an +`Integer -> Erased` element conversion, boxes each element into the canonical +array, and stores that reference in the variant's canonical field. `unwrap` +reads the canonical layout directly, and the outer boundary maps it back to +`Array Int`. No runtime tag and no nominal array cast is used. A bare-variable +field such as `data Hold a = Hold a` also uses the Array owner's erased-element +protocol when its checked value is an array. It maps specialized arrays before +storing them and maps them back on recovery. + +## Boundaries and interfaces + +- **From checking (P5/P6):** checked boundary `Evidence`, plus the representation + policies of constructors and definitions, as a side table. +- **To CC (P8):** `RuntimeRep` requirements and explicit `Adaptation` + operations; no source identity and no physical layout. +- **To MIR (P9):** the requirements and plans; P9 selects the physical layout + and lowers the operations. MIR does not re-plan or invent a policy. +- **To the ABI boundary:** the canonical exchange shape is the consumer + representation; its byte layout is owned by + [canonical ABI and WIT](../wasm/canonical-abi-and-wit.md) and + [linear memory](../wasm/linear-memory-and-canonical-abi-boundary.md). Erased + values and generic aggregates never cross it. + +## Open questions and future work + +- **Registry home.** Whether the representation-owner registry lives beside the + trusted-library identities or as its own module at the Core-to-CC boundary. +- **Monomorphization.** A specialization pass may remove boxes and casts at + statically known instantiations while preserving the erased representation as + the fallback; it must not become a second correctness mechanism. +- **Open rows.** Open-row records still need a canonical policy and conversion + contract before they join the registry. + +## Implementation notes + +Checked instantiation evidence and the representation policies of constructors +reach P8 through the Core-to-CC side table (`psrs-backend/src/boundary.rs`), +beside CC the way `ExternalBindings` carries the WIT boundary. P8 reads the +checked relation from the immutable source program and the registered +`RepresentationPolicy` per constructor; it does not reconstruct the relation +from the Core type arena, key a semantic decision on a HIR identity, or derive a +policy by enumerating signatures. The registry registers `Function` (whose fixed +parameters are its checked instantiation arguments) and the trusted `Effect` +(whose fixed parameter is its runtime token); an unregistered constructor is +reported rather than resolved by arity or a signature search. Protocol +signatures are interned once per registered callable constructor as the concrete +calling convention with the payload erased. The checker's relation itself is a +single `TypeMatcher::relate` parameterized by a `Variance` (subsumption or +invariant); P8 consumes the `Instantiation` evidence it produces and never +re-derives it. + +The effect token is the compiler-owned opaque state token +(`psrs_hir::TypeId::STATE_TOKEN`) that effect lowering threads through each +`Effect` closure, the `State# RealWorld` analogue. It has no source spelling and +one uninspectable value, so no later pass can treat it as an `Int`; its runtime +shape is a scalar because the synchronous, single-threaded effect carries no +payload ([effects](../../../implementation/backend/effects.md)). + +## References + +- Reynolds, *Types, Abstraction and Parametric Polymorphism* (1983). +- Crary, Weirich, and Morrisett, *Intensional Polymorphism in Type-Erasure + Semantics* (2002). +- Wadler and Blott, *How to Make Ad-hoc Polymorphism Less Ad hoc* (1989); + Peyton Jones, Jones, and Meijer, *Type Classes: Exploring the Design Space* + (1997). +- Eisenberg and Peyton Jones, *Levity Polymorphism* (2017); GHC `RuntimeRep`, + `Any`, and `unsafeCoerce#`. +- Launchbury and Peyton Jones, *State in Haskell* (1995); GHC `IO` as + `State# RealWorld`. +- Swift function calling conventions (`@convention(thin)`/`thick`), + reabstraction thunks, and witness tables. +- Java type erasure and bridge methods. +- [IR boundaries](../00-ir-boundaries.md), [functional core](../../frontend/semantics/functional-core.md), + [classes and evidence](../../frontend/type-system/classes-and-evidence.md), + [CC IR](cc-ir.md), [MIR](mir.md), + [polymorphism and erasure](polymorphism-and-erasure.md). +- [DEC-17](../../../decision/DEC-17-representation-and-evidence.md) records this + model as a durable decision; [DEC-15](../../../decision/DEC-15-unified-type-representation.md) + is the type-spine counterpart. diff --git a/docs/design/backend/fp/scalars-and-primitives.md b/docs/design/backend/fp/scalars-and-primitives.md index c76391d9..1824525d 100644 --- a/docs/design/backend/fp/scalars-and-primitives.md +++ b/docs/design/backend/fp/scalars-and-primitives.md @@ -3,7 +3,7 @@ **Feature:** F-02 **Status:** Stable (design) **Prerequisites:** [CC IR](cc-ir.md) and [MIR](mir.md); two's-complement -integer arithmetic, IEEE-754 binary64, floor division, and the Wasm numeric +integer arithmetic, IEEE-754 binary64, Euclidean division, and the Wasm numeric instruction set. Read [IR boundaries](../00-ir-boundaries.md) first. **Summary:** Scalars are the unboxed Wasm value types the backend uses for `Int`, `Number`, `Boolean`, `Char`, and `Unit`; `String` is a GC array of @@ -11,16 +11,14 @@ canonical UTF-8 bytes ([DEC-16](../../../decision/DEC-16-scalar-strings-and-utf8-storage.md)), copied only at the canonical ABI boundary, and not a scalar. This document fixes the complete unary and binary operation -vocabulary, the concrete Wasm lowering of every operation, the module-local -floor division and modulo helpers, and the saturating `Number`-to-`Int` +vocabulary, the concrete Wasm lowering of every operation, the library-owned Euclidean division policy, and the saturating `Number`-to-`Int` conversion. It is the reference a frontend uses to lower every built-in scalar operator without adding representation special cases. ## Scope This document owns the scalar value model shared by CC and MIR, the unary and -binary operation vocabularies and their lowering, the generated floor -helpers, the saturating float-to-int sequence, and scalar verification. It does +binary operation vocabularies and their lowering, the boundary to library arithmetic, the saturating float-to-int sequence, and scalar verification. It does not own the byte-oriented string and ABI boundary (see [linear memory and the canonical ABI boundary](../wasm/linear-memory-and-canonical-abi-boundary.md) and @@ -37,12 +35,13 @@ the Wasm `i32.add`/`sub`/`mul` behavior. The official compiler realizes `Int` on JavaScript with `| 0`, giving the same wrapping semantics; that behavior is the reference. -**Truncated versus floor division.** Truncated division rounds toward zero -and its remainder takes the sign of the dividend. `Data.Int`'s `div` rounds -toward negative infinity, and `mod` satisfies `a = b * div a b + mod a b`: -for nonzero `b`, the remainder is zero or has the sign of `b` and magnitude -less than `|b|`. Wasm `i32.div_s`/`i32.rem_s` provide the truncated pair, so -the floor pair is computed by a helper. +**Truncated versus Euclidean division.** The primitive quotient truncates toward +zero and its remainder takes the dividend's sign. Official `Data.EuclideanRing` +`div/mod` instead satisfy `a = b * div a b + mod a b` with a nonnegative +remainder below `abs b` for nonzero divisors; both return zero for divisor zero. +The ordinary target library implements that policy using the truncating +primitives. For example, `div 3 (-2) = -1` and `mod 3 (-2) = 1`. +The compiler does not provide a second floor-division policy. **IEEE-754 binary64.** `Number` is an IEEE-754 binary64 value. The ordered comparisons follow the usual rules: a `NaN` is unequal to everything including @@ -92,7 +91,7 @@ CC UnaryOp = IntNeg | IntComplement | NumberNeg | BooleanNot | CharToInt | IntToChar CC BinaryOp = IntAdd | IntSub | IntMul - | IntQuot | IntRem | IntDiv | IntMod + | IntQuot | IntRem | IntAnd | IntOr | IntXor | IntShl | IntShr | IntZshr | IntEq | IntNe | IntLt | IntLe | IntGt | IntGe | NumberAdd | NumberSub | NumberMul | NumberDiv @@ -129,8 +128,8 @@ reachable from the CC vocabulary. - `StringEq` takes two `ValueShape::String` values and produces `Boolean`. Canonical UTF-8 byte length and contents define equality; object identity is not observable. -- floor helpers are ordinary MIR functions with `i32` parameters and - result, generated only when the module contains `IntDiv` or `IntMod`. +- Euclidean division and modulo remain ordinary library functions, composed + from checked primitive arithmetic and conditionals. ## Design @@ -144,7 +143,6 @@ chosen mapping is: | --- | --- | --- | | `IntAdd` / `IntSub` / `IntMul` | `I32Add` / `I32Sub` / `I32Mul` | `i32.add` / `i32.sub` / `i32.mul` | | `IntQuot` / `IntRem` | `I32DivS` / `I32RemS` | `i32.div_s` / `i32.rem_s` | -| `IntDiv` / `IntMod` | helper `Call` | module-local MIR function | | `IntAnd` / `IntOr` / `IntXor` | `I32And` / `I32Or` / `I32Xor` | `i32.and` / `i32.or` / `i32.xor` | | `IntShl` / `IntShr` / `IntZshr` | `I32Shl` / `I32ShrS` / `I32ShrU` | `i32.shl` / `i32.shr_s` / `i32.shr_u` | | `IntEq`..`IntGe` | `I32Eq`..`I32GeS` | `i32.eq`/`ne`/`lt_s`/`le_s`/`gt_s`/`ge_s` | @@ -157,6 +155,10 @@ chosen mapping is: | `IntNeg` | `I32Neg` | `0 - x` | | `IntComplement` | `I32Complement` | `x ^ -1` | | `NumberNeg` | `F64Neg` | `f64.neg` | +| `NumberAbs` | `F64Abs` | `f64.abs` | +| `NumberSqrt` | `F64Sqrt` | `f64.sqrt` | +| `NumberTrunc` | `F64Trunc` | `f64.trunc` | +| `NumberFloor` / `NumberCeil` | `F64Floor` / `F64Ceil` | `f64.floor` / `f64.ceil` | | `BooleanNot` | `BoolNot` | `i32.eqz` | | `IntToNumber` | `I32ToF64` | `f64.convert_i32_s` | | `NumberToInt` | `F64ToI32Sat` | saturating sequence (below) | @@ -180,15 +182,65 @@ scalar sequence one byte sequence, so byte equality is scalar String equality. dividend. Division or remainder by zero traps because the Wasm instruction traps; `i32.div_s` also traps on `i32.min / -1`, and that is the defined two's-complement overflow behavior. -- `IntDiv`/`IntMod` use floor division and a remainder with the divisor's sign, - matching `Data.Int`; they are computed by - the helpers below. +- Public `div/mod` belong to the library and use nonnegative Euclidean + remainders and explicit zero handling; see the background above. - Comparisons are signed; shifts and bitwise operations act on the 32-bit pattern. ### Number arithmetic - Arithmetic maps to the four `f64` operations and negation to `f64.neg`. +- `NumberAbs` (`numberAbs :: Number -> Number`) lowers to `f64.abs`, clearing + the sign bit without changing the magnitude or NaN payload. Negative zero + becomes positive zero, either infinity becomes positive infinity, and NaN + remains NaN. Core, CC and MIR require Number/F64 operands and results. This + implements the official Data.Number.abs foreign slot; no integer conversion + or library-name recognition is involved. The primitive remains explicit for + constant operands; no new constant folding is claimed. See the + [WebAssembly absolute-value semantics](https://webassembly.github.io/spec/core/exec/numerics.html#op-fabs). +- `NumberSqrt` (`numberSqrt :: Number -> Number`) lowers to `f64.sqrt`. It + follows IEEE-754 square root: exact squares stay exact, positive infinity + stays positive infinity, a negative finite value or negative infinity becomes + NaN, NaN stays NaN, and negative zero stays negative zero. The operation does + not trap. It implements the official Data.Number.sqrt foreign slot and is not + folded when its operand is constant. See the + [WebAssembly square-root semantics](https://webassembly.github.io/spec/core/exec/numerics.html#op-fsqrt). +- `NumberAcos` (`numberAcos :: Number -> Number`) has no Wasm opcode. Core and + CC require Number operands and results, then MIR calls the checked scalar + export `number_acos` in the numeric runtime. The pinned libm 0.2.15 fdlibm + polynomial returns radians. Finite inputs outside [-1, 1], infinities, and + NaN produce NaN, and the operation does not trap. NaN payloads are not a + public guarantee. It implements the official Data.Number.acos foreign slot. + A whole inverse-cosine algorithm does not become a compiler intrinsic, and + the result is not folded when its operand is constant. +- `NumberAsin` (`numberAsin :: Number -> Number`) uses the same checked + scalar-runtime boundary. The pinned libm 0.2.15 fdlibm polynomial returns + radians and preserves the sign of zero. Finite inputs outside [-1, 1], + infinities, and NaN produce NaN, and the operation does not trap. NaN + payloads are not a public guarantee. It implements the official + Data.Number.asin foreign slot and is not folded when its operand is constant. +- `NumberAtan` (`numberAtan :: Number -> Number`) uses the same checked + scalar-runtime boundary. The runtime copies the pinned libm 0.2.15 fdlibm + polynomial and returns radians, preserving the sign of zero. Inputs whose + magnitude is below 2^-27, including subnormals, return unchanged. The copy + omits the host underflow flag because Wasm has no floating-point status + flags and must not write below the stack pointer. Positive and negative + infinity produce positive and negative pi/2. NaN produces NaN, and the + operation does not trap. NaN payloads are not a public guarantee. It + implements the official Data.Number.atan foreign slot and is not folded + when its operand is constant. +- `NumberAtan2` (`numberAtan2 :: Number -> Number -> Number`) uses the same + checked scalar-runtime boundary with two Number arguments. The argument + order is `y` then `x`, matching official `Math.atan2`. The runtime follows + the fdlibm exponent-gap cutoff of 60 and calls the checked inverse-tangent + export for moderate ratios. An exponent gap above 60 returns a signed + half-pi, and a negative `x` whose gap is below -60 contributes zero before + the pi adjustment. libm 0.2.15's own two-argument routine uses a wider gap + and is not the oracle. Returned bits match official `Math.atan2`, including + the sign of zero. Either NaN produces NaN, and the operation does not trap. + NaN payloads are not a public guarantee. It implements the official + Data.Number.atan2 foreign slot and is not folded when its operands are + constant. Wasm has no two-argument inverse-tangent instruction. - Comparisons use the ordered `f64` operations; `NumberEq`/`NumberNe` are `f64.eq`/`f64.ne`, so `NaN` is unequal to itself and `+0 = -0`. - The current vocabulary has no `Number` remainder. If the standard library @@ -205,6 +257,50 @@ scalar sequence one byte sequence, so byte equality is scalar String equality. are representation identities, and the source layer is responsible for the validity of the scalar value. +### Number integral rounding + +`NumberTrunc` (`numberTrunc :: Number -> Number`) lowers to `f64.trunc`: +finite values round toward zero without converting to i32. It preserves zero +signs and infinities; NaN produces NaN without a payload-bit guarantee. Core, +CC and MIR verify Number/F64 operand and result types. The primitive remains +available for constant operands even when the MIR optimizer does not fold it. +The library uses it for the official Data.Number.trunc foreign slot; public +Int.trunc retains its official finite check and range clamping wrapper. + +`NumberFloor` and `NumberCeil` have the same Number/F64 type contract and +nonfinite/zero-sign guarantees, with rounding toward negative and positive +infinity, respectively. They lower directly to `f64.floor` and `f64.ceil`, +without an integer representation boundary. The library owns ECMAScript +round-to-nearest with ties toward positive infinity: this differs from Wasm +`f64.nearest` and cannot be replaced by adding 0.5 before flooring. Public +Int.floor/ceil/round retain their unchanged finite checks and clamping wrappers. + +### Complete decimal conversion + +`NumberFromDecimal` (`numberFromDecimal :: String -> Number`) converts a complete +ASCII signed decimal token, with an optional decimal exponent, to binary64. +It uses the pinned Rust core parser's nearest-representable rounding, including +ties to even, signed underflow zero, and overflow to signed infinity. Empty, +partial, nondecimal, non-ASCII, and whitespace-containing tokens produce NaN. +`Infinity` and `NaN` spellings are outside this primitive's grammar. + +Core and CC validate String input and Number output before ABI erasure. MIR +copies canonical UTF-8 to a transient linear-memory buffer using the shared +string boundary protocol, calls the checked numeric-runtime export, and frees +the buffer. The runtime allocates nothing and retains no pointer. Its artifact +contract declares both numeric exports and private table initialization; +static stack analysis covers every path reachable from its entry points. +The compiler preserves potentially trapping canonical-buffer allocation for +both numeric formatting and conversion, even when the result is unused. + +The library owns ECMAScript whitespace, longest-prefix recognition, rollback of +an incomplete exponent, `Infinity` recognition, and ordinary predicate/builder +calls. It preserves the official public `Data.Number.fromString` wrapper and +the foreign slot's rank-N `Fn4` signature. Whole parsing functions are not +compiler intrinsics. The contracts are +[ECMAScript parseFloat](https://tc39.es/ecma262/multipage/global-object.html#sec-parsefloat-string) +and [Rust f64::from_str](https://doc.rust-lang.org/std/primitive.f64.html#impl-FromStr-for-f64). + ### Conversions - `IntToNumber` is `f64.convert_i32_s`. @@ -218,9 +314,9 @@ scalar sequence one byte sequence, so byte equality is scalar String equality. ### Rejected alternatives -- **Map `IntDiv`/`IntMod` to `i32.div_s`/`i32.rem_s`.** Rejected: these are - truncated, not floor, and produce the wrong remainder sign for negative - operands (`-5` by `3` would give remainder `-2` instead of `1`). +- **Compiler-owned Euclidean or floor helpers.** Rejected: public arithmetic + policy belongs to ordinary library definitions. The old `intDiv/intMod` + bindings implemented a different signed-divisor policy and are retired. - **Trapping `Number`-to-`Int`.** Rejected: an out-of-range or `NaN` operand would trap a type-correct program. The saturating lowering keeps the conversion total. @@ -250,63 +346,25 @@ The emitter builds this as nested `if` expressions over `f64.le`, `f64.ge`, and `f64.ne` and calls the trapping `i32.trunc_f64_s` only inside the safe range, so it never traps. -### Floor division and modulo - -Both helpers read their operands as `i32` parameters `a` and `b`. They first -compute the truncated remainder and decide whether an adjustment is needed: - -```text -r = a rem_s b # truncated remainder -nonzero = r != 0 -remainder_neg = r < 0 -divisor_neg = b < 0 -adjust = nonzero && (remainder_neg != divisor_neg) - -# divide: -if adjust: a div_s b - 1 else: a div_s b - -# modulo: -if adjust: r + b else: r -``` +### Library division and modulo -The adjustment changes a quotient or remainder only when the truncated remainder -and the divisor have opposite signs. Thus `a = b * div a b + mod a b`, with -`mod a b` taking the sign of `b` (or zero). This is floor division; the -nonnegative-remainder convention is a different rule. Both paths -execute `a rem_s b` (and `a div_s b` for the divide helper), so `b = 0` traps as -the underlying instruction does. This matches the official `Data.Int` -semantics. - -### Helper generation and linking - -```text -lower_scalar_helpers(module, first_function_id): - needs_div = any assignment (including nested if branches) is IntDiv - needs_mod = any assignment is IntMod - symbol_module = first function's module, else the intrinsics module - allocate each needed helper symbol downward from u32::MAX, skipping symbols - used by the module's functions and externals - emit each helper as an ordinary MIR Function with consecutive FunctionIds - starting at first_function_id -``` - -Each helper is a four-block MIR function (entry, then-block, else-block, merge -block) with a `Branch`, two `Jump`s with the computed value, and a `Return`. The -helper symbols are held beside the module in `ScalarHelpers`; when lowering an -`IntDiv`/`IntMod` assignment, P9 emits an `Instruction::Call` to the -corresponding helper instead of a `Primitive`. A module that uses neither -operation generates neither helper. +The official pure wrappers retain their source signatures and branches. The +Wasm-specific foreign delegates expose truncating quotient/remainder and wrapping +arithmetic. Their callers implement Euclidean adjustment and zero handling. +There is no `IntDiv` or `IntMod` CC operation or generated MIR helper. Retired +intrinsic IDs 26 and 27 remain reserved; the names are rejected rather than +being silently rebound to a different policy. ## Code map -The scalar design is owned by three module groups: the MIR operation -vocabularies, the MIR helper generator, and the Wasm emission modules. The -intended structure is: +The language vocabulary, exhaustive target selection, concrete MIR operations, +and Wasm emission have separate owners. See +[intrinsic implementations](intrinsic-implementations.md). The structure is: ```text mir/numeric.rs UnaryOp and NumericOp; CC -> MIR operation selection -mir/scalar_helpers.rs floor helper detection, generation, and - helper symbol allocation +target_intrinsics/ exhaustive language-to-target selection +mir/lower/runtime_call.rs artifact protocol adaptation wasm/lower/structure/ops.rs binary NumericOp -> wasm_encoder::Instruction, plus reference and memory operand helpers wasm/lower/structure/unary.rs UnaryOp emission, including the saturating @@ -320,30 +378,19 @@ vocabularies and the total conversion from the CC vocabularies: pub enum UnaryOp { I32Neg, I32Complement, F64Neg, BoolNot, I32ToF64, F64ToF32, F32ToF64, F64ToI32Sat, BoolToI32, I32ToBool, I32Identity } pub enum NumericOp { I32Add, I32Sub, I32Mul, I32DivS, I32RemS, I32And, I32Or, I32Xor, I32Shl, I32ShrS, I32ShrU, I32Eq, I32Ne, I32LtS, I32LeS, I32GtS, I32GeS, BoolAnd, BoolOr, BoolEq, BoolNe, F64Add, F64Sub, F64Mul, F64Div, F64Eq, F64Ne, F64Lt, F64Le, F64Gt, F64Ge } impl From for UnaryOp; // total -impl TryFrom for NumericOp; // IntDiv/IntMod have no instruction +impl TryFrom for NumericOp; // StringEq is lowered as a byte comparison ``` Every CC unary operation MUST map to exactly one `UnaryOp`, and every CC binary -operation except `IntDiv` and `IntMod` MUST map to exactly one `NumericOp`. +operation except `StringEq` MUST map to exactly one `NumericOp`. String equality +uses its checked GC byte-array lowering. -**Helper generation.** `mir/scalar_helpers.rs` MUST detect the -helper-requiring operations, allocate their symbols, and emit the helpers: - -```rust -pub fn lower_scalar_helpers(module: &cc::Module, first_function_id: u32) -> (ScalarHelpers, Vec); -impl ScalarHelpers { - pub fn binary_instruction(&self, op: cc::BinaryOp, destination: ValueId, left: ValueId, right: ValueId, span: TextRange) -> Result>; -} -``` - -`lower_scalar_helpers` MUST emit each needed helper as an ordinary `Function` -with consecutive `FunctionId`s starting at `first_function_id`, MUST allocate -each helper symbol downward from `u32::MAX` while skipping symbols already used -by the module's functions and externals, and MUST return the symbol table. A -module that uses neither `IntDiv` nor `IntMod` MUST generate no helper. -`ScalarHelpers::binary_instruction` MUST select a `Call` to the matching -generated helper for `IntDiv`/`IntMod` and MUST otherwise select the -corresponding `Primitive` from `mir/numeric.rs`. +**Implementation selection.** `target_intrinsics::implementation` MUST classify +all active HIR identities exhaustively as direct operations, generated operations, +artifact exports, elaborated values, or explicit unsupported values. This table +selects the implementation; the consuming pass performs its own typed conversion. +Artifact adaptation validates the language scheme before erasure, and linking +checks the actual MIR consumer signature against the provider. **Wasm emission.** `wasm/lower/structure/ops.rs` MUST expose: @@ -384,8 +431,7 @@ exactly; the MIR verifier checks each lowered operation's operand and result produce `Boolean`; - unary negation/complement match their operand and result types; - `I32ToF64` is `I32 -> F64`, `F64ToI32Sat` is `F64 -> I32`, `BoolToI32` is - `Boolean -> I32`, and `I32ToBool` is `I32 -> Boolean`; and -- the floor helper signatures are `(I32, I32) -> I32`. + `Boolean -> I32`, and `I32ToBool` is `I32 -> Boolean`. A mismatch is reported with the operation's source span. Every operation in this vocabulary is part of the core WebAssembly baseline; the target capability @@ -393,34 +439,12 @@ gate needs no new proposal for it. ## Worked example -floor division of `-5` by `3`. The CC fragment - -```text -v0 = -5 -v1 = 3 -v2 = IntDiv(v0, v1) -v3 = IntMod(v0, v1) -result = v2 + v3*... // in source, `div (-5) 3` and `mod (-5) 3` -``` - -has both operations replaced by helper calls, and the module gains -`__psrs_floor_int_div` and `__psrs_floor_int_mod`. Tracing the divide -helper: - -```text -a = -5, b = 3 -r = -5 rem_s 3 = -2 -nonzero = true -remainder_neg = true -divisor_neg = false -adjust = true -result = (-5 div_s 3) - 1 = -1 - 1 = -2 -``` - -The modulo helper computes `r + b = -2 + 3 = 1`. Substituting into -`a = b * div a b + mod a b` gives `-5 = 3 * (-2) + 1`, the floor identity. -The `div_mod` and `binary_matrix` fixtures execute this and the other sign -combinations through Wasm GC and check the combined boolean result. +For `a = -5` and `b = 3`, raw quotient/remainder produce `-1` and `-2`. +The library adjusts them to `div a b = -2` and `mod a b = 1`, satisfying +`-5 = 3 * (-2) + 1`. For `a = 3` and `b = -2`, the raw remainder is already +nonnegative: the library returns quotient `-1` and remainder `1`. +For `b = 0`, the wrapper returns zero before executing either trapping primitive. +Source tests exercise these wrappers through case branches and all operand signs. ## Boundaries and interfaces @@ -439,24 +463,24 @@ combinations through Wasm GC and check the combined boolean result. ## Open questions and future work - **`Number` remainder.** If a source `mod` for `Number` is added, its exact - semantics (JavaScript `%` versus floor) and helper must be fixed here. + semantics (JavaScript `%` versus Euclidean policy) must be owned by the + library wrapper rather than a new compiler arithmetic operation. - **Saturating-capability switch.** The profile enables the saturating float-to-int proposal; if the sequence were replaced by `i32.trunc_sat_f64_s` the capability gate would have to require it. - **`i64`/`f32` source types.** Adding them is a vocabulary extension with no change to the operand/result model. -- **Fast paths for known-sign constants.** The helper could inline the - adjustment when both operands are statically non-negative; this is an - optimization that must preserve the floor result. +- **Fast paths for known-sign constants.** Ordinary library code may inline its + adjustment when operands are known, preserving the Euclidean result. ## Implementation notes The CC and MIR vocabularies, lowerings, and verifiers implement the full unary and binary set above. The source bootstrap exposes the operations that do not already have symbolic integer syntax as specialized functions: `intNeg`, -`intComplement`, `numberNeg`, `booleanNot`, the six conversion names from the +`intComplement`, `numberNeg`, `numberTrunc`, `numberFloor`, `numberCeil`, `booleanNot`, the six conversion names from the table (`intToNumber`, `numberToInt`, `booleanToInt`, `intToBoolean`, -`charToInt`, and `intToChar`), `intDiv`, `intMod`, the six integer bitwise +`charToInt`, and `intToChar`), the six integer bitwise and shift names, all `number*`, `boolean*`, and `char*` binary names in the table. The existing symbols `+`, `-`, `*`, `/`, `%`, `==`, `/=`, `<`, `<=`, `>`, and @@ -464,7 +488,7 @@ The existing symbols `+`, `-`, `*`, `/`, `%`, `==`, `/=`, `<`, `<=`, `>`, and Fully saturated intrinsic applications lower to typed Core unary or binary primitives; Core verification checks their exact scalar operand and result types before P8 maps them into CC. A source-level driver fixture compiles every -operation and executes the documented floor, conversion, comparison, and +operation and executes the documented Euclidean, conversion, comparison, and wrapping behaviors through Wasmtime when it is available. ## References diff --git a/docs/design/backend/fp/type-classes-and-dictionaries.md b/docs/design/backend/fp/type-classes-and-dictionaries.md index e513bad7..557cf499 100644 --- a/docs/design/backend/fp/type-classes-and-dictionaries.md +++ b/docs/design/backend/fp/type-classes-and-dictionaries.md @@ -25,6 +25,9 @@ belong to the frontend's It does not own the general record and closure layouts ([data representation](data-representation.md)), or the erased representation of polymorphic values ([polymorphism and erasure](polymorphism-and-erasure.md)). +A dictionary is an instance of the shared representation policy and conversion +contract in [representation and evidence](representation-and-evidence.md): an +ordinary product of closures, with no dictionary-specific adaptation. ## Background @@ -58,9 +61,9 @@ Dict(C, a) = { m_1 : τ_1(a), ..., m_n : τ_n(a), - super_1 : Dict(S_1, a), + super_1 : Unit -> Dict(S_1, a), ..., - super_k : Dict(S_k, a), + super_k : Unit -> Dict(S_k, a), } ``` @@ -70,8 +73,12 @@ index, not by a target offset. An **instance** `instance C T` supplies one dictionary value of type `Dict(C, T)`: each method field is a closure (or the top-level method -implementation), and each superclass field is a dictionary value that the -instance provides. An instance with an **instance context**, such as +implementation), and each superclass field is a checked `Unit -> Dict(S_j, T)` thunk that +the instance provides. Construction must not evaluate these thunks: a method +implementation may reference a subclass instance whose superclass is this +instance. Eager superclass construction would make valid mutually referring +instance values recurse before any method executes. Selection forces the thunk +with the unique builtin `Unit` value. An instance with an **instance context**, such as `instance eqList :: Eq a => Eq (List a)`, is a function from its context dictionaries to its dictionary. @@ -179,13 +186,14 @@ Core lowering turns this evidence into dictionary values and projections: ```text lower_evidence(Given(local)) = local -lower_evidence(Superclass(parent, field)) = Project(field, lower_evidence(parent)) +lower_evidence(Superclass(parent, field)) = Apply(Project(field, lower_evidence(parent)), Unit) lower_evidence(Instance(instance, context)) = Apply(instance_dictionary_constructor(instance), map(lower_evidence, context)) ``` An instance dictionary constructor builds a record from its method values and -superclass dictionaries. A constrained declaration becomes a lambda over its +superclass thunks. THIR verification checks each thunk's Unit domain and selected +dictionary result; Core lowering emits ordinary projection and application. A constrained declaration becomes a lambda over its given dictionaries. Method and superclass field indices are fixed by the class record and checked by the Core and CC verifiers. This lowering cannot choose a different instance or resolve a new constraint. @@ -204,7 +212,7 @@ different instance or resolve a new constraint. instance omits the method field; the instance simply stores the default closure in that field. - **Derived instances.** Deriving generates an ordinary instance and dictionary - at elaboration time; it adds no backend representation (`FE-16`). + at elaboration time; it adds no backend representation (`FE-22`). - **Erased polymorphism.** A dictionary passed through a polymorphic function is an ordinary erased value; recovery at the concrete consumer uses the erased protocol, not the dictionary. @@ -313,7 +321,7 @@ no runtime check of `a`. - **To optimization.** Specialization reads dictionaries but preserves the dictionary-passing semantics; it may not be the only encoding. - **Not owned.** Resolution at the source level, overlap/orphan diagnostics, and - functional dependencies (`FE-15`), deriving (`FE-16`), and higher-rank + functional dependencies (`FE-15`), deriving (`FE-22`), and higher-rank subsumption (`FE-18`). ## Open questions and future work diff --git a/docs/design/backend/wasm/canonical-abi-and-wit.md b/docs/design/backend/wasm/canonical-abi-and-wit.md index 6493c492..99dd880d 100644 --- a/docs/design/backend/wasm/canonical-abi-and-wit.md +++ b/docs/design/backend/wasm/canonical-abi-and-wit.md @@ -7,6 +7,19 @@ ## Scope +Executable provider selection and artifact dependency closure are owned by the +draft [linking and runtime](linking-and-runtime.md) design. Resolving a WIT +interface checks a definition; it does not prove that a host or guest implements +it. ABI lowering supplies checked source/WIT and resource-ownership contracts to +the linker. Changing providers does not authorize a new source-type projection +or pointer/handle reinterpretation. + +The same proposal moves compiler-owned WIT source assets and the default world +to `psrs-runtime`, exposed through its immutable target catalog. `psrs-linker` +loads definitions and supplies one resolved-world context; the backend validates +source/WIT contracts and lowers canonical calls against that context. ABI lookup and +component assembly consume the same catalog. + This document owns source WIT bindings, the ABI registry, argument classification and flattening, result recovery, signature validation, and the ownership rules at the boundary. It does not own the linear-memory layout and allocator used by @@ -477,9 +490,12 @@ backend/src/ parameters/ mod.rs direct parameter flattening, records, flags indirect.rs Canonical ABI parameter-record layout and stores - component.rs vendored WIT loading and component packaging + linking/ checked target requirements and plan/diagnostic mapping ``` +WIT definition loading and component packaging now live in `psrs-linker`, over +the pinned definitions owned by `psrs-runtime/wit/`. + `abi.rs` and `abi/` own resolving source WIT bindings and must define: - `ExternalBindings` — the side table of the Model section, holding one @@ -531,20 +547,22 @@ It flattens arguments into canonical parameters, appends the return pointer when record and flags flattening. Every adaptation instruction must be an ordinary MIR operation, and no WIT name, interface, or canonical signature may enter MIR. -`component.rs` owns vendored WIT loading and packaging and must provide: +`psrs-linker` owns WIT definition loading and composition and provides: ```rust -pub fn load_vendored_wasi(resolve: &mut Resolve) -> Result<(), String>; -pub fn command_world() -> Result<(Resolve, WorldId), String>; -pub fn componentize(core: &[u8], resolve: &Resolve, world: WorldId) -> Result, String>; +pub fn resolve_default_definitions() -> Result; +pub fn compose(context: &ResolvedWorldContext, plan: &CheckedLinkPlan, application: &[u8]) + -> Result; ``` -It also owns the supported-interface set. `abi.rs` owns the +The backend's `abi` module loads the same pinned WIT from the `psrs-runtime` +catalog for isolated ABI fixtures and derives the supported-interface set from +the resolved default world. `abi` owns the `wasi_interface_enabled(target, interface)` package gate consulted before resolving a WASI import ([capability profile](capability-profile.md)). Dependencies must stay one-directional: `abi` and `mir/wit` may depend on -`capability` and shared types; `component` may depend on `abi` and `capability`; -none may depend on the front end. +`capability` and shared types; the backend `linking` module may depend on `abi` +and `capability`; none may depend on the front end. ## Invariants and verification diff --git a/docs/design/backend/wasm/canonical-abi-compositional.md b/docs/design/backend/wasm/canonical-abi-compositional.md index 5a4396b8..9baa37b8 100644 --- a/docs/design/backend/wasm/canonical-abi-compositional.md +++ b/docs/design/backend/wasm/canonical-abi-compositional.md @@ -706,8 +706,8 @@ the free plan frees the record after the call. lowering into loops and leaf loads/stores carrying the `MemoryId` and `MemArg` ([linear memory and the canonical ABI boundary](linear-memory-and-canonical-abi-boundary.md)). - **To the componentizer.** `FnAbi` must match `Resolve::wasm_signature` so - `componentize` links the core import - (`crates/psrs-backend/src/component.rs:74`). + `psrs_linker::compose` links the core import + (`crates/psrs-linker/src/compose.rs`). - **To the allocator.** The generic parameter record and return area allocate and free through `cabi_realloc`, following [canonical buffer allocation and lifetime](canonical-buffer-allocation-and-lifetime.md). diff --git a/docs/design/backend/wasm/encoding-and-structuring.md b/docs/design/backend/wasm/encoding-and-structuring.md index 36c5bc04..64b3f3ae 100644 --- a/docs/design/backend/wasm/encoding-and-structuring.md +++ b/docs/design/backend/wasm/encoding-and-structuring.md @@ -18,7 +18,9 @@ emission. It does not own: - concrete scalar, GC, and closure layouts, which MIR fixes; - canonical ABI adaptation, which is [canonical ABI and WIT](canonical-abi-and-wit.md); - target capability policy, which is the [capability profile](capability-profile.md); -- component packaging and WASI services, which is the +- target-provider selection, memory reservations, and artifact composition, + specified by the draft [linking and runtime](linking-and-runtime.md) design; +- the command world and WASI services, specified by the [WASI platform library](wasi-platform-library.md). ## Background @@ -208,6 +210,13 @@ table is installed; the segment exists only to satisfy the declaration rule. ### Data segments and the allocator +Under the [target linking contract](linking-and-runtime.md), P10 consumes +checked memory reservations and an allocator boundary from the same link plan +used by component assembly. It does not choose libraries or derive storage +ownership by scanning import names. This plan integration is implemented for +the formatter slice; the emitter consumes the plan's import names, heap start, +and minimum pages. + String literals are collected once, deduplicated by content, and placed in passive data segments. Each distinct literal used by the module is interned in one mutable `(ref null $string)` global: the first evaluation materializes the diff --git a/docs/design/backend/wasm/linear-memory-and-canonical-abi-boundary.md b/docs/design/backend/wasm/linear-memory-and-canonical-abi-boundary.md index f968b4f7..46bec911 100644 --- a/docs/design/backend/wasm/linear-memory-and-canonical-abi-boundary.md +++ b/docs/design/backend/wasm/linear-memory-and-canonical-abi-boundary.md @@ -7,6 +7,12 @@ ## Scope +The [linking and runtime](linking-and-runtime.md) design extends the +canonical-only contract with explicit artifact-owned static data and execution +stacks. The linker reserves these regions outside canonical scratch, allocator +state, and transient allocations; the formatter slice implements this. This does +not add a linear language heap. + This document owns the address model, the string and byte-list representation, the scratch return area, data-segment use, and the MIR byte-operation contract at the boundary. It does not own canonical ABI adaptation itself diff --git a/docs/design/backend/wasm/linking-and-runtime.md b/docs/design/backend/wasm/linking-and-runtime.md new file mode 100644 index 00000000..6f274888 --- /dev/null +++ b/docs/design/backend/wasm/linking-and-runtime.md @@ -0,0 +1,580 @@ +# Linking and Runtime Implementations + +**Feature:** [F-02 — Build Portable Program Artifacts](../../../feature/F-02-portable-programs.md) +**Status:** Core-Wasm formatter linking and explicit synchronous guest interface +composition are implemented; general loading extensions remain future work. + +**Prerequisites:** [IR boundaries](../00-ir-boundaries.md), +[primitive FFI](primitive-ffi-and-stdlib.md), +[canonical ABI and WIT](canonical-abi-and-wit.md), +[linear memory](linear-memory-and-canonical-abi-boundary.md), and +[DEC-06](../../../decision/DEC-06-runtime-interface-via-wit.md). +**Summary:** One checked target link plan connects language intrinsics, executable +runtime implementations, WIT interface providers, and component packaging. It +owns dependency closure, export/import agreement, memory reservations, and +instantiation order. Source typing, library algorithms, canonical adaptation, +and Wasm encoding retain their existing owners. + +## Scope + +This document owns target implementation selection and linking after checked +source-module assembly: the catalog, requirement closure, provider bindings, +artifact verification, link plan, and execution of that plan. It includes both +private core-Wasm runtime libraries and providers of WIT interfaces. + +It does not own PureScript name resolution, type inference, stdlib algorithms, +WIT syntax, GC layouts, or control-flow structuring. It consumes their checked +results. The driver continues to assemble source modules from the locked stdlib +package. The linker never repairs invalid typing with raw ABI compatibility. + +## Background + +### Definitions, declarations, and implementations + +| Input | Supplies | Does not establish | +| --- | --- | --- | +| PureScript source package | declarations, classes, instances, pure functions, typed foreign declarations | an executable implementation for every foreign value | +| Intrinsic registry | primitive identity, semantics, arity, language scheme | a particular raw ABI or artifact provider | +| WIT package | interfaces, worlds, function and resource types, definition dependencies | code implementing those interfaces | +| Core-Wasm library | typed raw exports and executable code, with memory/table requirements | compatibility with source types or a WIT interface | +| Guest component | component exports and imports, resource identities, executable implementation | permission to leave its dependencies as host imports | +| Host provider | implementations made available by the selected target world | arbitrary imports inferred from names or ABI shape | + +A WIT-defined library can have host or guest implementation providers. Its WIT +package and its executable artifact are separate dependency records. A package +version resolves definitions; a selected provider discharges execution needs. + +Core-module linking here means typed module composition inside a component. +It does not imply merging instruction streams or accepting native object-file +relocations. WIT component composition uses canonical lift/lower boundaries. +These differ from source-module linking, which unifies checked language +declarations before backend lowering. + +### Intrinsic implementations + +The same checked intrinsic may be realized by a direct Wasm instruction, a +compiler-generated helper, or a runtime artifact. These are implementation +choices under one language contract. `numberToString` returns a source String; +its runtime's pointer and length are not that source signature. A whole `Show` +implementation must not migrate into the compiler just because its numerical +leaf requires target code. + +## Model + +### Identities and checked inputs + +Use stable identities within their owning domain. An intrinsic uses its HIR +identity, source values use resolved symbols, WIT interfaces/types use resolved +package identities, and executable artifacts use an identity plus a content +digest. Final Wasm indices and readable names are encoding results. + +Canonical WIT names include package/interface/version identity. World aliases +are resolved through WIT before binding; strings and unversioned prefixes are +not authoritative identities. Pins choose exact definitions and artifacts. +Compatibility across versions requires an explicit adapter or rule with +evidence; the linker does not guess it from equal flattened signatures. + +The source-to-WIT contract retains source TypeId, representation evidence, +canonical widths/layouts, and own/borrow rules as described in DEC-12/DEC-13. +The linker consumes that contract; it does not create another source-type mirror. + +### Implementation catalog and requirements + +The following are logical contracts, not a demand for placeholder crates: + +```text +IntrinsicImplementation = { + intrinsic_id, semantic_contract_version, + implementation: Direct | GeneratedHelper | ArtifactExport, + requirements, recovery_protocol +} + +ArtifactContract = { + artifact_id, kind: CoreModule | Component, + digest, provenance, required_features, + verified_exports, required_imports, + storage_contract, initialization_contract +} + +BindingRequirement = { + origin: source_symbol/span | generated_operation/parent_origin, + boundary: CheckedPrimitive | ResolvedWit | RawCore, + expected_contract, + provider: Generated | ArtifactExport | HostInterface +} + +LinkPlan = { + selected_implementations, verified_artifacts, resolved_bindings, + memory_plan, instantiation_steps, external_world, + requirement_origins, input_digests +} +``` + +Catalog metadata has one owner. Lowerers request an implementation by checked +identity and consume its ABI/recovery descriptor. Artifact inspection verifies +that descriptor against real exports; it does not independently define it. +The public library API and source scheme remain with the library and HIR. + +Every executable requirement resolves to exactly one selected provider. +Explicitly compatible sharing is allowed, but duplicate or ambiguous providers +are errors. A provider's own requirements participate in the same closure. +Generated helpers and command-entry imports are roots even when absent from +source foreign declarations. + +### Storage and initialization + +Every shared-memory region records a memory identity, range/alignment, owner, +lifetime, and access protocol. Distinguish canonical scratch and allocator state, +runtime static data, runtime stack, and allocator-managed transient buffers. +Reservations are nonoverlapping; ranges and required memory pages are checked +for overflow and supported bounds. The allocator starts after all reserved +regions and cannot return their bytes. + +Separate guest components normally own separate memories. Their canonical ABI +conversion copies/lifts values through each provider's memory and realloc/ +post-return protocol. Equal pointer widths do not make pointers transferable. +Resource handles retain provider and resource-type identity; method families and +constructors/destructors cannot be split across incompatible provider instances. + +Instantiation steps distinguish dependency creation, memory aliases, function +shims, shim resolution, declared initialization, and command invocation. A call +cycle may use a supported shim protocol only if initialization cannot invoke an +unresolved shim. Unsupported eager cycles, start functions, or initialization +side effects are explicit errors. No command runs until linking succeeds. + +## Design + +### One plan, distinct stages + +```mermaid +flowchart TD + source["Locked source package"] --> checked["Checked Core + external declarations"] + intrinsic["Intrinsic semantics"] --> selection["Select target implementation"] + checked --> selection + wit["Pinned WIT definitions"] --> binding["Resolved source / WIT contract"] + checked --> binding + selection --> mir["MIR calls + explicit target requirements"] + binding --> mir + artifacts["Verified executable providers"] --> plan["Checked target link plan"] + mir --> plan + plan --> emission["Wasm emission uses memory / import plan"] + emission --> assembly["Linker assembles and verifies artifacts"] + plan --> assembly + assembly --> output["Component + permitted external world"] +``` + +P8 checks source declarations against primitive or WIT contracts and selects +implementation identities. P9 lowers recovery and ownership operations to MIR, +retaining explicit target requirements beside it. After MIR optimization, +planning closes reachable requirements and verifies artifact contracts before +P10 lays out memory or assigns imports. P10/P11 consume the same immutable plan. +Final binary inspection checks conformance to that plan; it never reconstructs +semantic bindings or chooses providers from emitted strings. + +If optimization changes requirements, rebuild the plan from the checked inputs +and new reachability result. An emitter cannot independently add another helper +or host capability. Catalog feature requirements must be visible before emission. + +### WIT providers and world construction + +#### Runtime package owns target definitions + +Move the compiler-owned WIT assets from `psrs-backend/wit/` to +`psrs-runtime/wit/`, including both the pinned WASI dependency definitions and +`psrs-app.wit`. They describe the distributed target contract and default world; +their version, hashes, source provenance, and dependency set belong with the +runtime package. Owning these definitions does not mean the package implements +WASI host services. + +The runtime package exposes immutable catalog data: WIT source names/bytes, +package pins, default-world identity, artifact bytes/digests, and raw export/ +storage contracts. The backend consumes this data rather than using cross-crate +`include_str!`/`include_bytes!` paths or maintaining a second embedded copy. +The same catalog supplies ABI resolution and component-world construction. + +The independent `psrs-linker` owns WIT catalog loading, resolved world identities, +provider contracts, and target composition. The backend owns source-signature +checks, canonical call planning, representation recovery, and language-to-target +capability requirements. The runtime package depends on neither compiler IR crates +nor backend/linker machinery; its metadata API returns data, not `Resolve`, MIR, +or link-plan values. Target execution +code and host-consumed catalog metadata have distinct build entry points so +reading WIT assets does not require compiling the formatter into the native +compiler or embedding WIT text in the executable target module. + +The resolved default world supplies interface membership. Capability policy is +checked separately against the selected target profile. Do not retain a second +handwritten `COMPONENT_INTERFACES` list as an independent source of permitted +interface identities. The final external world is still the live provider +closure, not every interface mentioned in the package's default world. + +#### Select executable providers + +Resolve all needed WIT definitions for type checking, but select executable +providers only for reachable interfaces and retained initialization needs. +Provider bindings are explicit build inputs: current compiler-owned WASI bindings +select host interfaces; explicit component bindings select pinned guest exports. +Merely placing WIT files in a directory never selects a provider. + +For a host binding, verify that the selected target world permits that interface +and leave it in the final external world. For a guest binding, verify its export +against the resolved interface, bind the application import to that export, +and recursively resolve the guest's imports. Guest dependencies that remain host +capabilities must also be permitted and reported. A guest cannot hide an extra +filesystem/network capability from the plan. + +Source-value conversion remains owned by backend ABI lowering. The linker uses +component tooling's canonical lift/lower operations for typed composition; it +does not reconstruct source layouts. WIT package inclusion, resource identity, +string encoding, ownership, realloc and export post-return are checked before +composition. Resource equivalence follows explicit instance/type aliases, +not method names. Conflicting pins or unresolved provider exports fail before +artifact emission. There is no silent host fallback when a guest binding fails. + +### Artifact-backed intrinsic calls + +Artifact exports use explicit raw signatures and recovery protocols. A raw +runtime accepts only representations declared by its contract; source GC +references cannot cross an independently built core library boundary without a +checked shared representation contract. The formatter needs scalars and bytes, +so it does not require shared GC types or a WIT wrapper. + +The `numberToString` implementation uses pinned `ryu-js` 1.0.2 in a small Rust +target crate. The caller supplies a 32-byte buffer, and the raw export has +`(f64, i32, i32) -> i32`: value, pointer, capacity, then initialized length. +The runtime writes ASCII, allocates nothing, and retains no pointer. Its +contract includes negative zero, NaN/infinities, notation boundaries, and +ECMAScript shortest-decimal choices. The library applies the official Show rule +for appending `.0`; that rule is not part of the token primitive. + +MIR allocates the buffer, calls the provider, validates/recovers UTF-8 into a GC +String, and frees the buffer on normal completion. The contract guarantees length +fits capacity; an unchecked arbitrary library cannot inherit this guarantee. +Traps abort the command; there is no promise to resume with a reusable allocator +after a runtime trap. Retained GC Strings must survive subsequent buffer reuse. + +### Shared runtime storage and artifact preparation + +The selected formatter reservation is static data in `[65536, 131072)`, a 64 KiB +private stack in `[131072, 196608)`, and application allocation beginning at +196608. Low addresses retain canonical scratch/allocator-state ownership. +When the formatter is absent, its reservation is absent too. + +These values belong to the artifact storage contract, not scattered allocator +constants. The preparer verifies the linked Rust layout before relocating the +identified stack-pointer and heap-boundary globals. It preserves data offsets, +checks imported memory bounds, and records the resulting bytes/digest. The +consumer verifies actual globals, imports, exports, data ranges, feature needs, +and forbidden initialization against that contract. An unexplained table or +other export/import must be understood and declared, not blanket-accepted. + +The numeric runtime also exports complete-decimal binary64 conversion. Its +contract declares active function-table initializers by table index, constant +offset and complete function-index sequence; different or unsupported element +segments are rejected. Stack analysis begins at exported functions and the +start function and includes every reachable private callee. Private unreachable +formatting helpers do not create entry points. Exported tables are rejected +because they would expose additional entry points; reachable indirect calls, +imported calls, recursion and unrecognized frames remain errors. Definition-only +modules with no entry points retain conservative whole-module analysis. + +The pinned nonrecursive numeric runtime's maximum stack use must be established by +artifact analysis or an explicit reviewed build assumption plus stress evidence. +Checking the initial stack pointer alone does not prove a bound. Reentrancy, +callbacks, or a different runtime provider requires revisiting the storage +contract. For the current allocator, successful memory growth preserves existing +addresses and bytes: reservations stay below the fixed heap boundary and the +runtime stack pointer does not move. Block metadata is committed only after +growth succeeds, with unsigned overflow checks before capacity calculations. +Allocator execution checks the exact page boundary, growth failure and wrapped +free-list sizes; source execution interleaves a 70,000-byte canonical WASI +allocation with formatting and retained GC Strings. This establishes growth +under the current nonreentrant calling protocol. Additional artifacts need +disjoint reservations or an explicit shared allocator; the linker must not reuse +this fixed region blindly. + +This extends the strict "canonical ABI only" linear-memory clause of DEC-09/10 +to permit declared private runtime execution storage. Language values remain on +the GC heap; transient buffer ownership and canonical resource rules persist. + +### Reproducibility and rejected alternatives + +Compiler-owned runtime artifacts are embedded with pinned dependency and source +identities, Rust toolchain/target, build profile, linker flags, preparation recipe, +and SHA-256 of final bytes. Regeneration must reproduce those bytes or explicitly +update the reviewed provenance. Normal compiler builds use the checked artifact. +The stdlib lock still owns source-package revision/fingerprint; target artifacts +and WIT definitions have distinct records included in compile lineage. + +Rejected: native-only formatting, full JS-engine embedding for one primitive, +late attachment triggered by import names, duplicated per-consumer ABI tables, +universal shared-memory composition, and treating WIT definitions as executable +providers. These either fail to execute the source contract or discard a boundary +that the linker must verify. + +## Algorithms + +1. Load locked source, WIT, implementation catalog, and artifact provenance. + Reject stale or conflicting identities; perform no implicit downloads. +2. Validate source primitive/WIT schemes at the Core boundary and retain binding + origins and representation/ownership evidence through P9. +3. Lower target operations and result recovery to MIR. Emit explicit provider, + helper, allocator, and capability requirements alongside the lowered module. +4. After optimization, close live requirements, provider dependencies, and + required initializers. Reject absent or ambiguous implementations. +5. Validate actual artifact signatures/features, WIT exports/resource identities, + and initialization contracts. Check allowed residual host imports. +6. Compute memory owners/reservations and a legal instantiation schedule. Publish + a checked immutable plan only when all checks succeed. +7. Emit application Wasm using that plan. Verify emitted imports, memory, helper + exports, and required initialization against it. +8. Execute planned composition, validate the final component, and compare its + unresolved imports with the planned external world. Record artifact digests + and origins; return errors without publishing a successful artifact on failure. + +## Code map + +### Independent linker crate + +Create `psrs-linker` when implementing this contract. It has a real input/output +boundary: verified target definitions and requirements enter; a checked link plan +and composed artifact leave. Its responsibilities do not require PureScript +syntax, type inference, Core, CC, MIR, or backend-specific symbols. + +The dependency direction is: + +```mermaid +flowchart LR + driver["psrs-driver"] --> backend["psrs-backend"] + backend --> linker["psrs-linker"] + backend --> runtime["psrs-runtime catalog"] + linker --> runtime + backend --> ir["Core / HIR / shared utilities"] + linker --> tools["WIT / Wasm tools"] +``` + +`psrs-linker` must not depend on `psrs-backend`, `psrs-core`, `psrs-hir`, or MIR +definitions. In particular, no `for_mir(&mir::Module)` API belongs in it. The +backend converts its checked requirements into linker-owned target records; +it keeps the mapping from requirement identity to source symbols/spans. The +linker returns structured errors with requirement/artifact identities, and the +backend attaches language diagnostics and compile-stage attribution. Linker-only +tests can construct target artifacts without compiling PureScript. + +WIT definition loading and default-world resolution move from backend component +glue into `psrs-linker`. It returns one immutable resolved-world context consumed +by both backend ABI lowering and linker provider validation. Backend code may +use `wit-parser` operations on that context for canonical flattening; it does +not load a second copy or regenerate interface identities. Provider artifacts +must agree with the context and pinned definitions before plan publication. + +The target organization is: + +```text +psrs-hir/intrinsic/ semantic identities and source schemes +psrs-core/verify/ checked uses and external schemes +psrs-backend/bindings/ source contract + implementation requirements +psrs-backend/abi/ source/WIT validation, canonical conversion, ownership +psrs-backend/target_intrinsics/ exhaustive implementation selection +psrs-backend/target_runtime/ artifact language/transport contracts using runtime metadata +psrs-backend/linking/ checked IR-to-linker requests and diagnostic mapping +psrs-backend/mir/ raw calls, value recovery, explicit lifetimes +psrs-backend/wasm/ planned memory/import emission and encoding +psrs-linker/definitions/ WIT loading and immutable resolved-world context +psrs-linker/plan/ provider closure, memory and initialization planning +psrs-linker/compose/ ComponentEncoder integration and final validation +psrs-linker/verify/ artifact contracts and emitted-plan agreement +psrs-runtime/ immutable target catalog and target code +psrs-runtime/wit/ pinned definitions and default command world +psrs-runtime/artifact/ core-Wasm bytes, contracts and build provenance +psrs-stdlib/lib/ public APIs and ordinary PureScript wrappers +psrs-stdlib/conformance/ pinned official-JS observations and case engines +``` + +These are owners, not instructions to add empty crates or split every file. +The formatter slice is implemented in `psrs-linker` (`definitions`, `plan`, +`verify`, `compose`, `runtime`), `psrs-runtime` (catalog plus `wit/` and +`artifact/`), and the backend's `target_runtime` and `linking` modules. The +earlier backend-local `linker/` prototype and the backend `component` module +have been removed; WIT loading and composition now live in `psrs-linker`. + +### API and trust boundary + +The intended API is conceptually: + +```text +resolve_definitions(runtime_catalog, pinned_wit_inputs) + -> Result +plan(context, TargetLinkInput) + -> Result +compose(plan, encoded_application) + -> Result +``` + +`TargetLinkInput` contains linker-owned requirement identities, target signatures, +provider selections, artifact references, storage demands, initialization roots, +and the permitted target features/host interfaces. It contains no compiler IR +nodes or language TypeIds. The backend explicitly converts its capability profile +to this target policy rather than making the linker import backend types. + +The linker trusts the backend's source-type and representation checks; it does +not claim to repeat them. It checks raw contracts, WIT provider agreement, storage, +features, and composition itself. Core-binary-only callers must supply +an explicit binding/artifact contract; they cannot bypass planning by scanning +imports. Wasm emission and component assembly both require the same checked plan. +The plan exposes verified memory/import data needed by emission, while keeping +construction private to successful planning. Artifact preparation is a build +tool, not a compiler emission pass. + +## Invariants and verification + +- Each live foreign/primitive/helper requirement has one checked provider and + a diagnostic origin. Raw signature equality never proves source compatibility. +- WIT definitions and executable provider closure are separately complete. + Host imports equal the permitted residual capabilities, not every loaded WIT + interface and not merely the source application's direct imports. +- The same plan controls raw imports, memory ownership, initialization, and + final composition. No late encoding path changes it silently. +- Each shared pointer belongs to the planned memory/region; buffers obey + allocation/recovery/release protocols. Component pointers and resource handles + do not cross unrelated providers by integer reinterpretation. +- Artifact bytes and requirements match recorded digests/features. Storage and + initializer checks are distinct from assumptions about algorithm correctness, + stack depth, termination, and effects; trusted assumptions are recorded. +- An unneeded provider contributes neither bytes nor its host dependencies. + Observable initializer effects are retained by explicit policy and live edges. +- Failed binding, preparation, planning, composition, or validation does not + produce a successful artifact or an empty fallback implementation. + +Validation covers malformed source bindings, raw artifact ABI/storage failures, +WIT version/provider/resource conflicts, legal and illegal initialization +dependencies, and final import closure. Runtime evidence must combine formatting, +buffer reuse, GC retention, and WASI calls to exercise the shared-memory boundary. +Acceptance obligations are tracked in +[the implementation record](../../../implementation/backend/linking-and-runtime.md). + +## Worked example + +For `log (show 1.0e21)`, the library Show instance calls its token primitive. +Core checks `Number -> String`. The catalog selects the formatter artifact; MIR +allocates output, calls its raw export, recovers the GC String, and releases +output. The library receives `"1e+21"` and leaves it unchanged. Console output +uses the independently checked WASI streams binding and canonical buffers. + +The plan includes formatter storage and the reachable WASI host services. The +application is instantiated with call shims and owns canonical memory. The +runtime is then instantiated with that memory; its data initializes only its +reserved range. Shims are resolved before the command runs. Formatting's private +core import is closed; WASI interfaces remain in the external world. A live +String remains valid after the formatter buffer is freed and reused by stdout. + +If the streams interface instead has an explicitly selected guest WIT provider, +that provider is composed through its interface, memory, and resource instance. +Its own allowed host dependencies join the external world. This is not achieved +by passing the application's output buffer pointer into the guest memory. + +## Boundaries and interfaces + +The driver supplies source package provenance and checked declarations. HIR/Core +supply primitive identities/types. ABI lowering supplies checked source/WIT +contracts and recovery operations. MIR optimization preserves their identities +and updates live requirements. The linker supplies a checked target plan; Wasm +encoding supplies conforming bytes. Component assembly consumes both and emits +the validated final artifact with the planned external world. + +Compile diagnosis should record selected provider identities, WIT/artifact +digests, plan inputs, checks, and failures as explicit lineage alongside MIR and +binary artifacts. Runtime observations remain separate evidence. A valid link +plan proves binding/layout checks, not official-library semantic conformance. + +## Supported component entry and future work + +The initial guest entry point is `psrs link application.wasm --manifest +providers.json -o linked.wasm [--report report.json]`. It consumes an already +validated application component, pinned executable guest components and explicit +whole-interface bindings. The version-1 manifest and report are specified in +[Explicit Component Linking](../../../workflow/component-linking.md). +`psrs build` owns source compilation and accepts `--manifest` explicitly after +completing the checked core runtime plan. It computes the produced application's +pin rather than requiring the user to predict its bytes. An optional supplied +root pin is still checked. `--report` joins the source diagnosis artifact to +component composition by exact digest. Standalone `psrs link` requires all pins; +neither command discovers providers. + +`psrs-linker::guest::plan_components` uses the pinned `wasm-compose` 0.245.1 +in-memory graph API. Every participating component is parsed with the same +validator so cross-component resource identities share one type universe. +Closure starts at the application, follows explicit guest edges, and retains +only permitted unbound interface instances as host imports. Unused candidates +are omitted. Duplicate bindings, digest drift, incompatible interfaces, missing +exports and instantiation cycles fail closed. An explicitly selected guest +never falls back to the host. Canonical adaptation preserves each component's +memory, realloc and post-return operations through typed instance connections. + +The core `CheckedLinkPlan` remains bound to one immutable resolved WIT context, +raw import signatures and memory reservations. Composition independently checks +the actual application's imports, minimum memory and active initialization +before attaching core libraries. Component composition uses a distinct sealed +`CheckedComponentPlan`: its memories remain component-owned, so it does not +fabricate an application-wide linear memory plan. The component plan holds the +validated graph encoding, exact root digest, selected binding edges, artifact +pins, validator feature policy and residual host imports. Emission accepts only +the root bytes checked by that plan. Both plans are linker-owned target records; +neither contains compiler IR or reinterprets source types. + +Guest graph closure requires exact equality with the encoded outer imports. +WIT type dependencies are not independent executable capabilities. The core-to-component path derives the same projection by encoding the planned +WIT function signatures in a nonexecuted import-only module. The authoritative +component encoder determines resource-owner imports, without expanding unused +interface methods. Final composition independently requires exact import-set +equality with this projection. Every retained resource owner must also satisfy +world membership and target policy. + +Only synchronous whole-interface instance imports with matching canonical +interface names are supported by the initial entry point. Non-interface +imports, implicit version adaptation, partial method providers, cyclic +instantiation, dynamic discovery and asynchronous composition require explicit +extensions. A definition-only WIT package is not an executable provider. + +The runtime stack bound is measured by a static call-graph analysis of the +pinned nonrecursive artifact, and the artifact-provenance representation is +settled as a catalog record; repeat builds are checked against the committed +bytes. Relocatable object files, dynamic loading, async/WASI 0.3 composition, +recursive or reentrant runtime libraries, and cross-module GC sharing require +explicit extensions. + +Existing WIT/WASI binding code precedes this plan model. The numeric runtime slice now +carries one checked plan from requirement closure through artifact verification, +Wasm emission, and component assembly, with a target-only linker test suite and +end-to-end formatter and decimal-conversion execution tests. The stack bound is measured over a restricted +frame protocol: one constant prologue, an immutable saved frame and checked restoration before returning. +Reachable unrecognized stack-pointer access, indirect/imported calls, recursion, exception +unwinding and suspension are rejected. The declared stack-pointer global must +be mutable and initialize at the reserved stack top. These checks establish the +numeric runtime bound; they do not establish arbitrary runtime-library memory safety. +Guest execution evidence is recorded separately from the numeric runtime slice. + +## References + +- [DEC-18 — Unified Target Linking](../../../decision/DEC-18-unified-target-linking.md). +- [DEC-12 — Resolved WIT Bindings](../../../decision/DEC-12-resolved-wit-bindings.md), + [DEC-13 — WIT to Source Mapping](../../../decision/DEC-13-wit-to-source-type-mapping.md), + [DEC-14 — Resource Ownership](../../../decision/DEC-14-resource-handle-ownership.md). +- [Component Model explainer](https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md) + and [WIT specification](https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md) + for interface definitions, canonical boundaries, and typed composition. +- [ryu-js](https://github.com/boa-dev/ryu-js) for the selected formatter implementation. + +### Consumer signatures and implementation selection + +The exhaustive [intrinsic implementation catalog](../fp/intrinsic-implementations.md) +owns direct/generated/artifact selection. HIR owns language schemes and semantic +effects; the runtime owns raw export contracts and transport protocols. CC keeps +the intrinsic identity on artifact calls until MIR discharges value transport. + +An artifact requirement's expected signature MUST come from the actual MIR +consumer import, independently of the runtime provider descriptor. The planner +checks it against both the provider contract and the actual artifact export. +Generated allocator/codec imports MUST likewise match the shared generated-body +signature and concrete GC byte-array representation. Internally consistent MIR +call typing alone does not establish either provider boundary. diff --git a/docs/design/backend/wasm/primitive-ffi-and-stdlib.md b/docs/design/backend/wasm/primitive-ffi-and-stdlib.md index 4ebb5ec2..d908aba3 100644 --- a/docs/design/backend/wasm/primitive-ffi-and-stdlib.md +++ b/docs/design/backend/wasm/primitive-ffi-and-stdlib.md @@ -13,6 +13,14 @@ the lowerer refuses to recognize, and how a wrapper encodes and decodes library types. It is the mechanism behind [DEC-11](../../../decision/DEC-11-primitive-ffi-stdlib-wrappers.md). +That contract applies to explicitly bound WIT imports. Ordinary upstream library +`foreign import name :: Type` declarations keep their source contract under +[foreign imports](../../frontend/semantics/foreign-imports.md) and +[source fidelity](../../../workflow/stdlib-vendoring.md). They are not implicit +WASI functions or subject to a fabricated canonical ABI. Until a checked target +implementation exists, P8 library linking rejects them explicitly; retaining a +declaration does not establish runtime support. + It does not own canonical flattening, `lift`/`lower`, or `cabi_realloc` ([canonical ABI and WIT](canonical-abi-and-wit.md)), buffer lifetime ([linear memory boundary](linear-memory-and-canonical-abi-boundary.md), @@ -63,6 +71,26 @@ mapping does not learn them. ## Model +### Intrinsic target implementations and linking + +`psrs:intrinsic#` selects a checked language primitive, not a raw runtime +function. An implementation may be a direct Wasm operation, a generated helper, +or an embedded target-library export. Backend implementation descriptors join +the HIR identity to the raw export signature and recovery protocol; a target +link plan verifies reachable imports, validates the runtime artifact, reserves +shared-memory regions, and schedules instantiation. See +[the linking design](linking-and-runtime.md) and +[DEC-18](../../../decision/DEC-18-unified-target-linking.md). The formatter slice +implements this contract. + +For `numberToString :: Number -> String`, the runtime receives a binary64 value +and a caller-owned 32-byte output buffer, returns its initialized UTF-8 length, +and retains nothing. MIR recovers a GC String and releases the temporary buffer. +The linker packages the pinned formatter and closes the private import inside +the emitted component. `Data.Show` keeps the official `.0` wrapper and ordinary +PureScript escaping and array callbacks in the library. The formatter's raw +signature is never substituted for the primitive's language signature. + ### The primitive set A foreign import the lowerer accepts uses only these source types: @@ -341,7 +369,7 @@ application world, and embedding stay in [canonical ABI and WIT](canonical-abi-and-wit.md). ```text -stdlib/lib/ +psrs-stdlib/lib/ Prelude.purs the Effect interface: pure, bind, runEffect, trap Data/Function.purs const, flip, apply, applyFlipped, on, $, # Data/Semigroup.purs class Semigroup, append, <> @@ -576,6 +604,11 @@ not in `Prelude` and not compiler builtins. Nullary enum, closed record, and flags-record foreign imports still lower; new library code should not use that path. Handles are declared as the `Resource a` newtype over `Int`. +`Data.Tuple.Tuple` is an ordinary two-field ADT. Its `Eq` instance compares +both fields, its `Ord` instance compares them lexicographically, and its `Show` +instance renders `(Tuple )` using each field's `Show` instance. +These source instances do not change native tuple syntax or WIT tuple layout. + ## References - [DEC-11 — Primitive foreign imports and standard-library wrappers](../../../decision/DEC-11-primitive-ffi-stdlib-wrappers.md). @@ -588,3 +621,91 @@ path. Handles are declared as the `Resource a` newtype over `Int`. `string`, and tuples. - Issue #56, whose request to grow compiler source types for `Maybe`, `Either`, and tuples this contract replaces. The user-facing types remain library types. + +## Primitive callable and array storage boundaries + +An explicit primitive binding retains its checked source signature. P8 reads +leading lexical quantifiers through Core's shared scheme operation, preserves +their identities in the declaration scope, and verifies the generated primitive +body. Unsupported categories and invalid signatures fail even when unused. +The complete candidate is published only after successful verification. + +Core then expands each checked primitive global occurrence into a typed lambda +at that occurrence's already checked type. This is mandatory elaboration, +independent of P7 optimization budgets. Shared Core local allocation prevents +capture. It handles bare values and partial/saturated applications with the +ordinary lambda/application path. A polymorphic array wrapper followed by an +array-mapping ABI adapter is insufficient for mutation: the adapter can copy the +array and direct writes at the copy. Occurrence expansion retains the exact +array representation required by the call, including aliases and partial calls. + +The compiler provides small storage operations, not one intrinsic per stdlib +algorithm. `arrayFill :: forall a. Int -> a -> Array a` allocates a fresh array +fully initialized with one checked value; a negative length traps. Its MIR form +validates the length, initializer, storage and destination types and lowers to +Wasm GC `array.new`. It never exposes uninitialized or default-null logical +elements. `arrayWrite :: forall a. Array a -> Int -> a -> Array a` is an unsafe +in-place write and returns the same array; invalid indices trap. Core checks +all element relationships and MIR preserves the write as an observable effect. +The raw writes are private to target library code with fresh output ownership. + +`psrs-stdlib/lib/PSRS/Array.purs` owns `arrayApply`, `arrayBind` and +`arrayExtend`. Original +Prelude and Control exports and public signatures remain; only their foreign +implementation +slots delegate to this target module. Class/instance and other pure code remain +official. No compiler registry entry, CC operation, callback invoker or MIR loop +is dedicated to any of these algorithms. Recursion and callbacks use ordinary +checked +PureScript calls, closure adaptation and tail-call lowering. + +The library applies functions in function-major order, caching each function +for its value traversal and invoking it once per pair. Bind visits inputs in +order, invokes each callback once, snapshots its returned array immediately, +and flattens the snapshots. The immediate shallow copy preserves the official +behavior when a later callback mutates a previously returned array. Extend +visits indices in increasing order, invokes its callback once per index, and +passes a fresh shallow suffix copy, so callback mutation cannot change the source +array. Storage and +copy work are linear in input/result size. Array apply checks signed-i32 product +capacity before callbacks; bind checks cumulative capacity before accepting a +chunk. Allocation exhaustion and unrepresentable sizes trap as target resource +boundaries. Empty inputs invoke no callbacks. + +Primitive additions require a runtime/storage justification and checked contracts. +Missing JS FFI alone does not justify a new whole-function intrinsic. Generic +Wasm/WASI adaptation belongs in source library code whenever these operations +and ordinary language features can express it. See the independent package's +`docs/array-kernels.md` and official JS generators for behavior evidence. Focused +observations do not establish whole-library compile or runtime/FFI acceptance. + +`Control.Monad.ST` follows the same boundary without a new intrinsic. The target +module `PSRS.ST` represents an action as a suspended `Unit -> a` thunk and a +reference as a fresh one-element mutable array over the private storage +primitives. `Control.Monad.ST.Internal` keeps the official `ST`/`STRef` newtypes +and every class instance, because this compiler does not resolve an instance +whose type is imported from another module; each operation is a thin adapter over +the target module. `STFn{N}` stays abstract as a newtype over the curried action +so rank-2 `STFn` arguments typecheck. The target's `ST` thunk and the compiler's +scalar-token `Effect` closure are not representationally equal, so +`Control.Monad.ST.Global.toEffect`'s `unsafeCoerce` is linked but not sound. See +the independent package's `docs/st.md`. + +Uncurried functions follow the same representation rule. `Data.Function.Uncurried` +keeps `Fn1` as a synonym and represents `Fn0` and `Fn2`..`Fn10` as abstract +newtypes over the curried function, so `mkFn{N}`/`runFn{N}` adapt currying and +rank-2 arguments stay abstract. This is a prerequisite for `Data.Array`, whose +foreign signatures use `Fn2` and `Fn3`. See the independent package's +`docs/function-uncurried.md`. + +`Data.Array`'s algorithms follow the same boundary. The Prelude-free target +module `PSRS.Array` owns them over the private storage primitives; the official +module delegates each foreign slot. Slots whose private signature carries a +rank-2 `Maybe` constructor or observer are written monomorphically and the loop +is inlined, leaving the public API unchanged, and `Data.Array.NonEmpty.Internal`'s +`traverse1` is implemented in the `Traversable1` instance over the `Applicative` +dictionary. See the independent package's `docs/array-operations.md`. A compiler +obligation is recorded there: a recursive loop that writes to its output array +only in one `if` branch produced wrong results; loops must write in a single +tail call and select only the value in the branch. `unsafeIndex` out of range +traps rather than returning JavaScript `undefined`. diff --git a/docs/design/backend/wasm/wasi-platform-library.md b/docs/design/backend/wasm/wasi-platform-library.md index 07c74b4d..70880d25 100644 --- a/docs/design/backend/wasm/wasi-platform-library.md +++ b/docs/design/backend/wasm/wasi-platform-library.md @@ -7,6 +7,17 @@ ## Scope +The proposed [target linker](linking-and-runtime.md) owns artifact providers, +dependency closure, storage, and composition schedules. This document owns the +WASI command world and service policy. Private runtime imports must be closed +inside the final component; only permitted residual host interfaces appear in +its external world. General guest WIT provider composition is not yet supported. + +Pinned WIT dependencies and the default command-world source are planned runtime +package assets. `psrs-linker` resolves them, and the backend supplies its target +capability policy and checked canonical call requirements; +catalog ownership does not make `psrs-runtime` the implementation of WASI. + This document owns the artifact shape, the application world, componentization with `wit-component`, the enabled WASI services, and validation and execution of the component. It does not own canonical ABI adaptation and call lowering @@ -86,12 +97,11 @@ world command { `wasi:io/streams` transitively pulls in the support interfaces `wasi:io/error@0.2.12` and `wasi:io/poll@0.2.12`. The `wasi:cli` terminal and `sockets/ip-name-lookup` interfaces are accepted but not wrapped by the platform -library. `component.rs` records the -full resolved set in `COMPONENT_INTERFACES`, next to the world, so ABI discovery -and component encoding share one contract; a test asserts the resolved world -matches that list exactly and imports only named interfaces. The vendored WASI -0.2.12 WIT is loaded in dependency order by `load_vendored_wasi`, and -`command_world` resolves `psrs:app` against it. +library. The `psrs-linker` resolved default world records the full set, and the +backend derives the permitted interface set from it, so ABI discovery and +component encoding share one contract; a test asserts the resolved world imports +only named interfaces. The pinned WASI 0.2.12 WIT is loaded from the +`psrs-runtime` catalog by `psrs-linker::resolve_default_definitions`. ### Enabled services @@ -128,7 +138,7 @@ wrapper owns the corpus-facing name. | `Data.Function` | `apply`, `applyFlipped`, `const`, `flip`, `on`, `$`, `#` | nothing: it is the definition site, and `$`/`#` are its fixity aliases | FE-05 | | `Data.Semigroup` | `class Semigroup`, `append`, `<>` | nothing: it is the definition site for the class, and `append` for `String` is built from the compiler's `stringToBytes` / `arrayAppend` / `bytesToString`; `append` for `Array a` is `arrayAppend` | FE-14, BE-10 | | `Data.Monoid` | `class Monoid`, `mempty` | `Data.Semigroup`; `String`, `Unit`, and `Array a` identities | FE-14 | -| `Data.Foldable` | `class Foldable`, `foldr`, `foldl`, `foldMap` | `Data.Monoid` and the array index primitives; `Array`, `Maybe`, and `Either a` instances | FE-14, FE-16 | +| `Data.Foldable` | `class Foldable`, `foldr`, `foldl`, `foldMap` | `Data.Monoid` and the array index primitives; `Array`, `Maybe`, and `Either a` instances | FE-14 | | `Data.Tuple` | `Tuple`, `fst`, `snd`, `curry`, `uncurry`, `swap` | the closed record `{ _1 :: a, _2 :: b }` that FE-06 already lowers a tuple to; `type Tuple a b` is that record, not an algebraic `data Tuple a b = Tuple a b` | FE-06 | | `Effect` | re-exports the `Prelude` surface above | `Prelude` | FE-02 | | `Effect.Console` | `log`, `warn`, `error`, `logShow` | `WASI.Console`, and the library `Data.Show.show` for `logShow` | BE-21 | @@ -145,7 +155,7 @@ consolidated capability layout: `WASI.Resource`, `WASI.IO`, `WASI.Console`, | Feature | Owner | State | | --- | --- | --- | -| On-disk `stdlib/lib` and the trusted prefix | WASI-10 | Done. The driver reads `stdlib/lib/trusted`. | +| On-disk `psrs-stdlib/lib` and the trusted prefix | WASI-10 | Done. The driver reads `psrs-stdlib/lib/trusted`. | | Exported wrappers | This library, [DEC-11](../../../decision/DEC-11-primitive-ffi-stdlib-wrappers.md) | Every service wrapper, plus the `WASI` umbrella. Raw imports stay unexported. | | `wasi:cli/exit.exit` (`status: result`) | Not wrapped | One canonical `i32`, and still not a library wrapper. See below. | | `Effect` as `foreign import data` | [Effects](../fp/effects.md) | Trusted library binding carries the resolved constructor and operation identities. Effect lowering turns applications into generic one-parameter closures, and import wrappers come only from plans formed from checked external schemes before `Effect` erasure. | @@ -187,17 +197,18 @@ pruned so an unused service adds no import. ### Componentization -`componentize(core, resolve, world)` embeds component metadata describing the -world the core module implements, then encodes the component: +`compose(context, plan, core)` embeds component metadata describing the +world the core module implements, attaches the plan's verified libraries, then +encodes the component: ```text -componentize(core, resolve, world): +compose(context, plan, core): bytes = core - embed_component_metadata(bytes, resolve, world, StringEncoding::UTF8) - ComponentEncoder::default() - .module(bytes)? - .validate(true) - .encode() + embed_component_metadata(bytes, context.resolve(), context.world(), StringEncoding::UTF8) + encoder = ComponentEncoder::default().module(bytes)? + for artifact in plan.artifacts(): + encoder = encoder.library(artifact.module_name, artifact.bytes, ...)? + encoder.validate(true).encode() ``` `embed_component_metadata` records how the core module's `(module, field)` @@ -232,8 +243,8 @@ observes the trailing constant, which exists to give the entry its declared ### The platform library -The platform library is source code under `stdlib/lib`, read from disk and -resolved, type-checked, and linked like any module. `stdlib/lib/trusted` fixes +The platform library is source code under `psrs-stdlib/lib`, read from disk and +resolved, type-checked, and linked like any module. `psrs-stdlib/lib/trusted` fixes the trusted prefix order (`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.Tuple`, `Data.Foldable`, `WASI.Resource`, `WASI.IO`, `WASI.Clock`, `WASI.Random`, `WASI.Console`, `WASI.Process`, `WASI.FileSystem`, `WASI.Network`, `WASI`). `Data.Function` declares the application operators and their fixities; `Data.Semigroup` @@ -293,11 +304,12 @@ that contract into the component world. ```text compile(program, target): cc = lower Typed Core to CC with ExternalBindings - (mir, wasi) = lower CC to MIR, resolving and validating WIT bindings + context = psrs_linker::resolve_default_definitions() + (mir, wasi) = lower CC to MIR over context's shared resolve wasm = structure MIR, synthesize the run entry, allocate indices core = encode_module(wasm) - (resolve, w) = command_world() - component = componentize(core, resolve, w) + plan = psrs_linker::plan(context, target requirements + artifacts) + component = psrs_linker::compose(context, plan, core) validate_all(component) with features from `target` wat = wasmprinter::print_bytes(component) return Artifact { wasm: component, wat } @@ -338,11 +350,17 @@ representation the imports ride on stays in [effects](../fp/effects.md). ```text -crates/psrs-backend/ +crates/psrs-runtime/ wit/psrs-app.wit wit/deps/ - src/ - component.rs + artifact/psrs_runtime.wasm +crates/psrs-linker/ + definitions.rs WIT loading and resolved-world context + plan.rs provider closure, memory and initialization planning + verify/ artifact contracts and emitted-plan agreement + compose.rs component assembly and final import closure +crates/psrs-backend/src/ + linking/ checked requirements and diagnostic mapping abi.rs abi/wasi.rs wasm/lower/mod.rs @@ -353,20 +371,15 @@ crates/psrs-driver/src/wasi.rs Responsibilities and required entry points: -- `component.rs` owns the application world and componentization. It must define - the `psrs:app` `command` world, the resolved interface set - `COMPONENT_INTERFACES`, the command entry name `RUN_CORE_EXPORT`, and the two - required entry points: - - `fn command_world() -> (Resolve, WorldId)`: load the vendored WASI 0.2.12 - WIT in dependency order and resolve `psrs:app` against it. - - `fn componentize(core: &[u8], resolve: &Resolve, world: WorldId) -> Result>`: - embed component metadata with `StringEncoding::UTF8`, lift the core module's - imports and exports, and validate the encoded component. The enabled service - set is the `psrs-app` `command` world: `wasi:io/error`, `wasi:io/poll`, - `wasi:io/streams`, `wasi:clocks/monotonic-clock`, `wasi:clocks/wall-clock`, - `wasi:random/random`, `wasi:random/insecure`, `wasi:random/insecure-seed`, - `wasi:cli/environment`, `wasi:cli/exit`, `wasi:cli/stdin`, - `wasi:cli/stdout`, `wasi:cli/stderr`, `wasi:filesystem/types`, +- `psrs-runtime/wit/` owns the `psrs:app` `command` world and the pinned WASI + 0.2.12 WIT; `psrs-linker` resolves it and owns componentization. The backend + `linking` module supplies the target policy and checked requirements and maps + link failures to source diagnostics. The enabled service set is the + `psrs-app` `command` world: `wasi:io/error`, `wasi:io/poll`, + `wasi:io/streams`, `wasi:clocks/monotonic-clock`, `wasi:clocks/wall-clock`, + `wasi:random/random`, `wasi:random/insecure`, `wasi:random/insecure-seed`, + `wasi:cli/environment`, `wasi:cli/exit`, `wasi:cli/stdin`, + `wasi:cli/stdout`, `wasi:cli/stderr`, `wasi:filesystem/types`, `wasi:filesystem/preopens`, `wasi:sockets/network`, `wasi:sockets/instance-network`, `wasi:sockets/udp`, `wasi:sockets/udp-create-socket`, `wasi:sockets/tcp`, and @@ -379,25 +392,27 @@ Responsibilities and required entry points: per-service host function. - `wasm/lower/mod.rs` must synthesize the `run` entry that calls `main`, passes the result to `wasi:cli/exit.exit-with-code`, and returns `0`. -- `lib.rs` must own the build pipeline: lower to CC, lower to MIR, structure, - encode, call `command_world` and `componentize`, validate with the target's - features, and return the component and its WAT form. +- `lib.rs` must own the build pipeline: lower to CC, lower to MIR over the + resolved-world context, structure, encode, build and check the target link + plan, compose, validate with the target's features, and return the component + and its WAT form. - The WASI library must be ordinary PureScript source resolved, type-checked, - and linked like any other module. The driver loads it from `stdlib/lib` + and linked like any other module. The driver loads it from `psrs-stdlib/lib` rather than embedding it, and defines `log`, `error`, `now`, `randomBytes`, `randomU64`, `exitWithCode`, and `arguments` over WIT imports; the portable `Prelude` must not import WASI. - `psrs-cli/src/main.rs` must expose `psrs build`, `psrs wat`, and `psrs dump`. -- `wit/psrs-app.wit` and `wit/deps/` own the vendored WASI 0.2.12 WIT sources. +- `psrs-runtime/wit/psrs-app.wit` and `psrs-runtime/wit/deps/` own the pinned + WASI 0.2.12 WIT sources. ## Invariants and verification The component test suite checks that the resolved application world imports -exactly `COMPONENT_INTERFACES` and only named interfaces, that a minimal core -module componentizes and validates, and, when `wasmtime` is available, that -`wasmtime run` executes the component. The build pipeline validates the encoded -component with the profile's features before returning it +only named interfaces, that a minimal core module composes and validates, and, +when `wasmtime` is available, that `wasmtime run` executes the component. The +build pipeline validates the encoded component with the profile's features +before returning it ([capability profile](capability-profile.md)). Observable behavior is checked by execution tests that run the component under the pinned runtime and compare standard output and the process exit code; structural validation alone is not @@ -413,7 +428,7 @@ produces the Canonical ABI call shown in [canonical ABI and WIT](canonical-abi-and-wit.md), and the string literal lives in a data segment ([linear memory boundary](linear-memory-and-canonical-abi-boundary.md)). The generated command wrapper runs the selected action once and returns zero. -`componentize` lifts the core module: the component imports +`compose` lifts the core module: the component imports `wasi:cli/stdout@0.2.12` and `wasi:io/streams@0.2.12` (plus their support interfaces) and exports `wasi:cli/run@0.2.12`. `wasmtime run hello.wasm` calls `run`, which writes `hello\n` and exits successfully. @@ -425,8 +440,8 @@ interfaces) and exports `wasi:cli/run@0.2.12`. `wasmtime run hello.wasm` calls - **Output:** a validated component artifact and its WAT form. - **To the ABI layer:** the world and the interface set it may import; a service outside the world is rejected before MIR emits a call. -- **To the driver:** `command_world` and `componentize`; the CLI writes the - artifact and the WAT form. +- **To the driver:** the compiled component artifact; the CLI writes the + artifact and the WAT form. WIT loading and composition live in `psrs-linker`. ## Open questions and future work @@ -449,7 +464,7 @@ wrapped. `WASI.Process.arguments` and `WASI.Process.environment` wrap with execution tests. `WASI.Network` wraps the socket services and lowers; it has no execution test, and HTTP/TLS are not implemented, so their capability flags stay disabled in the default profile. The standard library is read from -`stdlib/lib` at runtime (`stdlib/lib/trusted` lists `Prelude`, `Data.Function`, +`psrs-stdlib/lib` at runtime (`psrs-stdlib/lib/trusted` lists `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.Tuple`, `Data.Foldable`, `WASI.Resource`, `WASI.IO`, `WASI.Clock`, `WASI.Random`, `WASI.Console`, `WASI.Process`, `WASI.FileSystem`, `WASI.Network`, and `WASI` in trusted-prefix order). The driver discovers user modules from the entry files' directories (`psrs_driver::load_program_files`): it indexes sibling `.purs` diff --git a/docs/design/frontend/README.md b/docs/design/frontend/README.md index a7380c4b..1cc94c6c 100644 --- a/docs/design/frontend/README.md +++ b/docs/design/frontend/README.md @@ -45,6 +45,7 @@ flowchart LR | [Kinds](type-system/kinds.md) | Kinds, constructor application, synonym legality | Checked kinds | | [Type inference](type-system/type-inference.md) | Schemes, unification, signatures, typed terms | THIR types | | [Classes and evidence](type-system/classes-and-evidence.md) | Constraint solving, coherence, explicit dictionaries | THIR evidence | +| [Deriving](type-system/deriving.md) | Known deriving rules, field usage, generated members | Generated instance members | | [Rows and records](type-system/rows-and-records.md) | Row unification and record typing | Typed row operations | Each arrow is an explicit conversion or a verified same-representation pass. diff --git a/docs/design/frontend/semantics/desugaring.md b/docs/design/frontend/semantics/desugaring.md index 10d5a1a5..115be3c7 100644 --- a/docs/design/frontend/semantics/desugaring.md +++ b/docs/design/frontend/semantics/desugaring.md @@ -15,8 +15,13 @@ in-scope `negate` name. P4 rewrites surface constructs into a smaller resolved HIR, applying fixities, lowering unary minus, guards, equations, `do`/`ado`, and `where` scope while preserving source order and origins for P5 diagnostics. -The generated case and equation products use exact record patterns, so P5 -infers a closed row for the helper product. A source record pattern such as +The generated case and equation products retain an explicit `MatchProduct` +expression through P3 and P4, with exact record patterns. Each field is an +independent scrutinee, so P5 preserves a local field's structural `forall` +unless the corresponding source patterns require a monotype. P5 converts the +checked product into an ordinary typed record with a closed row. A source +record literal still instantiates its field expressions normally; its fields +must not acquire independent pattern-scrutinee polymorphism. A source record pattern such as `{ field }` remains partial and may leave the row tail open. This distinction keeps compiler-created products concrete without changing source record matching. @@ -105,8 +110,20 @@ remain as resolved `Typed` expressions for P5. Every rewrite evaluates source operands in the order defined by the language. A failed guard proceeds to the next guard without evaluating that guard's body. A generated temporary binds an expression once when duplication would change -evaluation. P4 preserves the source span of every retained user expression; -generated scaffolding points to the construct that introduced it. +evaluation. The saved scrutinee is bound in an outer `let`, and fallthrough helpers share +an inner `let` with the case. They refer to outer locals and the saved product +directly. Helpers exist only for rows after the first guarded row, including the +final failure continuation. Earlier rows are checked directly as case branches +and have no unused helper copies: such copies would create additional inferred +class obligations without the original branch's expected result type. +Separating the saved value from the helper binding group preserves +its scope without making that group recursive. Its calls +pass an empty token to delay evaluation, rather than passing the product's +polymorphic fields through a newly inferred helper parameter. Passing one of those locals in as a value +argument would instantiate a polymorphic scheme once, before the guard body +applies the arguments that determine its constraints. P4 preserves the source +span of every retained user expression; generated scaffolding points to the +construct that introduced it. Rejected alternatives: desugaring operators in P2 cannot respect imported fixities; waiting until MIR would discard useful source types and spans; and diff --git a/docs/design/frontend/semantics/foreign-imports.md b/docs/design/frontend/semantics/foreign-imports.md index 9e405e04..0399871c 100644 --- a/docs/design/frontend/semantics/foreign-imports.md +++ b/docs/design/frontend/semantics/foreign-imports.md @@ -11,12 +11,35 @@ handles, and [modules and resolution](modules-and-resolution.md). Read [canonical ABI](../../backend/wasm/canonical-abi-and-wit.md) first. Roadmap row FE-19 tracks this surface. -**Summary:** A `foreign import` names either a WIT function or an opaque type. +**Summary:** A `foreign import` declares a library foreign value, explicitly +names a WIT function, or introduces an opaque type. `foreign import data T :: Kind` introduces a nominal type with no constructors. A nullary opaque type is the source image of a WIT resource; `Int` remains accepted at the ABI boundary so existing integer placeholders keep compiling. JavaScript FFI is not part of this surface. +An ordinary `foreign import name :: Type` retains its source-declared signature +and declaring-module identity. AST stores the absence of an explicit target +binding; resolution produces a library external rather than inventing a WIT +name or a source body. Type checking, imports, operator fixities, and checked +external signatures apply normally. Foreign value signatures cannot contain +class constraints, matching the official source restriction. Backend linking must +reject a library value without a registered target implementation explicitly. +This supports faithful source inventory and checking, not JavaScript execution +or an assertion that all library values already have Wasm implementations. + +An explicit `"psrs:intrinsic#operation"` binding selects an operation from the +authoritative intrinsic registry. Resolution records `Primitive(Intrinsic)`; +the declaration still owns its source type and symbol. P8 linking generates an +ordinary typed Core function, checks its operand and result identities with the +existing Core intrinsic verifier, and discharges the external only after those +checks succeed. The generated function supports ordinary first-class use and +partial application. A wrong type is rejected even if the binding is unused. +The initial foreign-binding implementation covers monomorphic unary and binary +scalar operations. Other registered operations remain explicit unsupported +bindings until their full checked implementation exists. The binding string, +never the source function's name or declaring module, selects the operation. + ## Scope This document owns foreign value imports and foreign data declarations from @@ -50,14 +73,18 @@ corresponds to a handle. ## Model ```text -ForeignValue = { name, binding: Interface "#" Function, type, span } +ForeignValue = { name, binding: Optional(Interface "#" Function), type, span } +Binding = WIT(Interface, Function) | Primitive(Intrinsic) | Library(Module, Symbol) ForeignData = { name, kind, span } OpaqueType = nominal TypeId with no constructors SourceResource = { type_id: TypeId } ``` `ForeignValue` is a value-namespace declaration. Its binding text is -`#` and is metadata, not a term. `ForeignData` is a +`#` when present and is metadata, not a term. An absent +binding identifies an ordinary library declaration by its declaring module +and value symbol. Explicit source declarations and imports take precedence +over bootstrap intrinsic names. `ForeignData` is a type-namespace declaration. The type expression after `::` is the kind, not a value type. The kind may be `Type` or an arrow such as `Type -> Type`. There are no type parameters beside that kind, and there is no value constructor. @@ -93,7 +120,8 @@ Foreign data is lowered as a type declaration, not as a foreign value. The CST already records the optional `data` keyword and the kind expression; lowering rejects a WIT binding string on a data declaration, because a resource's identity is the nominal type rather than a function name. A foreign value -import still requires `#`. +import preserves either an explicit `#` binding or its +absence for a library declaration. Resolution allocates a type id and no value symbol. The type name occupies the uppercase namespace, so it conflicts with another type of the same name and @@ -141,7 +169,7 @@ lower_foreign(cst): reject a binding string emit ForeignData { name, kind: lower(cst.kind), span } else: - require a "#" binding + preserve an optional "#" binding emit ForeignValue { name, binding, type, span } resolve_module: @@ -166,7 +194,7 @@ validate_handle(source): ``` Edge cases: a binding string on foreign data is a lowering error; a missing -binding on a foreign value is a lowering error; a duplicate type name is a +binding on a foreign value denotes a library declaration; a duplicate type name is a declaration conflict; an imported opaque type stays opaque under an alias; a higher-kinded foreign constructor used without enough arguments fails kind checking rather than becoming a handle. diff --git a/docs/design/frontend/semantics/modules-and-resolution.md b/docs/design/frontend/semantics/modules-and-resolution.md index 580050a5..ae54b30c 100644 --- a/docs/design/frontend/semantics/modules-and-resolution.md +++ b/docs/design/frontend/semantics/modules-and-resolution.md @@ -57,8 +57,15 @@ The driver loads the transitive source graph, checks duplicate module names and cycles according to the source language's module rules, then resolves modules in dependency order. P3 first registers declarations and their namespaces, then resolves bodies so same-module references and recursive groups can name -their final IDs. Qualified lookup uses only the named imported module; -unqualified lookup combines local declarations and permitted imports and +their final IDs. Qualified lookup combines the selected names from all imports +under that qualifier. Several imports may share an explicit alias. A member is +ambiguous only when its namespace contains distinct declaration identities; +multiple paths to the same declaration remain one member. Merely declaring a +shared qualifier is not an error. A `module X` re-export checks each member of +the combined scope by the same qualified lookup relation and reports +`ScopeConflict` for an ambiguous member, while disjoint members form a union. + +Unqualified lookup combines local declarations and permitted imports and rejects ambiguity. Instance dictionary names follow the module's declared-identifier conflict @@ -94,7 +101,19 @@ declared identities through qualification and re-exports, and carry a reach the same compiler-owned identity. A source module named `Prim` or beginning with `Prim.` is rejected; source cannot replace a compiler interface. The root `undefined` value reaches source only through this interface, not as a -free name. +free name. The compiler-provided set is not limited to `Prim`. Compiler-provided modules +export primitive values, and `Safe.Coerce` and `Unsafe.Coerce` are two of them: +`coerce` is the checked coercion intrinsic with `Coercible` as the shared +declared class, and `unsafeCoerce` is the unchecked representation cast, the +`unsafeCoerce#` analogue. Both are compiler-owned primitive values, not the +bodies a faithful vendored source must carry. A module the compiler provides is +never shadowed by an on-disk file: the loader omits such a module and an import +of it resolves through the virtual interface, so the vendored source can stay +faithful to upstream without its uncompilable body taking effect. `unsafeCoerce` +is the unchecked `unsafeCoerce#` analogue: the compiler lowers it to a +representation-preserving cast at the value's erased boundary, so a program that +reaches it compiles and runs through the same compiler-owned value path +`coerce` uses. This document owns that interface: which names exist, which identity each carries, and that nothing in source can replace one. What a member *means* — which of them @@ -227,12 +246,18 @@ key policy. Orphan and overlapping instance visibility is specified with lookup alone. P3 checks explicit public signatures and declaration dependencies while their -resolved type references are available. It does not infer a public value's -type from expression syntax. After generalization, P5 traverses the checked -scheme by stable `TypeId` and reports hidden local types in inferred results, -function parameters, aliases, record fields, and constraints. The L2 export -scoreboard runs annotated transitive-export cases through the lenient typed -pipeline so each check is measured at its owning stage. +resolved type references are available. A fully applied local type synonym in +an explicit value signature is expanded first, and each in-module type +constructor or class that remains is a dependency. The synonym's own name is +not a dependency of that value. An exported data type, synonym, or class still +requires the in-module names written in its synonym body, superclasses, and +kinds. A data constructor's field types are dependencies only when that +constructor is part of the export. P3 does not infer a public value's type from +expression syntax. After generalization, P5 traverses the checked scheme by +stable `TypeId` and reports hidden local types in inferred results, function +parameters, aliases, record fields, and constraints. The L2 export scoreboard +runs annotated transitive-export cases through the lenient typed pipeline so +each check is measured at its owning stage. ## References diff --git a/docs/design/frontend/syntax/ast-lowering.md b/docs/design/frontend/syntax/ast-lowering.md index a678e4b6..999dc3f2 100644 --- a/docs/design/frontend/syntax/ast-lowering.md +++ b/docs/design/frontend/syntax/ast-lowering.md @@ -44,6 +44,8 @@ alias, and spans. Expression and type operator chains retain source order, and operator sections retain which side supplies their operand using explicit anonymous arguments such as `(_ + 1)` and `(1 + _)`. Parenthesized unary negation remains a negation expression; it is not interpreted as a section. +Backticked value sections such as ``(_ `eq` value)`` retain the same section +side and unresolved value identity, including local names. Constructor operator patterns remain chains as well. None of these forms binds an operator name or applies precedence in P2. @@ -56,6 +58,23 @@ and it keeps all forms whose meaning depends on imports, types, or the language's sequencing rules. The AST is a separate type, not a view or alias of CST. +Record constructors with immediate anonymous field arguments become lambdas +in written field order: `{ z: _, a: _ }` becomes `\z a -> { z, a }`, +independently of canonical type or runtime field ordering. Record updaters +use the same rule, with an anonymous base record as the first argument. +Anonymous leaves of nested update paths belong to the enclosing updater; +an explicit field expression or nested record literal introduces its own +scope. P2 gives generated binders source-inexpressible names and preserves +each underscore's span. An immediate `_` in an `if` condition, then branch, +or else branch is a lambda parameter in that written order, as a `case` +scrutinee already is. An underscore in any other expression position +stays an unresolved name. These rules follow the official compiler's +`Sugar.ObjectWildcards` conversion and require no resolved names or types. +AST update fields explicitly distinguish an expression from a nested path. +P3 projects a nested path from its enclosing base, while resolving explicit +expressions in lexical scope: `field = field { value = 7 }` must use the +local `field` value rather than the enclosing record's `field` member. + Pattern normalization removes grouping parentheses, maps tuples to closed records, and turns record puns into field-variable patterns. Record patterns carry a match mode: source `{ field }` patterns are partial and accept other diff --git a/docs/design/frontend/syntax/parsing-and-cst.md b/docs/design/frontend/syntax/parsing-and-cst.md index dc7ef621..c4fb6808 100644 --- a/docs/design/frontend/syntax/parsing-and-cst.md +++ b/docs/design/frontend/syntax/parsing-and-cst.md @@ -60,6 +60,13 @@ Rejected alternatives: parsing straight to AST would erase concrete forms; resolving fixities in P1 would require the module environment; and storing checked types on CST nodes would violate the representation boundary. +Record projection and record update bind to their atom before value +application. Thus `consume r.field` applies `consume` to the projected field, +and `consume r { field = value }` applies it to the updated record. Projecting +or updating the call result requires parentheses, as in `(consume r).field`. +The parser preserves those distinctions in CST; resolving or lowering does +not repair application grouping. + ## Algorithms ```text diff --git a/docs/design/frontend/type-system/README.md b/docs/design/frontend/type-system/README.md index 24418c78..9f5f89b7 100644 --- a/docs/design/frontend/type-system/README.md +++ b/docs/design/frontend/type-system/README.md @@ -1,7 +1,7 @@ # Frontend Type System -P5 consumes normalized resolved HIR and produces verified THIR. It has four -ordered concerns about the language's types, plus one about what the compiler +P5 consumes normalized resolved HIR and produces verified THIR. It has five +ordered concerns about the language's types and one about what the compiler itself provides: | Document | Owns | Dependency | @@ -9,9 +9,14 @@ itself provides: | [Kinds](kinds.md) | Constructor kinds, synonym expansion, kind legality, the program-wide checked kind and role environment | resolved HIR | | [Type inference](type-inference.md) | The shared type spine, schemes, signatures, unification, generalization, THIR | kinds | | [Classes and evidence](classes-and-evidence.md) | Constraints, instances, dictionaries, evidence elaboration | type inference | +| [Deriving](deriving.md) | The known deriving rules, their field-usage and variance checks, generated members, and newtype adaptation | classes, kinds | | [Rows and records](rows-and-records.md) | Row kinds, the row normalizer, row unification and equations | kinds and type inference | | [Primitives](prim.md) | The `Prim.*` member inventory, per-relation rules, report-only members, and the proof/dictionary split | kinds, type inference, classes, rows | +Deriving consumes the compiler-owned `Coercible` proof that [Primitives](prim.md) +defines, and it emits generated members that [Classes and evidence](classes-and-evidence.md) +checks and elaborates like written ones. + [Primitives](prim.md) answers what the compiler itself provides: it names every `Prim.*` member, says which layer owns each member's semantics, and specifies the contract a member's rule must satisfy. It owns no type form — a primitive is a diff --git a/docs/design/frontend/type-system/classes-and-evidence.md b/docs/design/frontend/type-system/classes-and-evidence.md index 0762039c..191547b0 100644 --- a/docs/design/frontend/type-system/classes-and-evidence.md +++ b/docs/design/frontend/type-system/classes-and-evidence.md @@ -54,10 +54,13 @@ the signature applies to that consecutive equation group. Consecutive equations for one member form one definition, while a later separated group with the same name is a duplicate declaration. A signature without its matching member is an orphan type declaration. Member signatures are checked against the class -method after substituting the instance head, and their unbound type variables -resolve in the instance-head scope. The implementation supports the existing -subsumption rules for these annotations; it does not yet solve a constrained -annotation merely to specialize it to a monomorphic expected method type. +method after substituting the instance head. Variables introduced by the +instance context share the same type identities in method bodies and +annotations; a context-only variable is valid when the context's functional +dependencies determine it from the instance head. The implementation supports +the existing subsumption rules for these annotations; it does not yet solve a +constrained annotation merely to specialize it to a monomorphic expected method +type. ## Design @@ -66,14 +69,13 @@ instances. Source default implementations are not part of PureScript syntax. A backend fixture placing a default closure in a dictionary does not add a source-language feature. -Compiler-supported deriving rules are selected by resolved class identity, -including the declaring module; re-exporting a class does not change its -identity, and an unrelated user class with the same short name does not gain -the rule. Structural rules traverse the declared type's normalized field -types. `derive newtype` delegates to the wrapped class dictionary and checked -coercion boundaries instead of generating per-class wrappers. Every generated -method and underlying dictionary obligation goes through ordinary instance -checking and evidence selection, including for classes with no methods. +Compiler-supported deriving is its own topic, [deriving](deriving.md). This +document owns only what every derivation consumes: the class environment, the +instance solver, and evidence elaboration. A derived instance is an ordinary +instance whose members happen to be generated, so it is checked and its evidence +selected by the same path as a written instance; the deriving registry is +selected by resolved class identity, including the declaring module, rather than +by a name at the use site. `Coercible` consults the role vector in the checked kind environment. Equal types are reflexive. Matching constructors decompose arguments by role: nominal @@ -101,6 +103,18 @@ relation, not a raw Wasm cast: array elements, functions, records, and ADT payloads follow their established conversion plans, and unsupported conversion shapes fail lowering. +Compiler-supported library class interfaces have one declared registration in +`psrs-hir`'s compiler-interface catalog. Resolution links the canonical export +to its normal source declaration identity; qualification, aliases, and +re-exports retain that identity. This is an explicit language/library interface +binding, not recognition by class shape. P5 validates the parameter kinds and +elaborated dictionary contract before enabling synthesis. The solver dispatches +on that recorded identity and never on a function name or the spelling of a +literal. A synthesized runtime dictionary retains its checked ordinary terms as +`DictionaryValue` evidence until Core lowers them. THIR checks those terms and +their dictionary type; it trusts the frontend's registered class rule for the +semantic authorization, as it trusts ordinary instance selection. + Type-level `Symbol` values use the same Unicode scalar sequence as source strings ([DEC-16](../../../decision/DEC-16-scalar-strings-and-utf8-storage.md)). `IsSymbol` evidence and `Reflectable` preserve or produce that sequence. @@ -110,11 +124,18 @@ literal is rejected before solving. `SymbolCons` splits or builds one scalar head and a scalar tail. These solver rules do not change value-level string storage. -Check class parameter kinds, dependency indices, superclass cycles, method signatures, instance heads and contexts, and coherence conditions before solving uses. Build a searchable instance environment respecting module visibility and the official orphan and instance-chain rules. Compiler-owned primitive evidence has an evidence-defined place in the search order: proof and relation rules run before direct given lookup; report rules run after it so a warning or unsolved report constraint can propagate through the enclosing declaration. For ordinary class constraints, search givens first, then superclass paths and candidate instances. Matching a given unifies flexible wanted arguments with the given's arguments transactionally; it never assigns a rigid given variable, and a failed candidate leaves no substitutions behind. Apply functional dependencies to improve unknowns using only the selected branch in each chain; repeat until stable. Compare every class argument in an instance head. Functional dependencies contribute the transitive closure of already matched positions, while arguments outside that closure can still prove a candidate apart. Within each visible chain, continue only when a branch is provably apart. A matching branch commits before its context is solved. An unknown non-final branch blocks later alternatives in that chain; unknown singleton and final branches are ignored. Unknown branches do not create an overlap with one definite match from an unrelated chain. Failure to solve a selected context does not fall through. Unrelated ordinary candidates must remain coherent; overlapping or unresolved obligations receive source-oriented diagnostics. Memoize and bound search to prevent cycles. +Check class parameter kinds, dependency indices, superclass cycles, method signatures, instance heads and contexts, and coherence conditions before solving uses. Build a searchable instance environment respecting module visibility and the official orphan and instance-chain rules. Compiler-owned primitive evidence has an evidence-defined place in the search order: proof and relation rules run before direct given lookup; report rules run after it so a warning or unsolved report constraint can propagate through the enclosing declaration. For ordinary class constraints, search givens first, then superclass paths and candidate instances. Matching a given compares all resolved wanted arguments with the given's arguments without choosing values for unknowns. Instantiated superclass arguments follow the same equality rule. Functional dependencies may improve unknowns separately, using the declared dependency relation. Apply functional dependencies to improve unknowns using only the selected branch in each chain; repeat until stable. Compare every class argument in an instance head. Functional dependencies contribute the transitive closure of already matched positions, while arguments outside that closure can still prove a candidate apart. Within each visible chain, continue only when a branch is provably apart. A matching branch commits before its context is solved. An unknown non-final branch blocks later alternatives in that chain; unknown singleton and final branches are ignored. Unknown branches do not create an overlap with one definite match from an unrelated chain. Failure to solve a selected context does not fall through. Unrelated ordinary candidates must remain coherent; overlapping or unresolved obligations receive source-oriented diagnostics. Memoize and bound search to prevent cycles. + +The common instance recorder rejects a class-argument count mismatch with +`ClassInstanceArityMismatch` before deriving or member inference. Derived +known-class rules separately diagnose a supported rule's own arity contract +with `InvalidDerivedInstance`. THIR compares instance context dictionaries +with semantic type equality, including alpha-equivalent method quantifiers; +Core lowering consumes that verified result rather than comparing arena IDs. Elaboration turns a constrained binding into explicit evidence parameters and inserts evidence at overloaded uses. A method selection projects from its dictionary; a superclass selection follows a dictionary field. The frontend proves and records the selected path. Backend optimization may specialize dictionaries but cannot change which instance was selected. Which constraints become parameters is decided by generalization, not here: a declaration's scheme carries the constraints inference retained, and elaboration realizes exactly those as dictionary parameters, so a signature and an inferred scheme produce the same evidence shape. -A superclass edge is instantiated, never re-derived. Dictionary construction and superclass search substitute the subclass's arguments into the edge's template and unify the result against the wanted constraint, so an edge written over an arbitrary type such as `C (Array a)` needs no separate rule. The dictionary field that stores a superclass dictionary is chosen from the edge's position, so the evidence and the field agree by construction. +A superclass edge is instantiated, never re-derived. Dictionary construction and superclass search substitute the subclass's arguments into the edge's template; construction checks the field type, while lookup compares the instantiated arguments against the wanted constraint, so an edge written over an arbitrary type such as `C (Array a)` needs no separate rule. The dictionary field that stores a superclass dictionary is chosen from the edge's position, so the evidence and the field agree by construction. Rejected alternatives: a global ban on overlapping heads would reject valid instance-chain programs; choosing the first ordinary candidate is incoherent; postponing instance choice to runtime changes PureScript semantics; and representing a superclass edge as a permutation of parameter names would leave a constraint with two forms that only one of them can express. @@ -207,7 +228,7 @@ Evidence verification in the checked IR has a stated boundary, following [type i ## Open questions and future work -Track the official compiler's exact orphan, instance-chain apartness, and primitive-class rules as executable compatibility cases. The current source subset covers role-aware higher-kinded given rewriting, checked kind compatibility, canonical open-row alignment, structural `Eq`/`Ord`, covariant `Functor.map`, `Bifunctor.bimap`, `Contravariant.cmap` through `Profunctor.lcmap`, and `derive newtype`. Class method `forall` signatures, quantified method parameters, and method-local constraints are checked with independent instantiation and scoped dictionary evidence; their verification is tracked in the [rank-N acceptance record](../../../implementation/frontend/rank-n.md). The remaining class-specific deriving traversals remain open. The function-based `Contravariant` case still reaches a backend closure-capture limit, and open-row runtime conversion remains outside the current CC layout. Implementation coverage belongs in [D-04](../../D-04-suite-roadmap.md) and the [roles and coercions acceptance record](../../../implementation/frontend/roles-and-coercions.md). +Track the official compiler's exact orphan, instance-chain apartness, and primitive-class rules as executable compatibility cases. The current source subset covers role-aware higher-kinded given rewriting, checked kind compatibility, and canonical open-row alignment. Class method `forall` signatures, quantified method parameters, and method-local constraints are checked with independent instantiation and scoped dictionary evidence; their verification is tracked in the [rank-N acceptance record](../../../implementation/frontend/rank-n.md). Class-specific deriving rules are owned by [deriving](deriving.md). The function-based `Contravariant` case still reaches a backend closure-capture limit, and open-row runtime conversion remains outside the current CC layout. Implementation coverage belongs in [D-04](../../D-04-suite-roadmap.md) and the [roles and coercions acceptance record](../../../implementation/frontend/roles-and-coercions.md). ## References @@ -221,13 +242,9 @@ The current bounded coercion solver is a dedicated checker helper, while the design target exposes it as a separate `solve_coercible` service. Implemented source cases cover higher-kinded application-head rewrites, kind compatibility, role-aware canonical-given interactions, and aligned open rows; open-row values -still lack a runtime layout. Structural `Eq`/`Ord`, nested `Functor.map`, -`Bifunctor.bimap`, and checked newtype-derived methods execute for the covered -method signatures. Function-result mapping and `Contravariant` through a -profunctor dictionary match the upstream source rules; the function-based -Contravariant case still lacks Wasmtime evidence because of closure capture. -Class-method local constraints and quantified parameters are covered under -FE-18; the remaining upstream deriving classes keep FE-16 partial. +still lack a runtime layout. Class-method local constraints and quantified +parameters are covered under FE-18; class-specific deriving rules and their +coverage are owned by [deriving](deriving.md). The primitive rule table now covers all twelve row, row-list, symbol, and integer relations as well as the `Coercible` proof. `Prim.TypeError.Warn` reports through diff --git a/docs/design/frontend/type-system/deriving.md b/docs/design/frontend/type-system/deriving.md new file mode 100644 index 00000000..763b04f2 --- /dev/null +++ b/docs/design/frontend/type-system/deriving.md @@ -0,0 +1,503 @@ +# Deriving + +**Feature:** F-02 + +**Status:** Draft + +**Prerequisites:** [Classes and evidence](classes-and-evidence.md), +[kinds](kinds.md), [primitives](prim.md), and +[type inference](type-inference.md); PureScript's standard class library, +algebraic data types, newtypes, and compiler code generation. + +**Summary:** A `derive instance` or `derive newtype instance` declaration is +elaborated at P5 into ordinary instance members and a concrete instance head. +The rule for a supported class is chosen by the resolved identity of that class +in one checked deriving registry, never by the name a use spelled. Generated +members then flow through the same instance checking and evidence selection as +written members, so a derivation creates no second evidence path. + +## Scope + +This document owns the compiler's deriving rules: which class identities have a +rule, the structural traversal each rule performs, the variance and +instance-existence checks those traversals require, the diagnostic for an +underivable field, and the adaptation a `derive newtype` method receives at each +function boundary. + +It does not own the class solver, instance search, coherence, evidence +elaboration, or superclass projection; those are +[classes and evidence](classes-and-evidence.md). It does not own the +`Coercible` proof; [primitives](prim.md) owns that rule. Newtype deriving does +not consume it: a wrapped dictionary is reused after the newtype check. It does +not own roles or the checked kind +environment ([kinds](kinds.md)), the constraint and `TypeTemplate` +representation, or the surface grammar of `derive` declarations +([parsing and CST](../syntax/parsing-and-cst.md)). Deriving is a distinct topic +from the [roles and coercions](../../../implementation/frontend/roles-and-coercions.md) +concerns that the two share a feature matrix row with. + +## Background + +PureScript's `derive instance` asks the compiler to synthesize a class instance +from a locally declared data type's constructors and fields. Two families exist, +matching the official compiler: + +- **Structural classes** derive each method by walking the declared fields. + `Eq` and `Ord` compare or order fields; `Functor`, `Bifunctor`, + `Contravariant`, and `Profunctor` map over fields; `Foldable`, `Bifoldable`, + `Traversable`, and `Bitraversable` fold or traverse them. `Eq1`/`Ord1` are + delegated to their monomorphic counterpart. +- **Head-shape classes** `Newtype` and `Generic` do not derive a method body + from fields so much as a *type argument*: the newtype's wrapped type, or the + `Data.Generic.Rep` representation of the data type. `Generic` also generates + its `to` and `from` methods from that representation. + +The official compiler splits the two families across stages for a reason: +`Newtype` and `Generic` depend only on the declaration's structure and replace a +user-written type wildcard, so `Sugar/TypeClasses/Deriving.hs` elaborates them +before type checking. The structural classes need types and the instance +environment to decide how a field is used, so +`TypeChecker/Deriving.hs` elaborates them during type checking. Both stages +synthesize ordinary expressions that the normal type checker then checks; neither +introduces a special evidence form. + +`derive newtype instance` is a separate strategy. It does not walk the wrapped +type's fields at all: it selects the dictionary for the wrapped class head and +adapts each method at function boundaries by a representation cast. That cast +is the official dictionary reuse, not a `Coercible` proof. +The wrapped head is a class constraint on the same constraint spine every other +instance context uses. + +## Model + +```text +Strategy = KnownClass | Newtype +KnownClass = Eq | Eq1 | Ord | Ord1 + | Functor | Bifunctor | Contravariant | Profunctor + | Foldable | Bifoldable | Traversable | Bitraversable + | Generic | Newtype +CovariantPair = { mono: class, bi: class } -- Functor/Bifunctor, Foldable/Bifoldable, ... +ContraPair = { contra: class, pro: class } -- Contravariant/Profunctor + +ParamUsage = IsParam + | IsLParam + | MentionsParam(ParamUsage) + | MentionsParamBi(These(ParamUsage, ParamUsage)) + | MentionsParamContravariantly(ParamUsage) + | IsRecord(labels) + +DerivingRegistry = { + class: TypeId -> KnownClass, + method: (KnownClass, name) -> SymbolId, + ordering: Option<{ ty: TypeId, lt: SymbolId, eq: SymbolId, gt: SymbolId }>, + generic: Option, +} + +GenericRep = { + no_constructors: TypeId, no_arguments: TypeId, argument: TypeId, + product: TypeId, sum: TypeId, + constructor: SymbolId, inl: SymbolId, inr: SymbolId, +} +``` + +`These(a, b)` is the shared three-way shape `This a`, `That b`, or `These a b`: +a field can mention the left parameter, the right, or both, and a bipartite rule +needs that distinction rather than a single "mentions the parameter" bit. + +The **deriving registry** is the single owner of "which classes the compiler +knows and what their methods and representation types are." It is built once +when the semantic environment is constructed, from the resolved declarations of +the program plus the compiler's trusted core identity. Every rule reads it by +`TypeId`; the module and class names are inputs to its construction and never a +runtime predicate. A class with no entry is not derivable, whatever its name. + +`ParamUsage` records how one occurrence of a declared type parameter is used in +a field. `IsParam`/`IsLParam` are the monomorphic/bipartite parameter itself; +`MentionsParam` is a monotone nested occurrence; `MentionsParamBi` is a +bipartite occurrence carrying left and right usages independently; +`MentionsParamContravariantly` is an occurrence that flips polarity; +`IsRecord` records a record whose fields are used independently. This is the +official `ParamUsage` model; it is what lets one traversal serve `Functor`, +`Foldable`, and `Traversable` and their bipartite counterparts instead of +hand-writing one shape test per class. + +The generated method terms are ordinary resolved HIR. They reference the +class's own method symbols, the registry's representation constructor symbols, +and freshly allocated locals, and they convert to THIR through the ordinary +inference entry point. + +## Design + +### One registry, selected by identity + +Selecting a rule is a lookup `registry.class[class_id]`, not a match on a +qualified name. `KnownClass` is therefore a closed enum with one constructor per +supported class, and `registry.method` supplies each method symbol from the same +resolved class declaration the solver uses. The registry also supplies the +`Data.Generic.Rep` type and constructor symbols and the `Data.Ordering` +constructors, so no rule re-derives a symbol by scanning the type namespace or +comparing module strings. + +The registry is built from the checked declarations, so a re-export preserves +the defining identity and a same-named user class in another module does not +gain a rule. A class is known because the compiler put it in the registry, not +because a string matched. + +### Head shape, not head spelling + +For a structural class, the instance head's final argument must be a locally +declared data or newtype constructor, fully applied for `Eq`/`Ord` (which +traverse every parameter), or applied to all but the final one for +`Functor`/`Contravariant` (one parameter), or to all but the final two for +`Bifunctor`/`Profunctor` (two parameters). The check is on the flattened spine +of the resolved type, not on its printed form. + +Before any field is inspected, the field types are synonym-expanded through the +shared normalizer, so a synonym that expands to `f a` is treated as an applied +variable exactly as `purs` treats it. Eq and Ord must expand fields too; a rule +that only inspects the raw field type diverges from `purs` on a synonym-to-`f a` +field. + +### Field usage and variance + +A structural rule does not decide per field whether to direct-map, recurse, or +reject. It first computes a `ParamUsage` for every constructor field and +validates it against the class's variance. The official rule is: + +- A field that does not mention any parameter is inert. +- The parameter itself is `IsParam` (or `IsLParam`). +- A field whose head is a class for which the required instance exists is a + monotone/bipartite mention and is mapped through that class. +- A parameter under a function input, or any occurrence the class cannot map, + is rejected with `CannotDeriveInvalidConstructorArg`, naming the related + source positions. + +The existence check consults the same visible instance environment the solver +uses. This is what makes the rule agree with the solver: a derivation is +rejected at the declaration when a field head has no instance for the mapping +class, instead of being accepted and failing later with `NoInstanceFound`. It is +also what lets a bipartite class choose between the mono and bi mapping when a +field head has an instance for one and not the other. + +### Generated terms and one builder + +Every rule emits terms through one small builder that owns fresh local +allocation, lambda/case construction, application, and the boolean and ordering +literals. A rule states only its traversal — the two-lambda/case skeleton is +shared, not re-written per class. The builder's output is resolved HIR handed to +`infer_derived_method`, which elaborates the method signature at the instance's +class arguments and infers the body with that expected type. Dictionary +selection for each mapped field therefore happens in the ordinary solver, not in +the rule. + +`Eq1` and `Ord1` are the trivial delegations the official rule uses: `eq1 = eq` +and `compare1 = compare`, typed at the applied argument, so the matching +monomorphic instance supplies the dictionary. + +### Newtype deriving + +`derive newtype instance C ... T` resolves `T` to a locally declared newtype, +computes its wrapped type, and pushes the wrapped constraint `C ... wrapped` as +an ordinary wanted. That wanted is solved by the ordinary instance solver, so +the wrapped type's instance is the dictionary that is reused. For each method +the implementation selects that dictionary's method and adapts it to the +derived head. Shared method quantifiers stay on the adapted value. At each +arrow boundary whose endpoints differ, the adapter inserts a representation +cast authorized by the checked newtype (a local single-field declaration) and +by the selected wrapped instance. A method whose type is already equal needs +no cast. + +This is not a `Coercible` proof. Ordinary `Coercible` cannot lift +`Coercible a b` through an unknown constructor `f` to `Coercible (f a) (f b)`, +and that refusal stays. `Traversable`'s `sequence` has the shape +`forall a m. Applicative m => t (m a) -> m (t a)`, so the two heads differ +under the unknown `m`. The official compiler therefore reuses the dictionary +after the newtype and underlying-instance checks (`DeferredDictionary`) rather +than discharging a coercion obligation. The cast records that deriving +authority; it does not invent a `Coercible` instance. + +Method-level `forall` binders are instantiated once with stable placeholders and +substituted separately at the two heads, so a quantifier shared by the method +signature has one identity on both sides of the adapter, while a quantifier that +shadows a class parameter stays scoped to the body. + +### Wildcard handling and Newtype/Generic + +`derive instance newtypeX :: Newtype T _` and +`derive instance genericX :: Generic T _` carry a trailing type wildcard: the +wrapped type or representation determines that argument. The wildcard is +resolved by the rule and the instance is recorded with a concrete final +argument, so instance-head validation and the solver never see a derivable +wildcard. Instance recording must not special-case a class identity to tolerate +one. + +### Diagnostics + +Each failure maps to its official `errorCode`, and there is one variant per +official error rather than one catch-all: + +| Condition | Code | +| --- | --- | +| Class has no registry rule | `CannotDerive` | +| Head is not the expected local constructor | `ExpectedTypeConstructor` | +| Structural class head has the wrong arity | `InvalidDerivedInstance` | +| `derive newtype` head is not a newtype | `InvalidNewtypeInstance` | +| `Newtype` class derived for a data type | `CannotDeriveNewtypeForData` | +| Field cannot be mapped under the class's variance | `CannotDeriveInvalidConstructorArg` | +| Deriving type cannot be found | `CannotFindDerivingType` | +| Newtype/Generic wildcard missing | `ExpectedWildcard` | + +Rejected alternatives: a single `UnsupportedClass`-style kind loses the code the +official suite matches on and collapses unrelated failures into one bucket. + +### Rejected: name-based selection and per-rule scaffolding + +Matching a class by its declarer's module string and short name re-derives an +identity the resolver already owns, lets a user module that reuses a core +module name claim a rule, and scatters the same table across every rule. Writing +a separate field walk per class, and a separate pattern/lambda skeleton per +rule, duplicates the variance decision and lets the rules drift apart. Both are +rejected in favor of one registry and one usage analysis feeding one traversal +builder. + +## Algorithms + +```text +derive_instance(instance): + strategy = instance.derivation or return instance unchanged + match strategy: + Newtype -> derive_newtype(instance) + KnownClass -> known_class = registry.class[instance.class_id] + or fail CannotDerive + match known_class: + Newtype -> derive_newtype_class(instance) # head-shape only + Generic -> derive_generic(instance) + _ -> derive_structural(known_class, instance) + +derive_structural(class, instance): + utc = require_local_constructor(instance.head, class) # arity per class + usages = validate_params(class, utc) # ParamUsage per field + for each method of the class: + body = mk_traversal(class, usages, utc) + emit Member(method, infer_derived_method(method, head_args, body)) + +validate_params(class, utc): + expand all field type synonyms + for each field: + usage = usage_of(class, field, polarity = covariant) + if usage is unsupported under class.variance: + fail CannotDeriveInvalidConstructorArg at the offending span + return usages + +usage_of(class, ty, polarity): + if ty does not mention any parameter: return inert + if ty is the parameter: return IsParam / IsLParam + if ty is an application f arg and f is a type constructor: + if registry instance env proves class.mapping_class at f: + return MentionsParam(usage_of(class, arg, polarity)) + elif contra/pro mapping class proves at f: + return MentionsParamContravariantly(usage_of(class, arg, flipped polarity)) + else: fail + if ty is a function input: return fail at this span + ... record, forall, constrained cases follow the model ... + +mk_traversal(class, usages, utc): + f, g = fresh locals; v = fresh local + branches = for each constructor: + bind fields to fresh locals + argument = for each field usage: map_field(usage, local) + reconstruct constructor with mapped arguments + return \f [\g] v -> case v of branches + +map_field(IsParam, x) = f x +map_field(MentionsParam(u), x) = map (map_field(u)) x +map_field(MentionsParamBi(These(uL,uR)), x) = bimap (map_field(uL)) (map_field(uR)) x +map_field(MentionsParamContravariantly(u),x) = cmap (map_field(u)) x # or lmap/dimap via Profunctor +map_field(IsRecord(fields), x) = record update applying map_field per field + +derive_newtype(instance): + wrapped = wrapped_type_of(instance head's final argument) # or fail InvalidNewtypeInstance + push_wanted(class, init(head_args) ++ [wrapped]) + for each method: + template = elaborate method signature with one placeholder per class parameter + source = template[wrapped]; target = template[head_args] + body = adapt(select(source), source, target) # representation cast + emit Member(method, body) + +adapt(value, source, target): + # Quantifiers stay on the result. Their binders are the scope of any + # cast underneath them; they are not discharged as Coercible. + peel matching shared foralls onto the result type + if source == target: return value + if source = a -> b and target = a' -> b': + return \x -> adapt(value (cast x), b, b') + return cast value # newtype-deriving authority, not Coercible +``` + +Edge cases: a data type with no constructors derives a `Functor`/`Foldable` body +that is an empty case, and `Generic` uses `NoConstructors`; a zero-field +constructor uses `NoArguments`; `Ord` orders constructors by declaration order +and fields lexicographically; an applied-variable field uses the class's `1` +counterpart when it has one; a nullary `derive` for a class the compiler knows +only as head-shape (`Newtype`) produces a dictionary with superclass fields and +no method. + +## Code map + +The deriving topic lives inside the P5 owner. The intended organization: + +- `typecheck/classes/deriving/mod.rs` — `Strategy`/`KnownClass` entry points and + `derive_instance`, dispatching to the rule families. +- `typecheck/classes/deriving/registry.rs` — `DerivingRegistry`, its construction from + the resolved declarations and trusted core identity, and the `TypeId` lookup. +- `typecheck/classes/deriving/usage/` — `ParamUsage`, `usage_of`, and + `validate_params`, reading the shared instance environment for the + existence checks. +- `typecheck/classes/deriving/syntax.rs` — the one term builder: fresh locals, + lambdas, cases, application, literals. No rule builds HIR by hand. +- `typecheck/classes/deriving/eq.rs`, `ord.rs`, `functor.rs`, `bifunctor.rs`, + `contravariant.rs`, `profunctor.rs` — the `Functor`-shaped rules. +- `typecheck/classes/deriving/foldable/` — `Foldable`/`Bifoldable`, driven by the + usage tree. +- `typecheck/classes/deriving/traversable.rs` — `Traversable`/`Bitraversable`. +- `typecheck/classes/deriving/newtype.rs` — newtype strategy and the + dictionary-reuse adapter. +- `typecheck/classes/deriving/generic.rs` — representation type and `to`/`from`. +- `typecheck/error.rs` — the official deriving diagnostic variants and their + `errorCode` mapping. + +`identity` is defined by `Control.Category` and re-exported by `Data.Function`; +the registry retains that defining SymbolId. Minimal standalone fixtures may +provide `Data.Function.identity` directly, resolved during registry construction. +Consumers do not infer identity from an import spelling. + +The registry also holds the trusted core value identities (`append`, `mempty`, +`identity`, `apply`, `pure`) the fold and traversal rules name, resolved from +the program's value declarations by declaring module and name. + +The registry is constructed where the semantic environment is, so every rule +reads it as immutable checked metadata. The current file inventory is not the +design; code is expected to conform to this map, not the reverse. + +## Invariants and verification + +- Every rule is selected by `TypeId`; no rule compares a class module or name at + use time. +- A generated method proves exactly its class method's type at the instance + head. The ordinary inference path establishes this; there is no deriving-only + evidence form. +- Field usage agrees with the class's variance and with the visible instance + environment: a derivation is accepted exactly when every field occurrence has + the mapping instance the traversal needs. +- A newtype method conversion is authorized by the checked newtype declaration + and the selected wrapped instance. The adapter does not emit a `Coercible` + wanted, and ordinary `Coercible` still refuses to lift through an unknown + constructor. +- A derivable wildcard is resolved before instance recording; no instance is + recorded with a wildcard it should not have. +- Each failure carries the official `errorCode` for its condition. + +The THIR verifier trusts the deriving rule's class selection and generated +evidence, on the same basis it trusts the solver's instance choice +([classes and evidence](classes-and-evidence.md#boundaries-and-interfaces)). A +guarantee that must be verified rather than trusted needs its metadata carried +across the P5 boundary; deriving does not change that boundary. + +Verification is by the official differential set: for each supported class, an +accepted source case and a rejected field-variance case compared with `purs` on +accept/reject and diagnostic code, plus the runtime cases for the classes whose +methods execute. + +## Worked example + +```purescript +data Pair a b = Pair a b | Left a | Right b + +derive instance bifunctorPair :: Bifunctor Pair +``` + +The rule is selected from the instance's class, +`Data.Bifunctor.Bifunctor`, through the registry built at P5; the type `Pair` +itself selects nothing. The head is locally declared and applied to all but its +final two parameters. Field usage is computed after synonym expansion: + +- `Pair a b`: `IsLParam` for `a`, `IsParam` for `b` — both covariant. +- `Left a`: `IsLParam` for `a`. +- `Right b`: `IsParam` for `b`. + +`map_field` maps `a` with `lmap`-style left marker `f` and `b` with right marker +`g`, so the generated body is + +```text +\f g v -> case v of + Pair a b -> Pair (f a) (g b) + Left a -> Left (f a) + Right b -> Right (g b) +``` + +`infer_derived_method` checks it against `bimap :: forall a b c d. (a -> b) -> +(c -> d) -> Pair a c -> Pair b d`, and the pair of mapper arguments fixes the +same parameter identities the instance head supplies. A field `a -> b` would +fail `validate_params` at its span with `CannotDeriveInvalidConstructorArg`, +because the left parameter appears under a function input. + +## Boundaries and interfaces + +Deriving consumes resolved HIR instance declarations, the checked kind and role +environment, the class environment, and the visible instance environment. It +produces generated method terms and, for `Newtype`/`Generic`, a concrete final +head argument, handing both to the instance-checking path in +[classes and evidence](classes-and-evidence.md). Newtype deriving does not +consume the `Coercible` proof from [primitives](prim.md); that rule still +refuses to lift through an unknown constructor. It adds no node to +CST, AST, or HIR; a derived instance is an ordinary instance after elaboration. + +## Open questions and future work + +- **Stage of `Newtype`/`Generic`.** The official compiler resolves their + wildcard in sugar. This design keeps all deriving at P5 so the registry and + the diagnostic surface are one owner. If the wildcard-before-recording rule is + easier to guarantee at P4, the head-shape rules may move there without moving + the structural rules; the split is a placement choice, not a semantic one. +- **Trusted core identity.** The registry needs stable identities for the core + declarations it names. How those identities are pinned (a trusted-declaration + table, or a checked marked set) is [primitives](prim.md)'s identity question; + deriving only requires that the registry is built once and read by `TypeId`. +- **Variance and the instance environment.** `validate_params` consults instance + existence. The exact boundary between "no instance yet" and "no instance + possible" is the solver's, and deriving must not form a second opinion; the + interaction belongs with the instance-search contract. +- **Remaining classes.** `Profunctor` and the foldable/traversable bipartite + classes have no implementation yet; this document specifies their intended + rules, and the acceptance record tracks which have source and runtime + evidence. + +## References + +- PureScript `TypeChecker/Deriving.hs` (structural rules, `ParamUsage`, + `validateParamsInTypeConstructors`, `mkTraversal`) and + `Sugar/TypeClasses/Deriving.hs` (`Generic` and `Newtype` elaboration). +- PureScript `Constants/Libs.hs` for the class, method, and representation + symbols the registry must resolve. +- [Classes and evidence](classes-and-evidence.md) for the solver and evidence + contract a derived instance uses. +- [Primitives](prim.md) for the `Coercible` proof boundary. +- [Kinds](kinds.md) for roles and the checked environment. + +## Implementation notes + +These record where the current code deviates from this design. They are not the +design; the sections above are normative. + +- **Usage owns structural traversal.** `usage/` computes field variance and + checks visible instance-head identities. As in official deriving, this is a + head-constructor availability test; ordinary inference checks the generated + member's complete instance constraints. All mapping families generate terms + from that tree, including canonical record fields and scoped `forall` + occurrences. Fold and traversal families consume the same analysis. An + unsupported mapped open row remains a diagnostic. +- **Coverage.** Every structural class has a rule. Generic round trips, + traversal effects, function Contravariant adapters, polymorphic newtype + adapters, and applied-variable Eq/Ord dictionaries have executed runtime + evidence. Coverage and remaining verification obligations are tracked in the + [deriving acceptance record](../../../implementation/frontend/deriving.md). diff --git a/docs/design/frontend/type-system/prim.md b/docs/design/frontend/type-system/prim.md index 811a2eb6..bd5fded0 100644 --- a/docs/design/frontend/type-system/prim.md +++ b/docs/design/frontend/type-system/prim.md @@ -59,6 +59,7 @@ Kinds are the official ones, since source compatibility requires them. "Provided | `Prim.Partial` | `Constraint` | Diagnostic | ReportOnly | yes | partial | | `Prim.Boolean.True`, `Prim.Boolean.False` | `Boolean` | Interface | — | yes | n/a | | `Safe.Coerce.coerce` (value) | `forall a b. Coercible a b => a -> b` | Interface | — | yes, as an intrinsic | n/a | +| `Unsafe.Coerce.unsafeCoerce` (value) | `forall a b. a -> b` | Interface | — | yes, as an intrinsic | yes | | `Prim.Coerce.Coercible` | `forall k. k -> k -> Constraint` | Proof | CompileTimeProof | yes | yes | | `Prim.Ordering.Ordering` | `Type` | Interface | — | yes | n/a | | `Prim.Ordering.LT`, `EQ`, `GT` | `Ordering` | Interface | — | yes | n/a | @@ -257,9 +258,26 @@ constraint and a source `import Prim (Partial)` therefore reach `TypeId::PRIM_PARTIAL`, and `purs` reads the same kind for `Partial` as this compiler does. -Type-level `Reflectable` and `IsSymbol` relations exist in later official versions -and are not part of the inventory above; adding a member is a registry change -with the same requirements as any other. +`Data.Symbol.IsSymbol` is a compiler-supported library interface rather than a +member of the `Prim` declaration inventory. The shared compiler-interface +registry owns its canonical export binding alongside `Safe.Coerce` and +`Unsafe.Coerce`; P3 associates the source declaration's ordinary `TypeId` with +the registered class identity. The source module and its exports still resolve +normally. P5 validates the checked parameter kind and elaborated method contract +before registering a rule for that resolved identity. Synonyms and import +aliases do not change the contract, and an unrelated user class with the same +spelling receives no rule. + +A known Symbol literal produces a runtime dictionary whose `reflectSymbol` +method returns that literal's Unicode scalar sequence. The primitive framework +checks both its decided arguments and its dictionary type. `DictionaryValue` +evidence retains the ordinary typed record and method lambda; THIR verifies +their types and lexical scope, and Core lowers the terms through the normal +expression path. Unknown symbols decline the rule and may use lexical givens +or ordinary instances; no default symbol is chosen. Runtime dictionaries never +authorize a `Coercible` conversion. `Reflectable` remains outside this +compiler-interface registry; its current library instances use ordinary class +solving. Implementation coverage belongs in [D-04](../../D-04-suite-roadmap.md). A rule for a member whose shared foundations are incomplete is not a local shortcut: the argument types it needs must participate in ordinary instantiation, substitution, unification, generalization, and scope checking first, and a rule that cannot satisfy that reports the limitation rather than approximating the member with a private path. diff --git a/docs/design/frontend/type-system/rows-and-records.md b/docs/design/frontend/type-system/rows-and-records.md index f66dac10..ee9db3d5 100644 --- a/docs/design/frontend/type-system/rows-and-records.md +++ b/docs/design/frontend/type-system/rows-and-records.md @@ -103,6 +103,16 @@ Row equality is independent of source label order; matched fields have equal che P5 receives kind-checked row expressions and resolved record operations from HIR. It supplies type equations and evidence to [type inference](type-inference.md) and emits verified THIR. Row normalization and the primitive row relations are internal to P5 and share the kind and evidence contracts of [kinds](kinds.md) and [classes](classes-and-evidence.md); a later stage receives only checked row types. The backend chooses record and variant representation only after Core lowering. +Core checks bare rows inside nominal type arguments with the same label-based +row relation as record rows. Nominal arguments stay invariant: fields must have +matching types, repeated bare-row labels retain their occurrence order, closed +rows must have the same label occurrences, and rigid tails cannot +absorb extra fields. Only quantified flexible tails receive checked residual +substitutions. Those residuals remain in instantiation evidence even when no +existing type-arena node represents them; later local-row materialization uses +that evidence rather than reconstructing the match. + + ## Open questions and future work Use official tests to pin down duplicate-label diagnostics, record-update edge cases, and primitive row improvement order; [primitives](prim.md) owns the rules those tests exercise. [DEC-04](../../../decision/DEC-04-official-test-suite-roadmap.md) tracks implementation coverage. diff --git a/docs/design/frontend/type-system/type-inference.md b/docs/design/frontend/type-system/type-inference.md index f371d63d..6dc6eef3 100644 --- a/docs/design/frontend/type-system/type-inference.md +++ b/docs/design/frontend/type-system/type-inference.md @@ -14,7 +14,7 @@ This document owns schemes, instantiation, generalization, bidirectional checkin ## Background -HM inference explains unannotated let polymorphism, but PureScript additionally supports `forall` beneath arrows, constrained types, explicit type application, and row-polymorphic records. A use of a polymorphic value instantiates its `forall`; checking against an expected `forall` skolemizes it and rejects escaping skolems. Subsumption handles function variance and inserts dictionary evidence at permitted expression boundaries. Recursive declarations are checked as dependency groups; signatures supply polymorphic recursion where accepted by the source language. +HM inference supplies declaration generalization; unannotated local bindings remain monomorphic. PureScript additionally supports `forall` beneath arrows, constrained types, explicit type application, and row-polymorphic records. A use of a polymorphic value instantiates its `forall`; checking against an expected `forall` skolemizes it and rejects escaping skolems. Subsumption handles function variance and inserts dictionary evidence at permitted expression boundaries. Recursive declarations are checked as dependency groups; signatures supply polymorphic recursion where accepted by the source language. ## Model @@ -39,6 +39,32 @@ Inference state has three owners with distinct lifetimes. The `SemanticEnv` is i Infer synthesizable expressions and check expressions with expected types. Instantiate `forall` and solve constrained uses through class entailment. When checking a signature or higher-rank argument, skolemize expected quantifiers, perform structural subsumption, and check that skolems do not escape. Function parameter comparison is contravariant and result comparison covariant; record subsumption compares common labels and checks closed-row extras and omissions. Evidence can be inserted at elaboration sites, while comparison under a type constructor cannot invent term-level dictionaries. +A multi-scrutinee case or multi-equation declaration retains a `MatchProduct` +until checking. Its fields are independent pattern scrutinees: a local +structural `forall` survives variable patterns, and each branch instantiates +its own bound value at use. Patterns requiring a monotype instantiate only +that field. The checked product becomes a closed record in THIR. Ordinary +source record construction continues to instantiate field expressions once. +Guard fallthrough captures the saved product to preserve the same rule. + +Coercion primitives checked against an expected arrow take its parameter and +result as their exact source and target boundary types. Function variance must +not instantiate a quantified input and leave the cast's stored source at an +unresolved monotype. Checked casts retain a `Coercible` obligation over those +same boundary types; unchecked casts retain their declared unchecked origin. +This contextual boundary check does not change ordinary function subsumption. + +Lexical givens and their superclass projections discharge a wanted only when +all resolved argument types already agree. Dictionary lookup does not unify an +unconstrained wanted variable with a given's skolem: that would prematurely +choose the type of an unannotated local binding before its uses constrain it. Functional +dependency improvement remains the owner of permitted argument refinement, +including dependencies exposed by the instantiated superclass closure of each +lexical given. +For example, under `BoundedEnum a` with an `Ord a` superclass, a local integer +stepper's `Ord ?state` must stay residual until its enclosing declaration +generalizes it or it is fixed by its integer seed; the superclass dictionary proves `Ord a`, not `Ord ?state`. + Infer a recursive SCC with shared placeholders, respecting explicit signatures, then solve and generalize only variables permitted by the environment and remaining constraints. Use kind-correct constructor and pattern types; type-check case alternatives, literals, arrays, record operations, newtypes, and foreign imports. Visible type application `e @T` substitutes `T` for the operand's outermost quantifier after a kind check and is erased, and `e @_` consumes that quantifier without choosing a type. Typed holes follow the official source rules. A quantified kind argument is instantiated implicitly, because no source form applies one to a type constructor. Build THIR only after zonking, ambiguity checks, and evidence elaboration. A declaration's scheme carries the constraints that were inferred for it, whether or not the source declared them. Generalization is therefore one sequence and not two: @@ -84,6 +110,13 @@ polymorphic expression before solving the unknown, following the official checker's rule. Explicit polymorphic fields and annotations supply the boundaries at which a polymorphic value may be retained. +Core replays those checked explicit instantiations, including universal types +used as nominal constructor arguments. Its shared type matcher binds only the +declaration's flexible parameters and retains a replacement's complete `ForAll` +structure. Repeated occurrences must agree, constructor fields use the same +bindings, and nominal slots remain invariant. This replay does not change P5's +rule for solving unconstrained inference unknowns. + Rejected alternatives: pure HM cannot check higher-rank signatures; unifying a `forall` as though it were a monotype is unsound; generalizing recursive uses before group checking admits unsound polymorphic recursion; and carrying solver cells into THIR breaks the P5 boundary. Demanding that a signatureless declaration's constraints already be solved is rejected because it makes a hand-written signature a precondition for inferring a qualified type. Letting each feature module check kinds for itself is rejected because whether an operation is kind-corrected would then depend on the caller's path. ## Algorithms @@ -187,7 +220,7 @@ Every THIR expression and binder has a kind-valid type; every reference is resol ## Worked example -`apply :: (forall a. a -> a) -> Int` requires an argument polymorphic at the call site. `apply (\x -> x)` checks the lambda against a skolemized `forall a. a -> a`; a monomorphic `Int -> Int` argument fails. By contrast, `let id = \x -> x in id id` generalizes `id` and instantiates its two uses independently. +`apply :: (forall a. a -> a) -> Int` requires an argument polymorphic at the call site. `apply (\x -> x)` checks the lambda against a skolemized `forall a. a -> a`; a monomorphic `Int -> Int` argument fails. A local binding needs an explicit polymorphic annotation for independent instantiation: `let id = (\x -> x) :: forall a. a -> a in id id`. An unannotated local identity is monomorphic, so self-application is an infinite type. For `class C a where method :: a -> a`, the declaration `f x = method x` has one wanted `C ?a` that no instance discharges. Generalization retains it, checks that `?a` occurs in `f`'s result type `?a -> ?a`, quantifies `?a` with kind `Type`, and gives `f` the scheme `forall a. C a => a -> a` with one dictionary parameter. `f 1` then instantiates that scheme and solves `C Int` at the use, while `f (\y -> y)` is rejected for lacking `C (Int -> Int)` if no such instance exists. @@ -255,6 +288,39 @@ so the body's evidence and the parameter the scheme hands on are one dictionary. `group.rs` runs the sequence and `entry.rs` hands it the module. A wanted from an earlier declaration is not re-solved: that declaration has already generalized or reported it, so a second attempt could bind a variable it has since quantified. + +A finished binding also owns the flexible implementation variables that no +public result type mentions, such as the phantom argument in `discard Proxy`. +After solving and ambiguity checking, generalization traverses its checked +expression, patterns, cast boundaries, and solved evidence. It records the +remaining free variables as additional scheme binders with their checked +kinds. Variables already bound by a nested scheme or rigid quantifier are not +captured, and variables belonging to an outer level stay in that scope. This +applies to declarations, local bindings, and instance dictionaries. It supplies +explicit THIR scope for internal types without choosing a default type or +adding evidence for an unsolved obligation; unused binders in the public type +remain meaningful when the implementation mentions them. + +Unannotated local `let` and `where` bindings are monomorphic, as in official +PureScript's `inferLetBinding`. They share the enclosing declaration's inference +unknowns and wanted constraints. Local uses can solve those unknowns before the +enclosing binding group solves, checks ambiguity, and generalizes. A local's +unused class obligation is still an obligation of that declaration: `f y = let +g x = method x in y` is ambiguous when `method :: C a => a -> a`, because the +result of `f` does not determine `a`. Functional dependencies use the same +closure over all retained constraints before deciding ambiguity. + +An explicit polymorphic local annotation owns its checked quantifiers and +constraint dictionaries. Each use instantiates that declared type; it does not +share the unannotated-binding unknowns. An enclosing body traversal preserves +these structural binders and must not capture them again. When a checked `let` +expression carries a leading `forall`, its binders scope both local definitions +and the body in THIR and Core. Beta reduction retains the quantifier on the +whole `let`; its argument binding remains monomorphic rather than rebinding +the same type variable. Local bindings do not infer a qualified scheme or abstract unsolved dictionaries independently of +the enclosing declaration. This also prevents implicit polymorphic recursion +in a local recursive group. + The scheme records the kind of each quantified variable, read through the kind owner, so an instantiation carries the declaration's own polymorphism rather than reading it back from the solver table. diff --git a/docs/feature/F-02-portable-programs.md b/docs/feature/F-02-portable-programs.md index 1becd537..aa9ce7d6 100644 --- a/docs/feature/F-02-portable-programs.md +++ b/docs/feature/F-02-portable-programs.md @@ -1,7 +1,8 @@ # F-02 — Build Portable Program Artifacts **Status:** In progress -**Design:** [wasm encoding — Wasm Lowering](../design/backend/wasm/encoding-and-structuring.md) +**Design:** [wasm encoding — Wasm Lowering](../design/backend/wasm/encoding-and-structuring.md), +[linking and runtime (Draft)](../design/backend/wasm/linking-and-runtime.md) ## User need @@ -24,6 +25,28 @@ Existing Node.js APIs and JavaScript FFI modules are not supported compatibility targets. Programs that use unsupported syntax, types, or platform services receive source-oriented diagnostics rather than a malformed artifact. +An already built application component can select pinned guest implementations +for its interface imports: + +```sh +psrs link main.wasm --manifest providers.json -o linked.wasm --report link.json +``` + +The manifest selects each guest by interface name and artifact digest. Paths are +relative to the manifest. The linker follows the selected guests' dependencies, +rejects incompatible exports or missing providers, and reports the exact host +interfaces left unresolved. An explicitly selected guest does not fall back to +a host implementation. The initial interface binding is synchronous and covers +one whole interface, preserving resource lifetime and provider-owned memory. +Source `build` can explicitly select the same providers with `--manifest` and +emit a joined source/component report with `--report`. The compiler pins its +produced root bytes; externally supplied application components require a root +pin in the manifest. Guest executable initialization is rejected until it has +an explicit checked effect contract. + +See [Explicit Component Linking](../workflow/component-linking.md) for the +manifest format and supported boundary. + The compiler selects `Main.main` when that declaration exists; otherwise it requires exactly one top-level declaration named `main`. The selected entry takes no arguments and may return `Int` or `Effect Unit`. An integer result @@ -110,8 +133,9 @@ receive a source diagnostic. `derive instance` generates implementations for the supported standard classes from a locally declared type's constructors and fields. Structural `Eq` and `Ord`, and the covered `Functor` and `Bifunctor` mappings, are supported. -`derive newtype instance` reuses an instance for the wrapped type, with checked -conversions at method boundaries. Derived instances participate in the same +`derive newtype instance` reuses an instance for the wrapped type. Method +boundaries are representation casts authorized by the newtype declaration and +the wrapped instance, not by ordinary `Coercible`. Derived instances participate in the same constraint checks and module imports as explicitly written instances. Other standard deriving rules and additional field shapes remain incomplete. @@ -134,7 +158,7 @@ function calls. ## Acceptance criteria -- A supported source program produces a validated core Wasm module at the +- A supported source program produces a validated WASI component at the requested output path. - The compiler prints WAT or writes it at the requested output path. - The WASI artifact runs in a compatible runtime and preserves observable @@ -143,6 +167,8 @@ function calls. trap. - Unsupported constructs fail with a source-oriented diagnostic. - Each added platform service has documented behavior and executable tests. +- Explicit guest linking verifies artifact pins and interface compatibility, + preserves canonical ownership, and reports the permitted residual host imports. ## Initial scope diff --git a/docs/feature/F-04-compile-diagnosis.md b/docs/feature/F-04-compile-diagnosis.md new file mode 100644 index 00000000..e65e8163 --- /dev/null +++ b/docs/feature/F-04-compile-diagnosis.md @@ -0,0 +1,86 @@ +# F-04 — Compile Diagnosis + +**Status:** In progress + +**Design:** [D-16 — Compile Diagnosis](../design/D-16-compile-diagnosis.md) + +## User need + +Compiler contributors need a repeatable way to identify where a source case +stops, compare compile behavior across revisions, and replay an investigation +without reconstructing its module inputs. When a pipeline boundary changes, +they need evidence tied to the actual artifacts and passes that ran. + +## User-visible behavior + +Given one source file or a selected part of the `passing` corpus, contributors +can run `psrs diagnose` to create a versioned report. The report lists each +diagnostic and its source origin, groups cases by first-blocker stage and +category, and records exclusions, timeouts, crashes, and elapsed time +separately from successful compilation. + +The default report keeps a lightweight manifest of pass executions, artifact +identities, and explicit input/output edges. `--trace` retains selected IR +dumps on both successful and failed cases and carries the option into replay. +Unsupported artifact summaries or lineage are identified as unavailable. A +report never treats a compile pass as evidence that generated Wasm executes +correctly. + +## Acceptance criteria + +- A single source file can be diagnosed with a per-case deadline. +- File diagnosis can take additional ordered `--input FILE` sources, compile + that exact list without import rediscovery, and reject duplicate canonical + paths or combining explicit inputs with corpus mode. +- The selected corpus subset reports progress and records FFI exclusions without + counting them as passing cases. +- Every case retains all ordered diagnostics and the origin of each diagnostic. +- The run manifest records the available input, compiler, target, toolchain, + schema, pass-contract, and trace-coverage metadata without inventing missing + compatibility evidence. +- Each observed pass has a stable identity, ordered input and output artifact + references, status, diagnostic references, and explicit artifact edges. +- Artifact production is distinct from verification evidence; a partial, + rejected, timed out, crashed, or unavailable result is represented + truthfully. +- Default diagnosis avoids formatting or cloning complete IR values solely for + dumps. `--trace` can retain the selected dumps for accepted and rejected + cases. Replay first creates a new trace report from the saved ordered inputs, + then runs the recorded build command so a compiler rejection keeps its + nonzero build exit status. +- Reports compare only compatible cohorts and identify changed or incomplete + source inputs. Where canonical summaries are absent, artifact-content + comparison is explicitly unavailable. +- Compile acceptance, compile lineage, and Wasmtime runtime evidence remain + separate; runtime correctness requires value-sensitive execution evidence. +- Compile outcomes remain data in a completed report; command failure indicates + invalid options or an operational error such as missing input or unwritable + output. + +## Implementation status + +Implemented: versioned run-local pass/artifact IO records, observed top-level +backend boundaries, a diagnosis API with optional dump capture, and the CLI +schema-v2 manifest. Default reports keep pass/artifact metadata without +formatting IR dumps; `--trace` retains selected Core, CC, and MIR dumps on +accepted and rejected cases. Explicit ordered `--input` sources and trace-aware +replay are supported. `--compare` retains v1 status/blocker/input comparison +and reports the first observed pass-sequence difference for compatible v2 +traces. It reports observed environment differences separately and marks the +compiler build toolchain unavailable because it is not embedded in the +binary. Exact nested-verifier coverage is recorded at the granularity exposed +by the backend. + +Planned: canonical versioned IR summaries and artifact-content comparison, +source/declaration/node lineage, many-to-many derivation queries by +case/symbol/span, and a separate Wasmtime runtime evidence record. Current +pass comparison follows execution order and does not establish the earliest +artifact divergence or explain a runtime failure. These capabilities are not +implied by the presence of a pass manifest or an IR dump. + +## Out of scope + +This workflow does not repair compiler behavior, infer lineage from Debug +output or span overlap, reduce a source file automatically, prove matching +diagnostics have a common root cause, or use compile acceptance as runtime +correctness evidence. diff --git a/docs/implementation/backend/control-flow-and-tail-calls.md b/docs/implementation/backend/control-flow-and-tail-calls.md index bf186b8f..a9627b0b 100644 --- a/docs/implementation/backend/control-flow-and-tail-calls.md +++ b/docs/implementation/backend/control-flow-and-tail-calls.md @@ -103,7 +103,7 @@ CF-01: CF-13: Implementation: crates/psrs-backend/src/mir/mod.rs (Terminator::Branch has no merge_block); mir/lower/assignments.rs, mir/lower/aggregate/array.rs, - mir/scalar_helpers.rs (producers no longer set it); mir/cfg.rs + mir/lower/assignments.rs; mir/cfg.rs (common_join/join_blocks derive joins from CFG edges); mir/verify/function.rs (merge check removed, target-parameter check kept); mir/opt/constants.rs (join parameters preserved); wasm/lower/structure/cfg/mod.rs and diff --git a/docs/implementation/backend/data-representation.md b/docs/implementation/backend/data-representation.md index d095a048..f5b87215 100644 --- a/docs/implementation/backend/data-representation.md +++ b/docs/implementation/backend/data-representation.md @@ -259,7 +259,8 @@ The physical layout families are verified on this revision. Open items outside this topic: - Open record rows and unknown foreign aggregate layouts remain unsupported - conversions, owned by [generic aggregate erasure](../../design/backend/fp/generic-aggregate-erasure.md). + conversions, owned by [representation and + evidence](../../design/backend/fp/representation-and-evidence.md). - The `i31` optimization for nullary cases of mixed sums and GC strings remain future work in the design. - The capture-array element mutability bit is part of the planned layout but is diff --git a/docs/implementation/backend/effects.md b/docs/implementation/backend/effects.md index 8902fbf8..5aa9af87 100644 --- a/docs/implementation/backend/effects.md +++ b/docs/implementation/backend/effects.md @@ -17,6 +17,19 @@ records below describe the earlier encoding and are not the current evidence. ## Scope and dependencies +The abstract-constructor/dictionary closure boundary is landed in the +vendored-library iteration. The 2026-10-05 +[constructor investigation](polymorphism-and-erasure.md#constructor-and-closure-investigation-2026-10-05) +reproduces a matching producer/consumer signature failure without Effect. At +`675f0e3` the discard, delayed-map, and fixed-payload Effect cases compiled +and then trapped. The landed checkpoint recorded there executes those three +programs and the Reader reproduction, and the representation policy is the same +one PE-13 verifies. Historical EF evidence below does not establish that +boundary. The general checked-conversion contract owns the repair; Effect +contributes its trusted token protocol. On this tree the L6/M7 scoreboard moved +from 164/413 to 210/413 and the D-04 and README runtime rows are updated; L1–L5 +are unchanged. + Complete the linked design's `Effect a` representation, `pure`, `bind`, `runEffect`, hidden execution token, and sequencing through CC/MIR/Wasm. An Effect value defers its operation until an explicit source runner or the @@ -49,6 +62,7 @@ Verified row needs behavior-sensitive execution, not only a closure-shaped IR. | EF-11 | Structural verification checks trusted identities and checked WIT operation signatures, every recorded Effect application closure, each import plan against its host wrapper, and the complete transformed Core including generated wrappers. | Malformed identity, checked WIT scheme, import-plan, closure-shape, wrapper-signature, and post-wrapper Core fixtures fail before CC/encoding; these checks are reported as structural evidence only. | Verified | | EF-12 | `trap` is the `Effect Unit` whose application ends the guest instead of returning, and the effect chain sequenced after it does not run. | A failing library assertion writes its message and traps; a held one lets the program finish; a statement after the trap never writes. | Verified | | EF-13 | Entry selection resolves one source declaration: prefer `Main.main`, otherwise require a unique top-level `main`. The same `SymbolId` drives the runner check and any generated adapter; accepted result types are `Int` and trusted `Effect Unit`. | Source tests for preferred/fallback/ambiguous selection, aliases of `Effect Unit`, and agreement between selected identity, runner diagnostic, and generated adapter. | Verified | +| EF-14 | Effect application-to-closure lowering composes with the common abstract-constructor representation policy across generic functions, dictionary methods and callbacks. | Execute discard, delayed map and `f Unit` cases with mandatory Wasmtime and exact output/status; retain the non-Effect Reader regression and verify the complete lowered representation. | Verified | ## Current evidence (2026-10-04) @@ -118,6 +132,35 @@ EF-13: Result: pass. Selection prefers Main.main, otherwise one top-level main. Effect Int is rejected. Effect Unit synonyms are accepted. Gaps: files with no selected main stay scoreboard blockers (63). +EF-14: + Implementation: crates/psrs-core/src/instantiation.rs (checked + instantiation), crates/psrs-backend/src/effects/mod.rs (the Effect + representation owner registers the runtime token as a + `RepresentationPolicy` in the Core-to-CC registry), crates/psrs-backend/ + src/boundary.rs (`BoundaryEvidence`), crates/psrs-backend/src/cc/lower/ + conversion/ (transport.rs and callable.rs) and cc/lower/{global,record,erased} + (evidence read from the boundary at each use). The token is the compiler-owned + opaque `TypeId::STATE_TOKEN`; effect lowering threads it and `run` supplies the + `StateToken` value, the `State# RealWorld` analogue. + Tests: tests::effects::discard_defined_from_bind_sequences_effects (stdout + `a\nb\n`, exit 0); tests::functor:: + mapping_an_effect_does_not_run_it_until_the_action_runs (stdout + `before\ntick\n2\n`, exit 0); tests::closure_protocol:: + fixed_unit_payload_through_a_bind_constraint_repeats_the_action + (`forall f. Bind f => f Unit -> f Unit` at Effect, stdout `again\nagain\n`, + exit 0) and reader_dictionary_returns_the_concrete_result (the non-Effect + regression, exit 42); tests::closure_protocol:: + abstract_callable_transport_emits_a_checked_adapter (the generated adapter + and factory). + Input boundary: source; executed Wasm component. + Commands: PSRS_REQUIRE_WASMTIME=1 cargo test --workspace. + Result: pass. The Effect discard, delayed-map, and fixed-payload programs + run to completion with the exact output and status, and the non-Effect + Reader regression still returns 42. + Revision: 675f0e3 plus this slice. + Gaps: none. Under-application of an indirect callee, including a dictionary + method, is lowered by the same generated indirect call the PE-13 evidence + records for [polymorphism and erasure](polymorphism-and-erasure.md). ``` ## Vertical execution order @@ -295,7 +338,8 @@ EF-09: Gaps: direct lexical reference restriction is not capability isolation; a runner passed from the selected entry to a helper may be invoked there. EF-10: - Implementation: crates/psrs-backend/src/component.rs, + Implementation: crates/psrs-backend/src/linking/mod.rs, + crates/psrs-linker/src/compose.rs, crates/psrs-backend/src/wasm/lower/mod.rs (entry wrapper), crates/psrs-driver/src/tests/effects.rs (component execution) Tests: every executed test above runs the produced component under @@ -379,9 +423,10 @@ EF-12: ## Discovered obligations -- The token is the Core `Int` chosen by effect lowering. Its current value - `0` is a placeholder: it does not schedule work or establish ordering. - Source programs cannot name the token; order comes from the calls and +- The token is the compiler-owned opaque `TypeId::STATE_TOKEN` chosen by effect + lowering. Its one value is the `StateToken` expression `run` supplies; the + runtime shape is a scalar constant because the synchronous token carries no + payload. Source programs cannot name the token; order comes from the calls and optimizer contracts. A later stateful token is an open question, not a second representation. - Partial application of a polymorphic declaration whose result is a type diff --git a/docs/implementation/backend/generic-aggregate-erasure.md b/docs/implementation/backend/generic-aggregate-erasure.md index 42f5cfbc..a35d87f0 100644 --- a/docs/implementation/backend/generic-aggregate-erasure.md +++ b/docs/implementation/backend/generic-aggregate-erasure.md @@ -1,7 +1,7 @@ # Generic Aggregate Erasure Implementation Acceptance **Feature:** [F-02](../../feature/F-02-portable-programs.md) -**Design:** [Generic aggregate erasure](../../design/backend/fp/generic-aggregate-erasure.md) +**Design:** [Runtime representation and checked boundaries](../../design/backend/fp/representation-and-evidence.md) **Progress:** Accepted against GA-01 through GA-20 after the independent review findings were fixed and required execution was repeated. The earlier audit and review findings below are retained as history; [resolution evidence](#resolution-of-independent-review-2026-09-25) diff --git a/docs/implementation/backend/intrinsic-implementations-2026-10-07.md b/docs/implementation/backend/intrinsic-implementations-2026-10-07.md new file mode 100644 index 00000000..86fa55d3 --- /dev/null +++ b/docs/implementation/backend/intrinsic-implementations-2026-10-07.md @@ -0,0 +1,103 @@ +# Intrinsic implementation separation acceptance — 2026-10-07 + +## Contract and baseline + +The governing contracts are [intrinsic implementations](../../design/backend/fp/intrinsic-implementations.md), +[D-15](../../design/D-15-compiler-builtins.md), +[scalars](../../design/backend/fp/scalars-and-primitives.md), and +[DEC-18](../../decision/DEC-18-unified-target-linking.md). +The compiler baseline was clean `b263b14` on `stdlib/vendor-core-libraries`; +the library baseline was `e0679521275277c0b005b2509d9e0a8570715c5e`. + +A focused source diagnosis accepted the old explicit `intDiv/intMod` bindings. +Execution demonstrated that they returned floor-policy results different from +public official Euclidean arithmetic for negative divisors. This was a duplicate +compiler policy, not a reason to rewrite official library functions. + +A separate malformed-MIR reproducer declared a formatter consumer with +`(i32, i32, i32) -> i32`, and made its call agree with that declaration. +MIR verification and Wasm lowering both accepted it before this change. After +this change local MIR verification still accepts the internally consistent +call, but target planning rejects it against the formatter's actual +`(f64, i32, i32) -> i32` provider. Both direct Wasm lowering and the checked-plan +path enforce this boundary. Baseline reports, raw programs and logs remain +outside the repository under `/private/tmp/psrs-intrinsic-*`. + +## Implemented ownership + +- HIR owns stable language identities, schemes, arity and conservative trap/mutation effects. +- The exhaustive backend catalog selects direct, generated or artifact implementations, with explicit elaborated/unsupported cases. Artifact enumeration derives from this selection. +- CC verifies artifact arguments and results before erasure. MIR adapts calls through a shared raw-scalar/UTF-8 protocol instead of per-numeric-operation assignments. +- Runtime metadata owns raw export ABI, output capacity and borrowed/caller-owned buffer protocols. Existing runtime executable bytes are retained. +- Linking derives expected ABI from actual MIR imports. Generated allocator/codec signatures are shared by MIR import production, provider verification and emission. + +Language `Int*` names replace HIR machine spellings while preserving active +symbol IDs. Retired floor IDs 26 and 27 cannot be reused or resolved as bindings. +The old CC operations, MIR helpers and helper-only tests are removed. Public +Euclidean division/modulo remain in ordinary library source, including zero +handling. Two target-only array helpers use `intQuot` for nonnegative lengths +and positive divisors; their official source callers remain unchanged. + +## Verification + +Measured on 2026-10-07 against compiler `b263b14` plus this change, with the +lock at library `7cace2a6b4808b36b01559e88d3ebc351a3b7426` +(`fnv1a64-v1:1fd8c04ec63c6ff0`). Wasmtime is 49.0.2. Cargo commands used +`CARGO_INCREMENTAL=0`. Runtime checks also used `PSRS_REQUIRE_WASMTIME=1`. +Raw logs stay outside the repository under `/private/tmp/psrs-intrinsic-*`. + +Compiler checks: + +- `cargo fmt --all --check` passed on the final Rust sources. +- `cargo clippy --workspace --all-targets -- -D warnings` passed. +- `cargo test -p psrs-backend --lib`: 408 passed. +- `cargo test -p psrs-driver --lib tests::scalars`: 16 passed. This includes + the repaired case-arm division test. It no longer requires a compiler floor + helper. +- `cargo test -p psrs-driver --lib intrinsic`: 7 passed. Retired `intDiv` and + `intMod` bindings are rejected. Public Euclidean division and modulo run for + both signs, zero divisors, and a higher-order call. A balanced public + `Maybe` traversal covers nine elements, an empty input, and a failed element. +- `PSRS_REQUIRE_WASMTIME=1 cargo test --workspace --no-fail-fast` finished all + 52 targets: 1690 passed, 3 failed, 5 ignored. The only failing target is + `psrs-driver --lib`, and its three failures are the established baseline: + `constrained_dictionary_parameters_precede_ordinary_arguments` (the expected + constrained MIR function is absent), + `runs_a_polymorphic_identity_with_a_number` (optimized WAT has no `f64`), + and `compiles_if_expression_through_cfg_to_structured_wasm` (optimized WAT + has no `br_if`). No new failure appeared. +- `crates/psrs-runtime/tools/check-reproducible.sh` rebuilt the runtime + artifact and found it byte-for-byte identical to the committed Wasm. +- A direct malformed formatter consumer, `(i32, i32, i32) -> i32`, still + passes local MIR verification. Target planning and Wasm lowering both reject + it against the formatter export `(f64, i32, i32) -> i32`. + +These checks do not by themselves establish library behavior. Source fidelity, +compilation, and runtime agreement are separate. + +Library evidence, with the package fingerprint unchanged during each run: + +- Source audit of the locked package: 170 identical, 36 modified, 24 platform + additions, 230 modules, and no upstream module absent from the vendor. The + audit records differences. It does not approve them. +- `npm test` in `psrs-stdlib`: 11 passed. +- The library-owned Node runner accepted these oracles. Integer arithmetic has + 209 official observations; the unrepresentable JavaScript quotient of the + signed minimum divided by `-1` is excluded, and the raw Wasm primitive traps. + Public Show has 333 observations, with stdout exactly `3\n1\n2\n`, empty + stderr, and exit 42. `Number.abs` has 306 checks, decimal parsing 304 + observations, and Number/Int rounding 414 checks. Array application has 8 + cases and 29 equality checks. Array algorithms, including sorting, have 30 + cases and 79 equality checks. Every non-Show command exits 42 with empty + stdout and stderr. +- The integer oracle built from the locked package, without a development + package override, is sha256 + `020e5fbf004c167f74f3a149a8f40d84bb0f9a68424187db5f9f42d960194674`. Wasmtime + executed that artifact with exit 42 and empty stdout and stderr. + +This refactor does not establish whole-stdlib runtime completion. Number +square root has a separate checked `f64.sqrt` acceptance. Number inverse +cosine, inverse sine, inverse tangent, and four-quadrant inverse tangent +each have a separate checked scalar-runtime acceptance. Other Number foreign +slots remain without target support until a fresh diagnosis names the next +blocker. diff --git a/docs/implementation/backend/linear-memory-and-canonical-abi.md b/docs/implementation/backend/linear-memory-and-canonical-abi.md index 743b07e9..893a2f85 100644 --- a/docs/implementation/backend/linear-memory-and-canonical-abi.md +++ b/docs/implementation/backend/linear-memory-and-canonical-abi.md @@ -263,7 +263,7 @@ ABI-06: ```text ABI-07: - Implementation: crates/psrs-backend/src/component.rs (wit-component lift). + Implementation: crates/psrs-linker/src/compose.rs (wit-component lift). Tests: psrs-driver tests::integration::emits_a_wasi_command_component; tests::wasi::runs_main_as_a_wasi_component_when_wasmtime_is_available. Input boundary: encoded core module. diff --git a/docs/implementation/backend/linking-and-runtime.md b/docs/implementation/backend/linking-and-runtime.md new file mode 100644 index 00000000..3f182c62 --- /dev/null +++ b/docs/implementation/backend/linking-and-runtime.md @@ -0,0 +1,225 @@ +# Linking and Runtime Acceptance + +**Design:** [Linking and Runtime](../../design/backend/wasm/linking-and-runtime.md). +**Decision:** [DEC-18 (Accepted)](../../decision/DEC-18-unified-target-linking.md). +**Status:** LK-01 through LK-15 verified for the supported stable synchronous +target contract, including source-build guest composition and locked Show. + +**Recorded:** 2026-10-07; implementation evidence recorded 2026-10-07. + +This is the acceptance contract for unified target linking. The core plan carries +requirements through artifact verification, Wasm emission and core component +assembly. The checked guest graph composes that produced component, preserving +its artifact lineage and the exact final host capability set. + +`Verified` means the complete required evidence is established for the supported +stable target contract. Future formats or target proposals remain explicit +design extensions; this status does not grant support to rejected inputs. + +| ID | Requirement | Required evidence | Status | +| --- | --- | --- | --- | +| LK-01 | A checked language intrinsic selects one catalog implementation; raw ABI does not replace its scheme | Accepted use plus malformed source/Core operand/result rejection; registry and implementation identities agree | Verified | +| LK-02 | Direct operations, generated helpers, artifact exports, and entry-generated imports participate in one requirement closure | Composed source/MIR examples, missing-provider rejection, unused implementation omission, initializer reachability | Verified | +| LK-03 | Artifact signatures, digests, features, imports, exports, tables, and initialization match declared contracts | Valid artifact plus independently malformed signature, stale digest, unexpected import/feature/table/start and missing-export rejection | Verified | +| LK-04 | One immutable checked plan drives emission and component assembly | API and trace evidence; deliberately mismatched emitted imports/memory rejected; no late import-name provider selection | Verified | +| LK-05 | Shared memory reservations and allocator boundaries cannot overlap or overflow | Malformed ranges/bounds rejected; runtime formatting interleaved with WASI allocation, output and memory growth | Verified | +| LK-06 | Formatter output is recovered into a GC String before temporary bytes are released | Retain earlier strings across many later formats and WASI calls, asserting exact contents; bounded buffer reuse evidence | Verified | +| LK-07 | Runtime stack use fits its declared region under supported calling behavior | Recorded static bound with stress evidence; unsupported reentrancy/initialization explicitly rejected | Verified | +| LK-08 | Instantiation closes function/memory dependencies before execution | Run a legitimate shim cycle; reject unresolved/eager initializer cycles; inspect that private runtime imports are absent from the final external world | Verified | +| LK-09 | WIT definition resolution and executable provider selection remain separate | A definition-only package does not satisfy a live import; pinned host/guest binding selection, version/provider conflicts and missing exports tested | Verified | +| LK-10 | Guest WIT composition preserves provider memory, canonical ownership and resource identity | Cross-component string/list/result execution, post-return/free behavior, resource constructor/method/destructor coherence; incompatible providers rejected | Verified | +| LK-11 | The external world contains exactly permitted residual host capabilities | Used/unused service tests, guest transitive host dependency tests, disallowed capability and silent-fallback rejection | Verified | +| LK-12 | Runtime artifact regeneration and compile lineage are reproducible | Pinned source/dependency/toolchain/recipe manifest, artifact hashes, repeat-build agreement, plan inputs and selected providers recorded in diagnosis | Verified | +| LK-13 | Public Show executes with official semantics and preserved pure source/API | Pinned official source audit and JS oracle for Int, Number, Char, String, arrays and callback order; mandatory Wasmtime with byte-exact outputs | Verified | +| LK-14 | Runtime owns one WIT/artifact catalog; linker owns definition resolution; backend owns source/ABI validation | Move pinned WIT/default-world assets without byte drift; ABI lookup and composition use one resolved context; reject pin drift; no compiler dependencies or formatter code in metadata-only consumption | Verified | +| LK-15 | Independent linker crate consumes target records without compiler IR dependencies | Dependency-graph audit; target-only plan/composition tests; backend request conversion and source-diagnostic attribution; no MIR/backend/HIR/Core types in linker APIs | Verified | + +## Public Show behavior cases + +The library-owned oracle must cover signed-i32 extrema; negative zero; finite, +NaN and infinite Number values; notation boundaries and neighboring values; +subnormal/minimum/maximum binary64 values; shortest-decimal tie choices; named +and decimal control escapes; decimal-escape termination before digits; quote and +backslash escaping; Unicode scalars/UTF-8; empty and nested arrays; and ordinary +callback order/multiplicity. Include mixed use with console output and retained +strings so a correct token alone cannot hide an incorrect memory boundary. + +Use `psrs-stdlib/conformance/` and its public executable-based runner for official +JS comparisons. Compiler regression tests additionally cover malformed Core, +MIR, raw artifacts, plans, and component contracts. Source import acceptance, +Wasm validation, execution, and official oracle agreement are separate evidence. + +## Evidence + +- `psrs-linker` target-only unit and integration tests: artifact contract + verification and the measured stack bound (`verify::tests`, `stack::tests`), + definition resolution (`definitions::tests`), provider closure, target-policy + gating, version-pin drift and memory planning (`tests/plan.rs`), and + composition (`tests/compose.rs`). The composition boundary rejects different + WIT contexts, actual signature/memory drift, unowned data and incorrect + allocator initialization. `closure.rs` projects actual WIT call signatures + through the same encoder used by final composition: `get-stdout` retains its + `streams` resource owner, without expanding unused `error`/`poll` methods. +- `psrs-linker` `tests/guest/` validates pinned whole-interface binding graphs, + transitive guest/host closure, unused candidates, conflicts, missing exports, + incompatible function shapes, definition-only packages, dependency cycles and + target feature restrictions. Mandatory Wasmtime execution passes UTF-8 strings + twice across distinct memories, checks earlier values after provider + post-return clears its buffer, verifies a byte-list result and three + post-return calls, then checks resource constructor/method/destructor coherence. +- `psrs-cli` `tests/link.rs` verifies manifest-relative provider paths, selected + pins, exact host closure and report/output digest agreement; stale bytes and + WASI interfaces outside the target profile fail before files are emitted. + [Explicit Component Linking](../../workflow/component-linking.md) specifies + this entry point. Source build reports join diagnosis and graph lineage. +- `psrs-runtime` formatter token tests, catalog digest, and + `tools/check-reproducible.sh`, which rebuilds the artifact and requires it to + reproduce the committed bytes. +- `psrs-backend` `mir::number_format_tests::formats_a_number_through_the_runtime_artifact` + executes the formatter artifact through the checked plan under Wasmtime and + asserts the initialized token length through the command exit code. +- `psrs-driver` `tests::show::show_renders_the_instances_the_corpus_prints` + executes `Data.Show` under Wasmtime and asserts byte-exact stdout across Int, + Number (including `1e+21` and `1e-5`), Char, String escapes, unit, booleans, + and arrays, interleaving formatting with WASI output and retained strings. + `tests::show::show_covers_number_and_aggregate_boundaries` adds NaN, + ±Infinity, negative zero, the `1e-6`/`1e-7` and `1e20` notation boundaries, + the minimum subnormal, the empty array, and a nested array. + `tests::show::formats_many_numbers_without_exhausting_the_runtime_stack` + formats a 64-element Number array through the same private stack region. + Memory growth is not exercised; the application allocator is pre-sized by the + plan. +- `psrs-driver` `tests::diagnosis_trace::target_plan_records_provider_and_memory_lineage` + and `tests::show::formatter_plan_records_the_pinned_artifact_digest` assert the + compile diagnosis records the selected providers, artifact digests, memory + boundary, stack bounds, and planned external world. +- Validation for this continuation uses focused linker, CLI, backend formatter + and source-level Show/diagnosis tests, required Wasmtime execution, formatting, + workspace Clippy and byte-for-byte runtime regeneration. Full workspace tests + are omitted under the user's explicit validation scope; this record does not + claim a new workspace-wide acceptance measurement. + +### Continuation validation + +Measured with mandatory Wasmtime execution and `PSRS_STDLIB_ROOT` pointing to +the development stdlib checkout where source-level tests require it: + +| Command or focused filter | Result | +| --- | --- | +| `cargo test -p psrs-linker` | 42 passed: 15 unit, 6 core composition, 9 guest composition, 12 plan | +| `cargo test -p psrs-cli --test link --test source_layout` | 2 passed | +| `cargo test -p psrs-backend --lib mir::number_format_tests` | 1 passed | +| `cargo test -p psrs-backend --lib mir::gc_tests` | 22 passed | +| `cargo test -p psrs-driver --lib tests::show::` | 5 passed | +| `cargo test -p psrs-driver --lib tests::wasi::wrappers::` | 19 passed | +| `cargo test -p psrs-driver --lib tests::diagnosis_trace::target_plan_records_provider_and_memory_lineage` | 1 passed | +| `cargo fmt --all --check` | Passed | +| `cargo clippy --workspace --all-targets -- -D warnings` | Passed | +| `sh crates/psrs-runtime/tools/check-reproducible.sh` | Committed artifact reproduced byte-for-byte | + +The guest execution test requires Wasmtime unconditionally; backend and driver +execution runs set `PSRS_REQUIRE_WASMTIME=1`. The full workspace test suite and +new corpus scoreboard measurements are outside this continuation's validation +scope. No new source syntax or official source diagnostic is introduced. + +## Supported-contract closure + +The acceptance boundary is the stable synchronous target stated in the design: +checked direct primitives and generated helpers, the complete pinned core +runtime catalog, explicit whole-interface guest providers, and exact residual +host capabilities. Unsupported relocatable/dynamic loading, async/WASI 0.3, +partial-method bindings, version adapters, reentrant runtime libraries and eager +provider initialization remain explicit errors or future design extensions. +They are not silently accepted and are not implemented by this verification. +The normative design's general invariants remain unchanged. + +### Requirement audit + +- LK-01/02: language schemes remain HIR/Core-owned. Raw artifact signatures + cannot establish a Number-to-String use. The runtime implementation descriptor + is shared by intrinsic lowering and requirement selection. Optimized command + closure starts at the entry, retaining direct calls, tail calls, function + references and closure construction; modules without an entry retain all + definitions. Generated codecs/allocators and command exit remain explicit + roots. Unused functions no longer keep foreign service imports alive. +- LK-03/04/08: encoded application imports, memory and active initialization + are independently checked against the plan. Even a scalar-only application + emits the plan's minimum memory. Source and synthesized command exit reuse + one checked import; execution distinguishes stored/forced actions by exit + codes 0/99. Core artifact eager starts and guest + executable starts are rejected; typed component instantiation closes guest + dependencies before command invocation. Backend fixtures use their real + explicit requirements instead of an empty-plan composition bypass. +- LK-05/06/07: the allocator checks block capacity overflow and grows memory + before writing fresh metadata. Its Wasmtime fixture ends one allocation at + exactly 65536, allocates beyond that boundary, observes two pages, checks old + bytes and subsequent reuse, and rejects wrapped free-list sizes. A source + program retains `show 1e21`, performs a 70,000-byte WASI random allocation, + formats the minimum subnormal, and prints/checks the retained String again. + Growth preserves reserved addresses and the fixed runtime stack region; + measured stack analysis and byte-reproducibility remain mandatory. +- LK-09/10/11: executable guest selection is explicit and pinned, never inferred + from definition packages or raw-signature coincidence. Shared validator type + identity and typed graph connections preserve canonical ownership and + resources. Source `WASI.Random.insecureSeed` is bound to a guest returning + `(5,37)` and executes to exit 42; its unused random services disappear from + the encoded outer world. Transitive host/guest, conflicts, type/feature/version + rejection, post-return and destructor evidence remains in linker tests. +- LK-12/14/15: `build --manifest --report` records source/pass/artifact lineage, + the exact produced component artifact/digest, manifest digest, checked graph + pins/edges/profile, residual host imports and final digest. Rejected guest plans retain the source lineage and diagnostic + without a successful output digest. Standalone `link` + still requires the root pin. Runtime owns the immutable WIT/artifact catalog; + the independent linker consumes target-only records, with no compiler IR + dependencies. Both input and output byte identities join the two plan stages. The final + dependency metadata audit confirms that linker/runtime depend on no + HIR/Core/backend/driver crates. +- LK-13: the library-owned oracle verifies the exact five typed foreign-slot + delegates against clean pinned Prelude source, preserving every other source + declaration. The official JS FFI supplies 333 observations: 8 Int, 174 Number + (Show plus raw token), 41 Char, 105 String and 5 array cases. Actual Wasmtime + execution returns 42 with exact callback stdout `3\n1\n2\n` and empty stderr. + Evidence is in `psrs-stdlib/docs/evidence/show/`; its reproduction contract is + `psrs-stdlib/docs/show.md`. The compiler lock pins that completed library + revision and content fingerprint; this does not promote the entire stdlib. + +### Final validation + +All execution commands below ran with `PSRS_REQUIRE_WASMTIME=1` where applicable. +Driver/CLI final runs use the locked package, without `PSRS_STDLIB_ROOT`. +The library oracle additionally uses its explicit public development-package +runner; a separate lock-checked build runs the same 333 checks. + +| Command | Result | +| --- | --- | +| `cargo test -p psrs-backend` | 400 passed | +| `cargo test -p psrs-linker -p psrs-runtime --features psrs-runtime/formatter` | 43 linker and 1 runtime test passed | +| `cargo test -p psrs-cli --test link --test source_layout` | 3 passed | +| `cargo test -p psrs-driver --lib tests::wasi::` | 146 passed | +| `cargo test -p psrs-driver --lib tests::show::` | 6 passed | +| `cargo test -p psrs-driver --lib loads_the_standard_library_from_disk_in_trusted_order` | 1 passed | +| Library `conformance/show.mjs` and public `run` | 333 official checks accepted, exit 42, exact stdout and empty stderr | +| Lock-checked `cargo run -- build Target.purs Main.purs` and Wasmtime | 333 checks, exit 42, exact stdout and empty stderr | +| `cargo fmt --all --check` | Passed | +| `cargo clippy --workspace --all-targets -- -D warnings` | Passed | +| `sh crates/psrs-runtime/tools/check-reproducible.sh` | Byte-for-byte agreement | + +[Locked Show evidence](linking-evidence/locked-show.json) records package pins, +compiler and Wasm digests, and execution observations. Source/API evidence and +the library-owned oracle are committed with the pinned library revision. The +full workspace suite and new corpus scoreboards were not run under the explicit +validation scope; no source syntax or diagnostic behavior changed. + +## Intrinsic selection and consumer ABI correction (2026-10-07) + +The backend now owns an exhaustive intrinsic implementation catalog, including +direct operations and generated representation operations as well as artifacts. +Artifact enumeration derives from that selection. The runtime supplies raw +signatures and value protocols, while CC/MIR own checked language adaptation. + +Artifact requirements use actual MIR consumer signatures, independently of the +provider descriptor. The earlier formatter path supplied both signatures from +the provider catalog and therefore did not verify the consumer. New rejection +fixtures cover parameter width/count, missing/wrong results and GC references; +reserved generated helpers also reject wrong signatures and string layouts. +See [the measured acceptance record](intrinsic-implementations-2026-10-07.md). diff --git a/docs/implementation/backend/linking-evidence/locked-show.json b/docs/implementation/backend/linking-evidence/locked-show.json new file mode 100644 index 00000000..30ec8f4c --- /dev/null +++ b/docs/implementation/backend/linking-evidence/locked-show.json @@ -0,0 +1,18 @@ +{ + "schema_version": 1, + "stdlib_lock": { + "schema_version": 1, + "path": "../psrs-stdlib", + "revision": "8aaaffad89acf7aa71da38b1405920d4f37356ef", + "source_fingerprint": "fnv1a64-v1:73d24a5e09ceea6c" + }, + "compiler_sha256": "9881768920f1295da5a679fd2b0254a7b15bfb0ddcaba7dca1c07156aef374ff", + "wasm_sha256": "ef6d1913578e840e1563a49f856692edbe2775d3fece65d7c6d4e0f9668d361b", + "official_oracle_checks": 333, + "execution": { + "exit_code": 42, + "stdout": "3\n1\n2\n", + "stderr": "" + }, + "accepted": true +} diff --git a/docs/implementation/backend/mir.md b/docs/implementation/backend/mir.md index 06346212..62888163 100644 --- a/docs/implementation/backend/mir.md +++ b/docs/implementation/backend/mir.md @@ -173,7 +173,7 @@ MIR-04: ```text MIR-05: Implementation: mir/instruction.rs (destination/operands/span), - mir/numeric.rs, mir/scalar_helpers.rs, mir/verify/instruction/*. + mir/numeric.rs, mir/verify/instruction/*. Tests: mir::gc_tests::binary_matrix:: verifies_and_executes_every_cc_binary_scalar_variant_on_both_targets; mir::gc_tests::unary:: diff --git a/docs/implementation/backend/pattern-matching.md b/docs/implementation/backend/pattern-matching.md index 3c71dd32..b9deb07c 100644 --- a/docs/implementation/backend/pattern-matching.md +++ b/docs/implementation/backend/pattern-matching.md @@ -149,8 +149,9 @@ PM-03: Gaps: none. PM-04: - Implementation: cc/case/coverage/mod.rs `analyze`, `useful`, `useful_with_active` - (cycle-safe), `signature`, `specialize`, `default_matrix`, `render`; + Implementation: cc/case/coverage/mod.rs `analyze`; coverage/engine.rs `useful`, + `useful_with_active` (cycle-safe), `signature`, `specialize`, `default_matrix`, + and finite witness construction; coverage/render.rs `render`; cc/case/mod.rs `require_exhaustive`/`report_redundant_branches`. Tests: coverage::tests::reports_the_missing_nullary_constructor, ::recognizes_exhaustive_nested_constructor_patterns, @@ -160,6 +161,10 @@ PM-04: ::recursive_adt_analysis_finds_a_finite_uncovered_witness, ::recursive_coverage_agrees_with_a_bounded_first_match_oracle (all small recursive matrices against a bounded oracle); + ::recursive_products::recursive_products_terminate_in_either_constructor_order + checks missing `Leaf` witnesses and redundant binary-tree rows; + ::recursive_products::an_uninhabited_recursive_product_does_not_invent_a_witness + rejects infinite recursive products as witness candidates; decision::compile::oracle_tests::coverage_agrees_with_the_first_match_oracle; adts::reports_a_missing_nested_constructor_as_a_coverage_witness checks the nested witness and exact case-expression span; @@ -269,7 +274,9 @@ PM-09: Gaps: none. PM-10: - Implementation: coverage/mod.rs cycle-safe `useful_with_active`; + Implementation: coverage/engine.rs default-matrix recursion for incomplete + signatures, cycle-safe `useful_with_active`, and type-path-scoped finite + witness construction; compile/mod.rs memoization and the all-irrefutable-column base case. Tests: coverage::tests::recursive_adt_wildcard_coverage_terminates, ::recursive_adt_analysis_finds_a_finite_uncovered_witness, @@ -277,6 +284,8 @@ PM-10: oracle_tests and oracle_record_tests (whole small matrices); pattern_matching_audit::recursive_pattern_compilation_terminates_and_stays_first_match executes a depth-four recursive ADT; adts::runs_nested_non_parameterized_gc_aggregates. + pattern_matching_audit::recursive_binary_products_execute_in_either_constructor_order + executes nested binary-tree patterns with either constructor ordering. Input boundary: verified Core and source. Commands: common commands. Result: pass; the recursive execution case ran under Wasmtime. diff --git a/docs/implementation/backend/polymorphism-and-erasure.md b/docs/implementation/backend/polymorphism-and-erasure.md index c18ac093..60346b5e 100644 --- a/docs/implementation/backend/polymorphism-and-erasure.md +++ b/docs/implementation/backend/polymorphism-and-erasure.md @@ -46,6 +46,7 @@ existing code or a fixture that only inspects WAT does not verify execution. | PE-10 | CC and MIR verifiers reject malformed adapters, captures, calls, and unresolved representation requirements. | Full-module negative fixtures for wrong signature, capture index/type, arity, cast provenance, and result shape before Wasm emission. | Verified | | PE-11 | Erased values are recovered before canonical WIT calls; optimized and unspecialized execution agree. | Source or verified Core fixture crossing a concrete ABI call, plus execution retaining an erased generic path and normal optimized execution. | Verified | | PE-12 | Nested quantifiers preserve each value's uniform definition signature, independent use-site conversions, and returned closure arity. | Verified Core and source execution at distinct instantiations; quantified parameters, fields, captures, returned functions and direct over-application; malformed boundary rejection. Coordinate RN-02 through RN-11. | Unverified | +| PE-13 | Abstract constructor applications preserve their producer storage protocol across generic calls, dictionary methods and returned closures. Source evidence survives until a common conversion plan fixes both physical endpoints. | Mandatory value-sensitive execution of the non-Effect Reader dictionary reproduction below, Effect discard/map and fixed-payload cases; inspect producer/recovery signatures; retain ordinary closure and aggregate regressions. | Verified | ## Vertical execution order @@ -270,10 +271,98 @@ PE-11: Revision: cc5d0f4 + audit diff. Gaps: there is no separate unspecialized compiler mode; the erased path is the one path and it is the executed path. + +PE-13: + Implementation: psrs-core/src/instantiation.rs (read-only checked + instantiation evidence), psrs-core/src/verify/types/matching/invariant.rs + (invariant matcher with explicit constructor binding), psrs-backend/src/ + boundary.rs (BoundaryEvidence and the RepresentationRegistry: scheme/use + evidence read from the immutable source module; Function and Effect + constructor policies; payload-erased protocol signatures), + cc/lower/conversion/transport.rs (constructor transport), + cc/lower/conversion/callable.rs (representation-only adapter emission), + cc/lower/conversion/mod.rs (plan_conversion consults transport first), + cc/lower/erased/mod.rs and cc/lower/{global,record}/mod.rs (evidence + read from the boundary at the use), cc/lower/call/partial.rs + (lower_indirect_partial_application captures an under-applied local or + dictionary callee). + Tests: psrs-driver tests::closure_protocol:: + reader_dictionary_returns_the_concrete_result (Reader `Chain ((->) Int)` + exits 42 with empty stdout), + abstract_callable_transport_emits_a_checked_adapter (the CC contains a + FunctionAdapter, a `protocol_adapter_factory_` capturing the producer + protocol closure, and a generated adapter returning `ValueShape::Integer`), + fixed_unit_payload_through_a_bind_constraint_repeats_the_action + (`forall f. Bind f => f Unit -> f Unit` at Effect prints `again` twice); + psrs-driver tests::partial_application:: + a_partial_application_of_a_local_closure_defers_the_remaining_argument and + a_partial_application_of_a_dictionary_method_defers_the_remaining_argument; + psrs-core tests::instantiation (distinct nominal applications rejected, + flexible heads bind from either side, dictionary quantifiers alpha-rename, + unsolved and cyclic bindings are not constructor identities); + psrs-driver tests::effects::discard_defined_from_bind_sequences_effects + (`a\nb\n`); psrs-driver tests::functor:: + mapping_an_effect_does_not_run_it_until_the_action_runs (`before\ntick\n2\n`). + Input boundary: linked source and source plus a verified CC module. + Commands: PSRS_REQUIRE_WASMTIME=1 cargo test -p psrs-driver --lib + closure_protocol; PSRS_REQUIRE_WASMTIME=1 cargo test --workspace. + Result: pass. The Reader method and its caller recover the stored protocol + and adapt; a reference cast is not used as the adapter. The Effect discard, + delayed-map and fixed-payload cases run to completion with the exact + output, and an under-applied local closure or dictionary method exposes its + remaining parameter through a generated indirect call. The L6/M7 scoreboard + moved from 164/413 to 210/413 on this tree: the stored-protocol recovery + cleared 29 of the 30 P8 CC verifier failures and 17 of the 19 runtime traps, + and the indirect partial-application handling cleared three more + closure-conversion failures. L1–L5 are unchanged. The workspace suite is not fully green: ten + pre-existing Phase-3 failures (bare library operators that now need an + import, a local `Data.Boolean` that duplicates the vendored module, + `Newtype`/`Coercible` type checking on the official `Data.Foldable` and + `Data.Monoid`, a resolver `ScopeConflict`, and a class-mediated constant + fold) are unrelated to this topic and are recorded under remaining work. + Revision: 675f0e3 plus this slice. + Gaps: none for the Reader, discard, delayed-map and fixed-payload cases. + Binding quantifiers that are not leading `forall` nodes on the type still + need explicit evidence review. ``` ## Remaining work and blockers +- **Abstract constructor and closure transport (PE-13, landed).** The 2026-10-05 + investigation below reproduces the runtime failure without Effect. The + repair is now landed and tested: the non-Effect Reader dictionary, Effect + `discard`, delayed map, and fixed-payload `f Unit` programs execute, and the + abstract callable boundary recovers the producer's stored protocol before + generating an adapter (see the PE-13 evidence). The L6/M7 scoreboard moved + from 164/413 to 210/413. An under-applied local closure or dictionary method + is supported by a generated closure that calls the captured callee + indirectly. What remains is binding quantifiers that are not leading `forall` + nodes on the type, and constructors other than `Function` and the registered + Effect token protocol do not each have explicit execution evidence. The two + experimental worktrees are stopped and are not integrated. +- **Representation evidence and policies (landed).** Checked instantiation + evidence and constructor representation policies reach P8 through the + Core-to-CC side table (`psrs-backend/src/boundary.rs`). The + `RepresentationRegistry` registers `Function` (checked instantiation arguments) + and the trusted `Effect` (runtime token); `BoundaryEvidence` provides the + checking-owned relation and the payload-erased protocol signature of each + callable constructor. `FunctionLowerer::source`, `constructor_protocols`, and + `transport_signatures` are gone. The effect token is the opaque + `TypeId::STATE_TOKEN`. The checker's relation is a single `TypeMatcher::relate` + entry parameterized by `Variance` (subsumption or invariant); P8 consumes its + `Instantiation` evidence and recomputes nothing. +- **Workspace suite still red on the Phase-3 migration.** Ten driver tests fail + for reasons this topic does not own: five use `-` or `/` without importing the + library operator that now owns it (`tests::scalars`, `tests::functions`, + `tests::operators` twice, and + `polymorphism_erasure_audit::linked_modules_round_trip_an_erased_high_bit_int`); + two declare a local `Data.Boolean` that the vendored library now also + provides (`tests::guards`); one folds `+` through the `Data.Semiring` class + and no longer reaches `i32.const 42` (`tests::integration`); one expects a + resolver `ScopeConflict` (`tests::declarations`); and one needs + `Newtype`/`Coercible` type checking on the official `Data.Foldable` and + `Data.Monoid` (`tests::foldable`). `cargo clippy --workspace --all-targets -- + -D warnings` also reports pre-existing lints on the same branch. - **MIR RefCast/RefTest nullability handoff.** `mir/verify/instruction` checks the cast target heap and destination type but does not reject a nullable operand or mismatched operand/target nullability; tightening it is owned by @@ -300,3 +389,161 @@ PE-11: erased identity fixture; type-class dictionaries and returned polymorphic functions remain independently tracked in [type classes and dictionaries](type-classes-and-dictionaries.md). + +## Constructor and closure investigation (2026-10-05) + +### Baseline and classification + +Revision `675f0e3` on `stdlib/vendor-core-libraries`. Experiments used a clean +`git archive` of that revision, built with `cargo build --offline -p psrs-cli +--bin psrs`, and Wasmtime 49.0.2. The active implementation checkout is the +main worktree on that existing iteration branch. No experimental Rust diff was +transferred there. No workspace suite or official scoreboard was run, and no +D-04 or README rate was changed. + +The independent reproduction defines its own class and does not import Prelude, +Effect, a host service or a trusted Effect operation: + +```purescript +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 +``` + +Compilation succeeds. Expected exit is 42 with empty stdout; actual exit is +134 with `wasm trap: cast failure`. In the emitted core module, function 14 +constructs a closure using `ref.func 13`, whose code type 9 is +`(ref struct, i32) -> eqref`. Function 16 receives the returned erased closure, +recovers it, and casts its code reference to type 10, +`(ref struct, i32) -> i32`, before `call_ref 10`. The generated CC records direct +`RecoverReference(TypeInstantiation)` to the consumer closure signature. +This is a producer/consumer calling-convention mismatch, not an Effect +operation or runtime instance-selection failure. Function/type numbers are +evidence for this artifact only. + +The same clean build measured these small source cases: + +| Case | Expected behavior | Observed behavior | +| --- | --- | --- | +| Generic `apply` receives a locally generalized identity | Exit 42, no stdout | Exit 42, no stdout | +| A closure passes through `forall a. a -> a` | Exit 42, no stdout | Exit 42, no stdout | +| A closure passes through a `Box a` constructor and projection | Exit 42, no stdout | Exit 42, no stdout | +| A closure is forwarded through `forall f a. f a -> f a` | Exit 42, no stdout | Exit 42, no stdout | +| Custom Reader `Chain` example above | Exit 42, no stdout | Cast trap, exit 134 | +| Effect `discard` | Exit 0, `a\nb\n` | Cast trap, exit 134, empty stdout | +| Mapping an Effect action | Exit 0, `before\ntick\n2\n` | Cast trap, exit 134, empty stdout | +| `forall f. Bind f => f Unit -> f Unit` instantiated at Effect | Exit 0, `again\nagain\n` | Cast trap, exit 134, empty stdout | +| Reader examples using the vendored Prelude instances | Compile, then execute | Rejected at P7; no runtime result | + +The passing forwarding case shows that erasure does not always require an +adapter. The failing Reader dictionary establishes a general representation +contract gap. It does not prove that every Effect failure follows an identical +adapter chain or that one local patch repairs them all. Effect additionally +rewrites source applications before P8; its explicit source-to-physical mapping +must compose with the general repair. Keep the separate P7 Reader failure out +of the runtime diagnosis. + +Artifacts are preserved at +`/private/tmp/psrs-closure-representation-audit-20261005/`: source files, +`measurements.json`, build/runtime stdout and stderr, and Reader CC/MIR/WAT. +The recorded command form was `baseline/target/debug/psrs build .purs +-o .wasm`, followed by `wasmtime run .wasm`, from that directory. +Expected nonzero successful exits were checked as values, not as shell success. + +### Disposition of stopped candidates + +Both worktrees remain at base `675f0e3`, with uncommitted checkpoints and exported +patches. Neither is a repair suitable for integration: + +| Candidate | Useful investigation result | Disposition | +| --- | --- | --- | +| `fix/stdlib-closure-contract` | Established the lost constructor relation and exercised Core traversals | Reject the closure-origin field approach; preserve the checkpoint, do not apply it | +| `fix/stdlib-p8-effect-contract` | Explored immutable source/physical views, shared checking evidence and layout roots | Restart implementation from the existing iteration branch; do not transplant this diff | + +The second candidate routes the public conversion entry through +`effect_aware_conversion`, scopes `effect_instantiation`/`effect_compatibility` +state and gathers evidence primarily for saturated direct Global calls. This +does not define the ordinary Reader constructor protocol and leaves other +call/storage boundaries incomplete. Its flat symmetric node correspondences +do not preserve position, checking direction or binder context. Its extra +scheme projection can reject a previously successful match; evidence extraction +has not been shown to preserve the source checker's acceptance contract. +Removing the rewritten module's ordinary verification also left the complete +physical expression/declaration contract unverified. Focused proof tests and a +successful package check cannot establish these obligations or runtime repair. + +Retain the reproduced programs, signature traces and test intentions. Reuse the +ideas of immutable source views and Core-owned checked relations after defining +their general contracts; review any code extracted for those ideas separately. +The Core matcher must not infer a source constructor from callable arity. + +### Restart order and acceptance gates + +1. Fix the general boundary model before coding: definition/use relation, + producer storage protocol, consumer requirement and source-to-physical map. + Preserve quantifier boundaries, direction and field/application positions. +2. Define one checked conversion-plan API for ordinary callable constructors, + scalar boxes, arrays and ADT/record storage. Separate planning from emission; + representation-only adapter endpoints need a physical contract, not an + invented source origin. Evidence collection must not change source acceptance. +3. Implement and execute the minimal non-Effect Reader dictionary path through + this API. Cover both the method producer and returned-closure consumer; + direct/indirect calls and partial applications must use the same contract or + report unsupported boundaries explicitly. Preserve the passing controls. +4. Connect the trusted Effect application-to-closure mapping to that API. + Execute discard, delayed map and fixed-payload cases with exact output/status. + Check arrays, dictionary fields and captures through the recursive planner. +5. Verify the complete lowered representation and generated CC before erasing + semantic evidence. Then run the agreed focused regression scope. Workspace + tests, scoreboard measurements and completion claims remain separate gates. + +This is a design and investigation checkpoint. No replacement implementation +is claimed complete, and the historical evidence above does not close PE-13. + +### Landed execution checkpoint (2026-10-05) + +The main worktree on `stdlib/vendor-core-libraries`, based on `675f0e3`, keeps +an immutable Core snapshot from before `lower_effects`. Expression +instantiation and compatibility whose type ids still exist use that snapshot. +`Function` with one fixed argument supplies the Reader domain. The Effect +representation owner supplies the runtime token for a registered user +constructor with no instantiation arguments. Dictionary-field selection and a +generalized local use pass that checked instantiation into the common +conversion planner. The planner emits an adapter when the producer and +consumer protocols differ. A reference cast is not used as that adapter. + +Wasmtime 49.0.2 executed these programs from the current debug `psrs` binary. +Exit status is the program value, including Reader's 42. + +| Case | Result | +| --- | --- | +| Custom Reader `Chain` | Exit 42, empty stdout | +| Effect `discard` | Exit 0, stdout `a\nb\n` | +| Mapping an Effect action | Exit 0, stdout `before\ntick\n2\n` | +| `forall f. Bind f => f Unit -> f Unit` at Effect | Exit 0, stdout `again\nagain\n` | + +The Reader and fixed-payload cases are committed as +`crates/psrs-driver/src/tests/closure_protocol.rs` tests; the discard and map +cases are the existing `effects` and `functor` tests. Under-application of an +indirect callee (a local closure or a dictionary method) is now lowered by a +generated closure that captures the callee and the supplied arguments and calls +it indirectly; the `tests::partial_application` cases execute it. Binding +quantifiers that are not leading `forall` nodes on the type still need explicit +evidence review. Array, ADT, and newtype protocols still belong to their +representation owners. Unsupported constructor transport is rejected with a +source-spanned diagnostic. On this tree the L6/M7 scoreboard moved from +164/413 to 210/413, so the D-04 and README runtime rows are updated with it; +L1–L5 are unchanged. diff --git a/docs/implementation/backend/scalars-and-primitives.md b/docs/implementation/backend/scalars-and-primitives.md index 53a9de59..ac11bb45 100644 --- a/docs/implementation/backend/scalars-and-primitives.md +++ b/docs/implementation/backend/scalars-and-primitives.md @@ -38,12 +38,12 @@ Verified requires exact test and executed result evidence. | SP-02 | `String` keeps a separate semantic CC shape and is a GC byte sequence, linearized only at the canonical ABI boundary. | Positive string literal/import use and verifier rejection of numeric operations on String values. | Verified | | SP-03 | Every specified CC unary/binary primitive has an exact MIR instruction or helper lowering. | Exhaustive operation table matching the design vocabulary; reject missing opcode mappings and wrong operand/result types. | Verified | | SP-04 | Integer add/subtract/multiply and bitwise operations wrap at 32 bits; shift counts follow the specified modulo-32 behavior. | Source or verified Core execution at overflow/underflow and shift counts 0, 31, 32, 33; compare exact bits/results. | Verified | -| SP-05 | Integer quotient/remainder truncate toward zero and trap for divisor zero and signed minimum divided by -1. | Positive signed combinations and expected-trap component cases; distinguish quotient/remainder from floor division/modulo. | Verified | -| SP-06 | Integer division/modulo helpers implement floor quotient and divisor-signed remainder without introducing unrelated traps. | Positive/negative dividend-divisor matrix, zero/overflow trap cases, helper interning, and execution with multiple call sites. | Verified | +| SP-05 | Integer quotient/remainder truncate toward zero and trap for divisor zero and signed minimum divided by -1. | Positive signed combinations and expected-trap component cases; distinguish raw primitives from library Euclidean division/modulo. | Verified | +| SP-06 | Library division/modulo own nonnegative Euclidean remainder and zero handling; no duplicate compiler arithmetic policy remains. | Source execution for all signs, negative divisors, zero divisors, and case branches; retired bindings rejected. | Verified | | SP-07 | Number arithmetic, comparisons, and unary operations retain Wasm/IEEE semantics including NaN and signed zero. | Value-sensitive f64 execution, bitwise inspection when sign of zero matters, and expected NaN comparison behavior. | Verified | | SP-08 | Boolean and character operations use their canonical shape and reject invalid values at appropriate boundaries. | True/false logical cases, Unicode scalar boundary cases, and malformed CC/MIR operation fixtures. | Verified | | SP-09 | `NumberToInt` truncates with saturation for finite overflow, infinities, and NaN; `IntToNumber` follows exact signed conversion. | Execute thresholds around both i32 limits, fractions, infinities, NaN, and full-width integers; assert exact results. | Verified | -| SP-10 | Generated helpers are emitted only when reachable, with collision-free symbols and deterministic scans through nested expressions. | Compare modules with/without div/mod/conversion, nested `If` uses, user-symbol collisions, and repeated calls. | Verified | +| SP-10 | Generated helpers are emitted only when reachable, with collision-free symbols and deterministic scans through nested expressions. | Conversion helper reachability/collisions and reserved generated binding signatures; public div/mod use ordinary library lowering. | Verified | | SP-11 | CC and MIR verifiers enforce exact scalar operand/result and helper-call signatures before encoding. | Negative full-module fixtures for mixed Int/Number, Boolean/i32 confusion, String arithmetic, wrong conversion types, and malformed helper calls. | Verified | | SP-12 | Normal optimization and Wasm encoding preserve values and expected traps under the selected target profile. | Execute optimized/unoptimized representative operations, validate core module/component, and compare against a reference arithmetic oracle. | Verified | | SP-13 | `stringToBytes` is lossless and `bytesToString` checks its input: every element must be a canonical byte and the sequence must be well-formed UTF-8, with a violation trapping rather than producing replacement text ([DEC-16](../../decision/DEC-16-scalar-strings-and-utf8-storage.md)). | Executed round-trip over ASCII, two-byte, four-byte, and empty text; the exact encoded bytes; an out-of-range element; and a malformed UTF-8 sequence. | Verified | @@ -159,7 +159,7 @@ SP-02: ```text SP-03: Implementation: cc/scalar.rs (full CC vocabularies), mir/numeric.rs - (`From`, `TryFrom`), mir/scalar_helpers.rs, + (`From`, `TryFrom`), target_intrinsics/, wasm/lower/structure/ops.rs::primitive, wasm/lower/structure/unary.rs Tests: mir/gc_tests/binary_matrix::verifies_and_executes_every_cc_binary_scalar_variant_on_both_targets (all CC binary variants), mir/gc_tests/unary::lowers_unary_and_conversion_operations_on_both_targets @@ -189,38 +189,14 @@ SP-04: ```text SP-05: - Implementation: mir/numeric.rs IntQuot/IntRem -> I32DivS/I32RemS; - wasm/lower/structure/ops.rs - Tests: mir/gc_tests/binary_matrix (IntQuot 7 2 = 3, IntRem 7 2 = 1); - driver tests/scalars.rs::truncated_and_floor_division_trap_on_zero_divisor_and_signed_overflow - asserts runtime traps `integer divide by zero` for `1 / 0`, `1 % 0`, - `intDiv 1 0`, `intMod 1 0` and `integer overflow` for - `i32.min / -1` through both `intQuot` and `intDiv` - Input boundary: source and executed Wasm - Commands: common commands above - Result: pass; executed under Wasmtime - Revision: cc5d0f4 + uncommitted - Gaps: none + Updated obligation (2026-10-07): Raw IntQuot/% remain truncating; zero and quotient overflow trap. Public library div/mod handle zero. The previous record incorrectly claimed public division traps on zero. + Evidence: intrinsic-implementations-2026-10-07.md ``` ```text SP-06: - Implementation: mir/scalar_helpers.rs (`lower_scalar_helpers`, - `contains_operation`, `euclidean_helper`, `ScalarHelpers::binary_instruction`) - Tests: mir/gc_tests/div_mod::lowers_euclidean_integer_division_and_modulo_on_both_targets - (all four dividend/divisor sign combinations, expected 3), - ...::detects_division_and_modulo_nested_in_tag_switch_cases (case-nested - `intDiv`/`intMod` lower and execute to 4), - driver tests/scalars.rs::generates_floor_helpers_for_division_nested_in_case_branches - (asserts both helpers exist once and `7 div 2 = 3` at runtime) - Input boundary: CC fixtures and source - Commands: common commands above - Result: pass; executed under Wasmtime; before the `contains_operation` - fix the nested case failed with `missing MIR helper for scalar operation - IntDiv` - Revision: cc5d0f4 + uncommitted (fix: recurse into `TagSwitch` cases and the - default arm, not only `If`/`Primitive`) - Gaps: none + Updated obligation (2026-10-07): The former compiler floor helpers have been removed. Their old signed-divisor tests did not prove official Euclidean behavior. Replacement source evidence covers the ordinary library wrappers and retired bindings. + Evidence: intrinsic-implementations-2026-10-07.md ``` ```text @@ -278,21 +254,8 @@ SP-09: ```text SP-10: - Implementation: mir/scalar_helpers.rs `lower_scalar_helpers` - Tests: mir/gc_tests/div_mod::does_not_emit_helpers_without_division_or_modulo - (a module using only IntAdd/IntQuot keeps one function); - ...::skips_helper_symbols_used_by_module_functions (a module symbol at - `(ModuleId(0), u32::MAX)` forces the allocator to skip it; helper symbols - stay distinct); ...::detects_division_and_modulo_nested_in_tag_switch_cases - (nested case); ...::detects_modulo_nested_in_a_tag_switch_default_arm - (nested default arm); ...::lowers_euclidean_integer_division_and_modulo_on_both_targets - (repeated call sites) - Input boundary: CC fixtures - Commands: common commands above - Result: pass; executed under Wasmtime - Revision: cc5d0f4 + uncommitted - Gaps: none. Conversion helpers named in the matrix belong to - generic-aggregate-erasure and are unchanged. + Updated obligation (2026-10-07): Scalar division no longer generates helpers. Conversion reachability remains in its owning generic-erasure topic. Reserved allocator/codec providers share signatures with the emitter and reject malformed MIR consumers. + Evidence: intrinsic-implementations-2026-10-07.md ``` ```text @@ -372,8 +335,55 @@ SP-13: - There is still no source-level string length or index operation, so a converted string can only be observed through `stringToBytes`. -Implementation deviation: the worked example names the helpers -`__psrs_floor_int_div`/`__psrs_floor_int_mod`, while the code emits -`__psrs_euclidean_int_div`/`__psrs_euclidean_int_mod`. The names are cosmetic -and the tests assert the emitted names; the design's symbol-allocation and -operand contract are satisfied. +The former floor-helper evidence was superseded on 2026-10-07. Public Euclidean +arithmetic now remains library-owned; see +[intrinsic implementation acceptance](intrinsic-implementations-2026-10-07.md). + +## Number truncation extension (2026-10-07) + +This extension adds NumberTrunc to SP-03 and SP-07; the existing NumberToInt +saturation contract remains separate. The HIR registry owns numberTrunc's +Number -> Number scheme. Core verification, CC lowering/verification and MIR +numeric lowering/verification preserve that contract; Wasm emits f64.trunc. +These mappings are in intrinsic/registry.rs, core/verify/types/mod.rs, +cc/lower/scalar.rs, cc/verify/scalar.rs, mir/numeric.rs, +mir/verify/instruction/unary.rs and wasm/lower/structure/unary.rs. + +SP-07 and SP-12 execution is supplied by driver tests/number_trunc.rs:: +number_trunc_preserves_number_range_nonfinite_values_and_signed_zero and +backend mir/gc_tests/number_trunc.rs:: +optimized_and_unoptimized_number_trunc_preserve_zero_sign_and_range. +The source test executes fractions, values beyond i32, both zero signs, +NaN and infinities. The CC fixture executes both unoptimized and optimized +MIR, observes negative zero by reciprocal sign, and retains a Number beyond +i32 range. Both artifacts return 42 under Wasmtime 49.0.2. + +SP-11 rejection evidence extends the existing CC/MIR malformed unary operand +tests to NumberTrunc/F64Trunc. The source test +number_trunc_checks_foreign_operand_and_result_contracts additionally rejects +unused wrong operand, result and polymorphic foreign schemes. The tested +revision is c13a078 plus this extension; local raw reports remain regenerable. + +```sh +PSRS_REQUIRE_WASMTIME=1 cargo test -p psrs-driver --lib tests::number_trunc +PSRS_REQUIRE_WASMTIME=1 cargo test -p psrs-backend optimized_and_unoptimized_number_trunc +``` + +Both focused commands pass (2 source tests and 1 optimization test; execution +is mandatory). Integrated validation and the unchanged standard-library wrapper +checks are recorded in the [checkpoint](../stdlib/number-trunc-2026-10-07.md). +This extension does not claim support for other Number rounding operations. + +### Number floor and ceiling extension + +NumberFloor/NumberCeil extend SP-03 and SP-07 through the authoritative HIR +registry and Core/CC/MIR operand/result checks, lowering to f64.floor/f64.ceil. +Existing CC and MIR malformed-unary tests cover both operations. Driver +`tests::number_rounding` executes fractions, Number range beyond i32, nonfinite +values and reciprocal observations of signed zero, and rejects invalid foreign +schemes. Backend `optimized_and_unoptimized_number_floor_and_ceil_preserve_sign_and_range` +executes both optimization paths. These primitives discharge rounding only; +library Int clamping and the ECMAScript round tie policy remain library-owned. + +This extension does not establish completion of the scalar topic or add MIR +constant folding for these operations. diff --git a/docs/implementation/backend/type-classes-and-dictionaries.md b/docs/implementation/backend/type-classes-and-dictionaries.md index 9a64c754..43f5c5e2 100644 --- a/docs/implementation/backend/type-classes-and-dictionaries.md +++ b/docs/implementation/backend/type-classes-and-dictionaries.md @@ -14,7 +14,7 @@ currently reaches a P8 closure-capture limit and is not runtime-verified. Scoped method-local constraints and quantified method parameters have evidence in the [rank-N acceptance record](../frontend/rank-n.md). FE-14/15 remain partial because the complete official-suite reconciliation is incomplete. Other deriving -rules remain tracked under FE-16. +rules remain tracked under FE-22. **Roadmap:** [D-04 backend matrix](../../design/D-04-suite-roadmap.md#backend-feature-matrix), supporting BE-02 and BE-09; FE-14 and FE-15 supply resolved evidence. @@ -108,7 +108,7 @@ DICT-01: Result: pass; the positive case executed under Wasmtime. Gaps: an instance-member constrained annotation is not yet solved merely to specialize it to a monomorphic expected class-method type; the frontend owns - this FE-14 boundary. FE-16 executes structural `Eq`/`Ord`, nested `Functor`, + this FE-14 boundary. FE-22 executes structural `Eq`/`Ord`, nested `Functor`, `Bifunctor`, and newtype-derived dictionaries. `Contravariant` and function-result `Functor` have source and upstream differential evidence, but no Wasmtime result for the function adapter due @@ -255,7 +255,7 @@ DICT-11: Result: source tests pass with required Wasmtime execution for imported chains, transitive fundep selection, independent-argument fallback, repeated-head apartness, and recursive variable-headed application heads. - Gaps: deriving under FE-16 is partial (structural `Eq`/`Ord`, `Functor`, + Gaps: deriving under FE-22 is partial (structural `Eq`/`Ord`, `Functor`, `Bifunctor`, and newtype methods execute; function-based `Contravariant` is type-checked but not runtime-verified due closure capture), explicit foralls or constraints in method signatures and full official-suite acceptance remain @@ -302,7 +302,7 @@ DICT-11: annotation still cannot be solved solely to specialize it to a monomorphic expected class-method type; see the [frontend class and evidence design](../../design/frontend/type-system/classes-and-evidence.md). - Deriving belongs to FE-16. Source default methods are not part of PureScript + Deriving belongs to FE-22. Source default methods are not part of PureScript syntax; Typed Core default-field fixtures establish only the backend dictionary behavior. - Official test suite: class/instance upstream cases have not been individually diff --git a/docs/implementation/backend/wasi-platform.md b/docs/implementation/backend/wasi-platform.md index 1fd18a61..225b286e 100644 --- a/docs/implementation/backend/wasi-platform.md +++ b/docs/implementation/backend/wasi-platform.md @@ -66,9 +66,11 @@ Revision: `81b2eee` plus the DEC-10 re-verification in this worktree. Runtime: ```text WASI-01: - Implementation: crates/psrs-backend/src/component.rs. - Tests: component::tests::{componentizes_a_command_exporting_run, - component_world_matches_the_capability_profile}; + Implementation: crates/psrs-linker/src/compose.rs, + crates/psrs-runtime/wit/. + Tests: psrs-linker tests/compose.rs::composition_attaches_the_library_and_closes_the_private_import, + tests/plan.rs; + psrs-backend mir::number_format_tests::formats_a_number_through_the_runtime_artifact; psrs-driver tests::integration::emits_a_wasi_command_component. Input boundary: encoded core module. Commands: cargo test -p psrs-backend component; diff --git a/docs/implementation/backend/wit-erased-payloads.md b/docs/implementation/backend/wit-erased-payloads.md new file mode 100644 index 00000000..01d8431b --- /dev/null +++ b/docs/implementation/backend/wit-erased-payloads.md @@ -0,0 +1,85 @@ +# WIT Aggregates in Erased Payload Slots + +Measured on 2026-10-07. + +**Design:** [Runtime representation and checked boundaries](../../design/backend/fp/representation-and-evidence.md#aggregates-in-bare-polymorphic-slots) + +## Starting point and boundary + +Baseline: `11db1757c20b76b5231e033395f000714f279baa`, branch +`stdlib/vendor-core-libraries`, clean worktree. The locked standard library +was unchanged throughout this compiler repair. + +`PSRS_REQUIRE_WASMTIME=1 cargo test -p psrs-driver --lib tests::wasi:: -- --nocapture` +ran 145 tests: 142 passed and three trapped with `cast failure`: + +- `filesystem::stats_and_reads_a_directory_through_preopens_when_wasmtime_is_available` +- `filesystem::writes_and_reads_a_file_through_preopens_when_wasmtime_is_available` +- `wrappers::reads_stdin_with_blocking_read_when_wasmtime_is_available` + +The minimal source reads five stdin bytes through `WASI.IO.blockingRead`, +extracts `Right bytes`, and returns `arrayIndex bytes 0`. Both before and after +compile diagnosis accepted this source. Identical source inputs and the locked +library were confirmed by `diagnose --compare`; no observed pass-status change +was reported. Compile acceptance did not establish runtime correctness. + +The mismatch was at canonical ABI decoding into the generic variant field. +The decoder constructed a specialized array or record and cast its reference +into the erased slot. Source constructors normalize these values into the +aggregate owner's canonical erased-element or erased-field storage protocol. +The checked consumer therefore tried to recover a protocol value from a +specialized aggregate. + +## Repair + +- CC and canonical ABI adapters share `cc::payload::PayloadPlanner` for recursive + scalar boxing and array/record normalization and recovery. Nominal references + retain their identity behavior. +- Decoded aggregate payloads normalize before entering bare erased fields. + Variant parameter lowering recovers the concrete checked value before ABI + flattening. Conversion control-flow exits are propagated to their consumers. +- ABI projection reachability traverses the same storage conversion plans before + target layouts are assigned, retaining protocol layouts and required boxes. + Missing owner protocols produce an error. +- Variant parameter branches pass call-local buffer pointers, byte lengths and + element counts through the merge block. Inactive cases pass null and zero; + post-call cleanup no longer uses branch-local values outside their dominance + scope. Nested variant cleanup composes through the same merge mechanism. + +## Evidence and limits + +Runtime evidence requires Wasmtime, using version `49.0.2` in this run. The +existing stdin test asserts `hello\n`; file tests assert file contents and +console output, including nested record/option payloads. The new +`payloads::wit_result_bytes_recover_values_and_empty_arrays_when_wasmtime_is_available` +asserts empty-array length and exact byte values `0`, `255`, `128`, `42`, then +returns 42 with empty stdout and stderr. Arbitrary bytes are tested as +`Array Int`, without requiring valid UTF-8. + +Three backend regressions cover record and record-array ABI result-to-parameter +round trips and rejection of a missing owner protocol. These lower, optimize, +encode and validate Wasm; they do not execute a synthetic host import. Actual +runtime evidence comes from the driver tests above. + +Focused validation commands: + +```sh +PSRS_REQUIRE_WASMTIME=1 cargo test -p psrs-driver --lib tests::wasi:: -- --nocapture +PSRS_REQUIRE_WASMTIME=1 cargo test -p psrs-backend --lib cc:: -- --nocapture +cargo test -p psrs-backend --lib mir:: -- --nocapture +cargo test -p psrs-cli --test source_layout +cargo fmt --all --check +cargo clippy --workspace --all-targets -- -D warnings +``` + +The final WASI selection passed 146 tests (the original 145 plus the new byte +payload regression), with zero failures or ignored tests. The standalone stdin +reproducer returned 104 for `hello`, with empty stdout and stderr. Its Wasm +SHA-256 was `a9a672b121379ebd7c3cd26518953bb9c5365fd3ff6a0382b75e0e4003fa7dbf`. + +The CC selection passed 114 tests. The MIR selection passed 188 tests, including +all three new regressions. Source-layout validation, formatting, and workspace clippy passed. +Full workspace tests and corpus scoreboards were +not run; the user requested focused validation. This is acceptance for this ABI +repair, not completion of the broader backend or unrelated Show, row-evidence, +or ambiguity work. diff --git a/docs/implementation/frontend/core-row-instantiation-observations.json b/docs/implementation/frontend/core-row-instantiation-observations.json new file mode 100644 index 00000000..f5e0180a --- /dev/null +++ b/docs/implementation/frontend/core-row-instantiation-observations.json @@ -0,0 +1,71 @@ +{ + "schema_version": 1, + "date": "2026-10-07", + "diagnoses": { + "c": { + "before": { + "compiler": { + "head": "11db1757c20b76b5231e033395f000714f279baa", + "dirty": true, + "working_tree_fingerprint": "fnv1a64:35c7343b32dd42cc", + "binary_fingerprint": "fnv1a64:6e3f9e8e06b2a857" + }, + "cohort": { + "mode": "file", + "corpus": null, + "filter": null, + "limit": null, + "timeout_seconds": 30, + "trusted_stdlib_fingerprint": "fnv1a64-v1:b2890fecd9c42aa3" + }, + "input_fingerprint": "fnv1a64:e4f7f9a8beba4416", + "status": "failed", + "first_blocker": { + "stage": "P7 Core verification", + "category": "uncoded", + "message": "Core expression type is inconsistent with its context" + }, + "diagnostics": [ + { + "origin": "source", + "source": "/tmp/psrs-c-union.purs", + "stage": "P7 Core verification", + "start": 476, + "end": 482, + "code": null, + "kind": null, + "message": "Core expression type is inconsistent with its context" + } + ] + }, + "after": { + "compiler": { + "head": "11db1757c20b76b5231e033395f000714f279baa", + "dirty": true, + "working_tree_fingerprint": "fnv1a64:790e9733c3f1e58b", + "binary_fingerprint": "fnv1a64:0532bc7848f9b3ae" + }, + "cohort": { + "mode": "file", + "corpus": null, + "filter": null, + "limit": null, + "timeout_seconds": 30, + "trusted_stdlib_fingerprint": "fnv1a64-v1:b2890fecd9c42aa3" + }, + "input_fingerprint": "fnv1a64:e4f7f9a8beba4416", + "status": "passed", + "first_blocker": null, + "diagnostics": [] + } + } + }, + "runtime": { + "fixture": "crates/psrs-driver/tests/prim_row/deferred_execution.rs::SOURCE", + "wasm_sha256": "d172428ce1bf5735e67b9c055de0906a864e23b1df0502028f941f266c775d42", + "wasmtime_version": "wasmtime 49.0.2 (3c8a3e79a 2026-10-02)", + "exit_code": 42, + "stdout": "", + "stderr": "" + } +} diff --git a/docs/implementation/frontend/core-row-instantiation.md b/docs/implementation/frontend/core-row-instantiation.md new file mode 100644 index 00000000..ab60c18f --- /dev/null +++ b/docs/implementation/frontend/core-row-instantiation.md @@ -0,0 +1,59 @@ +# Checked instantiation of bare row arguments + +Measured on 2026-10-07. + +## Contract and change + +Core's checked type relation owns declaration instantiation and the residual +row substitutions consumed by local-row materialization. A row beneath a nominal +constructor, such as `Proxy (y :: Int | r)`, is invariant but still matches by +label. Distinct label order is irrelevant; duplicate bare-row labels retain +their occurrence order. Closed and rigid rows cannot absorb unmatched fields. + +Previously Core compared bare `RowExtend` nodes positionally, although record +rows already had a residual relation and P5 accepted the nominal argument. +The inferred nested `Union` use consequently failed at P7 when `y` moved behind +`a` and `b` in the concrete row. Both invariant and subsumption entry points now +delegate bare rows to the shared row relation. Record-specific duplicate checks +stay at the record boundary. Residual field equality also uses the shared +invariant relation, including nested nominal row arguments. + +The matcher retains a checked residual even when no arena node represents it. +No verifier check was bypassed, and no Union-specific backend conversion was +added. Tests verify reordered fields, duplicate-label occurrence order, +consistent repeated residuals, nested row fields, rigid-tail rejection, +missing-field rejection, and incompatible field types. + +## Evidence + +The baseline is HEAD `11db1757c20b76b5231e033395f000714f279baa` with the preceding +WIT payload repair in the working tree. The locked package remains revision +`01d6cd406cdce68a7ea1ec4c26a44793ead34571`, fingerprint +`fnv1a64-v1:b2890fecd9c42aa3`. +`core-row-instantiation-observations.json` records the comparable single-file diagnosis and +the executed Wasm artifact separately. + +| Check | Baseline | Result | +| --- | --- | --- | +| Nested Union diagnosis | P7 Core type/context mismatch, span 476–482 | Compilation accepted | +| Nested Union Wasmtime execution | Blocked at Core verification | Exit 42, empty stdout/stderr | +| `prim_row` integration tests | Nested execution failed | 12 passed | +| Core unit and optimizer tests | Existing cases | 87 passed | +| Rank-N source/runtime tests | Existing cases | 2 passed | +| Cross-declaration runtime tests | Existing cases | 9 passed | + +The complete fixture is `tests/prim_row/deferred_execution.rs::SOURCE`. +Its inferred constraint requires `Union (y :: Int | left) right output`; `main` +instantiates it with `left = (a :: Int)`, `right = (b :: Boolean)`, and the +three-field output. The test requires Wasmtime when `PSRS_REQUIRE_WASMTIME=1`. + +```sh +cargo test -p psrs-core --lib +PSRS_REQUIRE_WASMTIME=1 cargo test -p psrs-driver --test prim_row +PSRS_REQUIRE_WASMTIME=1 cargo test -p psrs-driver --test rank_n +PSRS_REQUIRE_WASMTIME=1 cargo test -p psrs-driver --lib declaration_calls:: +``` + +The preceding WIT payload fix is checked separately with the 146 WASI driver +tests. Formatting, source layout, and workspace clippy are integration checks; +the full workspace test suite was omitted by the user's explicit instruction. diff --git a/docs/implementation/frontend/deriving.md b/docs/implementation/frontend/deriving.md new file mode 100644 index 00000000..1bc58d2d --- /dev/null +++ b/docs/implementation/frontend/deriving.md @@ -0,0 +1,140 @@ +# Deriving Implementation Acceptance + +**Feature:** [F-02](../../feature/F-02-portable-programs.md) + +**Design:** [Deriving](../../design/frontend/type-system/deriving.md), with +[classes and evidence](../../design/frontend/type-system/classes-and-evidence.md), +[kinds](../../design/frontend/type-system/kinds.md), and +[primitives](../../design/frontend/type-system/prim.md). + +**Progress:** The seven deriving handoff items are implemented, with expanded +source, differential, and runtime evidence. The shared usage tree now drives +all mapping families and parameter-bearing record fields. Generic round trips, +Traversable/Bitraversable, function Contravariant adapters, and polymorphic +newtype adapters execute. Whole-workspace validation retains the unrelated +failures listed below; FE-22 remains partial for corpus mismatches in the +shared instance-declaration checks rather than a missing structural rule. + +## Acceptance matrix + +| ID | Requirement | Required evidence | State | +| --- | --- | --- | --- | +| DR-01 | AST/HIR retain the derivation strategy and distinguish `derive instance` from `derive newtype instance`. | AST lowering and HIR derivation field; source and runtime cases for both strategies. | Verified | +| DR-02 | A rule is selected by resolved class identity: a re-exported class keeps its rule, and an unrelated same-name class does not gain one. | Differential cases `deriving-recognizes-the-reexported-data-eq-identity`, `deriving-custom-prelude-eq-is-not-a-known-class`, `deriving-user-eq-is-not-selected-by-unqualified-name`. | Verified for covered cases | +| DR-03 | One deriving registry, read by `TypeId`, selects rules; no rule compares a class module or name at use time. | `DerivingRegistry::build` maps resolved class identities and their method symbols once; `known_class`/`method` read it by `TypeId`. No deriving rule, and no wildcard check, compares a module or name at use time. | Verified | +| DR-04 | A structural head is a locally declared data or newtype constructor applied at the class-appropriate arity, and a newtype head is a newtype. | Source cases `derives_newtype_for_a_partially_applied_type_constructor`, `rejects_newtype_deriving_for_a_data_declaration`, `derives_a_higher_kinded_newtype_instance_through_the_wrapped_type`; differential newtype cases. | Verified for covered cases | +| DR-05 | Every structural rule synonym-expands field types before deciding how a field is used. | Every implemented family (`Eq`, `Ord`, `Functor`, `Bifunctor`, `Contravariant`) normalizes its field types before the usage test; the alias case is exercised for `Functor` (`derives_nested_functor_mapping_through_an_imported_dictionary`). Runtime `eq_and_ord_expand_applied_variable_aliases_and_use_higher_kinded_dictionaries` checks both alias expansion and Eq1/Ord1 field dispatch. | Verified for covered cases | +| DR-06 | Field usage and variance are validated against the visible instance environment, and an unmappable field is `CannotDeriveInvalidConstructorArg` at its span. | `usage/` walks every field with polarity and instance existence; `deriving_failures_report_their_official_error_codes` covers a contravariant field and a field head with no mapping instance. | Verified for covered cases | +| DR-07 | Generated members are ordinary expressions: inference elaborates the method at the instance head and selects each field's dictionary. | Runtime cases execute derived methods through ordinary dictionaries. | Verified | +| DR-08 | Structural `Eq` compares constructor tags and fields and recurses through the instance dictionary. | Runtime `imported_structural_eq_instance_executes_constructor_and_field_comparisons`, `derives_recursive_eq_through_its_instance_dictionary`; differential `deriving-structural-eq`. | Verified | +| DR-09 | `derive Eq1`/`Ord1` delegate to their monomorphic counterpart, and a structural `Eq`/`Ord` field of type `f a` is compared through `eq1`/`compare1`. | Source `derives_eq1_and_ord1_by_delegating_to_the_monomorphic_method`; Runtime `eq_and_ord_expand_applied_variable_aliases_and_use_higher_kinded_dictionaries` executes the applied-variable field, derived Eq1/Ord1 dictionaries, and unequal/order cases. | Verified for covered cases | +| DR-10 | Structural `Ord` orders constructors by declaration order and fields lexicographically. | Runtime `derived_ord_executes_constructor_and_lexicographic_field_order`; differential `deriving-structural-ord`. | Verified | +| DR-11 | `Functor` maps nested applications and function results, and rejects a parameter under a function input. | Runtime `derives_nested_functor_mapping_through_an_imported_dictionary`; differential function-result and nested-function cases and `deriving-functor-rejects-contravariant-field`. | Verified | +| DR-12 | `Bifunctor` maps the final two parameters and rejects a negative parameter position. | Runtime `derives_bifunctor_mapping_for_both_type_parameters`; differential `deriving-bifunctor-final-two-parameters` and `deriving-bifunctor-rejects-negative-parameter`. | Verified | +| DR-13 | `Contravariant` maps function inputs through the `Profunctor.lcmap` dictionary and rejects a positive parameter. | Differential `deriving-contravariant-function-input` and `deriving-contravariant-rejects-positive-parameter`; runtime `function_contravariant_deriving_executes_its_adapter` and its record-field counterpart check both predicate outcomes and retained inert fields. | Verified | +| DR-14 | `derive newtype` selects the wrapped class dictionary and adapts each method by a representation cast authorized by the checked newtype and that dictionary. It does not prove ordinary `Coercible`, which still refuses to lift through an unknown constructor. Quantifiers on the method stay in scope across the cast. | Runtime `imported_newtype_derived_dictionary_executes_its_coercion_adapter`, `derives_a_higher_kinded_newtype_instance_through_the_wrapped_type`, `newtype_deriving_reuses_a_dictionary_under_an_unknown_constructor`; source `newtype_deriving_reuses_the_dictionary_under_an_unknown_type_constructor` and `ordinary_coercible_does_not_lift_through_an_unknown_type_constructor`; differential empty-class cases; source `derives_newtype_methods_from_the_wrapped_instance`. | Verified for covered cases | +| DR-15 | A rank-1 method `forall` is instantiated once and shared across the adapter's two heads. | Source `newtype_deriving_accepts_a_polymorphic_class_method`; runtime `polymorphic_newtype_deriving_executes_its_adapter` executes the same method at Boolean and Array argument instantiations. | Verified | +| DR-16 | A derivable `Newtype`/`Generic` wildcard is resolved before the instance is recorded. | `record_instance` resolves the wildcard through the registry and stores the concrete wrapped/representation type in the recorded head; a non-wildcard final argument is `ExpectedWildcard`. `derives_newtype_class_for_a_newtype_with_a_wildcard` covers the resolved path. | Verified | +| DR-17 | `Generic` derives the `Data.Generic.Rep` representation and its `to`/`from` methods. | Source `derives_generic_representation_for_a_data_type`; Runtime `generic_deriving_round_trips_constructor_tags_and_fields` checks nullary, unary, and multi-field constructors. | Verified for covered cases | +| DR-18 | `Profunctor`, `Foldable`, `Bifoldable`, `Traversable`, and `Bitraversable` have their structural rules. | Source `derives_profunctor_through_a_contravariant_field`, `derives_foldable_for_single_field_constructors`, `derives_bifoldable_for_a_two_parameter_type`, `derives_traversable_for_single_field_constructors`, `derives_bitraversable_for_a_two_parameter_type`; runtime `derived_foldable_executes_through_its_instances`. Runtime modules `adapters`, `folds`, `records`, and `traversals` execute every listed class, both fold directions, sequencing, multi-field effects, and preserved inert fields. `differential_remaining_deriving_rules_against_purs` checks every remaining family, record fields, rejection polarity, given dictionaries, and scoped forall parameters. | Verified for covered cases | +| DR-19 | Each deriving failure carries its official `errorCode` (`CannotDerive`, `ExpectedTypeConstructor`, `InvalidDerivedInstance`, `InvalidNewtypeInstance`, `CannotDeriveNewtypeForData`, `CannotDeriveInvalidConstructorArg`, `CannotFindDerivingType`, `ExpectedWildcard`). | `deriving_failures_report_their_official_error_codes` asserts `CannotDerive`, `CannotDeriveNewtypeForData`, `CannotDeriveInvalidConstructorArg`, `InvalidNewtypeInstance`, and `ExpectedWildcard`; `differential_deriving_error_codes_against_purs` confirms the code agrees with `purs` on rejected cases. `fold_deriving_reports_missing_core_values_at_the_declaration` covers `CannotFindDerivingType` for both fold classes: multiple contributing fields require `append`, and constructors without contributions require `mempty`. `deriving_rejects_non_constructor_heads_and_invalid_class_arity` directly checks `ExpectedTypeConstructor` and `InvalidDerivedInstance`, also confirmed by the differential diagnostic battery. The common recorder reports `ClassInstanceArityMismatch` before deriving when the declaration does not supply one argument per class parameter. | Verified for covered conditions | +| DR-20 | Source behavior agrees with official PureScript across the deriving surface, including diagnostics. | Differential batteries cover 18 original rules, 17 remaining-family/context/scoped-parameter cases, and six rejected diagnostic cases; a separate case agrees with purs on record postfix precedence. | Verified for covered cases | + +## Evidence and commands + +Source-only cases are in `crates/psrs-driver/src/tests/deriving/` (18 tests). +Value-level execution is in +`crates/psrs-driver/src/tests/wasi/classes/deriving/` (25 tests), with observable +assertions for every structural class. The official differential battery is +`crates/psrs-driver/tests/upstream/deriving/` (four Rust tests). + +```sh +cargo fmt --all --check +cargo test -p psrs-driver --lib tests::deriving +PSRS_REQUIRE_WASMTIME=1 cargo test -p psrs-driver --lib tests::wasi::classes::deriving +PURESCRIPT_REPO=/Users/biu/Projects/purescript cargo test -p psrs-driver --test upstream deriving +PSRS_REQUIRE_WASMTIME=1 cargo test --workspace --no-fail-fast +cargo clippy --workspace --all-targets -- -D warnings +PSRS_ORACLE=annotations cargo test -p psrs-driver --test suite types::l4_l5_scoreboard_with_annotations -- --ignored --nocapture +``` + +The fold diagnostic regression reproduced a silently discarded multi-field +instance before the fix. Both fold classes now report `CannotFindDerivingType` +at the derive declaration for required missing `append` or `mempty`; a single +contribution requires neither an unnecessary append nor an unused folding +method. Generated mapping, fold, traversal, and Generic terms use the shared +syntax builder. + +Traversal exposed two shared backend representation boundaries. Aggregate +layouts and callable signatures now converge together rather than publishing +partially normalized product identities. Bare polymorphic function slots use +a registered unary erased calling protocol; erasure and recovery adapt every +argument and result instead of casting a concrete callable to a consumer +signature. The independent `erased_function_slots_preserve_partial_application_and_captures` +case checks flattened multi-argument functions and captured partial +applications. The existing 33-case generic aggregate battery also executes. +Record projection/update precedence agrees with the official parser; the +nested aggregate fixture explicitly parenthesizes the call result it projects. + +Generic representation literals compare by value in inference evidence and +Core's invariant type relation. THIR instance context parameters compare by +semantic equality, retaining rejection of an actually different dictionary. +Core lowering consumes that verified check; P8 uses the shared Core type +relation for alpha-equivalent lambda parameter types. A derived higher-kinded +Traversable context has source differential and runtime execution evidence. + +The trusted core-library path also executes `Generic` to/from round trips, +Traversable traverse/sequence, and Bitraversable bitraverse/bisequence with +`Maybe` effects and record fields. `identity` retains its defining +`Control.Category` symbol even when re-exported by `Data.Function`. +`Data.Tuple` supplies its ordinary derived `Functor (Tuple a)` instance, +required by `Data.Traversable`'s existing Tuple instance. The standard-library +loader's trusted-order regression passes. + +Validation on 2026-10-06 used Wasmtime 49.0.2 and purs 0.15.16. The 18 source +tests, 25 required-runtime deriving tests, and four differential batteries pass. +The backend (393), Core (79), THIR (25), and typechecker (134) unit tests pass. +Formatting, the local diff check, and changed Markdown link checks pass. + +The full required-runtime workspace run retains three failing targets: +`psrs-cli --test source_layout` (the pre-existing 510-line `partial.rs`), +`psrs-driver --test prim_row` (the pre-existing nested Union deferred execution +case), and the driver library. The newly required Tuple dependency first +exposed two isolated source fixtures that omitted `Data.Functor`; those tests +now load the real trusted library graph and both execute. After that test-only +correction, the full driver library rerun reports **581 passed / 10 failed**, +with exactly the handoff's ten pre-existing failures. + +Default workspace Clippy fails on the handoff's pre-existing +`unnecessary_to_owned`, `too_many_arguments`, and `result_large_err` findings. +The workspace check with the handoff's known lint categories allowed passes +(including its test-only `field_reassign_with_default`, `manual_contains`, and +`dead_code` allowances). This does not establish a clean default Clippy gate. +No L6/M7 corpus runtime measurement was run; the 25 execution cases are focused +acceptance evidence and are not a replacement for that scoreboard. + +## Known boundaries + +The L5 scoreboard includes unrelated shared class-rule mismatches, notably +orphan checks and invalid record/synonym instance heads. The annotated L4/L5 +run on 2026-10-06 measured L4 39/50 and L5 75/97, +with 11 cases blocked before a mapped diagnostic. L5 decomposes into +`OverlappingInstances` 8/8, `NoInstanceFound` 46/53, `MissingClassMember` 2/2, +`InvalidNewtypeInstance` 6/6, `InvalidInstanceHead` 1/7, +`DuplicateInstance` 1/1, `ClassInstanceArityMismatch` 4/4, +`CannotDeriveInvalidConstructorArg` 7/7, `PossiblyInfiniteInstance` 0/1, +`OrphanInstance` 0/7, and `DuplicateTypeClass` 0/1. Unsupported mapped open rows +remain explicit diagnostics rather than +empty usage results. Structural rule availability is a visible head-identity +check, matching official deriving; ordinary inference owns complete instance +constraint selection. + +## Completion rule + +Do not mark the deriving topic complete until DR-01..DR-20 have implementation +and source-level acceptance/rejection evidence, the single registry and the +field-usage analysis replace the scattered identity and shape checks, every +deriving failure reports its official `errorCode`, and every runtime requirement +executes with `PSRS_REQUIRE_WASMTIME=1`. The official differential set must then +include diagnostic-code agreement and the remaining structural classes. Run the +workspace test, format, and Clippy gates after code changes. diff --git a/docs/implementation/frontend/rank-n.md b/docs/implementation/frontend/rank-n.md index 91fbb86c..feea4d99 100644 --- a/docs/implementation/frontend/rank-n.md +++ b/docs/implementation/frontend/rank-n.md @@ -61,8 +61,8 @@ inconsistent scheme instance, closed records with extra fields, rigid open rows, and one row variable with two residuals are rejected; one repeated residual is accepted. Global inlining leaves a `ForAll` signature and a body that binds its own type variable in place. The linker renumbers `TypeVariableId` across modules. -`derive newtype` peels a shared method quantifier before building `Coercible` -evidence. +`derive newtype` keeps a shared method quantifier as the scope of its +representation cast. That cast is not `Coercible` evidence. RN-12 stays unverified. The differential battery agrees with `purs` 0.15.16, and the official higher-rank and skolem corpus, including its library dependencies, diff --git a/docs/implementation/frontend/residual-ambiguity-observations.json b/docs/implementation/frontend/residual-ambiguity-observations.json new file mode 100644 index 00000000..5802b3d7 --- /dev/null +++ b/docs/implementation/frontend/residual-ambiguity-observations.json @@ -0,0 +1,63 @@ +{ + "schema_version": 1, + "date": "2026-10-07", + "diagnoses": { + "d": { + "before": { + "compiler": { + "head": "11db1757c20b76b5231e033395f000714f279baa", + "dirty": true, + "working_tree_fingerprint": "fnv1a64:35c7343b32dd42cc", + "binary_fingerprint": "fnv1a64:6e3f9e8e06b2a857" + }, + "cohort": { + "mode": "file", + "corpus": null, + "filter": null, + "limit": null, + "timeout_seconds": 30, + "trusted_stdlib_fingerprint": "fnv1a64-v1:b2890fecd9c42aa3" + }, + "input_fingerprint": "fnv1a64:6b1b440f4fc6ba6b", + "status": "passed", + "first_blocker": null, + "diagnostics": [] + }, + "after": { + "compiler": { + "head": "11db1757c20b76b5231e033395f000714f279baa", + "dirty": true, + "working_tree_fingerprint": "fnv1a64:790e9733c3f1e58b", + "binary_fingerprint": "fnv1a64:0532bc7848f9b3ae" + }, + "cohort": { + "mode": "file", + "corpus": null, + "filter": null, + "limit": null, + "timeout_seconds": 30, + "trusted_stdlib_fingerprint": "fnv1a64-v1:b2890fecd9c42aa3" + }, + "input_fingerprint": "fnv1a64:6b1b440f4fc6ba6b", + "status": "failed", + "first_blocker": { + "stage": "P5 typecheck", + "category": "AmbiguousTypeVariables", + "message": "ambiguous constraint C _T3 in the type inferred for `f`: _T3 is not determined by the result type or a functional dependency" + }, + "diagnostics": [ + { + "origin": "source", + "source": "/tmp/psrs-d-ambiguity.purs", + "stage": "P5 typecheck", + "start": 55, + "end": 56, + "code": "AmbiguousTypeVariables", + "kind": null, + "message": "ambiguous constraint C _T3 in the type inferred for `f`: _T3 is not determined by the result type or a functional dependency" + } + ] + } + } + } +} diff --git a/docs/implementation/frontend/residual-ambiguity.md b/docs/implementation/frontend/residual-ambiguity.md new file mode 100644 index 00000000..5c80904b --- /dev/null +++ b/docs/implementation/frontend/residual-ambiguity.md @@ -0,0 +1,72 @@ +# Residual ambiguity through local bindings + +Measured on 2026-10-07. + +## Contract and change + +The declaration checker owns residual solving, functional-dependency closure, +ambiguity checks, and generalization. Unannotated `let` and `where` bindings are +monomorphic and share its unknowns and wanted constraints. Official +`TypeChecker/Types.hs::inferLetBinding` uses this contract too. + +Previously local inference generalized an unused constraint into a local +dictionary parameter. Consequently `f y = let g x = method x in y`, with +`method :: C a => a -> a`, hid the ambiguous `C a` from the enclosing check. +Local inference now retains that obligation for the declaration checker; +the existing ambiguity check reports `AmbiguousTypeVariables` before +generalization. Explicit local `forall` annotations retain polymorphism. +Lexical local environments are restored on both successful and failed inference. + +A checked leading `forall` on a whole `let` scopes its definitions as well as +its body. THIR and Core verify this scope. Beta reduction preserves the binder +on the enclosing expression instead of repeating it on the generated local +binding. Direct IR tests accept that scope and reject unrelated free variables. +Erasure fixtures that previously relied on implicit local polymorphism now +declare their `forall` explicitly, preserving the runtime behavior they test. + +## Comparable evidence + +The baseline is HEAD `11db1757c20b76b5231e033395f000714f279baa` with the preceding +WIT payload changes in the working tree. The package stays pinned to revision +`01d6cd406cdce68a7ea1ec4c26a44793ead34571`, fingerprint +`fnv1a64-v1:b2890fecd9c42aa3`. No package sources or lock entries changed. + +`residual-ambiguity-observations.json` retains the input and compiler identities from +the single-file diagnosis snapshots. The ambiguity fixture adds `main = 0` to +the source in `reports_ambiguous_variables_before_generalizing`, so an absent +entry point does not mask the erroneous acceptance. + +| Check | Baseline | Result | +| --- | --- | --- | +| Ambiguity fixture diagnosis | Passed compilation | P5 `AmbiguousTypeVariables` | +| `residual_constraints` integration tests | 7 passed, 1 failed | 8 passed | +| Local-constraint driver tests | Existing acceptance fixtures | 14 passed | +| Typechecker unit tests | Existing cases | 135 passed | +| THIR tests, including quantified-local rejection | Existing cases | 28 passed | +| Official differential | Six local-binding sources | All six agree on acceptance and diagnostic code | +| `failing/ConstraintInference.purs` | Annotated `AmbiguousTypeVariables` | Same diagnostic from P5 | +| Polymorphism erasure runtime audit | Implicit-polymorphism fixtures corrected | 21 passed | + +The six differential sources cover unused and used constraints, conflicting +monomorphic uses, an explicit polymorphic local, an explicitly constrained local, +and functional-dependency determination. They run through the installed official +`purs`; the corpus assertion uses the official case's annotation separately. +`PolykindGeneralizationLet.purs` remains blocked by an existing P5 kind mismatch +before reaching its annotated type error; this work does not claim coverage of +that case or completion of polymorphic kind support. + +Reproduce with: + +```sh +cargo test -p psrs-driver --test residual_constraints +cargo test -p psrs-typecheck --lib +cargo test -p psrs-thir --lib +PSRS_REQUIRE_WASMTIME=1 cargo test -p psrs-driver --lib let_constraints:: +PSRS_REQUIRE_WASMTIME=1 cargo test -p psrs-driver --lib polymorphism_erasure_audit:: +PURESCRIPT_REPO=/Users/biu/Projects/purescript cargo test -p psrs-driver --test upstream residual_constraints:: -- --nocapture +``` + +The L2 rerun remains 72/72 failing agreement and 402/413 passing resolution. +The 11 blocked passing cases remain P3: 6, P0: 4, harness loading: 1. +D-04 and README therefore retain their measurements. The full workspace test +suite was deliberately omitted under the user's focused-validation instruction. diff --git a/docs/implementation/frontend/roles-and-coercions.md b/docs/implementation/frontend/roles-and-coercions.md index 26e9aef2..51461d73 100644 --- a/docs/implementation/frontend/roles-and-coercions.md +++ b/docs/implementation/frontend/roles-and-coercions.md @@ -5,16 +5,13 @@ **Design:** [Kinds and type constructors](../../design/frontend/type-system/kinds.md), [classes and evidence](../../design/frontend/type-system/classes-and-evidence.md), [primitives](../../design/frontend/type-system/prim.md), and [polymorphism and erasure](../../design/backend/fp/polymorphism-and-erasure.md). **Progress:** Partial. Role inference/checking and the covered role-aware -`Coercible` rules are implemented through source, Typed Core, and CC. Structural -`Eq`/`Ord`, nested `Functor.map`, direct `Bifunctor.bimap`, and checked -`derive newtype` method adapters execute through ordinary dictionaries. -`Contravariant.cmap` follows the resolved `Profunctor.lcmap` dictionary and -matches the upstream source rule. Known deriving rules are selected by the -resolved class owner and name, so a re-export keeps its defining identity. -Other upstream deriving classes remain incomplete. Rank-1 method `forall` +`Coercible` rules are implemented through source, Typed Core, and CC. Deriving is +a distinct topic: its selected rules, coverage, and requirement IDs are recorded +in the [deriving acceptance record](deriving.md). Rank-1 method `forall` signatures and scoped method-local constraints are checked and instantiated independently at use sites, including quantifiers that shadow class parameters. -The acceptance evidence and remaining runtime limits are recorded below. +The roles and `Coercible` acceptance evidence and remaining runtime limits are +recorded below. ## Scope @@ -37,7 +34,7 @@ polymorphism-and-erasure design. | RC-06 | Newtype unwrapping requires its constructor to be visible. Nested visible newtypes and imported constructors are supported; visible nominal newtypes unwrap before parameter roles are compared. | `an_imported_newtype_requires_its_constructor_for_unwrapping`; Wasmtime tests `unwraps_nested_visible_newtypes_when_wasmtime_is_available`, `coerces_through_an_imported_visible_newtype_when_wasmtime_is_available`, and `unwraps_both_sides_before_applying_a_nominal_newtype_role_when_wasmtime_is_available`. | Verified for covered cases | | RC-07 | Checked coercion evidence retains source/target types through THIR and Core; malformed evidence cannot authorize a different boundary. | THIR `verifier_rejects_coercion_evidence_for_a_different_boundary`; Core verifier checks cast source and target against its value and result. | Verified | | RC-08 | Lowering uses the existing typed value-conversion protocol, including nested ADT fields, arrays, and function adapters; a source proof never becomes an arbitrary Wasm reference cast. | Wasmtime tests cover coercing parameterized newtypes to scalar, function, and array payloads; visible nested newtypes; phantom sums with constructor-tag preservation; representational data fields and imported aliases; imported generic constrained functions; and cross-module given transitivity. | Verified for listed shapes | -| RC-09 | FE-16 deriving generates checked instance evidence and handles newtype deriving rules. | AST/HIR retain the derivation strategy. Source and runtime tests cover structural `Eq` and `Ord`, `Functor` through imported and nested dictionaries, `Bifunctor`, recursive `Eq`, constructor and field order, rank-1 method polymorphism, cross-module newtype-derived dictionaries with checked coercion adapters, and partially applied higher-kinded newtype heads. Source/differential cases also cover `Contravariant` through `Profunctor.lcmap`, function-result traversal, alias expansion, empty-class validation, re-exported canonical class identity, and rejection of same-name user classes. `differential_deriving_rules_against_purs` compares 18 accepted/rejected cases. | Partial | +| RC-09 | Deriving is its own topic with requirement IDs DR-01..DR-20; this record does not own its rules or coverage. | See the [deriving acceptance record](deriving.md). | Moved | ## Source and runtime coverage @@ -47,30 +44,28 @@ hidden newtype constructors, imported aliases in data fields, higher-kinded given rewriting, kind mismatch, open-row alignment, and recursive given interactions. Value-sensitive Wasmtime tests execute the conversions, including parameterized newtype payloads represented as scalars, functions, and arrays; -a multi-constructor phantom value verifies its tag survives coercion. Separate -class tests execute structural `Eq`, recursive `Eq`, structural `Ord`, nested -`Functor.map`, `Bifunctor.bimap`, rank-1 method polymorphism, and the -cross-module newtype-derived dictionary. `Contravariant` and function-result -mapping type-check against upstream rules. The `Contravariant` function -adapter's source implementation currently reaches a P8 closure-capture limit, -so that case has no Wasmtime execution evidence yet. Focused runtime commands: +a multi-constructor phantom value verifies its tag survives coercion. Derived +instance tests are owned by the [deriving acceptance record](deriving.md). +Focused runtime commands: ```sh PSRS_REQUIRE_WASMTIME=1 cargo test -p psrs-driver --lib tests::wasi::coercion -PSRS_REQUIRE_WASMTIME=1 cargo test -p psrs-driver --lib tests::wasi::classes::deriving ``` +Deriving runtime commands are in the [deriving acceptance record](deriving.md). + The compiler-provided value shim follows upstream's `Safe.Coerce.coerce` -source API; `Prim.Coerce.Coercible` remains the compiler-owned class. The +source API; `Prim.Coerce.Coercible` remains the compiler-owned class, and +`Unsafe.Coerce.unsafeCoerce` is the compiler-owned intrinsic that `coerce` is +defined from upstream. A focused execution case +(`tests::coercion::unsafe_coerce_is_a_compiler_primitive_identity_cast`) lowers a +newtype through `unsafeCoerce` and runs to its value under mandatory Wasmtime. The durable `differential_role_and_coercible_rules_against_purs` test compares 20 accepted and rejected fixtures with `purs 0.15.16`, including role decomposition, higher-kinded given rewriting, checked kind compatibility, -open-row label alignment, and recursive-given behavior. The -`differential_deriving_rules_against_purs` test compares 18 deriving cases, -including structural `Eq` and `Ord`, alias and function-result `Functor.map`, -`Bifunctor`, `Contravariant` through a `Profunctor` instance, ordinary and -higher-kinded newtype adaptation, re-exported canonical classes, same-name -user classes, and empty-class derivation validation. +open-row label alignment, and recursive-given behavior. The deriving +differential battery is recorded in the +[deriving acceptance record](deriving.md). ## Known boundaries @@ -82,26 +77,17 @@ call-site special case rather than the primitive rule table the [primitives design](../../design/frontend/type-system/prim.md) specifies; both are recorded there as the deviations they are. Open-row values still have no runtime layout in the current CC path, while closed reordered records execute through Wasmtime. -The structural deriving subset covers `Eq`, `Ord`, `Functor.map` through -direct, nested application, and result-position function fields, -`Bifunctor.bimap` for final parameter pairs, and `Contravariant.cmap` through -the `Profunctor.lcmap` dictionary. `Eq1`, `Ord1`, `Foldable`, `Traversable`, -`Bifoldable`, `Bitraversable`, and deriving `Profunctor` still need their -upstream rules. Class-method `forall` and scoped method-local constraints are supported, -with evidence in the [rank-N acceptance record](rank-n.md). Record-field -traversal and full deriving variance checking remain open. Runtime closure capture limits also leave the function-based -Contravariant example unexecuted. Polymorphic newtype-derived methods share a -single method-quantifier instantiation at the checked adaptation boundary; -their source/typecheck regression passes, but their Wasmtime execution remains -unverified because the generated adapter reaches the same P8 closure-capture -limit. Existing non-polymorphic newtype-derived methods have runtime evidence. -FE-16 remains Partial until the other upstream deriving rules and remaining -official coercion cases have source and runtime evidence. +Class-method `forall` and scoped method-local constraints are supported, with +evidence in the [rank-N acceptance record](rank-n.md). Deriving boundaries, +including its variance checks, `Eq1`/`Ord1`, and the remaining upstream classes, +are owned by the [deriving acceptance record](deriving.md). FE-16 remains Partial +until the remaining official coercion cases have source and runtime evidence. ## Completion rule -Do not mark FE-16 complete until RC-01..RC-09 have implementation and +Do not mark FE-16 complete until RC-01..RC-08 have implementation and source-level acceptance/rejection evidence, every runtime requirement executes with `PSRS_REQUIRE_WASMTIME=1`, and the official compatibility set includes the -remaining coercion solver cases and deriving behavior. Run the workspace test, -format, and Clippy gates after code changes. +remaining coercion solver cases. Deriving has its own completion rule in the +[deriving acceptance record](deriving.md). Run the workspace test, format, and +Clippy gates after code changes. diff --git a/docs/implementation/stdlib/array-apply-2026-10-06/report.md b/docs/implementation/stdlib/array-apply-2026-10-06/report.md new file mode 100644 index 00000000..49e61275 --- /dev/null +++ b/docs/implementation/stdlib/array-apply-2026-10-06/report.md @@ -0,0 +1,51 @@ +# Polymorphic array application checkpoint + +The target implements `Control.Apply.arrayApply` through the explicit registry +binding `psrs:intrinsic#arrayApply`. The independent library revision is +`b367cd7cc4b2e9f5933bffb9938a1cdbe221fd8f`. Its only additional official-source +difference is the foreign binding string; the signature and pure declarations +remain unchanged. The upstream remains pinned prelude v6.0.1 at +`f4cad0ae8106185c9ab407f43cf9abf05c256af4`. + +The prerequisite is a general primitive foreign-function link contract: leading +quantifier identities move into the generated declaration's scope, while the +body retains its checked type relationships. Existing array/byte operations +share their Core verification and backend implementation. Unsupported categories +still fail, and linking retains its complete rollback contract. + +Array application carries its callback signature, array representations, and +invoker through CC. The invoker uses the common application/partial-call path, +including returned functions. The full CC verifier checks its actual ABI and +reachability retains it. MIR allocates once and emits function-major nested +loops, caching each function for its entire inner traversal. It preserves both +inputs and traps on unrepresentable lengths before allocation or callbacks. + +Evidence: + +- Driver primitive foreign tests: 13 passed with mandatory Wasmtime. They cover + captures, callback/result order, Int/Number/String/record/nested-array values, + empty arrays, returned curried functions through both entry points, invalid + signatures, and an overflowing `2^32` result length that must trap. +- Official JS oracle: eight cases and 29 value checks; Wasmtime exit 42 with + empty stdout and stderr. `run.json` records binary/source/package/Wasm hashes + and commands; `observations.json` records the pinned official observations. +- CC invalid-invoker test: one passed; missing helpers and incorrect result ABIs + are rejected by the complete check. +- Primitive transactional/link-identity tests: three passed. +- Core scheme scope/malformed-spine tests: two passed. +- Intrinsic descriptor/arity test: one passed. +- Let-constraint regressions: 14 passed. +- CLI build, formatting, and workspace clippy with warnings denied passed. +- Full source audit: 41 packages, 215 modules, 194 exact, 12 modified, nine + platform additions, no missing upstream modules, no detected direct + same-argument self-recursions. Audit counts do not approve every adaptation. + +The full stdlib reproducer remains failed. P8 missing-library-implementation +reports decreased from 233 to 232; the first is now `Control.Bind.arrayBind`. +The input file is unchanged, but package content and its diagnosis cohort +fingerprint changed deliberately; these are migration measurements, not an +unchanged-cohort comparison or an official-suite scoreboard update. + +No full workspace tests or full scoreboard were run. Whole-library compile and +runtime/FFI acceptance, remaining implementations, and independent CI package +acquisition remain open. No push, PR, or issue operation was performed. diff --git a/docs/implementation/stdlib/array-operations-2026-10-06/report.md b/docs/implementation/stdlib/array-operations-2026-10-06/report.md new file mode 100644 index 00000000..286a0113 --- /dev/null +++ b/docs/implementation/stdlib/array-operations-2026-10-06/report.md @@ -0,0 +1,54 @@ +# Array operations checkpoint + +`Data.Array` and `Data.Array.NonEmpty.Internal` keep their official signatures, +exports and other pure code; their foreign implementation slots now delegate to +`PSRS.Array` or are implemented inline. `PSRS.Array` owns the target algorithms +over the private `arrayFill`, `arrayWrite`, `arrayIndex`, `arrayLength` and +integer primitives: range, replicate, `fromFoldable`, length, uncons, reverse, +concat, filter, partition, `scanl`/`scanr`, a stable merge sort, slice, +`zipWith`, `any`, `all` and `unsafeIndex`. The independent `psrs-stdlib` +revision is `332c69b16f240254f74375b7babed7f752f7361a` with content fingerprint +`fnv1a64-v1:3ab22be17dbadc5c`. + +Two kinds of foreign slot could not be delegated directly. `Data.Array`'s +`index`, `findMap`, `findIndex`, `findLastIndex`, `insertAt`, `deleteAt` and +`updateAt` take rank-2 `Maybe` constructors or observers; their private foreign +signatures are written monomorphically and the loops are implemented inline over +the target helpers, leaving the public API unchanged. `Data.Array.NonEmpty.Internal`'s +`foldr1`/`foldl1` are inline `Fn2` wrappers and `traverse1` is implemented +directly in the `Traversable1` instance using the `Applicative` dictionary, +because its `apply`/`map` arguments are rank-2. + +A compiler behavior is recorded rather than hidden: a recursive loop that writes +into its output array only in one branch of an `if` produced wrong results, while +the same loop written as a single tail call with the write in a `let` and only +the selected value chosen by the branch is correct. `PSRS.Array`'s loops use the +single-tail-call shape. The evaluation/alias behavior is a compiler obligation, +not a property of valid official source. `unsafeIndex` out of range traps +instead of returning JavaScript `undefined`, an explicit target difference. + +Validation: + +- Mandatory Wasmtime driver regressions passed, including the new + `library_array_operations_match_pinned_official_observations` and the earlier + apply, bind, extend, ST, uncurried and primitive checks. +- Official JS observations: the pinned `Data/Array.js` generates thirty cases + with seventy-nine observable checks covering every delegated algorithm, + boundary and empty inputs, negative and clamped slices, stable sorting and + out-of-range access. The mandatory runner executes the Prelude-free target + fixture under Wasmtime with exit 42 and empty output. +- Library-owned Node tooling regressions: 8 passed. +- Complete pinned audit: 41 packages, 217 modules, 189 exact, 19 modified and + 11 explicitly recorded target additions; three value foreign declarations are + recorded as removed because their functionality moved into an inlined caller + or instance. No absent official module and no detected exact direct + same-argument recursion. +- Formatting and strict workspace clippy with warnings denied passed. + +The full reproducer still fails. Unsupported-library P8 reports fell from 179 to +152 and the first is now `Data.Array.ST.unsafeFreezeImpl`; no `Data.Array` or +`Data.Array.NonEmpty.Internal` foreign value remains. Package content changed +from `fnv1a64-v1:03a4b91b0b29e0ec` to `fnv1a64-v1:3ab22be17dbadc5c`. Remaining +work in this chain is the mutable `Data.Array.ST` and `Data.Array.ST.Partial` +group. No full workspace tests or full scoreboard were run. No push, PR or issue +operation was performed. diff --git a/docs/implementation/stdlib/array-public-2026-10-07/report.md b/docs/implementation/stdlib/array-public-2026-10-07/report.md new file mode 100644 index 00000000..061040c3 --- /dev/null +++ b/docs/implementation/stdlib/array-public-2026-10-07/report.md @@ -0,0 +1,75 @@ +# Public `Data.Array` and mutable `Data.Array.ST` checkpoint + +This slice makes the public `Data.Array` API executable end to end and lands the +mutable `Data.Array.ST`/`Data.Array.ST.Partial` group. It extends the +[array operations](../array-operations-2026-10-06/report.md) and +[ST](../st-2026-10-06/report.md) checkpoints. The independent +`psrs-stdlib` revision is `332c69b16f240254f74375b7babed7f752f7361a` with content +fingerprint `fnv1a64-v1:f3b995062d3662ba`. + +The mutable array representation changes from an opaque foreign data type to +`newtype STArray h a = STArray (STRef h (Array a))`, an alias of the shared +`Control.Monad.ST.Internal` cell. Every `Data.Array.ST` foreign slot is replaced +by a source adapter over `PSRS.Array.Mutable`, which owns the fixed-length array +algorithms over the private `arrayFill`, `arrayWrite`, `arrayIndex`, +`arrayLength`, `arrayAppend` and integer primitives. `Data.Array.ST` keeps its +official signatures, exports and pure code; the rank-2 `shift`/`pop` observers +are wrapped through `mkSTFnN` and an `unsafeCoerce` bridge. + +Several compiler prerequisites are part of this slice rather than the library: + +- **Executable library reachability.** `Core::prune_unreachable` now drops a + static library import and its checked signature unless the executable + dependency graph reaches it, while explicit primitive and WIT declarations keep + their source-wide protocol validation. This is the contract recorded in + [D-17](../../../design/D-17-stdlib-and-conformance-boundaries.md). +- **`Unsafe.Coerce` as a checked primitive.** `Unsafe.Coerce` is a + compiler-provided module now; `unsafeCoerce` lowers to a checked + `RepresentationCast` instead of a source body that cannot compile. +- **Superclass dictionary thunks.** A superclass field is a `Unit -> Dict` + thunk, forced at selection, so mutually referring instance values no longer + recurse before a method runs. +- **Closed local row instantiations.** Checked row residuals are retained as + instantiation evidence even when they have no single type-arena node, and a + finite closed use of a nonrecursive row-polymorphic local lambda is + materialized before CC layout. The reusable type substitution moved to + `psrs-core::instantiation`. +- **Aggregate protocols for bare polymorphic slots.** The array and record + layout owners register canonical erased-element array and erased-field product + protocols before conversion planning; the array protocol is registered only + when an array layout exists. Newtype callable fields get their own protocol. +- **Partial authorization.** A checked report-only `Partial` scope adds a + marked trap fallback that survives erasure and contributes coverage instead of + hiding redundant source alternatives. +- **Qualified symbolic operators.** Contiguous module qualifiers followed by a + symbolic name resolve, so `Data.Array.(..)`, `A.:`, `A.!!` and + `Foo.Bar.-#-` parse, resolve and lower. + +Validation: + +- Mandatory Wasmtime driver regressions for the touched areas pass: + `library_array_callbacks_match_pinned_official_observations` (the new + sixteen-case/69-value callback fixture), the earlier apply/bind/extend/ST/ + uncurried/primitive array checks, `declaration_calls`, `library_foreign`, + `cc_ir_audit` and `generic_aggregate_audit`. +- Public API oracle: `conformance/array-public-cases.mjs` covers all 93 public + `Data.Array` exports with 105 cases and 396 checks. The library-owned + `array-public-oracle` command pinned all 41 upstream packages, evaluated the + pinned official JavaScript, and wrote `Main.purs`. The mandatory runner + compiled it on the target and executed the artifact under Wasmtime with exit + 42 and empty stdout/stderr. `public-run.json` records the binary/source/package + hashes and `observations.json` records the coverage. +- Library-owned Node tooling regressions: 8 passed. +- `cargo fmt --all --check` and `cargo clippy --workspace --all-targets -- -D + warnings` pass. + +The slice introduces no new driver-library failure: the driver suite returns to +its pre-slice set and the 174 cases this work unblocks stay green. The +`Data.Array.ST.Partial` missing final newline is repaired. + +Two limits are recorded rather than hidden. Open runtime rows remain an explicit +unsupported layout, so the same machinery does not define an open-row ABI. The +`Data.Show` foreign slots still have no target binding, so programs that reach +`show` fail at P8 library linking; that gap predates this slice and is unchanged. +The full reproducer was not re-measured, so no unsupported-library count is +published here. No push, PR or issue operation was performed. diff --git a/docs/implementation/stdlib/compile-progress-2026-10-07/report.md b/docs/implementation/stdlib/compile-progress-2026-10-07/report.md new file mode 100644 index 00000000..2dd8b3f1 --- /dev/null +++ b/docs/implementation/stdlib/compile-progress-2026-10-07/report.md @@ -0,0 +1,97 @@ +# Stdlib compilation progress + +This slice repairs three compiler contracts and implements the public Bounded +foreign slots in the independent library. It starts at compiler `7f93266` and +stdlib `8aaaffa`. The final library pin is +`18987e3fec9025830ce576e4815a6d7cf58bacb4`, content fingerprint +`fnv1a64-v1:b7f12516354db01c`. + +## Compiler contracts + +- P3 qualified scopes combine disjoint imports. Ambiguity depends on canonical + declaration identity at lookup or pseudo-module re-export, rather than a + blanket ban on shared aliases. Six official-purs differential cases cover + disjoint members, repeated canonical declarations, unused conflicting scopes, + and value/type/re-export ambiguity. +- P4 guard lowering creates fallthrough helpers only for rows that can be + entered after the first guarded row. Previously unused copies introduced + fresh inferred result constraints in Data.Enum, Data.Enum.Generic and + Data.List.Lazy. The repair preserves scoped dictionary checking; a missing + dictionary is still rejected. Three official-purs cases and value-sensitive + guarded/fallthrough executions cover the boundary. +- P8 coverage analysis checks the default matrix for incomplete finite + signatures before constructing a finite missing witness. Witness construction + tracks the type path, so binary recursive products cannot continually grow + the query columns. Missing uninhabited constructors still require analysis of + observed fields. Core tests cover either constructor order, redundancy, + missing witnesses, and uninhabited recursive products. Source execution + covers both binary-tree constructor orders. Data.Map.singleton/size now + compiles and returns 42 under Wasmtime; the prior reproducer aborted with a + stack overflow in usefulness recursion. + +## Library boundary + +Data.Bounded changes only its six typed foreign slots and one target import. +PSRS.Bounded owns i32 bounds and binary64 infinities over existing checked +scalar primitives. The official pure code, APIs, classes and instances remain. +DEC-16 justifies Char's U+10FFFF scalar upper bound instead of JS's U+FFFF. +The pinned-source generator verifies the exact allowed transformation and reads +actual upstream JS constants. The 18 checks include three Enum integration +observations, which are not claimed as an upstream Enum differential oracle. +`bounded-run.json` records Wasmtime exit 42 and empty stdout/stderr. + +## Validation and limits + +The L2 annotations scoreboard reports M2 72/72 and passing resolution 403/413. +The ten remaining blockers are six P3 and four P0; there are no missing-library +or harness-loading blockers. Nineteen sibling modules load successfully. +The package audit covers 224 modules in 41 packages: 172 identical, 34 modified, +18 target additions, no upstream module omitted, and no direct-self-recursion +replacement. Audit categories do not by themselves approve target differences. +Library Node tooling tests: 8 passed. + +`diagnosis-checkpoints.json` distinguishes the initial P3 failure (165 +diagnostics), alias-only P5 failure (7 diagnostics), and the guard-fixed full +import acceptance (137559 ms with a 240-second cohort). The same 211-import +source with unused integer main timed out under 120 seconds after the coverage +repair. Different timeout cohorts and library fingerprints are not presented as +compatible diagnosis comparisons. The final locked-package remeasurement passes in 172290 ms under the +240-second cohort, with no diagnostics. It retains the same input fingerprint. + +Compile acceptance of an unused main does not prove every exported API executes +or every declaration is retained in the executable. The focused List.length, +Set.singleton/size, Map.singleton/size and Enum range executions establish only +those paths. The final locked-package String and Int parsing probes still fail explicitly +at P8 library linking: `Data.String.CodeUnits.length` and +`Data.Int.fromStringAsImpl` have no target implementation (9489 ms and +13615 ms). Some Number operations, Lazy and Effect.Ref also retain foreign slots +without target support in the earlier survey. This slice does not close the entire stdlib. + +The real Map runtime fixture is: + +```purescript +module Main where +import Prelude +import Data.Map as M +main :: Int +main = if M.size (M.singleton 1 2) == 1 then 42 else 1 +``` + +Formatting and strict workspace clippy pass. The backend library suite passes +403 tests; the driver library suite passes 685 tests with the three failures +below (mandatory Wasmtime, 233.18 seconds). The trusted-order loader and all +new guard/recursive-product runtime regressions pass. The original HEAD independently +reproduces three driver-library failures: `constrained_dictionary_parameters_precede_ordinary_arguments`, +`runs_a_polymorphic_identity_with_a_number` and +`compiles_if_expression_through_cfg_to_structured_wasm`. Their optimized-artifact +assertions are a pre-existing validation baseline, not a passing full workspace. +The complete `PSRS_REQUIRE_WASMTIME=1 cargo test --workspace --no-fail-fast` +run finishes with 1658 passed, 3 failed and 5 ignored tests, exit 101. +The run also reports E0463 dependency-artifact load failures in backend and +driver rustdoc targets. A sequential +`cargo test -p psrs-backend -p psrs-driver --doc` recheck passes, exit 0, +without source changes. These artifact errors do not remain unresolved. All +other targets and doc-tests pass. +The 19 upstream differential groups pass. Ignored scoreboards remain +unmeasured here except for the separately executed L2 board. `validation.json` +records the result and the independently verified baseline failures. diff --git a/docs/implementation/stdlib/deep-expression-2026-10-07/report.md b/docs/implementation/stdlib/deep-expression-2026-10-07/report.md new file mode 100644 index 00000000..8631c409 --- /dev/null +++ b/docs/implementation/stdlib/deep-expression-2026-10-07/report.md @@ -0,0 +1,73 @@ +# Deep expression traversal checkpoint + +Starting point: compiler 502930d on stdlib/vendor-core-libraries, with an +uncommitted compiler repair handed over for review. The independent library +remains pinned to c8020c00e227f256c0a365e22d5ebcbd1601607a, fingerprint +fnv1a64-v1:6d4f0e0ccbe37bc0. No library sources are changed by this topic. + +## Boundary and repair + +The ordinary Prelude expression `true && ... && true` with 448 operands was +accepted by official purs but aborted this compiler. The ungrouped String +oracle exposed recursive P5 inference frames; on a 2 MiB worker stack, a later +backtrace located Expr::clone within Core lower_expr_inner. Saturated intrinsic +lowering cloned the already lowered right remainder while ancestor frames +were still live. + +The owning contracts are P4 fixity and P6 checked intrinsic lowering in +[D-01](../../../design/D-01-frontend-and-ir-boundaries.md). Fixity and evaluation +order are preserved: operator association still determines the application +tree. Intrinsic recognition still requires a registered global head and exact +descriptor arity. The saturated path consumes the application spine and +reverses its collected arguments into source order, avoiding recursive copies. +Partial and oversaturated calls continue through ordinary application lowering. + +Expression entry points in lowering, checking, verification, optimization and +capture analysis use psrs_span::with_sufficient_stack. It checks a 256 KiB red +zone and continues on an 8 MiB heap stack segment. Recursive entries must pass +through the wrapper; checking only once at the pass root is insufficient. +HIR, THIR and Core Expr clone and equality use the same mechanism at each node. +The change introduces no Boolean/String name exception, re-association or new +runtime primitive. It does not claim arbitrary-depth safety for every tree +operation: ordinary derived destruction and Debug formatting are unchanged; +destruction is exercised at depth 800. + +## Validation + +`PSRS_REQUIRE_WASMTIME=1 cargo test --workspace --no-fail-fast` completes with +1664 passed, 3 failed and 5 ignored. The three failures are the same previously +reproduced [baseline assertions](../compile-progress-2026-10-07/validation.json): +dictionary parameter ordering expects a pruned function name, polymorphic +Number identity expects an unused f64 value in WAT, and a constant conditional +expects a br_if after optimization. Driver-library results are 687 passed and +3 failed. All other workspace targets, including documentation tests, pass. +The full log is /private/tmp/psrs-stack-workspace-20261007.log. + +`cargo fmt --all --check`, `git diff --check` and +`cargo clippy --workspace --all-targets -- -D warnings` pass. validation.json +records the commands, aggregate outcomes and the unchanged baseline failures. + +Runtime evidence for the original ungrouped String +fixture is in string-run.json; all 448 projected FFI value checks execute under +Wasmtime 49.0.2 and return 42 with empty stdout/stderr. This extends the grouped +String checkpoint without changing its scalar-indexing oracle or source +fidelity claim. + +The additional right-associative subtraction test uses 448 operands: its +expected result is 0; left association would produce -446. Official purs +accepts the same source; subtraction-run.json records Wasmtime execution. +Both new driver tests pass within the full required-runtime workspace run. +HIR, THIR and Core tests exercise cloning, equality +and destruction of an 800-node application spine, and psrs-span directly +exercises stack growth with large recursive frames. + +The 211-module import fixture passes in 114081 ms with a 240-second timeout, +zero exclusions, crashes or timeouts. import-summary.json records the compiler, +input and library fingerprints and captured pass/validation records. An unused integer +entry point establishes import compilation only, not execution of every +declaration or full standard-library runtime closure. + +Fresh follow-up probes reach explicit P8 missing-binding diagnostics for +Data.Int.fromStringAsImpl and Data.String.CodeUnits._charAt. Those target-library +implementation boundaries remain open; this compiler repair does not replace +them with successful-looking placeholders. diff --git a/docs/implementation/stdlib/explicit-polymorphic-arguments-2026-10-07.md b/docs/implementation/stdlib/explicit-polymorphic-arguments-2026-10-07.md new file mode 100644 index 00000000..79827574 --- /dev/null +++ b/docs/implementation/stdlib/explicit-polymorphic-arguments-2026-10-07.md @@ -0,0 +1,87 @@ +# Explicit polymorphic arguments in Core + +## Contract and repair + +The unchanged public `Data.Number.fromString "42.5"` wrapper stopped at P7 +Core verification. Its foreign implementation has a `Fn4` signature containing +explicit universal callback and result arguments. P5 retained those arguments, +but Core's shared invariant matcher rejected any flexible nominal parameter +whose replacement was `ForAll`. Constructor field checking had the same blanket +restriction. + +Core now retains the whole checked explicit replacement, including its binders. +The shared binding operation still owns occurrence checking and consistency +between repeated parameters. Nominal positions remain invariant; neither +direction admits replacing a universal slot with a specialized function. The +constructor checker uses the same operation for its explicit arguments and +field templates. No inference rule, official source, or library lock changes. + +The minimal checked-instantiation regression failed before the repair. Positive +and negative tests cover alpha-equivalent replacements, repeated-parameter +consistency, rigid parameters, specialized functions, constructor fields, and +occurrences hidden beneath quantifiers. A reduced source foreign declaration +with an uncurried newtype and a universal argument now survives Core lowering. +Existing rank-N source acceptance/rejection and mandatory Wasmtime execution +tests both pass, including rejection of inferred impredicative constructors. + +## Comparable public replay + +Both diagnoses use library revision +`2ee2d1fdaff8a2841824cd1bb5d3fb864f594630` and fingerprint +`fnv1a64-v1:801317111aa80a8f`: + +```purescript +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 +``` + +| Checkpoint | First blocker | +| --- | --- | +| Before | P7 Core verification: expression type inconsistent with context | +| After | P8 library linking: `Data.Number.fromStringImpl` has no target implementation | + +Raw diagnosis snapshots, replay inputs, and validation logs remain under +`/private/tmp/psrs-polymorphic-*` and `/private/tmp/psrs-number-parsing-*`. +They are not committed. This replay establishes removal of the P7 blocker; +it does not establish parsing behavior or whole-library compilation. + +## Rust validation + +`cargo fmt --all --check` and +`cargo clippy --workspace --all-targets -- -D warnings` pass. +Focused Core tests and the reduced source +regression pass. `PSRS_REQUIRE_WASMTIME=1 cargo test -p psrs-driver --test rank_n` +passes both source and execution tests. + +`CARGO_INCREMENTAL=0 PSRS_REQUIRE_WASMTIME=1 cargo test --workspace --no-fail-fast` +completes all 52 target suites, including doc tests: 1674 passed, three existing +failures, and five ignored. The failures match the preceding checkpoint: + +- `dictionary_audit::execution::constrained_dictionary_parameters_precede_ordinary_arguments`: + the test cannot find its expected MIR constrained function. +- `tests::functions::runs_a_polymorphic_identity_with_a_number`: + its optimized WAT does not contain the asserted `f64` token. +- `tests::integration::compiles_if_expression_through_cfg_to_structured_wasm`: + its optimized constant branch does not contain the asserted `br_if` token. + +Full workspace validation is not green. No official scoreboard or gate +measurement is changed. + +## Remaining work + +Implement the missing parsing target with the official pure wrapper preserved. +The library owns prefix recognition and callback behavior; a checked decimal +conversion primitive may own correctly rounded binary64 conversion. Validate +against the pinned official JS FFI, including whitespace, accepted decimal +prefixes, malformed exponents, overflow, underflow, and signed zero. Other +Number foreign slots and whole-standard-library behavior remain separate work. + +The subsequent [Number parsing acceptance](number-parsing-2026-10-07.md) +implements and executes that missing target. The replay and measurements above +describe this earlier Core checkpoint. diff --git a/docs/implementation/stdlib/int-formatting-2026-10-07/report.md b/docs/implementation/stdlib/int-formatting-2026-10-07/report.md new file mode 100644 index 00000000..e7b2bfbf --- /dev/null +++ b/docs/implementation/stdlib/int-formatting-2026-10-07/report.md @@ -0,0 +1,65 @@ +# Int formatting checkpoint + +Starting compiler: dab23d7, with a clean worktree. The independent library moves +from 1ad9a1a9fba01f35c694d877fd4890f8d2e46cb9 to 02929696a90db4a8277151aab9a8c0a9978f8aa3, +content fingerprint fnv1a64-v1:83cdad11334226e0. This compiler commit changes +its lock and conformance documentation; no Rust source changes. + +## Owner and source fidelity + +Data.Int unwraps its opaque checked Radix and delegates the toStringAs foreign +slot to PSRS.Int.Format. Its signature, exports and all official pure declarations +remain unchanged. The entire module is checked against clean pinned +purescript-integers v6.0.0 at 54d712b25c594833083d15dc9ff2418eb9c52822; +the verifier permits exactly the native toNumber binding, parsing delegate and +formatting delegate with their target imports. + +The library owns radix digit extraction over raw truncating intQuot and signed +remainder. Keeping the accumulator nonpositive supports MIN without overflow. +A checked base in 2..36 proves remainder digits in 0..35, all output bytes ASCII +and termination within 32 digits. Zero produces one digit and negatives one +leading minus. Existing array append and checked bytesToString primitives +assemble canonical UTF-8; no new compiler intrinsic or library-name exception +is added. The previous PSRS.Show decimal digit implementation now calls the +same intBytes operation with fixed base 10, including numeric control escapes. + +## Verification + +- 857 public toStringAs observations agree byte-for-byte with the actual pinned + official Data.Int.js FFI across all bases 2..36, MIN/MAX and adjacent values, + zero, signs, digit transitions and powers of each base. +- 857 public parse/format round trips, 15 official Data.Show.js showInt comparisons + and 5 named-radix checks bring the generated fixture to 1734 checks. Each + group is bounded to 24 conjunction terms to limit fixture nesting; the + separate deep-expression compiler regression remains its own evidence. +- The generated program returns 42 with empty stdout/stderr in both development + and locked-package builds. run.json records direct build with PSRS_STDLIB_ROOT + unset, lock/input/compiler/Wasm digests and actual Wasmtime execution. The + development runner confirms the package unchanged during execution. +- The preceding 846 parsing checks return 42 with empty output against the same + package fingerprint. Observation and runtime data live in the library under + docs/evidence/int-formatting/. +- Node tooling: 10 passed, zero skips. Mandatory Wasmtime Show driver tests: + 6 passed, including number/aggregate boundaries, control escaping, retained + strings through allocation and memory growth. Development and locked trusted + loader tests: one passed each; Prelude remains first. +- Source audit: 227 modules in 41 packages; 171 identical, 35 modified, 21 target + additions, zero absent upstream modules and zero direct recursive foreign + placeholders. Exact source verification and behavior evidence approve this + slot; aggregate inventory categories do not approve other changes. +- The same public hexadecimal formatting probe changes from explicit P8 missing + Data.Int.toStringAs implementation to Passed through the locked loader. + diagnosis.json retains both snapshots; different package fingerprints preclude + a compatible diagnose --compare claim. +- Formatting and diff checks pass. Full workspace/clippy were not rerun for this + library-only slice. The preceding Rust checkpoint recorded 1664 passed, + 3 established baseline assertion failures and 5 ignored, with strict clippy + passing. That earlier run does not validate this entire new library pin. + +No import cohort or official suite scoreboard is remeasured. This checkpoint +establishes public integer formatting behavior for the tested values, not full +Data.Int or standard-library support. + +A fresh public `Int.fromNumber 42.0` probe reaches P8 library linking with +explicit missing Data.Int.fromNumberImpl support. next-blocker.json records +this continuation point under the new locked package. diff --git a/docs/implementation/stdlib/int-number-2026-10-07/report.md b/docs/implementation/stdlib/int-number-2026-10-07/report.md new file mode 100644 index 00000000..159efc93 --- /dev/null +++ b/docs/implementation/stdlib/int-number-2026-10-07/report.md @@ -0,0 +1,66 @@ +# Checked Number-to-Int checkpoint + +Starting compiler: deb8304, with a clean worktree. The independent library moves +from 02929696a90db4a8277151aab9a8c0a9978f8aa3 to 3e3736dec40e873da6930d4864e7db4b525570ce, +content fingerprint fnv1a64-v1:5e7c1b9bf50e4915. This compiler commit changes +its lock and conformance evidence; no Rust source changes. + +## Owner and source fidelity + +Data.Int.fromNumber remains its official pure wrapper. The private fromNumberImpl +foreign slot delegates to PSRS.Int.Number with its full rank-N constructor +signature. The source verifier checks the entire official purescript-integers +v6.0.0 module at 54d712b25c594833083d15dc9ff2418eb9c52822. It permits +exactly four typed foreign-slot adaptations and their target imports: native +toNumber, parsing, formatting and checked Number conversion. All other pure +functions, exports, signatures, classes, instances and opaque Radix remain. + +The target library widens the existing total saturating numberToInt result and +checks exact Number equality before calling the success builder. Every i32 is +exactly representable in f64. Fractions and overflow fail equality; infinities +differ from finite bounds and NaN is unequal to every widened result. This +proves success exactly for integral, in-range inputs without relying on a +trapping conversion. No new intrinsic or compiler name exception is added. + +Both Number zero signs are accepted. Int has a single signed-i32 zero, so +widening its result canonicalizes negative zero to positive Number zero. The +official JS implementation can preserve negative Number zero inside its Int; +observations retain that representation difference explicitly rather than +claiming identical signed-zero bits. + +## Verification + +- 94 public fromNumber observations agree with the actual pinned Data.Int.js + FFI as Maybe Int values. Cases cover both i32 endpoints, immediately adjacent + IEEE-754 values on either side, fractions, subnormals, signed zeros, huge finite + values, unsigned and 53-bit boundaries, NaN and positive/negative infinity. +- Eleven public toNumber/fromNumber round trips and six local rank-N builder + checks are recorded separately: 111 checks total. Development and direct + locked builds return 42 with empty stdout/stderr. run.json records the direct + build with PSRS_STDLIB_ROOT unset, input/compiler/Wasm digests and actual + Wasmtime result. The development runner records immutable package content. +- The preceding 1734 public formatting/parse-round-trip checks return 42 with + empty output against the same package fingerprint. Library observations and + runtime evidence live under docs/evidence/int-number/. +- Node tooling: 10 passed, zero skips. Mandatory Wasmtime driver scalar tests: + 16 passed. Development and locked trusted-order loader tests: one passed each; + Prelude remains first. +- Source audit: 228 modules in 41 packages; 171 identical, 35 modified, 22 target + additions, zero missing upstream modules and zero direct recursive foreign + placeholders. Aggregate categories do not approve other source differences. +- The same public fromNumber 42.0 probe changes from explicit P8 missing + Data.Int.fromNumberImpl support to Passed through the locked package. + diagnosis.json retains both snapshots; differing fingerprints preclude a + compatible diagnose --compare result. +- Formatting and diff checks pass. Full workspace/clippy were not rerun for this + library-only slice. The preceding Rust checkpoint recorded 1664 passed, + 3 established baseline assertion failures and 5 ignored, with strict clippy + passing; that historical run does not validate the entire new library pin. + +No full import cohort or official suite scoreboard is remeasured. This checkpoint +does not establish all Data.Int APIs; its rounding/clamping wrappers require +additional Data.Number implementations and independent behavior evidence. + +A fresh public `Int.trunc 42.9` probe first reaches explicit P8 missing +Data.Number.isFinite support. next-blocker.json records this continuation +point under the new locked package. diff --git a/docs/implementation/stdlib/int-parsing-2026-10-07/report.md b/docs/implementation/stdlib/int-parsing-2026-10-07/report.md new file mode 100644 index 00000000..18a07a2f --- /dev/null +++ b/docs/implementation/stdlib/int-parsing-2026-10-07/report.md @@ -0,0 +1,67 @@ +# Int parsing checkpoint + +Starting compiler: 559695c. The library moves from +d6ab1527e1711eb3c712f5d7bc70e5079bb8eaee to +1ad9a1a9fba01f35c694d877fd4890f8d2e46cb9, with fingerprint fnv1a64-v1:f728b54c2aadd888. This compiler +commit updates its package lock and acceptance evidence; no Rust source changes. + +## Owner and source fidelity + +Data.Int's private fromStringAsImpl slot delegates to PSRS.Int.Parse. Data.Int +owns unwrapping its opaque Radix. Official exports, pure public wrappers, +instances and rank-N constructor signatures remain unchanged. The library +verifies the entire module against the clean purescript-integers v6.0.0 checkout +at 54d712b25c594833083d15dc9ff2418eb9c52822, allowing exactly this delegate +and the previous toNumber native binding. + +The target library implements strict ASCII digit grammar over UTF-8 bytes. +A nonpositive accumulator supports MIN without wrapping; checks before multiply +and subtract reject overflow. The threshold uses truncating intQuot, rather +than the compiler's floor intDiv operation. There is no new intrinsic or +compiler exception for library names. + +The same quotient-owner error existed in PSRS.Int: its Euclidean sign adjustment +was applied to an already rounded intDiv result. The old pinned library's public +`div (-7) 3 == -3` fixture compiled but returned 1. The helper now binds intQuot +and retains its existing raw remainder, zero handling and single library-owned +Euclidean correction. Backend primitive semantics are unchanged. + +## Verification + +- 836 pinned official parsing FFI observations cover all bases 2 through 36, + both i32 boundaries and adjacent rejection, signs, leading zeros, upper/lower + ASCII letters, invalid digits, prefix-looking input, whitespace, non-ASCII, + embedded NUL and long overflow strings. Public fromString/fromStringAs and + named radix constants execute. Six public radix-factory checks and four local + rank-N builder checks are recorded separately: 846 checks in total. +- 209 public degree/div/mod observations agree with pinned Prelude FFI for + representable i32 results, including zero divisors, negative divisors and + boundary values. MIN / -1 is recorded as an unrepresentable JS result; + target signed division still traps, so this is not a raw-JS equality claim + for that input. +- Both generated programs return 42 with empty stdout and stderr. Development + reports show the package unchanged during each run. run.json additionally + records direct builds through stdlib.lock.json with PSRS_STDLIB_ROOT unset, + compiler/input/Wasm digests and actual Wasmtime output. +- Node tooling: 10 passed, zero skips. Mandatory Wasmtime driver scalar tests: + 16 passed. Development-root and normal locked trusted-order loader tests: + one passed each; Prelude remains first. +- Source audit: 226 modules, 41 packages; 171 identical, 35 modified, 20 target + additions, zero missing upstream modules and zero direct recursive foreign + placeholders. These aggregate counts do not approve other source differences. +- The same minimal public Int.fromString probe changes from explicit P8 missing + fromStringAsImpl support to Passed through the locked package. diagnosis.json + retains both snapshots; differing fingerprints preclude a compatible + diagnose --compare claim. +- Formatting and diff checks pass. Full workspace/clippy were not repeated for + this library-only slice. The preceding compiler checkpoint recorded 1664 pass, + 3 established baseline assertion failures and 5 ignored tests, with strict + clippy passing. Those results do not establish full verification of this pin. + +No full import cohort or suite scoreboard is remeasured. This checkpoint does +not establish full Data.Int or standard-library support. Formatting integers +and other remaining foreign slots are outside its scope. + +A fresh public `Int.toStringAs Int.hexadecimal 42` probe reaches P8 library +linking with explicit missing Data.Int.toStringAs support. next-blocker.json +records the new pinned package and this continuation point. diff --git a/docs/implementation/stdlib/number-abs-2026-10-07.md b/docs/implementation/stdlib/number-abs-2026-10-07.md new file mode 100644 index 00000000..c07ebec7 --- /dev/null +++ b/docs/implementation/stdlib/number-abs-2026-10-07.md @@ -0,0 +1,98 @@ +# Number absolute-value acceptance + +## Contract and implementation + +Starting compiler revision: 131a1fe on stdlib/vendor-core-libraries, with a +clean worktree. The starting package was 4c4795a9c87496242398554b83f665062e3af2fd +(fnv1a64-v1:5e52e376007f1a23). A fresh Number.abs (-42.5) diagnosis stopped at +P8 library linking because Data.Number.abs had no target implementation. + +The compiler now owns numberAbs :: Number -> Number. Its HIR identity is +appended, preserving existing intrinsic IDs. Core and CC require Number +operand/result types, MIR requires F64, and Wasm emits f64.abs. Incorrect +foreign binding schemes are rejected before ABI erasure. Existing malformed +CC/MIR unary-operation checks cover the new operation. No additional runtime +artifact, host binding, library-name recognition, integer conversion, or +constant folding is introduced. + +The independent library changes only the foreign slot's explicit binding: +`foreign import "psrs:intrinsic#numberAbs" abs :: Number -> Number`. +Its signature, exports, and all official pure declarations remain unchanged. +The complete-module source verifier checks this transformation against pinned +purescript-numbers v9.0.1 (27d54effdd2c0e7a86fe356b1cd813dca5981c2d). + +Wasm absolute value clears the sign bit while preserving finite magnitudes +and NaN payloads. Negative zero becomes positive zero, either infinity becomes +positive infinity, and NaN remains NaN. The public oracle checks NaN value +behavior rather than a JS payload identity. See the +[Wasm scalar contract](../../design/backend/fp/scalars-and-primitives.md). + +## Package and runtime evidence + +Locked package: e0679521275277c0b005b2509d9e0a8570715c5e +(fnv1a64-v1:4e357ed0c7f9f10f). + +```sh +node ../psrs-stdlib/conformance/number-abs.mjs \ + /private/tmp/ps-pkgs/purescript-numbers /tmp/psrs-number-abs-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-number-abs-oracle/Main.purs \ + --expected-exit 42 --timeout 240 --out /tmp/psrs-number-abs-runtime +env -u PSRS_STDLIB_ROOT ./target/debug/psrs build \ + /tmp/psrs-number-abs-oracle/Main.purs -o /tmp/psrs-number-abs-locked.wasm +wasmtime /tmp/psrs-number-abs-locked.wasm +``` + +The actual pinned official JS abs produces 153 input observations and 306 +checks, exercising direct public calls and higher-order calls. Inputs include +both zero signs, subnormals, minimum normal values, fractions, magnitudes +beyond i32, maximal finite values, infinities, NaN, and 128 deterministically +generated binary64 patterns. Reciprocal observations distinguish zero signs. +Both development-package and locked-package executions return 42 with empty +stdout/stderr. The final package also passes the existing 304 parsing checks +and 414 public Number/Int rounding checks, returning 42 with empty output. + +The complete source inventory records 230 modules across 41 packages: +170 identical, 36 modified and 24 target additions, no absent upstream modules +and no direct recursive foreign placeholders. This inventory does not approve +all existing adaptations or establish behavior of every API. Node tooling: +11 passed, none skipped. All 230 vendored source hashes match their inventory. +Raw reports, generated inputs, runtime artifacts and +validation logs remain local under /private/tmp and are not committed. + +## Rust validation + +The two new driver regressions pass with mandatory Wasmtime: public behavior +and rejection of invalid foreign contracts. Backend execution passes with +optimization enabled and disabled, checking negative-zero canonicalization +and a negative magnitude beyond i32. + +`CARGO_INCREMENTAL=0 PSRS_REQUIRE_WASMTIME=1 cargo test --workspace --no-fail-fast` +completes all 52 target suites, including doc tests: 1686 passed, three existing +failures, and five ignored. The failures match the preceding checkpoint: + +- `dictionary_audit::execution::constrained_dictionary_parameters_precede_ordinary_arguments`: + the test cannot find its expected MIR constrained function. +- `tests::functions::runs_a_polymorphic_identity_with_a_number`: + its optimized WAT does not contain the asserted f64 token. +- `tests::integration::compiles_if_expression_through_cfg_to_structured_wasm`: + its optimized constant branch does not contain the asserted br_if token. + +Full workspace validation is not green because of these baseline failures. +There is no new failing test in this run. + +`cargo fmt --all --check`, `git diff --check`, and +`CARGO_INCREMENTAL=0 cargo clippy --workspace --all-targets -- -D warnings` +pass. + +## Remaining work + +The original public abs reproducer now compiles. The package fingerprint +changed, so this is replay across a package update rather than a comparable +same-fingerprint diagnose --compare measurement. + +A fresh Number.sqrt 4.0 reproducer stops at P8 library linking because +Data.Number.sqrt has no target implementation. This is absolute-value +acceptance, not complete Number FFI or whole-standard-library acceptance. +No official scoreboard or gate measurement changes. diff --git a/docs/implementation/stdlib/number-acos-2026-10-07.md b/docs/implementation/stdlib/number-acos-2026-10-07.md new file mode 100644 index 00000000..877e36b9 --- /dev/null +++ b/docs/implementation/stdlib/number-acos-2026-10-07.md @@ -0,0 +1,89 @@ +# Number inverse-cosine acceptance + +## Contract and implementation + +Starting compiler revision: b300392 on stdlib/vendor-core-libraries, with a +clean worktree. The starting package was +ef004e75998995ea8ac00840e36c1d9b71226b25 +(fnv1a64-v1:19dabd2d6d4033b4). A fresh Number.acos 1.0 diagnosis stopped at +P8 library linking because Data.Number.acos had no target implementation. + +Wasm has no inverse-cosine instruction, so this is not an `f64.sqrt`-style +opcode. The compiler owns numberAcos :: Number -> Number. Its HIR identity is +appended as 70, preserving existing intrinsic IDs. Core and CC require Number +operand and result types. MIR calls the scalar export `number_acos` in the +shared numeric runtime. Incorrect foreign binding schemes are rejected before +ABI erasure. + +The export is pinned libm 0.2.15, the fdlibm polynomial. Finite inputs in the +closed interval [-1, 1] match the official JavaScript Math.acos results used +by purescript-numbers. Finite inputs outside that interval, infinities, and +NaN produce NaN. The operation does not trap. NaN payloads are not part of the +public contract. No constant folding is introduced. A whole inverse-cosine +algorithm does not become a compiler intrinsic. + +The numeric runtime retains number_to_string and number_from_decimal and adds +number_acos. Its reproducible artifact SHA-256 is +092c1aca5a1270c0c792568da6c230ebdd25fcf536e64f17729315db75e3c683. +The private table still has two funcref slots. The active initializer now +points at function 22. Static stack analysis accepts the artifact and still +measures 1680 bytes inside the existing 65536-byte reserve. + +The independent library changes only the foreign slot's explicit binding: +`foreign import "psrs:intrinsic#numberAcos" acos :: Number -> Number`. +Its signature, exports, and all official pure declarations remain unchanged. +The complete-module source verifier checks this transformation against pinned +purescript-numbers v9.0.1 (27d54effdd2c0e7a86fe356b1cd813dca5981c2d). + +## Package and runtime evidence + +Locked package: e05d4f765c949d50832b7d01b5cf992b5efc903e +(fnv1a64-v1:eccd4891551858bb). + +```sh +node ../psrs-stdlib/conformance/number-acos.mjs \ + /private/tmp/ps-pkgs/purescript-numbers /tmp/psrs-number-acos-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-number-acos-oracle/Main.purs \ + --expected-exit 42 --timeout 240 --out /tmp/psrs-number-acos-runtime +env -u PSRS_STDLIB_ROOT ./target/debug/psrs build \ + /tmp/psrs-number-acos-oracle/Main.purs -o /tmp/psrs-number-acos-locked.wasm +wasmtime run /tmp/psrs-number-acos-locked.wasm +``` + +The actual pinned official JS acos produces 149 input observations and 298 +checks, exercising direct public calls and higher-order calls. Inputs include +both zero signs, the closed interval endpoints, values one ulp outside that +interval, subnormals, nonfinite values, and 128 deterministically generated +binary64 patterns. A zero result is distinguished by its reciprocal. NaN +checks do not require a payload. Both the development-package run and the +locked-package artifact +(sha256 2b2ba1dd721be0b84db2b7a8eeb5385c974ea701fbd2cdb4a4fa656b5e02abb2) +return 42 with empty stdout and stderr. Wasmtime is 49.0.2. + +The same public acos 1.0 probe now passes against the locked package, in +8522 ms. A fresh Number.asin 0.0 probe stops at P8 library linking because +Data.Number.asin has no target implementation. The package fingerprint +changed, so the before and after diagnoses are not a same-fingerprint compare. + +Node tooling: 11 passed, none skipped. The numeric runtime rebuild is +byte-for-byte reproducible. + +## Rust validation + +Two driver regressions pass with mandatory Wasmtime: public behavior, +including a higher-order call and the linked `number_acos` export, and +rejection of invalid foreign contracts. The runtime unit test checks the +fdlibm boundary bit patterns. `cargo fmt --all --check` and +`cargo clippy --workspace --all-targets -- -D warnings` pass. + +`PSRS_REQUIRE_WASMTIME=1 CARGO_INCREMENTAL=0 cargo test --workspace --offline --no-fail-fast` +finishes all 52 targets: 1696 passed, 3 failed, 5 ignored. The only failing +target is `psrs-driver --lib`. Its three failures are the established +baseline: `constrained_dictionary_parameters_precede_ordinary_arguments`, +`runs_a_polymorphic_identity_with_a_number`, and +`compiles_if_expression_through_cfg_to_structured_wasm`. No new failure +appeared. Full workspace validation is not green because of those baseline +failures. This change does not establish complete Number FFI support or +whole-standard-library runtime behavior. diff --git a/docs/implementation/stdlib/number-asin-2026-10-08.md b/docs/implementation/stdlib/number-asin-2026-10-08.md new file mode 100644 index 00000000..1e55bc4e --- /dev/null +++ b/docs/implementation/stdlib/number-asin-2026-10-08.md @@ -0,0 +1,89 @@ +# Number inverse-sine acceptance + +## Contract and implementation + +Starting compiler revision: 450759c on stdlib/vendor-core-libraries, with a +clean worktree. The starting package was +e05d4f765c949d50832b7d01b5cf992b5efc903e +(fnv1a64-v1:eccd4891551858bb). A fresh Number.asin 0.0 diagnosis stopped at +P8 library linking because Data.Number.asin had no target implementation. + +Wasm has no inverse-sine instruction. The compiler owns +numberAsin :: Number -> Number. Its HIR identity is appended as 71, preserving +existing intrinsic IDs. Core and CC require Number operand and result types. +MIR calls the scalar export `number_asin` in the shared numeric runtime. +Incorrect foreign binding schemes are rejected before ABI erasure. + +The export is pinned libm 0.2.15, the fdlibm polynomial. Finite inputs in the +closed interval [-1, 1] match the official JavaScript Math.asin results used +by purescript-numbers, and negative zero stays negative zero. Finite inputs +outside that interval, infinities, and NaN produce NaN. The operation does not +trap. NaN payloads are not part of the public contract. No constant folding is +introduced. A whole inverse-sine algorithm does not become a compiler +intrinsic. + +The numeric runtime retains its formatter, decimal conversion, and inverse +cosine exports and adds number_asin. Its reproducible artifact SHA-256 is +7e697803a15ed67e812051d78036e50a1213401296e48895d6cb7dceb27f36f7. +The private table still has two funcref slots. The active initializer now +points at function 24. Static stack analysis accepts the artifact and still +measures 1680 bytes inside the existing 65536-byte reserve. + +The independent library changes only the foreign slot's explicit binding: +`foreign import "psrs:intrinsic#numberAsin" asin :: Number -> Number`. +Its signature, exports, and all official pure declarations remain unchanged. +The complete-module source verifier checks this transformation against pinned +purescript-numbers v9.0.1 (27d54effdd2c0e7a86fe356b1cd813dca5981c2d). + +## Package and runtime evidence + +Locked package: 30f22bdb3b207d908ed00ac779d7b0a0a32cb00d +(fnv1a64-v1:db4b18f8950a407f). + +```sh +node ../psrs-stdlib/conformance/number-asin.mjs \ + /private/tmp/ps-pkgs/purescript-numbers /tmp/psrs-number-asin-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-number-asin-oracle/Main.purs \ + --expected-exit 42 --timeout 240 --out /tmp/psrs-number-asin-runtime +env -u PSRS_STDLIB_ROOT ./target/debug/psrs build \ + /tmp/psrs-number-asin-oracle/Main.purs -o /tmp/psrs-number-asin-locked.wasm +wasmtime run /tmp/psrs-number-asin-locked.wasm +``` + +The actual pinned official JS asin produces 149 input observations and 298 +checks, exercising direct public calls and higher-order calls. Inputs include +both zero signs, the closed interval endpoints, values one ulp outside that +interval, subnormals, nonfinite values, and 128 deterministically generated +binary64 patterns. Reciprocal observations distinguish zero signs. NaN checks +do not require a payload. Both the development-package run and the +locked-package artifact +(sha256 c41a980e0e75412e84e217c37643145d71873e0164fd27a3cfbdb9a0305cb011) +return 42 with empty stdout and stderr. Wasmtime is 49.0.2. + +The same public asin 0.0 probe now passes against the locked package, in +7825 ms. A fresh Number.atan 0.0 probe stops at P8 library linking because +Data.Number.atan has no target implementation. The package fingerprint +changed, so the before and after diagnoses are not a same-fingerprint compare. + +Node tooling: 11 passed, none skipped. The numeric runtime rebuild is +byte-for-byte reproducible. + +## Rust validation + +Two driver regressions pass with mandatory Wasmtime: public behavior, +including zero signs, a higher-order call, and the linked `number_asin` +export, and rejection of invalid foreign contracts. The runtime unit test +checks the fdlibm boundary bit patterns. `cargo fmt --all --check` and +`cargo clippy --workspace --all-targets -- -D warnings` pass. + +`PSRS_REQUIRE_WASMTIME=1 CARGO_INCREMENTAL=0 cargo test --workspace --offline --no-fail-fast` +finishes all 52 targets: 1699 passed, 3 failed, 5 ignored. The only failing +target is `psrs-driver --lib`. Its three failures are the established +baseline: `constrained_dictionary_parameters_precede_ordinary_arguments`, +`runs_a_polymorphic_identity_with_a_number`, and +`compiles_if_expression_through_cfg_to_structured_wasm`. No new failure +appeared. Full workspace validation is not green because of those baseline +failures. This change does not establish complete Number FFI support or +whole-standard-library runtime behavior. diff --git a/docs/implementation/stdlib/number-atan-2026-10-08.md b/docs/implementation/stdlib/number-atan-2026-10-08.md new file mode 100644 index 00000000..c86d289e --- /dev/null +++ b/docs/implementation/stdlib/number-atan-2026-10-08.md @@ -0,0 +1,99 @@ +# Number inverse-tangent acceptance + +## Contract and implementation + +Starting compiler revision: 957b133 on stdlib/vendor-core-libraries, with a +clean worktree. The starting package was +30f22bdb3b207d908ed00ac779d7b0a0a32cb00d +(fnv1a64-v1:db4b18f8950a407f). A fresh Number.atan 0.0 diagnosis stopped at +P8 library linking because Data.Number.atan had no target implementation. + +Wasm has no inverse-tangent instruction. The compiler owns +numberAtan :: Number -> Number. Its HIR identity is appended as 72, preserving +existing intrinsic IDs. Core and CC require Number operand and result types. +MIR calls the scalar export `number_atan` in the shared numeric runtime. +Incorrect foreign binding schemes are rejected before ABI erasure. + +The export copies the fdlibm polynomial pinned by libm 0.2.15 and returns the +same bits, including subnormals. Every finite input matches the official +JavaScript Math.atan results used by purescript-numbers, and negative zero +stays negative zero. Positive infinity is pi/2 and negative infinity is +negative pi/2. NaN produces NaN. The operation does not trap. NaN payloads +are not part of the public contract. No constant folding is introduced. A +whole inverse-tangent algorithm does not become a compiler intrinsic. + +The published libm routine forces an f32 evaluation on subnormal inputs so a +host can raise the underflow flag. That evaluation writes below the stack +pointer without reserving a frame, which makes the linker's static stack +bound unknown. Wasm has no floating-point status flags. The runtime copy +returns the input unchanged when its magnitude is below 2^-27 and does not +touch the stack pointer. + +The numeric runtime retains its formatter, decimal conversion, inverse-cosine, +and inverse-sine exports and adds number_atan. Its reproducible artifact +SHA-256 is +41064c763782dc2a184cfc1a47e0816e8dc0b9f8ea465320cc7c294b60807afe. +The private table still has two funcref slots. The active initializer now +points at function 25. Static stack analysis accepts the artifact and still +measures 1680 bytes inside the existing 65536-byte reserve. + +The independent library changes only the foreign slot's explicit binding: +`foreign import "psrs:intrinsic#numberAtan" atan :: Number -> Number`. +Its signature, exports, and all official pure declarations remain unchanged. +The complete-module source verifier checks this transformation against pinned +purescript-numbers v9.0.1 (27d54effdd2c0e7a86fe356b1cd813dca5981c2d). + +## Package and runtime evidence + +Locked package: f2cdf0341ccb166ed827723ee305095780e3b2b6 +(fnv1a64-v1:369a7661425142dc). + +```sh +node ../psrs-stdlib/conformance/number-atan.mjs \ + /private/tmp/ps-pkgs/purescript-numbers /tmp/psrs-number-atan-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-number-atan-oracle/Main.purs \ + --expected-exit 42 --timeout 240 --out /tmp/psrs-number-atan-runtime +env -u PSRS_STDLIB_ROOT ./target/debug/psrs build \ + /tmp/psrs-number-atan-oracle/Main.purs -o /tmp/psrs-number-atan-locked.wasm +wasmtime run /tmp/psrs-number-atan-locked.wasm +``` + +The actual pinned official JS atan produces 149 input observations and 298 +checks, exercising direct public calls and higher-order calls. Inputs include +both zero signs, unit slopes, both infinities, subnormals, nonfinite values, +and 128 deterministically generated binary64 patterns. Reciprocal observations +distinguish zero signs. NaN checks do not require a payload. Both the +development-package run and the locked-package artifact +(sha256 00008e3259148e8c40d65d0198e06c8f333143f69102fc034f30e888bc462b0c) +return 42 with empty stdout and stderr. Wasmtime is 49.0.2. + +The same public atan 0.0 probe now passes against the locked package, in +7701 ms. A fresh Number.atan2 0.0 1.0 probe stops at P8 library linking +because Data.Number.atan2 has no target implementation (7360 ms). Both new +diagnoses use fnv1a64-v1:369a7661425142dc. The earlier atan failure used +fnv1a64-v1:db4b18f8950a407f, so the before and after diagnoses are not a +same-fingerprint compare. + +Node tooling: 11 passed, none skipped. The numeric runtime rebuild is +byte-for-byte reproducible. + +## Rust validation + +Two driver regressions pass with mandatory Wasmtime: public behavior, +including zero signs, both infinities, a higher-order call, and the linked +`number_atan` export, and rejection of invalid foreign contracts. The runtime +unit tests check the public boundary bit patterns and agreement with +libm::atan, including subnormals. `cargo fmt --all --check` and +`cargo clippy --workspace --all-targets -- -D warnings` pass. + +`PSRS_REQUIRE_WASMTIME=1 CARGO_INCREMENTAL=0 cargo test --workspace --offline --no-fail-fast` +finishes all 52 targets: 1703 passed, 3 failed, 5 ignored. The only failing +target is `psrs-driver --lib`. Its three failures are the established +baseline: `constrained_dictionary_parameters_precede_ordinary_arguments`, +`runs_a_polymorphic_identity_with_a_number`, and +`compiles_if_expression_through_cfg_to_structured_wasm`. No new failure +appeared. Full workspace validation is not green because of those baseline +failures. This change does not establish complete Number FFI support or +whole-standard-library runtime behavior. diff --git a/docs/implementation/stdlib/number-atan2-2026-10-08.md b/docs/implementation/stdlib/number-atan2-2026-10-08.md new file mode 100644 index 00000000..2abfc5bb --- /dev/null +++ b/docs/implementation/stdlib/number-atan2-2026-10-08.md @@ -0,0 +1,99 @@ +# Number four-quadrant inverse-tangent acceptance + +## Contract and implementation + +Starting compiler revision: e8b5b9a on stdlib/vendor-core-libraries, with a +clean worktree. The starting package was +f2cdf0341ccb166ed827723ee305095780e3b2b6 +(fnv1a64-v1:369a7661425142dc). The preceding inverse-tangent diagnosis recorded +that Number.atan2 0.0 1.0 stopped at P8 library linking because +Data.Number.atan2 had no target implementation. + +Wasm has no two-argument inverse-tangent instruction. The compiler owns +numberAtan2 :: Number -> Number -> Number. Its HIR identity is appended as 73, +preserving existing intrinsic IDs. Core and CC require Number operands and a +Number result. MIR calls the scalar export `number_atan2` in the shared +numeric runtime. Incorrect foreign binding schemes are rejected before ABI +erasure. + +The argument order is `y` then `x`, matching official Math.atan2. The export +uses the fdlibm exponent-gap cutoff of 60. A larger gap returns a signed +half-pi, and a negative `x` with a gap below -60 contributes zero before the +pi adjustment. Moderate ratios call the checked inverse-tangent primitive. +libm 0.2.15's own atan2 uses a wider gap. On 5721 pairs, that routine had 31 +finite mismatches against Math.atan2, all one ulp. The cutoff of 60 had 0 +finite mismatches and 0 NaN-payload mismatches on the same pairs. The pairs +were the edge product, an exponent sweep, and 256 random binary64 patterns. +Either NaN produces NaN. The operation does not trap. NaN payloads are not +part of the public contract. No constant folding is introduced. A whole +inverse-tangent algorithm does not become a compiler intrinsic. + +The numeric runtime retains its formatter, decimal conversion, inverse-cosine, +inverse-sine, and inverse-tangent exports and adds number_atan2. Its +reproducible artifact SHA-256 is +e295fd40bcb247ee77b8890107b2dea74838ded7b383895ce8db7c428d0e7ac2. +The private table still has two funcref slots. The active initializer now +points at function 27. Static stack analysis accepts the artifact and still +measures 1680 bytes inside the existing 65536-byte reserve. + +The independent library changes only the foreign slot's explicit binding: +`foreign import "psrs:intrinsic#numberAtan2" atan2 :: Number -> Number -> Number`. +Its signature, exports, and all official pure declarations remain unchanged. +The complete-module source verifier checks this transformation against pinned +purescript-numbers v9.0.1 (27d54effdd2c0e7a86fe356b1cd813dca5981c2d). + +## Package and runtime evidence + +Locked package: 767ffd7f5bbdbf1a2e1bc568b2dce88deeb5ce88 +(fnv1a64-v1:fe305ab57e585eca). + +```sh +node ../psrs-stdlib/conformance/number-atan2.mjs \ + /private/tmp/ps-pkgs/purescript-numbers /tmp/psrs-number-atan2-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-number-atan2-oracle/Main.purs \ + --expected-exit 42 --timeout 240 --out /tmp/psrs-number-atan2-runtime +env -u PSRS_STDLIB_ROOT ./target/debug/psrs build \ + /tmp/psrs-number-atan2-oracle/Main.purs -o /tmp/psrs-number-atan2-locked.wasm +wasmtime run /tmp/psrs-number-atan2-locked.wasm +``` + +The actual pinned official JS atan2 produces 289 input observations and 578 +checks, exercising direct public calls and higher-order calls. Inputs include +quadrant boundaries, both zero signs, both infinities, subnormals, large +ratios, nonfinite values, and 64 deterministically generated binary64 pairs. +Reciprocal observations distinguish zero signs. NaN checks do not require a +payload. Both the development-package run and the locked-package artifact +(sha256 59a9f6c3a30e84e307ef4d9b1ed970805c3026933ba7e87fde7cd9400e46883a) +return 42 with empty stdout and stderr. Wasmtime is 49.0.2. + +The same public atan2 0.0 1.0 probe now passes against the locked package, in +7758 ms. A fresh Number.cos 0.0 probe stops at P8 library linking because +Data.Number.cos has no target implementation (7575 ms). Both new diagnoses use +fnv1a64-v1:fe305ab57e585eca. The earlier atan2 failure used +fnv1a64-v1:369a7661425142dc, so the before and after diagnoses are not a +same-fingerprint compare. + +Node tooling: 11 passed, none skipped. The numeric runtime rebuild is +byte-for-byte reproducible. + +## Rust validation + +Two driver regressions pass with mandatory Wasmtime: public behavior, +including quadrant boundaries, zero signs, infinities, a large ratio, a +higher-order call, and the linked `number_atan2` export, and rejection of +invalid foreign contracts. The runtime unit tests check the public boundary +bit patterns, agreement with libm::atan2 on moderate ratios, and the large +ratio results that match Math.atan2. `cargo fmt --all --check` and +`cargo clippy --workspace --all-targets -- -D warnings` pass. + +`PSRS_REQUIRE_WASMTIME=1 CARGO_INCREMENTAL=0 cargo test --workspace --offline --no-fail-fast` +finishes all 52 targets: 1707 passed, 3 failed, 5 ignored. The only failing +target is `psrs-driver --lib`. Its three failures are the established +baseline: `constrained_dictionary_parameters_precede_ordinary_arguments`, +`runs_a_polymorphic_identity_with_a_number`, and +`compiles_if_expression_through_cfg_to_structured_wasm`. No new failure +appeared. Full workspace validation is not green because of those baseline +failures. This change does not establish complete Number FFI support or +whole-standard-library runtime behavior. diff --git a/docs/implementation/stdlib/number-parsing-2026-10-07.md b/docs/implementation/stdlib/number-parsing-2026-10-07.md new file mode 100644 index 00000000..7393ed88 --- /dev/null +++ b/docs/implementation/stdlib/number-parsing-2026-10-07.md @@ -0,0 +1,119 @@ +# Number parsing acceptance + +## Contract and ownership + +Starting compiler revision: a3ea5e8 on stdlib/vendor-core-libraries, with a +clean worktree. The starting library was 2ee2d1fdaff8a2841824cd1bb5d3fb864f594630 +(fnv1a64-v1:801317111aa80a8f). The unchanged public Number.fromString "42.5" +wrapper reached P8 library linking, where Data.Number.fromStringImpl had no +target implementation. The preceding +[Core repair](explicit-polymorphic-arguments-2026-10-07.md) owns its explicit +polymorphic instantiation; this change adds no inference or Coercible rule. + +The compiler owns numberFromDecimal :: String -> Number. It accepts a complete +ASCII decimal token, returns NaN for invalid tokens, and uses the pinned Rust +core binary64 converter for rounding, overflow and signed underflow. HIR/Core +check the String/Number contract; CC rejects incorrect operands and results +before erasure. MIR uses the existing canonical UTF-8 copy/release protocol to +call the raw runtime export with pointer and length. No pointer is retained. +Allocation failure remains a trap, including when the result is unused. + +The independent library owns PSRS.Number.Parse: ECMAScript whitespace, +longest-prefix recognition, incomplete-exponent rollback, Infinity/NaN, and +the supplied predicate and polymorphic result callbacks. These are ordinary +target-library functions. The official pure public wrapper and foreign slot's +signature are preserved. Explicit type applications and intermediate functions +with explicit signatures construct Fn4 while retaining both universal arguments. The reduced +wrapper executes in psrs and compiles with official purs; a specialized value +cannot satisfy a universal argument. + +The shared numeric runtime retains numberToString and adds numberFromDecimal. +Its reproducible artifact SHA-256 is +c6d50a6b005471bca9777562860cd8a3b2fc1ba5227126f755ee0d10297408de. +The linker checks the declared private table and active initializer exactly. +Static stack analysis follows exported/start entry points, rejects reachable +indirect calls, imported calls and recursion, and rejects exported tables. +Unreachable private formatting helpers do not establish execution paths. +The measured stack bound is 1680 bytes within the existing 65536-byte reserve. +Existing nonreentrant storage and allocator boundaries remain in force. + +## Package and behavior evidence + +Locked package: 4c4795a9c87496242398554b83f665062e3af2fd +(fnv1a64-v1:5e52e376007f1a23). Source adaptations, provenance, source verifier, +and Node oracle belong to psrs-stdlib; the compiler retains its lock, primitive, +linker contracts, runtime artifact, and Rust regressions. + +```sh +node ../psrs-stdlib/conformance/number-parsing.mjs \ + /private/tmp/ps-pkgs/purescript-numbers /tmp/psrs-parsing-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-parsing-oracle/Main.purs \ + --expected-exit 42 --timeout 240 --out /tmp/psrs-parsing-runtime +env -u PSRS_STDLIB_ROOT ./target/debug/psrs build \ + /tmp/psrs-parsing-oracle/Main.purs -o /tmp/psrs-parsing-locked.wasm +wasmtime /tmp/psrs-parsing-locked.wasm +``` + +The pinned official JS FFI supplies 304 checks: 244 public inputs and 60 +callback, uncurried and partial-application checks. Coverage includes every +ECMAScript whitespace code point, rejected lookalikes, incomplete exponents, +trailing text, nonfinite results, negative zero, subnormals, exact rounding +ties, long tokens, and 128 deterministic generated decimal cases. Custom +predicates and builders exercise accepted/rejected NaN and Infinity paths. +Both explicit-package and locked-package executions return 42 with empty +stdout/stderr. All 414 public Number/Int rounding checks also pass against the +final package. A fresh +locked-package diagnosis of the original public reproducer passes. Since the +package fingerprint changed, this is replay across a package update rather +than a same-fingerprint diagnose --compare measurement. + +The full source inventory covers 230 modules across 41 pinned packages: +170 identical, 36 modified, 24 target additions, no absent upstream modules, +and no direct recursive foreign placeholders. This inventory does not approve +all pre-existing adaptations or establish whole-library runtime behavior. +The Node tooling tests pass: 11 passed, zero skipped. The numeric runtime +rebuild is byte-for-byte reproducible. Raw reports, logs, oracle programs, and +execution artifacts remain under /private/tmp and are not committed. + +## Rust validation + +The four focused driver regressions pass with mandatory Wasmtime: official +public wrapper execution, decimal precision/range/zero signs, invalid foreign +schemes, and explicit universal newtype arguments. Native runtime tests cover +decimal grammar and rounding boundaries. CC malformed-operation checks and +linker tests cover element-contract mismatches and reachable stack paths. + +`cargo fmt --all --check`, `git diff --check`, and +`CARGO_INCREMENTAL=0 cargo clippy --workspace --all-targets -- -D warnings` +pass. All 230 vendored source hashes match the final package's inventory. + +`CARGO_INCREMENTAL=0 PSRS_REQUIRE_WASMTIME=1 cargo test --workspace --no-fail-fast` +completes all 52 target suites, including doc tests: 1683 passed, three existing +failures, and five ignored. The failures match the preceding Core checkpoint: + +- `dictionary_audit::execution::constrained_dictionary_parameters_precede_ordinary_arguments`: + the test cannot find its expected MIR constrained function. +- `tests::functions::runs_a_polymorphic_identity_with_a_number`: + its optimized WAT does not contain the asserted f64 token. +- `tests::integration::compiles_if_expression_through_cfg_to_structured_wasm`: + its optimized constant branch does not contain the asserted br_if token. + +The first full run additionally exposed a stale formatter-artifact identity +assertion. It now checks the numeric artifact identity and its pinned digest; +both its focused rerun and the complete workspace rerun pass that regression. +Full workspace validation is therefore not green because of the three baseline +failures, with no new failing test in the completed rerun. + +## Remaining work + +This establishes the parsing slice. It does not establish complete stdlib +compilation, source fidelity of every adaptation, or behavior of every API. +A fresh public Number.abs (-42.5) case stops at P8 library linking because +Data.Number.abs has no target implementation. Other Number foreign slots +remain separate work. No official scoreboard or gate measurement changes. + +The subsequent [absolute-value acceptance](number-abs-2026-10-07.md) resolves +the abs blocker with a checked scalar operation and public JS comparisons. +The measurements above describe the preceding parsing checkpoint. diff --git a/docs/implementation/stdlib/number-rounding-2026-10-07.md b/docs/implementation/stdlib/number-rounding-2026-10-07.md new file mode 100644 index 00000000..dd58b83c --- /dev/null +++ b/docs/implementation/stdlib/number-rounding-2026-10-07.md @@ -0,0 +1,110 @@ +# Number and Int rounding acceptance + +## Scope and ownership + +Starting compiler revision: 820de13 on stdlib/vendor-core-libraries, with a +clean worktree. The starting package was 0e3273498523ab81742ad0751efce5fde55e9794 +(fnv1a64-v1:9ff76cd44757e7e6). A fresh diagnosis of Int.floor 42.9 failed at +P8 library linking: Data.Number.floor had no target implementation. + +The compiler now owns the generic checked numberFloor/numberCeil primitives, +not library algorithms or Int range policy. Their stable identities are +appended to the HIR registry, preserving existing IDs. Core, CC and MIR check +Number/F64 operands and results; Wasm encodes f64.floor/f64.ceil. Existing +malformed unary-operation tests cover both additions. Driver source tests +execute nonfinite values, fractions, signed zero and Number range above i32, +and reject incorrect foreign binding schemes. Backend tests execute both MIR +optimization paths. No new constant folding is claimed. + +The independent library owns the actual ECMAScript round implementation in +PSRS.Number: preserve zero signs in [-0.5, 0], otherwise compare the fractional +part with 0.5 before adding one to the floor. This avoids both Wasm nearest's +ties-to-even and the precision loss from flooring value + 0.5. The +[ECMAScript contract](https://tc39.es/ecma262/2023/multipage/numbers-and-dates.html#sec-math.round) +is checked against actual pinned official JS results. Full-module source +transformations preserve all official pure declarations and signatures. +Data.Int's floor/ceil/round/unsafeClamp wrappers are unchanged. + +## Package and behavior evidence + +Locked package: 2ee2d1fdaff8a2841824cd1bb5d3fb864f594630 +(fnv1a64-v1:801317111aa80a8f). The package owns source adaptations, provenance, +the Node oracle and source-verifier test; the compiler retains its lock and +Rust regression tests. + +```sh +node ../psrs-stdlib/conformance/number-rounding.mjs \ + /private/tmp/ps-pkgs/purescript-numbers \ + /private/tmp/ps-pkgs/purescript-integers /tmp/psrs-rounding-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-rounding-oracle/Main.purs \ + --expected-exit 42 --timeout 240 --out /tmp/psrs-rounding-runtime +# Also build directly with PSRS_STDLIB_ROOT unset, then execute the artifact. +env -u PSRS_STDLIB_ROOT ./target/debug/psrs build \ + /tmp/psrs-rounding-oracle/Main.purs -o /tmp/psrs-rounding-locked.wasm +wasmtime /tmp/psrs-rounding-locked.wasm +``` + +- 69 inputs, 207 rounded observations and 414 public Number/Int checks pass: + both signs of zero, subnormals, nonfinite values, half-integer ties and their + adjacent values, Int boundaries/overflow, values around 2^52 and maximal + finite Number values. Development-package and locked-package executions + return 42 with empty stdout/stderr. +- The preceding 147 public isFinite/trunc checks still pass with exit 42 and + empty stdout/stderr. Node tooling: 11 passed, zero skipped. +- Complete source audit: 229 modules across 41 packages, 170 identical, + 36 modified and 23 target additions, no absent upstream modules and no + direct recursive foreign placeholders. All 229 stored vendored hashes match + their actual files, including one pre-existing stale ST.Partial hash corrected + without changing its source. +- Fresh locked diagnosis of the original Int.floor case passes. Its library + fingerprint differs from the baseline, so this is a replay across a package + update, not a same-fingerprint diagnose --compare result. + +Raw JSON reports and Wasm artifacts remain local under /private/tmp. They are +not committed. See the library's docs/number-rounding.md for source ownership +and its conformance command contract. + +## Rust validation + +cargo fmt --all --check and cargo clippy --workspace --all-targets -- -D warnings +pass. The two new driver source/foreign-contract tests pass under mandatory +Wasmtime. The two backend optimized/unoptimized rounding tests also pass, +including negative-zero floor operands. + +PSRS_REQUIRE_WASMTIME=1 cargo test --workspace initially encountered storage +exhaustion while writing test components. Old regenerable incremental caches +were removed, then the entire workspace was rerun with --no-fail-fast. Completed +unit and integration tests record 1670 passed, 3 established failures and +5 ignored. Two doc targets encountered missing dependency artifacts; after +cargo clean -p psrs-runtime, cargo test --workspace --doc passes serially. +All 52 target suites are accounted for after that recovery. + +The three established driver failures match the preceding checkpoint: +dictionary_audit::execution::constrained_dictionary_parameters_precede_ordinary_arguments +(MIR constrained function), tests::functions::runs_a_polymorphic_identity_with_a_number +(the optimized WAT no longer contains an unused f64), and +tests::integration::compiles_if_expression_through_cfg_to_structured_wasm +(the optimized constant branch no longer contains br_if). Full workspace +validation is therefore not green. No official scoreboard measurement was run. + +The subsequent metadata-only ST.Partial hash refresh preserves all Purs source; +the final package is rechecked through the trusted-loader test and both locked +public API programs. Raw environment-failed logs remain local for diagnosis. + +## Remaining work + +This is rounding acceptance, not whole-standard-library acceptance. A fresh +public Number.fromString "42.5" reproducer currently stops at P7 Core +verification at the rounding checkpoint. The subsequent +[explicit-polymorphic-argument repair](explicit-polymorphic-arguments-2026-10-07.md) +resolves that mismatch with the official wrapper unchanged. The same locked +reproducer now stops at P8 library linking because `Data.Number.fromStringImpl` +has no target implementation. Parsing behavior and other Number FFI remain +unverified. No scoreboard or gate measurement is changed by this focused +behavior evidence. + +The subsequent [Number parsing acceptance](number-parsing-2026-10-07.md) +implements and executes the parser. The blockers above describe the rounding +and Core checkpoints, before that package update. diff --git a/docs/implementation/stdlib/number-sqrt-2026-10-07.md b/docs/implementation/stdlib/number-sqrt-2026-10-07.md new file mode 100644 index 00000000..658f4bd1 --- /dev/null +++ b/docs/implementation/stdlib/number-sqrt-2026-10-07.md @@ -0,0 +1,83 @@ +# Number square-root acceptance + +## Contract and implementation + +Starting compiler revision: 0aab025 on stdlib/vendor-core-libraries, with a +clean worktree. The starting package was +7cace2a6b4808b36b01559e88d3ebc351a3b7426 +(fnv1a64-v1:1fd8c04ec63c6ff0). A fresh Number.sqrt 4.0 diagnosis stopped at +P8 library linking because Data.Number.sqrt had no target implementation. + +The compiler now owns numberSqrt :: Number -> Number. Its HIR identity is +appended as 69, preserving existing intrinsic IDs. Core and CC require Number +operand and result types, MIR requires F64, and Wasm emits f64.sqrt. Incorrect +foreign binding schemes are rejected before ABI erasure. The operation does not +trap: a negative finite value or negative infinity becomes NaN, NaN stays NaN, +positive infinity stays positive infinity, and negative zero stays negative +zero. No additional runtime artifact, host binding, library-name recognition, +or constant folding is introduced. + +The independent library changes only the foreign slot's explicit binding: +`foreign import "psrs:intrinsic#numberSqrt" sqrt :: Number -> Number`. +Its signature, exports, and all official pure declarations remain unchanged. +The complete-module source verifier checks this transformation against pinned +purescript-numbers v9.0.1 (27d54effdd2c0e7a86fe356b1cd813dca5981c2d). + +## Package and runtime evidence + +Locked package: ef004e75998995ea8ac00840e36c1d9b71226b25 +(fnv1a64-v1:19dabd2d6d4033b4). + +```sh +node ../psrs-stdlib/conformance/number-sqrt.mjs \ + /private/tmp/ps-pkgs/purescript-numbers /tmp/psrs-number-sqrt-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-number-sqrt-oracle/Main.purs \ + --expected-exit 42 --timeout 240 --out /tmp/psrs-number-sqrt-runtime +env -u PSRS_STDLIB_ROOT ./target/debug/psrs build \ + /tmp/psrs-number-sqrt-oracle/Main.purs -o /tmp/psrs-number-sqrt-locked.wasm +wasmtime run /tmp/psrs-number-sqrt-locked.wasm +``` + +The actual pinned official JS sqrt produces 155 input observations and 310 +checks, exercising direct public calls and higher-order calls. Inputs include +both zero signs, subnormals, minimum normal values, exact squares, magnitudes +beyond i32, maximal finite values, infinities, NaN, and 128 deterministically +generated binary64 patterns. Reciprocal observations distinguish zero signs. +Both the development-package run and the locked-package artifact +(sha256 1098509065918c064f27cbe5a6d75508a08b7de5d304a565c94796d680f73a0e) +return 42 with empty stdout and stderr. Wasmtime is 49.0.2. + +The same public sqrt 4.0 probe now passes against the locked package. A fresh +Number.acos 1.0 probe stops at P8 library linking because Data.Number.acos has +no target implementation. The package fingerprint changed, so the before and +after diagnoses are not a same-fingerprint compare. + +The complete source inventory records 230 modules across 41 packages: +170 identical, 36 modified and 24 target additions, no absent upstream modules +and no direct recursive foreign placeholders. This inventory does not approve +all existing adaptations. Node tooling: 11 passed, none skipped. + +## Rust validation + +Backend execution of f64.sqrt passes with optimization enabled and disabled, +checking that the square root of negative zero stays negative and that the +square root of 2^32 is 65536. Two driver regressions pass with mandatory +Wasmtime: public behavior, including a higher-order call, and rejection of +invalid foreign contracts. `cargo fmt --all --check` and +`cargo clippy --workspace --all-targets -- -D warnings` pass. + +`PSRS_REQUIRE_WASMTIME=1 cargo test --workspace --no-fail-fast` finishes all +52 targets: 1693 passed, 3 failed, 5 ignored. The only failing target is +`psrs-driver --lib`. Its three failures are the established baseline: +`constrained_dictionary_parameters_precede_ordinary_arguments`, +`runs_a_polymorphic_identity_with_a_number`, and +`compiles_if_expression_through_cfg_to_structured_wasm`. No new failure +appeared. Full workspace validation is not green because of those baseline +failures. + +## Remaining work + +This is square-root acceptance, not complete Number FFI or whole-standard-library +acceptance. No official scoreboard or gate measurement changes. diff --git a/docs/implementation/stdlib/number-trunc-2026-10-07.md b/docs/implementation/stdlib/number-trunc-2026-10-07.md new file mode 100644 index 00000000..6aa34b6a --- /dev/null +++ b/docs/implementation/stdlib/number-trunc-2026-10-07.md @@ -0,0 +1,73 @@ +# Number classification and truncation checkpoint + +Starting compiler: c13a078, with a clean worktree. The library advances from +3e3736d to 0e3273498523ab81742ad0751efce5fde55e9794, fingerprint fnv1a64-v1:9ff76cd44757e7e6. + +The compiler adds the generic unary numberTrunc :: Number -> Number primitive, +with authoritative HIR registry metadata and Core, CC and MIR type verification. +It lowers to f64.trunc, preserving Number range and signed zeros; NaN remains +NaN without a payload guarantee. New runtime evidence observes fractions, +values beyond i32 range, zero signs, NaN and infinities. Incorrect foreign +operand/result schemes and malformed CC/MIR operands are rejected. + +Data.Number.trunc binds that primitive and isFinite delegates to the library's +IEEE-754 self-subtraction/equality check. The full official purescript-numbers +v9.0.1 source at 27d54effdd2c0e7a86fe356b1cd813dca5981c2d is verified, +allowing exactly these two foreign adaptations and their target import. +Data.Int and its public trunc/unsafeClamp wrappers remain unchanged. The +library owns finite classification and integer clamping, while the compiler +owns the scalar primitive and target encoding. + +## Evidence + +The pinned official FFI engine generates 147 public Number.isFinite, +Number.trunc and Int.trunc checks across 49 inputs. Number expectations execute +the actual official JS FFI; Int clamping expectations compose the unchanged +pinned PureScript rules. Programs built through development and locked roots both execute with exit +42 and empty stdout/stderr. The locked build removes PSRS_STDLIB_ROOT and +records compiler, source and Wasm digests locally. The original Int.trunc 42.9 +probe changes from P8 missing isFinite and trunc bindings to Passed. Differing +library fingerprints preclude a compatible diagnose --compare claim. + +Node tooling: 10 passed, zero skips. Source audit: 229 modules in 41 packages; +170 identical, 36 modified, 23 target additions, zero missing upstream modules +and zero direct recursive foreign placeholders. The development trusted-order +loader passes; Prelude remains first. Two focused mandatory-Wasmtime compiler +tests pass. Strict workspace clippy and formatting pass. + +Combined validation: 1667 passed, 3 established baseline failures, 5 ignored. +The workspace commands cover 1666 passing tests; the additional post-audit +backend optimization test passes separately. Repeated target results are not +counted twice. Driver lib reports 689 passed and the same three pre-existing +artifact assertions: constrained_dictionary_parameters_precede_ordinary_arguments, +runs_a_polymorphic_identity_with_a_number, and +compiles_if_expression_through_cfg_to_structured_wasm. Failure names and messages +match the preceding deep-expression checkpoint. These remain failures; this is +not a green workspace result. + +The default workspace command stops at driver lib, so the remaining targets +were explicitly executed: + +```sh +PSRS_REQUIRE_WASMTIME=1 cargo test --workspace +cargo test --workspace --exclude psrs-driver +PSRS_REQUIRE_WASMTIME=1 cargo test -p psrs-driver --test '*' +cargo test -p psrs-driver --doc +PSRS_REQUIRE_WASMTIME=1 cargo test -p psrs-backend optimized_and_unoptimized_number_trunc +cargo clippy --workspace --all-targets -- -D warnings +cargo fmt --all --check +``` + +The backend optimization fixture executes both pipelines with exact exit 42, +observing negative zero through its reciprocal and preserving Number range +beyond i32. The scalar acceptance checklist records the SP-03/SP-07/SP-11/SP-12 +mapping and evidence. All remaining crate, integration and doc tests pass; +the locked trusted-order loader also passes. Wasmtime version: 49.0.2. + +Raw observations and reports remain untracked local artifacts. Reproduce with +[the conformance workflow](../../workflow/stdlib-conformance.md). This checkpoint +does not measure the full import cohort or an official-suite scoreboard and +does not establish all Number or standard-library APIs. + +A fresh public `Int.floor 42.9` probe still reaches explicit P8 missing +Data.Number.floor support; other rounding operations remain a continuation. diff --git a/docs/implementation/stdlib/package-split-2026-10-06/report.md b/docs/implementation/stdlib/package-split-2026-10-06/report.md new file mode 100644 index 00000000..8beb6f40 --- /dev/null +++ b/docs/implementation/stdlib/package-split-2026-10-06/report.md @@ -0,0 +1,45 @@ +# Independent stdlib package checkpoint + +The library subtree was extracted into the local `psrs-stdlib` repository with +its Git history. All 216 library files (215 `.purs` modules and `lib/trusted`) +matched the compiler copy byte for byte before that copy was removed. Package +metadata, upstream pins, conformance cases, and repository guidance were then +committed as `a08b4c8`. The compiler's `stdlib.lock.json` selects that package +revision and content fingerprint. No remote repository, push, or PR was created. + +The standalone component in `tools/stdlib-conformance` accepts package and +executable paths. Source audit and scalar cases ran against the external package. +The audit covered all 41 pinned packages: 195 identical modules, 11 modified +modules, nine platform additions, no omitted upstream modules, and no detected +direct same-argument self-recursions. These counts describe differences; they do +not approve every adaptation or prove full FFI implementation. + +Validation: + +- Package identity/protocol/dirty-content tests: three passed. +- Trusted on-disk loader: one passed after removing the compiler source copy. +- Primitive foreign tests with mandatory Wasmtime: seven passed. +- Let-constraint regressions: 14 passed. +- Official `purs` Symbol differential: one passed, using the unchanged official + `.purs` and its pinned upstream JavaScript companion (fixture trailing blank line + omitted). +- Independent official scalar oracle: 20 bindings, 46 cases; actual Wasmtime + exit 42, empty stdout and stderr. `scalar-run.json` records binaries, input + hashes, commands, package identity, Wasm hash, and observations. Signed i32 + normalization remains an explicit representation difference in the oracle. +- Invalid package overrides and incorrect exit-code goldens failed explicitly. +- CLI build, formatting, and workspace clippy with warnings denied passed. +- Python syntax compilation and Node syntax checking passed. The component + also ran successfully after copying it outside the compiler checkout and + against a Git-free archive of the locked library package. + +Full compile acceptance remains failed: `/tmp/psrs-stdlib-all.purs` reported +233 P8 library-linking diagnostics; the first was the missing target +implementation of `Control.Apply.arrayApply`. `summary.json` records this +measurement. No full workspace tests or full scoreboard were run. No roadmap +runtime figures were changed. + +Independent source acquisition for fresh checkouts/CI and remote publication +remain pending. The package's development status is incomplete. Full stdlib +compilation and value-sensitive runtime/FFI correctness remain separate open +obligations; the scalar subset is not a whole-library acceptance claim. diff --git a/docs/implementation/stdlib/primitive-bindings-2026-10-06/bindings.json b/docs/implementation/stdlib/primitive-bindings-2026-10-06/bindings.json new file mode 100644 index 00000000..269bdf17 --- /dev/null +++ b/docs/implementation/stdlib/primitive-bindings-2026-10-06/bindings.json @@ -0,0 +1,145 @@ +{ + "schema_version": 1, + "bindings": [ + { + "module": "Data/Eq.purs", + "name": "eqBooleanImpl", + "operation": "booleanEq", + "signature": "Boolean -> Boolean -> Boolean", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Eq.purs#L77" + }, + { + "module": "Data/Eq.purs", + "name": "eqIntImpl", + "operation": "intEq", + "signature": "Int -> Int -> Boolean", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Eq.purs#L78" + }, + { + "module": "Data/Eq.purs", + "name": "eqNumberImpl", + "operation": "numberEq", + "signature": "Number -> Number -> Boolean", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Eq.purs#L79" + }, + { + "module": "Data/Eq.purs", + "name": "eqCharImpl", + "operation": "charEq", + "signature": "Char -> Char -> Boolean", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Eq.purs#L80" + }, + { + "module": "Data/HeytingAlgebra.purs", + "name": "boolConj", + "operation": "booleanAnd", + "signature": "Boolean -> Boolean -> Boolean", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/HeytingAlgebra.purs#L103" + }, + { + "module": "Data/HeytingAlgebra.purs", + "name": "boolDisj", + "operation": "booleanOr", + "signature": "Boolean -> Boolean -> Boolean", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/HeytingAlgebra.purs#L104" + }, + { + "module": "Data/HeytingAlgebra.purs", + "name": "boolNot", + "operation": "booleanNot", + "signature": "Boolean -> Boolean", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/HeytingAlgebra.purs#L105" + }, + { + "module": "Data/Int/Bits.purs", + "name": "and", + "operation": "intAnd", + "signature": "Int -> Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int/Bits.purs#L13" + }, + { + "module": "Data/Int/Bits.purs", + "name": "or", + "operation": "intOr", + "signature": "Int -> Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int/Bits.purs#L18" + }, + { + "module": "Data/Int/Bits.purs", + "name": "xor", + "operation": "intXor", + "signature": "Int -> Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int/Bits.purs#L23" + }, + { + "module": "Data/Int/Bits.purs", + "name": "shl", + "operation": "intShl", + "signature": "Int -> Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int/Bits.purs#L28" + }, + { + "module": "Data/Int/Bits.purs", + "name": "shr", + "operation": "intShr", + "signature": "Int -> Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int/Bits.purs#L31" + }, + { + "module": "Data/Int/Bits.purs", + "name": "zshr", + "operation": "intZshr", + "signature": "Int -> Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int/Bits.purs#L34" + }, + { + "module": "Data/Int/Bits.purs", + "name": "complement", + "operation": "intComplement", + "signature": "Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int/Bits.purs#L37" + }, + { + "module": "Data/Int.purs", + "name": "toNumber", + "operation": "intToNumber", + "signature": "Int -> Number", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int.purs#L81" + }, + { + "module": "Data/Ring.purs", + "name": "intSub", + "operation": "intSub", + "signature": "Int -> Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Ring.purs#L54" + }, + { + "module": "Data/Ring.purs", + "name": "numSub", + "operation": "numberSub", + "signature": "Number -> Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Ring.purs#L55" + }, + { + "module": "Data/Semiring.purs", + "name": "intAdd", + "operation": "intAdd", + "signature": "Int -> Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Semiring.purs#L89" + }, + { + "module": "Data/Semiring.purs", + "name": "numAdd", + "operation": "numberAdd", + "signature": "Number -> Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Semiring.purs#L91" + }, + { + "module": "Data/Semiring.purs", + "name": "numMul", + "operation": "numberMul", + "signature": "Number -> Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Semiring.purs#L92" + } + ] +} diff --git a/docs/implementation/stdlib/primitive-bindings-2026-10-06/report.md b/docs/implementation/stdlib/primitive-bindings-2026-10-06/report.md new file mode 100644 index 00000000..6309acf2 --- /dev/null +++ b/docs/implementation/stdlib/primitive-bindings-2026-10-06/report.md @@ -0,0 +1,110 @@ +# Checked scalar foreign bindings + +This iteration follows the [source restoration checkpoint](../vendor-restoration-2026-10-06/report.md) +at `f0b1b71`. It implements 20 previously missing foreign values through explicit +`psrs:intrinsic` bindings. All official public signatures and ordinary source +bodies remain unchanged; only the target binding string is inserted into those +foreign declarations. The [binding manifest](bindings.json) records each +operation, exact signature, and pinned upstream declaration. + +## Implementation contract + +The compiler resolves the explicit operation through the existing authoritative +intrinsic registry. It retains the declaring symbol and checked source type. +P8 constructs typed Core functions over those parameters, validates them with +the common Core intrinsic rules, and transfers the declaration identity only +after validation. Ordinary closure conversion handles first-class functions, +partial applications, and imported fixity aliases. No source function name or +module name selects an implementation. + +The input Core is verified before traversing the signature; cyclic or missing +type evidence is invalid IR. Linking uses a complete candidate and does not +publish a previously generated binding if another binding fails. A declaration +with the wrong argument or result type fails even when unused. Non-scalar +categories remain explicit unsupported bindings; they are not approximated. + +## Source and target evidence + +| Official module | Implemented foreign values | +| --- | --- | +| `Data.Int` | toNumber | +| `Data.Int.Bits` | and, or, xor, shl, shr, zshr, complement | +| `Data.Eq` | eqBooleanImpl, eqIntImpl, eqNumberImpl, eqCharImpl | +| `Data.Ring` | intSub, numSub | +| `Data.Semiring` | intAdd, numAdd, numMul | +| `Data.HeytingAlgebra` | boolConj, boolDisj, boolNot | + +Wasm uses signed i32 for Int and IEEE-754 f64 for Number, as specified by +[scalars and primitives](../../../design/backend/fp/scalars-and-primitives.md). +DEC-16 supplies the Char scalar-value contract. Numeric conversion of every +i32 to f64 is exact; it replaces the missing foreign implementation without +inventing a source equation. + +The oracle generator executes the original JavaScript from the exact clean +package revisions pinned in the restoration inventory. It records JavaScript +file hashes and both official and target observations. The committed +[fixtures](../../../../crates/psrs-driver/tests/fixtures/stdlib-scalar/observations.json) +cover 20 bindings in 46 cases, including integer wrapping, shift-count masking, +negative values, NaN, infinities, signed zero, and non-ASCII/supplementary Char. +Node v26.10.0 produced these observations; Wasmtime 49.0.2 executed the target. + +Two cases deliberately differ in representation: official JS evaluates +`zshr(-1, 0)` and `zshr(-1, 32)` to `4294967295`; this target returns `-1`, the +same 32-bit bit pattern interpreted as the specified signed Int. Both raw and +normalized results remain visible. This is a Wasm Int representation difference, +not an assertion of identical JavaScript number behavior. The source signature +and shift-count masking are preserved. + +The large-product behavior of official `intMul`, division/remainder edge cases, +string equality, callback-based array equality, and other foreign values were +not silently mapped to superficially similar primitives. They remain explicit +gaps pending their implementation and behavior review. + +## Validation and remaining work + +Mandatory Wasmtime driver tests cover the 46-case official observation fixture, +the complete actual vendored Bits module, first-class and partially applied +functions, imported operator identity, unused wrong signatures, unknown +operations, and unimplemented operation categories. The fixture test also checks +that every tested declaration is still the actual vendored declaration. + +Seven primitive-foreign driver tests passed with `PSRS_REQUIRE_WASMTIME=1`. +Fourteen let-constraint tests and six ordinary-library-foreign tests passed. +Backend tests cover transactional failure, checked input cycles, identity +transfer, and verification of the generated function; all three passed. Format +checking and strict workspace clippy with all targets also passed. The full +workspace test suite and full scoreboards were not run. + +A 46-level right-nested fixture originally overflowed the Rust test thread's +stack. The generator now combines the same independent conditions as a +balanced tree; no case was removed. Arbitrarily deep expression robustness was +not established by this iteration. + +After rebuilding the CLI, `/tmp/psrs-stdlib-all.purs` still fails at P8 library +linking. The missing-implementation diagnostics decreased from 253 to 233, with +`Control.Apply.arrayApply` still first. The report is +`/tmp/psrs-stdlib-primitive2.json`; the observed run took 43,198 ms. Twenty of the +275 inventory foreign values now have target implementations, leaving 255 in +the full inventory, including modules outside this reproducer's closure. +Full stdlib compilation and full runtime acceptance remain incomplete. No +scoreboard, README, or D-04 measurement was changed. + +The fixtures isolate these foreign declarations from other missing imports; +they do not establish runtime behavior of the complete Eq/Semiring/Prelude +closures. The Bits runtime test imports the actual complete module. A future +standalone conformance component must keep this distinction visible when +moving the oracle generator and fixture runner out of compiler tests. + +## Reproduce + +```sh +node docs/workflow/tools/stdlib-scalar-oracle.mjs \ + /tmp/purescript-prelude /tmp/ps-pkgs/purescript-integers \ + crates/psrs-driver/tests/fixtures/stdlib-scalar +PSRS_REQUIRE_WASMTIME=1 \ + cargo test -p psrs-driver --lib primitive_foreign --offline +cargo test -p psrs-backend --lib bindings::primitives --offline +``` + +The source-fidelity policy still applies. This manifest covers target bindings; +it does not authorize edits to ordinary PureScript bodies or signatures. diff --git a/docs/implementation/stdlib/source-array-kernels-2026-10-06/report.md b/docs/implementation/stdlib/source-array-kernels-2026-10-06/report.md new file mode 100644 index 00000000..ba273041 --- /dev/null +++ b/docs/implementation/stdlib/source-array-kernels-2026-10-06/report.md @@ -0,0 +1,69 @@ +# Library array algorithms and checked storage checkpoint + +The compiler no longer owns whole-function array application, binding or +extension kernels. The independent `psrs-stdlib` revision is +`e8b21f78b31d4317f330c9886571d94ebe96e24d`. It owns ordinary PureScript +`PSRS.Array` implementations and explicit target provenance. Official +`Control.Apply`, `Control.Bind` and `Control.Extend` only add an import and +replace their foreign slots with target aliases; their public signatures and +other pure code remain. + +The compiler adds initialized `arrayFill` and unsafe in-place `arrayWrite`. +Core validates element identities, CC carries checked representations and values, +MIR validates length/initializer/storage/destination and lowers initialized +allocation directly to Wasm GC `array.new`. Writes use existing array stores. +No callback or traversal algorithm is hidden inside these primitives. The old +`ArrayApply` registry/CC/MIR operation and callback invoker were removed. + +A general primitive linking defect was exposed by mutation: a polymorphic +foreign wrapper adapted its concrete input by copying the array, so a write +changed the copy. Mandatory Core occurrence elaboration now expands every +validated primitive global using its checked occurrence type before ABI erasure, +including first-class and partial uses. The common collision-free Core local +allocator is shared with optimization. Complete candidate verification and +rollback remain required; unused incorrect primitive signatures still fail. + +Validation: + +- Mandatory Wasmtime driver primitive/library regressions: 17 passed, including + the new `arrayExtend` oracle and the earlier apply, bind and primitive checks. + They include first-class/partial bindings, array aliases and unused writes, + initialized references/records/closures, invalid lengths/indices and element + relationships, captures, returned functions, empty arrays and product overflow. +- Official JS observations: 8 apply cases/29 checks, 9 bind cases/37 checks and + 9 extend cases/41 checks, all exit 42 with empty output. Bind includes callback + count and immediate snapshots of a shared mutable array; extend includes suffix + content and order, callback count and order, empty input, returned closures and + that mutating a received suffix does not alias the source array. The 46-case + scalar oracle also passed. The independent library owns the generators and + writes the observations and runtime reports to a local, git-ignored + directory. +- Malformed MIR filled-array test: passed; wrong initializer or noninteger + length is rejected, and a valid initialized allocation is accepted. +- Transactional primitive linking tests: 3 passed. +- Core optimization regressions after sharing local allocation: 24 passed. +- Intrinsic descriptor arity test: passed. +- Locked trusted-library loader regression: passed; Prelude remains first. +- Let-constraint regressions: 14 passed. +- Library-owned Node tooling regressions: 8 passed. +- CLI rebuild, formatting, and workspace clippy with warnings denied passed in the + previous checkpoint; the extend change adds library source, a fixture and a + focused regression only. +- Complete pinned audit: 41 packages, 216 modules, 192 exact, 14 modified and + 10 explicitly recorded target additions; no absent official module and no + detected exact direct same-argument recursion. The audit is comparison + evidence, not blanket approval of every adaptation. + +The unchanged full reproducer still fails. Unsupported-library P8 reports fell +from 231 to 230 and the first is now `Control.Monad.ST.Internal.map_`. The +snapshot `diagnose.json` records the development run and complete diagnostics. +Package content changed from `fnv1a64-v1:9c725bfe5140502e` to +`fnv1a64-v1:e4195415d1de337c`, so these are explicit migration measurements, +not an unchanged-cohort `diagnose --compare` result or a suite scoreboard update. + +Remaining work includes full stdlib compile/runtime/FFI acceptance, unsupported +bindings and layouts, general polymorphic array identity/mutation interactions, +and independent CI package acquisition. Physical array mapping can still copy +across ordinary polymorphic function boundaries; these private builder tests +prove no broader alias guarantee. No full workspace tests or full scoreboard +were run. No push, PR or issue operation was performed. diff --git a/docs/implementation/stdlib/st-2026-10-06/report.md b/docs/implementation/stdlib/st-2026-10-06/report.md new file mode 100644 index 00000000..9c131ee0 --- /dev/null +++ b/docs/implementation/stdlib/st-2026-10-06/report.md @@ -0,0 +1,57 @@ +# ST computations and references checkpoint + +The compiler gains no `ST` intrinsic. `Control.Monad.ST.Internal` and +`Control.Monad.ST.Uncurried` keep their official signatures, exports, classes, +instances and other pure code; their foreign implementation slots delegate to +the ordinary PureScript target module `PSRS.ST`. The independent `psrs-stdlib` +revision is `567c00dd9d4c7dc53814f12aaf2aaf6e5b4f34d9` with content fingerprint +`fnv1a64-v1:5b51b652c77ed0b7`. + +`PSRS.ST` represents an `ST` action as `Action r a`, a suspended `Unit -> a` +thunk, and an `ST` reference as `Cell r a`, a fresh one-element mutable array +over the existing private `arrayFill`, `arrayWrite` and `arrayIndex` primitives. +`Control.Monad.ST.Internal` declares `ST`/`STRef` as newtypes over those and +keeps every class instance; the instances cannot move to `PSRS.ST` because this +compiler does not resolve an instance whose type comes from another module +(orphan). Each operation is therefore a thin adapter, and the exercised +algorithm lives in the Prelude-free target module. `STFn{N}` stays abstract as a +newtype over the curried action so `Data.Array.ST`'s rank-2 uses typecheck. + +Validation: + +- Mandatory Wasmtime driver regressions: 18 passed, including the new + `library_st_matches_pinned_official_observations` and the earlier apply, bind, + extend and primitive checks. +- Official JS observations: the pinned `Control/Monad/ST/Internal.js` generates + ten ST cases with ten observable checks covering `pure`/`map`/`bind`, fresh + distinct cells, read, write, modify, `while`, `for`, `foreach`, and empty + inputs that must not invoke their callbacks. The mandatory runner executes the + Prelude-free target fixture under Wasmtime with exit 42 and empty output. + Observations and the runtime report are written to a local, git-ignored + directory. +- Library-owned Node tooling regressions: 8 passed. +- Complete pinned audit: 41 packages, 217 modules, 190 exact, 16 modified and + 11 explicitly recorded target additions; no absent official module and no + detected exact direct same-argument recursion. The audit is comparison + evidence, not blanket approval of every adaptation. +- Formatting and strict workspace clippy with warnings denied passed. + +The full reproducer still fails. Unsupported-library P8 reports fell from 230 to +199 and the first is now `Data.Array.fromFoldableImpl`; no +`Control.Monad.ST.Internal` or `Control.Monad.ST.Uncurried` foreign value +remains. The snapshot `diagnose.json` records the development run and complete +diagnostics. Package content changed from `fnv1a64-v1:e4195415d1de337c` to +`fnv1a64-v1:5b51b652c77ed0b7`, so these are explicit migration measurements, not +an unchanged-cohort `diagnose --compare` result or a suite scoreboard update. + +Two limits are recorded rather than hidden. First, the target's `ST r a` is a +thunk over `Unit` while the compiler's `Effect a` is a closure over a scalar +state token, so `Control.Monad.ST.Global.toEffect`'s `unsafeCoerce` is linked +but not sound on this target and is not exercised. Second, physical array +copying across general polymorphic function boundaries remains a compiler +obligation; the cell newtypes are parameterized so the exercised paths preserve +identity, which does not prove a broader alias guarantee. Runtime execution of +the official wrapper modules is not separately observable until the rest of the +Prelude closure links; the tested target module has no Prelude dependency. No +full workspace tests or full scoreboard were run. No push, PR or issue operation +was performed. diff --git a/docs/implementation/stdlib/string-characters-2026-10-07/report.md b/docs/implementation/stdlib/string-characters-2026-10-07/report.md new file mode 100644 index 00000000..936641c4 --- /dev/null +++ b/docs/implementation/stdlib/string-characters-2026-10-07/report.md @@ -0,0 +1,60 @@ +# Scalar String character checkpoint + +Starting compiler: fafda68, with a clean worktree. The independent library moves +from c8020c00e227f256c0a365e22d5ebcbd1601607a to +d6ab1527e1711eb3c712f5d7bc70e5079bb8eaee, content fingerprint +fnv1a64-v1:17383a9270e6c69e. This compiler commit updates its lock, the DEC-16 +implementation status and the conformance workflow; no Rust source changes. + +## Owner and source fidelity + +Data.String.CodeUnits.charAt and toChar retain their official pure wrappers, +exports and rank-N constructor signatures. The _charAt and _toChar foreign +slots delegate to PSRS.String.charAtImpl and toCharImpl. The library owns scalar +position traversal and private canonical UTF-8 decoding over existing checked +primitives. At each decoder call, the validated String producer and scalar +boundary scan establish bounds and prove a scalar result before intToChar's +identity representation conversion. No compiler name exception or new primitive +is added. + +The shared library source verifier compares the entire module against the +clean pinned purescript-strings v6.0.1 source at +3d3e2f7197d4f7aacb15e854ee9a645489555fff, permitting exactly the seven foreign +delegates and their target import. All official pure declarations are preserved. +The source audit covers 225 modules in 41 packages: 171 identical, 35 modified, +19 target additions, zero missing upstream modules and zero direct recursive +foreign placeholders. Aggregate audit categories do not approve source changes; +the exact transformation and DEC-16 behavior evidence govern these two slots. + +## Verification + +- 269 public charAt/toChar observations execute the pinned official FFI on a + one-BMP-unit-per-scalar projection, then map Maybe results back to the original + scalar. Raw JS observations are retained separately, including surrogate + halves and raw _toChar rejection of supplementary text. This explicitly + follows DEC-16 and does not claim raw-JS equality on supplementary strings. +- 6 target-helper checks exercise local rank-N builders, including builders + that discard successful values. The generated 275-term conjunction compiles + and returns 42 under Wasmtime 49.0.2 with empty stdout/stderr. The final direct + build unsets PSRS_STDLIB_ROOT and consumes stdlib.lock.json; run.json records + the lock, input/compiler/Wasm digests and actual runtime result. +- The preceding 448 length/slicing observations also return 42 against the same + package fingerprint. The library retains all observation data and both runtime + reports under docs/evidence/string-characters/. +- Node tooling: 9 passed, no skips. Both development-root and normal locked-root + trusted-order loader tests pass; Prelude remains first. Mandatory Wasmtime + String driver regressions: 16 passed. +- The same minimal supplementary-charAt probe changes from explicit P8 missing + _charAt support to compilation success (4820 ms) through the locked loader. + diagnosis.json records both snapshots. The library fingerprints differ, so + these are not presented as a compatible diagnose --compare result. +- Formatting and diff checks pass. Full workspace/clippy were not repeated for + this library-only slice. The preceding compiler checkpoint records 1664 pass, + 3 independently established baseline failures and 5 ignored tests, with strict + clippy passing; those results do not claim full verification of the new pin. + +The 211-import fixture is not remeasured for this library pin. Unsafe character +indexing, character arrays, predicate traversal, searching and other foreign +slots are outside this checkpoint. A fresh Int.fromString probe still reaches +the explicit P8 missing Data.Int.fromStringAsImpl implementation. This slice +does not establish full String or standard-library API/runtime acceptance. diff --git a/docs/implementation/stdlib/string-slicing-2026-10-07/report.md b/docs/implementation/stdlib/string-slicing-2026-10-07/report.md new file mode 100644 index 00000000..7d4fbc7d --- /dev/null +++ b/docs/implementation/stdlib/string-slicing-2026-10-07/report.md @@ -0,0 +1,55 @@ +# Scalar String length and slicing checkpoint + +The compiler prerequisite topics are committed as 138da4e (canonical qualified +scopes), cdeefed (guard continuations and Bounded pin), and 4aa5b51 (recursive +product coverage). The String library slice changes no Rust compiler mechanism. + +The independent library pin is c8020c00e227f256c0a365e22d5ebcbd1601607a, +content fingerprint fnv1a64-v1:6d4f0e0ccbe37bc0. Data.String.CodeUnits keeps +all official exports, signatures and pure declarations. Its five foreign slots +length, take, drop, slice and splitAt delegate to PSRS.String. The library scans +validated canonical UTF-8 at scalar boundaries, copies through the existing +PSRS.Array.sliceImpl, and validates output with bytesToString. It adds no +whole-function intrinsic and keeps mutation primitives private to the array +storage owner. Prelude remains first in trusted loading. + +DEC-16 makes these source indices Unicode scalar indices, not bytes or UTF-16 +units. The pinned official FFI executes against a one-BMP-unit-per-scalar +projection, then output maps back to original scalars. Raw UTF-16 results and +the FFI digest are recorded separately in the library's observations. This is +an explicit target difference, not raw-JS equality on supplementary text. + +Validation: + +- 448 projected-FFI/runtime observations return 42 under Wasmtime 49.0.2 with + empty stdout/stderr. Cases cover NUL, combining characters, all UTF-8 width + transitions, supplementary scalars, U+10FFFF, empty/reversed/negative ranges, + oversized indices and both signed i32 extrema. The unchanged takeRight and + dropRight wrappers also execute. The final package run is in run.json. +- The same String-length probe changes from explicit P8 missing binding to + compile acceptance (4794 ms). Library fingerprints differ, so this is not + presented as a compatible diagnose --compare result. diagnosis.json records + both observations. +- The source audit covers 225 modules in 41 pinned packages: 171 identical, + 35 modified and 19 platform additions, no missing upstream module and no + direct-self-recursion replacement. The exact five-slot transformation is + checked by the oracle generator; aggregate audit categories alone do not + approve differences. +- Library Node tooling: 8 passed. Development-root and locked-root trusted-order + loader checks pass. Mandatory Wasmtime driver string regressions: 16 passed. +- Formatting and diff checks pass. No Rust source changes were made in this + slice, so the full workspace/clippy checks from the prerequisite checkpoint + were not repeated. That checkpoint still records three independently + reproduced driver-library baseline failures. + +The finite-expression stack overflow recorded for this checkpoint is repaired +by the [compiler follow-up](../deep-expression-2026-10-07/report.md). +The original ungrouped 448-observation String fixture now compiles and returns +42 with empty stdout/stderr under the same library fingerprint. The library +generator retains its grouped fixture; no library source or oracle result was +changed to accommodate the compiler repair. + +Character indexing, predicate traversal, searching and other String slots +remain explicitly unsupported. This slice does not claim full String or +stdlib API/runtime closure. The later compiler follow-up separately remeasures +the full 211-import reproducer under this library pin. diff --git a/docs/implementation/stdlib/uncurried-2026-10-06/report.md b/docs/implementation/stdlib/uncurried-2026-10-06/report.md new file mode 100644 index 00000000..4542edbd --- /dev/null +++ b/docs/implementation/stdlib/uncurried-2026-10-06/report.md @@ -0,0 +1,34 @@ +# Uncurried functions checkpoint + +The compiler gains no `Fn` intrinsic. `Data.Function.Uncurried` keeps its +official signatures and exports; `Fn1` stays the synonym `a -> b`, and `Fn0` and +`Fn2` through `Fn10` become abstract newtypes over the corresponding curried +function. `mkFn{N}` and `runFn{N}` adapt currying only, and keeping each type +abstract preserves rank-2 arguments. This unblocks `Data.Array`, whose foreign +signatures use `Fn2` and `Fn3`. The independent `psrs-stdlib` revision is +`b405dddf67a902387dda316cdf331f2cc82db5ce` with content fingerprint +`fnv1a64-v1:03a4b91b0b29e0ec`. + +Validation: + +- Mandatory Wasmtime driver regressions passed, including the new + `library_uncurried_matches_pinned_official_observations` and the earlier apply, + bind, extend, ST and primitive checks. +- Official JS observations: the pinned `Data/Function/Uncurried.js` generates + eleven cases with eleven observable checks covering the + `runFn{N} (mkFn{N} f)` round trip for `N = 0` and `N = 2..10` plus a + first-class `Fn2` passed through an ordinary function. The mandatory runner + executes the Prelude-free fixture under Wasmtime with exit 42 and empty output. +- Library-owned Node tooling regressions: 8 passed. +- Complete pinned audit: 41 packages, 217 modules, 189 exact, 17 modified and + 11 explicitly recorded target additions; no absent official module and no + detected exact direct same-argument recursion. +- Formatting and strict workspace clippy with warnings denied passed. + +The full reproducer still fails. Unsupported-library P8 reports fell from 199 to +179 and the first is still `Data.Array.fromFoldableImpl`; no +`Data.Function.Uncurried` foreign value remains. Package content changed from +`fnv1a64-v1:5b51b652c77ed0b7` to `fnv1a64-v1:03a4b91b0b29e0ec`. Remaining work in +this chain includes the `Data.Array` algorithms, `Data.Array.NonEmpty.Internal`, +and the mutable `Data.Array.ST` group. No full workspace tests or full scoreboard +were run. No push, PR or issue operation was performed. diff --git a/docs/implementation/stdlib/vendor-audit-2026-10-06/inventory.json b/docs/implementation/stdlib/vendor-audit-2026-10-06/inventory.json new file mode 100644 index 00000000..49f790db --- /dev/null +++ b/docs/implementation/stdlib/vendor-audit-2026-10-06/inventory.json @@ -0,0 +1,7155 @@ +{ + "schema_version": 1, + "compiler_revision": "67369ba012755b1586d1250fac6a7254e096bb03", + "counts": { + "identical": 149, + "modified": 50, + "newline_only": 3, + "platform_addition": 9, + "vendored_modules": 211, + "packages": 41, + "direct_self_recursions": 200, + "modules_with_direct_self_recursions": 30, + "upstream_modules_absent_from_vendor": [ + "Effect/Class.purs", + "Effect/Class/Console.purs", + "Effect/Uncurried.purs", + "Effect/Unsafe.purs" + ], + "upstream_value_foreign_declarations": { + "nonrecursive_replacement": 40, + "direct_self_recursion": 200, + "declaration_removed": 27 + } + }, + "packages": [ + { + "name": "purescript-arrays", + "checkout": "/private/tmp/ps-pkgs/purescript-arrays", + "commit": "6554b3d9c1ebb871477ffa88c2f3850d714b42b0", + "tag": "v7.3.0", + "remote": "https://github.com/purescript/purescript-arrays.git" + }, + { + "name": "purescript-bifunctors", + "checkout": "/private/tmp/ps-pkgs/purescript-bifunctors", + "commit": "d35e0f1e5a33d59a226859b6bc53075fb58ced87", + "tag": "v6.1.0", + "remote": "https://github.com/purescript/purescript-bifunctors.git" + }, + { + "name": "purescript-const", + "checkout": "/private/tmp/ps-pkgs/purescript-const", + "commit": "ab9570cf2b6e67f7e441178211db1231cfd75c37", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-const.git" + }, + { + "name": "purescript-contravariant", + "checkout": "/private/tmp/ps-pkgs/purescript-contravariant", + "commit": "9ad3e105b8855bcc25f4e0893c784789d05a58de", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-contravariant.git" + }, + { + "name": "purescript-control", + "checkout": "/private/tmp/ps-pkgs/purescript-control", + "commit": "a6033808790879a17b2729e73747a9ed3fb2264e", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-control.git" + }, + { + "name": "purescript-distributive", + "checkout": "/private/tmp/ps-pkgs/purescript-distributive", + "commit": "6005e513642e855ebf6f884d24a35c2803ca252a", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-distributive.git" + }, + { + "name": "purescript-either", + "checkout": "/private/tmp/ps-pkgs/purescript-either", + "commit": "af655a04ed2fd694b6688af39ee20d7907ad0763", + "tag": "v6.1.0", + "remote": "https://github.com/purescript/purescript-either.git" + }, + { + "name": "purescript-enums", + "checkout": "/private/tmp/ps-pkgs/purescript-enums", + "commit": "cd373c580b69fdc00e412bddbc299adabe242cc5", + "tag": "v6.0.1", + "remote": "https://github.com/purescript/purescript-enums.git" + }, + { + "name": "purescript-exists", + "checkout": "/private/tmp/ps-pkgs/purescript-exists", + "commit": "f765b4ace7869c27b9c05949e18c843881f9173b", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-exists.git" + }, + { + "name": "purescript-filterable", + "checkout": "/private/tmp/ps-pkgs/purescript-filterable", + "commit": "7c5b8c72779997f2b17d12ce478ff81e7ddda285", + "tag": "v5.0.0", + "remote": "https://github.com/purescript/purescript-filterable.git" + }, + { + "name": "purescript-foldable-traversable", + "checkout": "/private/tmp/ps-pkgs/purescript-foldable-traversable", + "commit": "b3926f870532d287ea59e2d5cd3873b81ef2a93a", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-foldable-traversable.git" + }, + { + "name": "purescript-functions", + "checkout": "/private/tmp/ps-pkgs/purescript-functions", + "commit": "f626f20580483977c5b27a01aac6471e28aff367", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-functions.git" + }, + { + "name": "purescript-functors", + "checkout": "/private/tmp/ps-pkgs/purescript-functors", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "tag": "v5.0.0", + "remote": "https://github.com/purescript/purescript-functors.git" + }, + { + "name": "purescript-gen", + "checkout": "/private/tmp/ps-pkgs/purescript-gen", + "commit": "9fbcc2a1261c32e30d79c5418edef4d96fe76931", + "tag": "v4.0.0", + "remote": "https://github.com/purescript/purescript-gen.git" + }, + { + "name": "purescript-identity", + "checkout": "/private/tmp/ps-pkgs/purescript-identity", + "commit": "ef6768f8a52ab0bc943a85f5761ba07c257f639f", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-identity.git" + }, + { + "name": "purescript-integers", + "checkout": "/private/tmp/ps-pkgs/purescript-integers", + "commit": "54d712b25c594833083d15dc9ff2418eb9c52822", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-integers.git" + }, + { + "name": "purescript-invariant", + "checkout": "/private/tmp/ps-pkgs/purescript-invariant", + "commit": "1d2a196d51e90623adb88496c2cfd759c6736894", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-invariant.git" + }, + { + "name": "purescript-lazy", + "checkout": "/private/tmp/ps-pkgs/purescript-lazy", + "commit": "48347841226b27af5205a1a8ec71e27a93ce86fd", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-lazy.git" + }, + { + "name": "purescript-lists", + "checkout": "/private/tmp/ps-pkgs/purescript-lists", + "commit": "b113451e5b41cad87d669a3165f955c71cd863e2", + "tag": "v7.0.0", + "remote": "https://github.com/purescript/purescript-lists.git" + }, + { + "name": "purescript-maybe", + "checkout": "/private/tmp/ps-pkgs/purescript-maybe", + "commit": "c6f98ac1088766287106c5d9c8e30e7648d36786", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-maybe.git" + }, + { + "name": "purescript-newtype", + "checkout": "/private/tmp/ps-pkgs/purescript-newtype", + "commit": "29d8e6dd77aec2c975c948364ec3faf26e14ee7b", + "tag": "v5.0.0", + "remote": "https://github.com/purescript/purescript-newtype.git" + }, + { + "name": "purescript-nonempty", + "checkout": "/private/tmp/ps-pkgs/purescript-nonempty", + "commit": "28150ecc7419238b187abd609a92a645273348bb", + "tag": "v7.0.0", + "remote": "https://github.com/purescript/purescript-nonempty.git" + }, + { + "name": "purescript-numbers", + "checkout": "/private/tmp/ps-pkgs/purescript-numbers", + "commit": "27d54effdd2c0e7a86fe356b1cd813dca5981c2d", + "tag": "v9.0.1", + "remote": "https://github.com/purescript/purescript-numbers.git" + }, + { + "name": "purescript-ordered-collections", + "checkout": "/private/tmp/ps-pkgs/purescript-ordered-collections", + "commit": "b9c8ec72b53ee56d6cb6dc962d5ad7ea4708b438", + "tag": "v3.2.0", + "remote": "https://github.com/purescript/purescript-ordered-collections.git" + }, + { + "name": "purescript-orders", + "checkout": "/private/tmp/ps-pkgs/purescript-orders", + "commit": "f86db621ec5eef1274145f8b1fd8ebbfe0ed4a2c", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-orders.git" + }, + { + "name": "purescript-partial", + "checkout": "/private/tmp/ps-pkgs/purescript-partial", + "commit": "0fa0646f5ea1ec5f0c46dcbd770c705a6c9ad3ec", + "tag": "v4.0.0", + "remote": "https://github.com/purescript/purescript-partial.git" + }, + { + "name": "purescript-profunctor", + "checkout": "/private/tmp/ps-pkgs/purescript-profunctor", + "commit": "17f927ce69710085f2bbefc9a37506828d1ebc5a", + "tag": "v6.0.1", + "remote": "https://github.com/purescript/purescript-profunctor.git" + }, + { + "name": "purescript-refs", + "checkout": "/private/tmp/ps-pkgs/purescript-refs", + "commit": "f8e6216da4cb9309fde1f20cd6f69ac3a3b7f9e8", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-refs.git" + }, + { + "name": "purescript-safe-coerce", + "checkout": "/private/tmp/ps-pkgs/purescript-safe-coerce", + "commit": "7fa799ae80a38b8d948efcb52608e58e198b3da7", + "tag": "v2.0.0", + "remote": "https://github.com/purescript/purescript-safe-coerce.git" + }, + { + "name": "purescript-st", + "checkout": "/private/tmp/ps-pkgs/purescript-st", + "commit": "fc2fe2972bb12e6a2bd3b295baf01577240c23ac", + "tag": "v6.2.0", + "remote": "https://github.com/purescript/purescript-st.git" + }, + { + "name": "purescript-strings", + "checkout": "/private/tmp/ps-pkgs/purescript-strings", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "tag": "v6.0.1", + "remote": "https://github.com/purescript/purescript-strings.git" + }, + { + "name": "purescript-tailrec", + "checkout": "/private/tmp/ps-pkgs/purescript-tailrec", + "commit": "5661a10afbd4849bd2e45139ea567beb40b20f9f", + "tag": "v6.1.0", + "remote": "https://github.com/purescript/purescript-tailrec.git" + }, + { + "name": "purescript-tuples", + "checkout": "/private/tmp/ps-pkgs/purescript-tuples", + "commit": "4f52da2729b448c8564369378f1232d8d2dc1d8b", + "tag": "v7.0.0", + "remote": "https://github.com/purescript/purescript-tuples.git" + }, + { + "name": "purescript-type-equality", + "checkout": "/private/tmp/ps-pkgs/purescript-type-equality", + "commit": "0525b7d39e0fbd81b4209518139fb8ab02695774", + "tag": "v4.0.1", + "remote": "https://github.com/purescript/purescript-type-equality.git" + }, + { + "name": "purescript-typelevel-prelude", + "checkout": "/private/tmp/ps-pkgs/purescript-typelevel-prelude", + "commit": "dca2fe3c8cfd5527d4fe70c4bedfda30148405bf", + "tag": "v7.0.0", + "remote": "https://github.com/purescript/purescript-typelevel-prelude.git" + }, + { + "name": "purescript-unfoldable", + "checkout": "/private/tmp/ps-pkgs/purescript-unfoldable", + "commit": "493dfe04ed590e20d8f69079df2f58486882748d", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-unfoldable.git" + }, + { + "name": "purescript-unsafe-coerce", + "checkout": "/private/tmp/ps-pkgs/purescript-unsafe-coerce", + "commit": "ab956f82e66e633f647fb3098e8ddd3ec58d689f", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-unsafe-coerce.git" + }, + { + "name": "purescript-prelude", + "checkout": "/private/tmp/purescript-prelude", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "tag": "v6.0.1", + "remote": "https://github.com/purescript/purescript-prelude.git" + }, + { + "name": "purescript-assert", + "checkout": "/private/tmp/psrs-stdlib-audit-20261006/upstream/purescript-assert", + "commit": "27c0edb57d2ee497eb5fab664f5601c35b613eda", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-assert.git" + }, + { + "name": "purescript-console", + "checkout": "/private/tmp/psrs-stdlib-audit-20261006/upstream/purescript-console", + "commit": "3b83d7b792d03872afeea5e62b4f686ab0f09842", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-console.git" + }, + { + "name": "purescript-effect", + "checkout": "/private/tmp/psrs-stdlib-audit-20261006/upstream/purescript-effect", + "commit": "a192ddb923027d426d6ea3d8deb030c9aa7c7dda", + "tag": "v4.0.0", + "remote": "https://github.com/purescript/purescript-effect.git" + } + ], + "modules": [ + { + "path": "Control/Alt.purs", + "vendored_sha256": "f02098c849efebd9eaeff8340215637a6152db97b10743a86dec6fae149061a1", + "vendored_lines": 42, + "self_recursions": [], + "package": "purescript-control", + "tag": "v6.0.0", + "commit": "a6033808790879a17b2729e73747a9ed3fb2264e", + "upstream_sha256": "f02098c849efebd9eaeff8340215637a6152db97b10743a86dec6fae149061a1", + "upstream_url": "https://github.com/purescript/purescript-control/blob/a6033808790879a17b2729e73747a9ed3fb2264e/src/Control/Alt.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Alternative.purs", + "vendored_sha256": "86e1c20be4b0d570377b607f53fdbce0c642065973b79b9211883dc2b9d9b329", + "vendored_lines": 50, + "self_recursions": [], + "package": "purescript-control", + "tag": "v6.0.0", + "commit": "a6033808790879a17b2729e73747a9ed3fb2264e", + "upstream_sha256": "86e1c20be4b0d570377b607f53fdbce0c642065973b79b9211883dc2b9d9b329", + "upstream_url": "https://github.com/purescript/purescript-control/blob/a6033808790879a17b2729e73747a9ed3fb2264e/src/Control/Alternative.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Applicative.purs", + "vendored_sha256": "4cf9abb5b98e7569c66375a725ce3815c57f020f1dc0513fc4bd853cd70b567b", + "vendored_lines": 70, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "4cf9abb5b98e7569c66375a725ce3815c57f020f1dc0513fc4bd853cd70b567b", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Control/Applicative.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Apply.purs", + "vendored_sha256": "f174b8f2d019ad34d38fdc69deddcad303fa78cfebd127e05b8361319443f4bf", + "vendored_lines": 119, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "02712ac645e851672d659d9b21171514b6d727efa7de08461399d3e7e581859b", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Control/Apply.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "arrayApply", + "line": 63, + "declaration": "foreign import arrayApply :: forall a b. Array (a -> b) -> Array a -> Array b", + "vendored_status": "nonrecursive_replacement" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Biapplicative.purs", + "vendored_sha256": "5c3baecdf32b1e0eb8c871de70eac69aa558b2dca8710b7b3d84badb20ef00bb", + "vendored_lines": 12, + "self_recursions": [], + "package": "purescript-bifunctors", + "tag": "v6.1.0", + "commit": "d35e0f1e5a33d59a226859b6bc53075fb58ced87", + "upstream_sha256": "5c3baecdf32b1e0eb8c871de70eac69aa558b2dca8710b7b3d84badb20ef00bb", + "upstream_url": "https://github.com/purescript/purescript-bifunctors/blob/d35e0f1e5a33d59a226859b6bc53075fb58ced87/src/Control/Biapplicative.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Biapply.purs", + "vendored_sha256": "e2adb06728bb1a789579e3876c0a3894f1cd3ee7e5acef3268a13023a1733de0", + "vendored_lines": 59, + "self_recursions": [], + "package": "purescript-bifunctors", + "tag": "v6.1.0", + "commit": "d35e0f1e5a33d59a226859b6bc53075fb58ced87", + "upstream_sha256": "e2adb06728bb1a789579e3876c0a3894f1cd3ee7e5acef3268a13023a1733de0", + "upstream_url": "https://github.com/purescript/purescript-bifunctors/blob/d35e0f1e5a33d59a226859b6bc53075fb58ced87/src/Control/Biapply.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Bind.purs", + "vendored_sha256": "967aca77c298e0d9a9774c18c6b57ba73d21cb51e220e0970b368772ff274718", + "vendored_lines": 158, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "ea03e5ffa8c55c101018ceb9bf19fca9af792b49113a73be4a85867ef2c022f7", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Control/Bind.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "arrayBind", + "line": 97, + "declaration": "foreign import arrayBind :: forall a b. Array a -> (a -> Array b) -> Array b", + "vendored_status": "nonrecursive_replacement" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Category.purs", + "vendored_sha256": "9cd6a331c6e9a7e17f35adb8fd11ba9fbe4fd2034eed34979d9cfbef68fe4230", + "vendored_lines": 22, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "9cd6a331c6e9a7e17f35adb8fd11ba9fbe4fd2034eed34979d9cfbef68fe4230", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Control/Category.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Comonad.purs", + "vendored_sha256": "8ddf03de6841c4e570432c7186445ef912d9a63d1eb272e668657ab73d981521", + "vendored_lines": 21, + "self_recursions": [], + "package": "purescript-control", + "tag": "v6.0.0", + "commit": "a6033808790879a17b2729e73747a9ed3fb2264e", + "upstream_sha256": "8ddf03de6841c4e570432c7186445ef912d9a63d1eb272e668657ab73d981521", + "upstream_url": "https://github.com/purescript/purescript-control/blob/a6033808790879a17b2729e73747a9ed3fb2264e/src/Control/Comonad.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Extend.purs", + "vendored_sha256": "6f03ca3513cfc0004437042ae7341eaccfec255d184b87c9a8a0499d22d6eae7", + "vendored_lines": 60, + "self_recursions": [ + { + "name": "arrayExtend", + "line": 31, + "equation": "arrayExtend a0 a1 = arrayExtend a0 a1", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-control", + "tag": "v6.0.0", + "commit": "a6033808790879a17b2729e73747a9ed3fb2264e", + "upstream_sha256": "20d5e8394e1e755187e594cda0a3f63147fa69f703e18d8b99244d59c55d2900", + "upstream_url": "https://github.com/purescript/purescript-control/blob/a6033808790879a17b2729e73747a9ed3fb2264e/src/Control/Extend.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "arrayExtend", + "line": 30, + "declaration": "foreign import arrayExtend :: forall a b. (Array a -> b) -> Array a -> Array b", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Lazy.purs", + "vendored_sha256": "05fce3e86c1c1b409f863368edb7991894c2c0d83ce99cdf7a3f84cdc30e9d0a", + "vendored_lines": 25, + "self_recursions": [], + "package": "purescript-control", + "tag": "v6.0.0", + "commit": "a6033808790879a17b2729e73747a9ed3fb2264e", + "upstream_sha256": "05fce3e86c1c1b409f863368edb7991894c2c0d83ce99cdf7a3f84cdc30e9d0a", + "upstream_url": "https://github.com/purescript/purescript-control/blob/a6033808790879a17b2729e73747a9ed3fb2264e/src/Control/Lazy.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Monad/Gen/Class.purs", + "vendored_sha256": "88fc916494c75e3309471a0852d0c740691570d68dd9dbb92ed4547e9e20f17a", + "vendored_lines": 29, + "self_recursions": [], + "package": "purescript-gen", + "tag": "v4.0.0", + "commit": "9fbcc2a1261c32e30d79c5418edef4d96fe76931", + "upstream_sha256": "88fc916494c75e3309471a0852d0c740691570d68dd9dbb92ed4547e9e20f17a", + "upstream_url": "https://github.com/purescript/purescript-gen/blob/9fbcc2a1261c32e30d79c5418edef4d96fe76931/src/Control/Monad/Gen/Class.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Monad/Gen/Common.purs", + "vendored_sha256": "d31ab1b6246acefd0e92f320483d413861e5727de6429d1b4ca391f8120e89a4", + "vendored_lines": 67, + "self_recursions": [], + "package": "purescript-gen", + "tag": "v4.0.0", + "commit": "9fbcc2a1261c32e30d79c5418edef4d96fe76931", + "upstream_sha256": "d31ab1b6246acefd0e92f320483d413861e5727de6429d1b4ca391f8120e89a4", + "upstream_url": "https://github.com/purescript/purescript-gen/blob/9fbcc2a1261c32e30d79c5418edef4d96fe76931/src/Control/Monad/Gen/Common.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Monad/Gen.purs", + "vendored_sha256": "1af7bc60432e7fb6f3e6e75da3b78be19bc2d1ad7e9cfcdcfcba4446ec317f1b", + "vendored_lines": 132, + "self_recursions": [], + "package": "purescript-gen", + "tag": "v4.0.0", + "commit": "9fbcc2a1261c32e30d79c5418edef4d96fe76931", + "upstream_sha256": "1af7bc60432e7fb6f3e6e75da3b78be19bc2d1ad7e9cfcdcfcba4446ec317f1b", + "upstream_url": "https://github.com/purescript/purescript-gen/blob/9fbcc2a1261c32e30d79c5418edef4d96fe76931/src/Control/Monad/Gen.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Monad/Rec/Class.purs", + "vendored_sha256": "fd2ea9d0000c0194b78949bcaf9c931dfc12c92de322f0cadf73d566c994ea2f", + "vendored_lines": 191, + "self_recursions": [], + "package": "purescript-tailrec", + "tag": "v6.1.0", + "commit": "5661a10afbd4849bd2e45139ea567beb40b20f9f", + "upstream_sha256": "fd2ea9d0000c0194b78949bcaf9c931dfc12c92de322f0cadf73d566c994ea2f", + "upstream_url": "https://github.com/purescript/purescript-tailrec/blob/5661a10afbd4849bd2e45139ea567beb40b20f9f/src/Control/Monad/Rec/Class.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Monad/ST/Class.purs", + "vendored_sha256": "227bf813cc68b69b167ade39f2531d0ba45dc259252001bb97758e08bc93247d", + "vendored_lines": 17, + "self_recursions": [], + "package": "purescript-st", + "tag": "v6.2.0", + "commit": "fc2fe2972bb12e6a2bd3b295baf01577240c23ac", + "upstream_sha256": "227bf813cc68b69b167ade39f2531d0ba45dc259252001bb97758e08bc93247d", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Class.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Monad/ST/Global.purs", + "vendored_sha256": "4cb7ab62ecd6c6dfacfce32920bf5d9a3dc0f7ce7d9a25a16b453d0a4fc0f9e3", + "vendored_lines": 18, + "self_recursions": [], + "package": "purescript-st", + "tag": "v6.2.0", + "commit": "fc2fe2972bb12e6a2bd3b295baf01577240c23ac", + "upstream_sha256": "4cb7ab62ecd6c6dfacfce32920bf5d9a3dc0f7ce7d9a25a16b453d0a4fc0f9e3", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Global.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Monad/ST/Internal.purs", + "vendored_sha256": "75990cb65730636563ed0a92e718f82b5fc5f3f42942ad43a38d7642fda2b411", + "vendored_lines": 147, + "self_recursions": [ + { + "name": "map_", + "line": 39, + "equation": "map_ a0 a1 = map_ a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "pure_", + "line": 42, + "equation": "pure_ a0 = pure_ a0", + "replaces_upstream_foreign": true + }, + { + "name": "bind_", + "line": 45, + "equation": "bind_ a0 a1 = bind_ a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "run", + "line": 93, + "equation": "run a0 = run a0", + "replaces_upstream_foreign": true + }, + { + "name": "while", + "line": 101, + "equation": "while a0 a1 = while a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "for", + "line": 108, + "equation": "for a0 a1 a2 = for a0 a1 a2", + "replaces_upstream_foreign": true + }, + { + "name": "foreach", + "line": 115, + "equation": "foreach a0 a1 = foreach a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "new", + "line": 125, + "equation": "new a0 = new a0", + "replaces_upstream_foreign": true + }, + { + "name": "read", + "line": 129, + "equation": "read a0 = read a0", + "replaces_upstream_foreign": true + }, + { + "name": "modifyImpl", + "line": 138, + "equation": "modifyImpl a0 a1 = modifyImpl a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "write", + "line": 147, + "equation": "write a0 a1 = write a0 a1", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-st", + "tag": "v6.2.0", + "commit": "fc2fe2972bb12e6a2bd3b295baf01577240c23ac", + "upstream_sha256": "00159fcd8f3fa41d7ba5b50c596043bc46859f95f8d877b9ff18dbc37c7fad99", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Internal.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "map_", + "line": 38, + "declaration": "foreign import map_ :: forall r a b. (a -> b) -> ST r a -> ST r b", + "vendored_status": "direct_self_recursion" + }, + { + "name": "pure_", + "line": 40, + "declaration": "foreign import pure_ :: forall r a. a -> ST r a", + "vendored_status": "direct_self_recursion" + }, + { + "name": "bind_", + "line": 42, + "declaration": "foreign import bind_ :: forall r a b. ST r a -> (a -> ST r b) -> ST r b", + "vendored_status": "direct_self_recursion" + }, + { + "name": "run", + "line": 89, + "declaration": "foreign import run :: forall a. (forall r. ST r a) -> a", + "vendored_status": "direct_self_recursion" + }, + { + "name": "while", + "line": 96, + "declaration": "foreign import while :: forall r a. ST r Boolean -> ST r a -> ST r Unit", + "vendored_status": "direct_self_recursion" + }, + { + "name": "for", + "line": 102, + "declaration": "foreign import for :: forall r a. Int -> Int -> (Int -> ST r a) -> ST r Unit", + "vendored_status": "direct_self_recursion" + }, + { + "name": "foreach", + "line": 108, + "declaration": "foreign import foreach :: forall r a. Array a -> (a -> ST r Unit) -> ST r Unit", + "vendored_status": "direct_self_recursion" + }, + { + "name": "new", + "line": 117, + "declaration": "foreign import new :: forall a r. a -> ST r (STRef r a)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "read", + "line": 120, + "declaration": "foreign import read :: forall a r. STRef r a -> ST r a", + "vendored_status": "direct_self_recursion" + }, + { + "name": "modifyImpl", + "line": 128, + "declaration": "foreign import modifyImpl :: forall r a b. (a -> { state :: a, value :: b }) -> STRef r a -> ST r b", + "vendored_status": "direct_self_recursion" + }, + { + "name": "write", + "line": 136, + "declaration": "foreign import write :: forall a r. a -> STRef r a -> ST r a", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Monad/ST/Ref.purs", + "vendored_sha256": "cd155ad39db037f2b26d422461da697bc3b7b5fe9bfc93702df55cba38158d3a", + "vendored_lines": 3, + "self_recursions": [], + "package": "purescript-st", + "tag": "v6.2.0", + "commit": "fc2fe2972bb12e6a2bd3b295baf01577240c23ac", + "upstream_sha256": "cd155ad39db037f2b26d422461da697bc3b7b5fe9bfc93702df55cba38158d3a", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Ref.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Monad/ST/Uncurried.purs", + "vendored_sha256": "dfd09b8d765830dc50da8c1ed248ef4de9099c6c0c072752dffd56177c1a638e", + "vendored_lines": 101, + "self_recursions": [ + { + "name": "mkSTFn1", + "line": 62, + "equation": "mkSTFn1 a0 = mkSTFn1 a0", + "replaces_upstream_foreign": true + }, + { + "name": "mkSTFn2", + "line": 64, + "equation": "mkSTFn2 a0 = mkSTFn2 a0", + "replaces_upstream_foreign": true + }, + { + "name": "mkSTFn3", + "line": 66, + "equation": "mkSTFn3 a0 = mkSTFn3 a0", + "replaces_upstream_foreign": true + }, + { + "name": "mkSTFn4", + "line": 68, + "equation": "mkSTFn4 a0 = mkSTFn4 a0", + "replaces_upstream_foreign": true + }, + { + "name": "mkSTFn5", + "line": 70, + "equation": "mkSTFn5 a0 = mkSTFn5 a0", + "replaces_upstream_foreign": true + }, + { + "name": "mkSTFn6", + "line": 72, + "equation": "mkSTFn6 a0 = mkSTFn6 a0", + "replaces_upstream_foreign": true + }, + { + "name": "mkSTFn7", + "line": 74, + "equation": "mkSTFn7 a0 = mkSTFn7 a0", + "replaces_upstream_foreign": true + }, + { + "name": "mkSTFn8", + "line": 76, + "equation": "mkSTFn8 a0 = mkSTFn8 a0", + "replaces_upstream_foreign": true + }, + { + "name": "mkSTFn9", + "line": 78, + "equation": "mkSTFn9 a0 = mkSTFn9 a0", + "replaces_upstream_foreign": true + }, + { + "name": "mkSTFn10", + "line": 80, + "equation": "mkSTFn10 a0 = mkSTFn10 a0", + "replaces_upstream_foreign": true + }, + { + "name": "runSTFn1", + "line": 83, + "equation": "runSTFn1 a0 a1 = runSTFn1 a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "runSTFn2", + "line": 85, + "equation": "runSTFn2 a0 a1 a2 = runSTFn2 a0 a1 a2", + "replaces_upstream_foreign": true + }, + { + "name": "runSTFn3", + "line": 87, + "equation": "runSTFn3 a0 a1 a2 a3 = runSTFn3 a0 a1 a2 a3", + "replaces_upstream_foreign": true + }, + { + "name": "runSTFn4", + "line": 89, + "equation": "runSTFn4 a0 a1 a2 a3 a4 = runSTFn4 a0 a1 a2 a3 a4", + "replaces_upstream_foreign": true + }, + { + "name": "runSTFn5", + "line": 91, + "equation": "runSTFn5 a0 a1 a2 a3 a4 a5 = runSTFn5 a0 a1 a2 a3 a4 a5", + "replaces_upstream_foreign": true + }, + { + "name": "runSTFn6", + "line": 93, + "equation": "runSTFn6 a0 a1 a2 a3 a4 a5 a6 = runSTFn6 a0 a1 a2 a3 a4 a5 a6", + "replaces_upstream_foreign": true + }, + { + "name": "runSTFn7", + "line": 95, + "equation": "runSTFn7 a0 a1 a2 a3 a4 a5 a6 a7 = runSTFn7 a0 a1 a2 a3 a4 a5 a6 a7", + "replaces_upstream_foreign": true + }, + { + "name": "runSTFn8", + "line": 97, + "equation": "runSTFn8 a0 a1 a2 a3 a4 a5 a6 a7 a8 = runSTFn8 a0 a1 a2 a3 a4 a5 a6 a7 a8", + "replaces_upstream_foreign": true + }, + { + "name": "runSTFn9", + "line": 99, + "equation": "runSTFn9 a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 = runSTFn9 a0 a1 a2 a3 a4 a5 a6 a7 a8 a9", + "replaces_upstream_foreign": true + }, + { + "name": "runSTFn10", + "line": 101, + "equation": "runSTFn10 a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 = runSTFn10 a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-st", + "tag": "v6.2.0", + "commit": "fc2fe2972bb12e6a2bd3b295baf01577240c23ac", + "upstream_sha256": "bd2411e5e23d8b446ef2f6b924422071eb99c6c629395df21eb9821a341988a6", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Uncurried.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "mkSTFn1", + "line": 61, + "declaration": "foreign import mkSTFn1 :: forall a t r. (a -> ST t r) -> STFn1 a t r", + "vendored_status": "direct_self_recursion" + }, + { + "name": "mkSTFn2", + "line": 63, + "declaration": "foreign import mkSTFn2 :: forall a b t r. (a -> b -> ST t r) -> STFn2 a b t r", + "vendored_status": "direct_self_recursion" + }, + { + "name": "mkSTFn3", + "line": 65, + "declaration": "foreign import mkSTFn3 :: forall a b c t r. (a -> b -> c -> ST t r) -> STFn3 a b c t r", + "vendored_status": "direct_self_recursion" + }, + { + "name": "mkSTFn4", + "line": 67, + "declaration": "foreign import mkSTFn4 :: forall a b c d t r. (a -> b -> c -> d -> ST t r) -> STFn4 a b c d t r", + "vendored_status": "direct_self_recursion" + }, + { + "name": "mkSTFn5", + "line": 69, + "declaration": "foreign import mkSTFn5 :: forall a b c d e t r. (a -> b -> c -> d -> e -> ST t r) -> STFn5 a b c d e t r", + "vendored_status": "direct_self_recursion" + }, + { + "name": "mkSTFn6", + "line": 71, + "declaration": "foreign import mkSTFn6 :: forall a b c d e f t r. (a -> b -> c -> d -> e -> f -> ST t r) -> STFn6 a b c d e f t r", + "vendored_status": "direct_self_recursion" + }, + { + "name": "mkSTFn7", + "line": 73, + "declaration": "foreign import mkSTFn7 :: forall a b c d e f g t r. (a -> b -> c -> d -> e -> f -> g -> ST t r) -> STFn7 a b c d e f g t r", + "vendored_status": "direct_self_recursion" + }, + { + "name": "mkSTFn8", + "line": 75, + "declaration": "foreign import mkSTFn8 :: forall a b c d e f g h t r. (a -> b -> c -> d -> e -> f -> g -> h -> ST t r) -> STFn8 a b c d e f g h t r", + "vendored_status": "direct_self_recursion" + }, + { + "name": "mkSTFn9", + "line": 77, + "declaration": "foreign import mkSTFn9 :: forall a b c d e f g h i t r. (a -> b -> c -> d -> e -> f -> g -> h -> i -> ST t r) -> STFn9 a b c d e f g h i t r", + "vendored_status": "direct_self_recursion" + }, + { + "name": "mkSTFn10", + "line": 79, + "declaration": "foreign import mkSTFn10 :: forall a b c d e f g h i j t r. (a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> ST t r) -> STFn10 a b c d e f g h i j t r", + "vendored_status": "direct_self_recursion" + }, + { + "name": "runSTFn1", + "line": 82, + "declaration": "foreign import runSTFn1 :: forall a t r. STFn1 a t r -> a -> ST t r", + "vendored_status": "direct_self_recursion" + }, + { + "name": "runSTFn2", + "line": 84, + "declaration": "foreign import runSTFn2 :: forall a b t r. STFn2 a b t r -> a -> b -> ST t r", + "vendored_status": "direct_self_recursion" + }, + { + "name": "runSTFn3", + "line": 86, + "declaration": "foreign import runSTFn3 :: forall a b c t r. STFn3 a b c t r -> a -> b -> c -> ST t r", + "vendored_status": "direct_self_recursion" + }, + { + "name": "runSTFn4", + "line": 88, + "declaration": "foreign import runSTFn4 :: forall a b c d t r. STFn4 a b c d t r -> a -> b -> c -> d -> ST t r", + "vendored_status": "direct_self_recursion" + }, + { + "name": "runSTFn5", + "line": 90, + "declaration": "foreign import runSTFn5 :: forall a b c d e t r. STFn5 a b c d e t r -> a -> b -> c -> d -> e -> ST t r", + "vendored_status": "direct_self_recursion" + }, + { + "name": "runSTFn6", + "line": 92, + "declaration": "foreign import runSTFn6 :: forall a b c d e f t r. STFn6 a b c d e f t r -> a -> b -> c -> d -> e -> f -> ST t r", + "vendored_status": "direct_self_recursion" + }, + { + "name": "runSTFn7", + "line": 94, + "declaration": "foreign import runSTFn7 :: forall a b c d e f g t r. STFn7 a b c d e f g t r -> a -> b -> c -> d -> e -> f -> g -> ST t r", + "vendored_status": "direct_self_recursion" + }, + { + "name": "runSTFn8", + "line": 96, + "declaration": "foreign import runSTFn8 :: forall a b c d e f g h t r. STFn8 a b c d e f g h t r -> a -> b -> c -> d -> e -> f -> g -> h -> ST t r", + "vendored_status": "direct_self_recursion" + }, + { + "name": "runSTFn9", + "line": 98, + "declaration": "foreign import runSTFn9 :: forall a b c d e f g h i t r. STFn9 a b c d e f g h i t r -> a -> b -> c -> d -> e -> f -> g -> h -> i -> ST t r", + "vendored_status": "direct_self_recursion" + }, + { + "name": "runSTFn10", + "line": 100, + "declaration": "foreign import runSTFn10 :: forall a b c d e f g h i j t r. STFn10 a b c d e f g h i j t r -> a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> ST t r", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Monad/ST.purs", + "vendored_sha256": "316ee75613cd7960cdaefdf6c26613c14badcc8a06ee2405617ed089072598a0", + "vendored_lines": 3, + "self_recursions": [], + "package": "purescript-st", + "tag": "v6.2.0", + "commit": "fc2fe2972bb12e6a2bd3b295baf01577240c23ac", + "upstream_sha256": "316ee75613cd7960cdaefdf6c26613c14badcc8a06ee2405617ed089072598a0", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Monad.purs", + "vendored_sha256": "0bad91df889b370157f300b5e5e0241272d9149380a6c5322a81af58a6fdc67c", + "vendored_lines": 86, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "0bad91df889b370157f300b5e5e0241272d9149380a6c5322a81af58a6fdc67c", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Control/Monad.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/MonadPlus.purs", + "vendored_sha256": "4a5f2eeb965af319ffb1427c1571235ca3de44881e19aa83f2cb12e8404df68f", + "vendored_lines": 32, + "self_recursions": [], + "package": "purescript-control", + "tag": "v6.0.0", + "commit": "a6033808790879a17b2729e73747a9ed3fb2264e", + "upstream_sha256": "4a5f2eeb965af319ffb1427c1571235ca3de44881e19aa83f2cb12e8404df68f", + "upstream_url": "https://github.com/purescript/purescript-control/blob/a6033808790879a17b2729e73747a9ed3fb2264e/src/Control/MonadPlus.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Plus.purs", + "vendored_sha256": "308ba7c0a3a65e9b81cf2cc6f0a7d8e4f77043c1c2c481c04b5ab22ccf991f19", + "vendored_lines": 27, + "self_recursions": [], + "package": "purescript-control", + "tag": "v6.0.0", + "commit": "a6033808790879a17b2729e73747a9ed3fb2264e", + "upstream_sha256": "308ba7c0a3a65e9b81cf2cc6f0a7d8e4f77043c1c2c481c04b5ab22ccf991f19", + "upstream_url": "https://github.com/purescript/purescript-control/blob/a6033808790879a17b2729e73747a9ed3fb2264e/src/Control/Plus.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Semigroupoid.purs", + "vendored_sha256": "aefe8332e035b2f990e25981ac56abb63a71efed5d5e986657483dd51c04834a", + "vendored_lines": 25, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "aefe8332e035b2f990e25981ac56abb63a71efed5d5e986657483dd51c04834a", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Control/Semigroupoid.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Array/NonEmpty/Internal.purs", + "vendored_sha256": "fd4e4b50ed2df17c5ed38899b505cd256fc0c719c111e2cb75c237905d7e16d6", + "vendored_lines": 81, + "self_recursions": [ + { + "name": "foldr1Impl", + "line": 76, + "equation": "foldr1Impl = foldr1Impl", + "replaces_upstream_foreign": true + }, + { + "name": "foldl1Impl", + "line": 78, + "equation": "foldl1Impl = foldl1Impl", + "replaces_upstream_foreign": true + }, + { + "name": "traverse1Impl", + "line": 81, + "equation": "traverse1Impl = traverse1Impl", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-arrays", + "tag": "v7.3.0", + "commit": "6554b3d9c1ebb871477ffa88c2f3850d714b42b0", + "upstream_sha256": "59ce6b9e860ccdd2a089fb055008b629cd9362bb3a7f703e6bd1fb2f7a642ae6", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/NonEmpty/Internal.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "foldr1Impl", + "line": 75, + "declaration": "foreign import foldr1Impl :: forall a. Fn2 (a -> a -> a) (NonEmptyArray a) a", + "vendored_status": "direct_self_recursion" + }, + { + "name": "foldl1Impl", + "line": 76, + "declaration": "foreign import foldl1Impl :: forall a. Fn2 (a -> a -> a) (NonEmptyArray a) a", + "vendored_status": "direct_self_recursion" + }, + { + "name": "traverse1Impl", + "line": 78, + "declaration": "foreign import traverse1Impl :: forall m a b . Fn3 (forall a' b'. (m (a' -> b') -> m a' -> m b')) (forall a' b'. (a' -> b') -> m a' -> m b') (a -> m b) (NonEmptyArray a -> m (NonEmptyArray b))", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Array/NonEmpty.purs", + "vendored_sha256": "4e42eb0655fef616939cdf91f8e87b8c5e3581f34a538a2066cd9a644302b8d4", + "vendored_lines": 598, + "self_recursions": [], + "package": "purescript-arrays", + "tag": "v7.3.0", + "commit": "6554b3d9c1ebb871477ffa88c2f3850d714b42b0", + "upstream_sha256": "4e42eb0655fef616939cdf91f8e87b8c5e3581f34a538a2066cd9a644302b8d4", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/NonEmpty.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Array/Partial.purs", + "vendored_sha256": "21c2d565d728a650e1f0e3521570c518434a97aaad087893237d20c18fda8184", + "vendored_lines": 35, + "self_recursions": [], + "package": "purescript-arrays", + "tag": "v7.3.0", + "commit": "6554b3d9c1ebb871477ffa88c2f3850d714b42b0", + "upstream_sha256": "21c2d565d728a650e1f0e3521570c518434a97aaad087893237d20c18fda8184", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/Partial.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Array/ST/Iterator.purs", + "vendored_sha256": "96dd803b0c983a56714d1dbbf6d86193c245660e6acb4faffa2cec6f6e1d2583", + "vendored_lines": 80, + "self_recursions": [], + "package": "purescript-arrays", + "tag": "v7.3.0", + "commit": "6554b3d9c1ebb871477ffa88c2f3850d714b42b0", + "upstream_sha256": "96dd803b0c983a56714d1dbbf6d86193c245660e6acb4faffa2cec6f6e1d2583", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST/Iterator.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Array/ST/Partial.purs", + "vendored_sha256": "f834cdae62ba2e555925960d091ffc4d0af4f4c90c0cf3b98b9ae2446ba00258", + "vendored_lines": 38, + "self_recursions": [ + { + "name": "peekImpl", + "line": 25, + "equation": "peekImpl = peekImpl", + "replaces_upstream_foreign": true + }, + { + "name": "pokeImpl", + "line": 38, + "equation": "pokeImpl = pokeImpl", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-arrays", + "tag": "v7.3.0", + "commit": "6554b3d9c1ebb871477ffa88c2f3850d714b42b0", + "upstream_sha256": "dd9dfaae9bd0942eb341324035ed96880dcf2e167033ef8164d700270af5957a", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST/Partial.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "peekImpl", + "line": 24, + "declaration": "foreign import peekImpl :: forall h a. STFn2 Int (STArray h a) h a", + "vendored_status": "direct_self_recursion" + }, + { + "name": "pokeImpl", + "line": 36, + "declaration": "foreign import pokeImpl :: forall h a. STFn3 Int a (STArray h a) h Unit", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Array/ST.purs", + "vendored_sha256": "1c64f5c7bc92eb07d5d5bee4671e226c45ed1235a6854183a4cf71cf43da3024", + "vendored_lines": 265, + "self_recursions": [ + { + "name": "unsafeFreezeImpl", + "line": 79, + "equation": "unsafeFreezeImpl = unsafeFreezeImpl", + "replaces_upstream_foreign": true + }, + { + "name": "unsafeThawImpl", + "line": 87, + "equation": "unsafeThawImpl = unsafeThawImpl", + "replaces_upstream_foreign": true + }, + { + "name": "new", + "line": 91, + "equation": "new = new", + "replaces_upstream_foreign": true + }, + { + "name": "thawImpl", + "line": 101, + "equation": "thawImpl = thawImpl", + "replaces_upstream_foreign": true + }, + { + "name": "cloneImpl", + "line": 111, + "equation": "cloneImpl = cloneImpl", + "replaces_upstream_foreign": true + }, + { + "name": "shiftImpl", + "line": 123, + "equation": "shiftImpl = shiftImpl", + "replaces_upstream_foreign": true + }, + { + "name": "sortByImpl", + "line": 139, + "equation": "sortByImpl = sortByImpl", + "replaces_upstream_foreign": true + }, + { + "name": "freezeImpl", + "line": 159, + "equation": "freezeImpl = freezeImpl", + "replaces_upstream_foreign": true + }, + { + "name": "peekImpl", + "line": 170, + "equation": "peekImpl = peekImpl", + "replaces_upstream_foreign": true + }, + { + "name": "pokeImpl", + "line": 182, + "equation": "pokeImpl = pokeImpl", + "replaces_upstream_foreign": true + }, + { + "name": "lengthImpl", + "line": 185, + "equation": "lengthImpl = lengthImpl", + "replaces_upstream_foreign": true + }, + { + "name": "popImpl", + "line": 196, + "equation": "popImpl = popImpl", + "replaces_upstream_foreign": true + }, + { + "name": "pushImpl", + "line": 204, + "equation": "pushImpl = pushImpl", + "replaces_upstream_foreign": true + }, + { + "name": "pushAllImpl", + "line": 216, + "equation": "pushAllImpl = pushAllImpl", + "replaces_upstream_foreign": true + }, + { + "name": "unshiftAllImpl", + "line": 233, + "equation": "unshiftAllImpl = unshiftAllImpl", + "replaces_upstream_foreign": true + }, + { + "name": "spliceImpl", + "line": 254, + "equation": "spliceImpl = spliceImpl", + "replaces_upstream_foreign": true + }, + { + "name": "toAssocArrayImpl", + "line": 265, + "equation": "toAssocArrayImpl = toAssocArrayImpl", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-arrays", + "tag": "v7.3.0", + "commit": "6554b3d9c1ebb871477ffa88c2f3850d714b42b0", + "upstream_sha256": "4af1b59e242a89e3e5aa427c4486fce587173b3008f369d9d00de398a84ea4dc", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "unsafeFreezeImpl", + "line": 78, + "declaration": "foreign import unsafeFreezeImpl :: forall h a. STFn1 (STArray h a) h (Array a)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "unsafeThawImpl", + "line": 85, + "declaration": "foreign import unsafeThawImpl :: forall h a. STFn1 (Array a) h (STArray h a)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "new", + "line": 88, + "declaration": "foreign import new :: forall h a. ST h (STArray h a)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "thawImpl", + "line": 97, + "declaration": "foreign import thawImpl :: forall h a. STFn1 (Array a) h (STArray h a)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "cloneImpl", + "line": 106, + "declaration": "foreign import cloneImpl :: forall h a. STFn1 (STArray h a) h (STArray h a)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "shiftImpl", + "line": 117, + "declaration": "foreign import shiftImpl :: forall h a . STFn3 (forall b. b -> Maybe b) (forall b. Maybe b) (STArray h a) h (Maybe a)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "sortByImpl", + "line": 134, + "declaration": "foreign import sortByImpl :: forall a h . STFn3 (a -> a -> Ordering) (Ordering -> Int) (STArray h a) h (STArray h a)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "freezeImpl", + "line": 155, + "declaration": "foreign import freezeImpl :: forall h a. STFn1 (STArray h a) h (Array a)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "peekImpl", + "line": 165, + "declaration": "foreign import peekImpl :: forall h a r. STFn4 (a -> r) r Int (STArray h a) h r", + "vendored_status": "direct_self_recursion" + }, + { + "name": "pokeImpl", + "line": 176, + "declaration": "foreign import pokeImpl :: forall h a. STFn3 Int a (STArray h a) h Boolean", + "vendored_status": "direct_self_recursion" + }, + { + "name": "lengthImpl", + "line": 178, + "declaration": "foreign import lengthImpl :: forall h a. STFn1 (STArray h a) h Int", + "vendored_status": "direct_self_recursion" + }, + { + "name": "popImpl", + "line": 188, + "declaration": "foreign import popImpl :: forall h a . STFn3 (forall b. b -> Maybe b) (forall b. Maybe b) (STArray h a) h (Maybe a)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "pushImpl", + "line": 197, + "declaration": "foreign import pushImpl :: forall h a. STFn2 a (STArray h a) h Int", + "vendored_status": "direct_self_recursion" + }, + { + "name": "pushAllImpl", + "line": 208, + "declaration": "foreign import pushAllImpl :: forall h a . STFn2 (Array a) (STArray h a) h Int", + "vendored_status": "direct_self_recursion" + }, + { + "name": "unshiftAllImpl", + "line": 226, + "declaration": "foreign import unshiftAllImpl :: forall h a . STFn2 (Array a) (STArray h a) h Int", + "vendored_status": "direct_self_recursion" + }, + { + "name": "spliceImpl", + "line": 248, + "declaration": "foreign import spliceImpl :: forall h a . STFn4 Int Int (Array a) (STArray h a) h (Array a)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "toAssocArrayImpl", + "line": 260, + "declaration": "foreign import toAssocArrayImpl :: forall h a . STFn1 (STArray h a) h (Array (Assoc a))", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Array.purs", + "vendored_sha256": "3ad283dd652df5defdd1e9f58ca18f331a72e38ef725fc28150f2cb4c0335c0a", + "vendored_lines": 1335, + "self_recursions": [ + { + "name": "fromFoldableImpl", + "line": 178, + "equation": "fromFoldableImpl = fromFoldableImpl", + "replaces_upstream_foreign": true + }, + { + "name": "rangeImpl", + "line": 195, + "equation": "rangeImpl = rangeImpl", + "replaces_upstream_foreign": true + }, + { + "name": "replicateImpl", + "line": 205, + "equation": "replicateImpl = replicateImpl", + "replaces_upstream_foreign": true + }, + { + "name": "length", + "line": 245, + "equation": "length a0 = length a0", + "replaces_upstream_foreign": true + }, + { + "name": "unconsImpl", + "line": 376, + "equation": "unconsImpl = unconsImpl", + "replaces_upstream_foreign": true + }, + { + "name": "indexImpl", + "line": 407, + "equation": "indexImpl = indexImpl", + "replaces_upstream_foreign": true + }, + { + "name": "findMapImpl", + "line": 463, + "equation": "findMapImpl = findMapImpl", + "replaces_upstream_foreign": true + }, + { + "name": "findIndexImpl", + "line": 476, + "equation": "findIndexImpl = findIndexImpl", + "replaces_upstream_foreign": true + }, + { + "name": "findLastIndexImpl", + "line": 489, + "equation": "findLastIndexImpl = findLastIndexImpl", + "replaces_upstream_foreign": true + }, + { + "name": "_insertAt", + "line": 503, + "equation": "_insertAt = _insertAt", + "replaces_upstream_foreign": true + }, + { + "name": "_deleteAt", + "line": 517, + "equation": "_deleteAt = _deleteAt", + "replaces_upstream_foreign": true + }, + { + "name": "_updateAt", + "line": 531, + "equation": "_updateAt = _updateAt", + "replaces_upstream_foreign": true + }, + { + "name": "reverse", + "line": 605, + "equation": "reverse a0 = reverse a0", + "replaces_upstream_foreign": true + }, + { + "name": "concat", + "line": 614, + "equation": "concat a0 = concat a0", + "replaces_upstream_foreign": true + }, + { + "name": "filterImpl", + "line": 638, + "equation": "filterImpl = filterImpl", + "replaces_upstream_foreign": true + }, + { + "name": "partitionImpl", + "line": 656, + "equation": "partitionImpl = partitionImpl", + "replaces_upstream_foreign": true + }, + { + "name": "scanlImpl", + "line": 820, + "equation": "scanlImpl = scanlImpl", + "replaces_upstream_foreign": true + }, + { + "name": "scanrImpl", + "line": 834, + "equation": "scanrImpl = scanrImpl", + "replaces_upstream_foreign": true + }, + { + "name": "sortByImpl", + "line": 879, + "equation": "sortByImpl = sortByImpl", + "replaces_upstream_foreign": true + }, + { + "name": "sliceImpl", + "line": 898, + "equation": "sliceImpl = sliceImpl", + "replaces_upstream_foreign": true + }, + { + "name": "zipWithImpl", + "line": 1220, + "equation": "zipWithImpl = zipWithImpl", + "replaces_upstream_foreign": true + }, + { + "name": "anyImpl", + "line": 1284, + "equation": "anyImpl = anyImpl", + "replaces_upstream_foreign": true + }, + { + "name": "allImpl", + "line": 1299, + "equation": "allImpl = allImpl", + "replaces_upstream_foreign": true + }, + { + "name": "unsafeIndexImpl", + "line": 1335, + "equation": "unsafeIndexImpl = unsafeIndexImpl", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-arrays", + "tag": "v7.3.0", + "commit": "6554b3d9c1ebb871477ffa88c2f3850d714b42b0", + "upstream_sha256": "d336825bf660ee79f03c559eaa1a22f41ec5a194b8cce79c5b24877160c32455", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "fromFoldableImpl", + "line": 177, + "declaration": "foreign import fromFoldableImpl :: forall f a . Fn2 (forall b. (a -> b -> b) -> b -> f a -> b) (f a) (Array a)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "rangeImpl", + "line": 195, + "declaration": "foreign import rangeImpl :: Fn2 Int Int (Array Int)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "replicateImpl", + "line": 204, + "declaration": "foreign import replicateImpl :: forall a. Fn2 Int a (Array a)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "length", + "line": 243, + "declaration": "foreign import length :: forall a. Array a -> Int", + "vendored_status": "direct_self_recursion" + }, + { + "name": "unconsImpl", + "line": 373, + "declaration": "foreign import unconsImpl :: forall a b . Fn3 (Unit -> b) (a -> Array a -> b) (Array a) b", + "vendored_status": "direct_self_recursion" + }, + { + "name": "indexImpl", + "line": 405, + "declaration": "foreign import indexImpl :: forall a . Fn4 (forall r. r -> Maybe r) (forall r. Maybe r) (Array a) Int (Maybe a)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "findMapImpl", + "line": 462, + "declaration": "foreign import findMapImpl :: forall a b . Fn4 (forall c. Maybe c) (forall c. Maybe c -> Boolean) (a -> Maybe b) (Array a) (Maybe b)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "findIndexImpl", + "line": 481, + "declaration": "foreign import findIndexImpl :: forall a . Fn4 (forall b. b -> Maybe b) (forall b. Maybe b) (a -> Boolean) (Array a) (Maybe Int)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "findLastIndexImpl", + "line": 500, + "declaration": "foreign import findLastIndexImpl :: forall a . Fn4 (forall b. b -> Maybe b) (forall b. Maybe b) (a -> Boolean) (Array a) (Maybe Int)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "_insertAt", + "line": 520, + "declaration": "foreign import _insertAt :: forall a . Fn5 (forall b. b -> Maybe b) (forall b. Maybe b) Int a (Array a) (Maybe (Array a))", + "vendored_status": "direct_self_recursion" + }, + { + "name": "_deleteAt", + "line": 541, + "declaration": "foreign import _deleteAt :: forall a . Fn4 (forall b. b -> Maybe b) (forall b. Maybe b) Int (Array a) (Maybe (Array a))", + "vendored_status": "direct_self_recursion" + }, + { + "name": "_updateAt", + "line": 561, + "declaration": "foreign import _updateAt :: forall a . Fn5 (forall b. b -> Maybe b) (forall b. Maybe b) Int a (Array a) (Maybe (Array a))", + "vendored_status": "direct_self_recursion" + }, + { + "name": "reverse", + "line": 642, + "declaration": "foreign import reverse :: forall a. Array a -> Array a", + "vendored_status": "direct_self_recursion" + }, + { + "name": "concat", + "line": 650, + "declaration": "foreign import concat :: forall a. Array (Array a) -> Array a", + "vendored_status": "direct_self_recursion" + }, + { + "name": "filterImpl", + "line": 673, + "declaration": "foreign import filterImpl :: forall a . Fn2 (a -> Boolean) (Array a) (Array a)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "partitionImpl", + "line": 692, + "declaration": "foreign import partitionImpl :: forall a . Fn2 (a -> Boolean) (Array a) { yes :: Array a, no :: Array a }", + "vendored_status": "direct_self_recursion" + }, + { + "name": "scanlImpl", + "line": 857, + "declaration": "foreign import scanlImpl :: forall a b. Fn3 (b -> a -> b) b (Array a) (Array b)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "scanrImpl", + "line": 870, + "declaration": "foreign import scanrImpl :: forall a b. Fn3 (a -> b -> b) b (Array a) (Array b)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "sortByImpl", + "line": 914, + "declaration": "foreign import sortByImpl :: forall a. Fn3 (a -> a -> Ordering) (Ordering -> Int) (Array a) (Array a)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "sliceImpl", + "line": 932, + "declaration": "foreign import sliceImpl :: forall a. Fn3 Int Int (Array a) (Array a)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "zipWithImpl", + "line": 1253, + "declaration": "foreign import zipWithImpl :: forall a b c . Fn3 (a -> b -> c) (Array a) (Array b) (Array c)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "anyImpl", + "line": 1322, + "declaration": "foreign import anyImpl :: forall a. Fn2 (a -> Boolean) (Array a) Boolean", + "vendored_status": "direct_self_recursion" + }, + { + "name": "allImpl", + "line": 1336, + "declaration": "foreign import allImpl :: forall a. Fn2 (a -> Boolean) (Array a) Boolean", + "vendored_status": "direct_self_recursion" + }, + { + "name": "unsafeIndexImpl", + "line": 1371, + "declaration": "foreign import unsafeIndexImpl :: forall a. Fn2 (Array a) Int a", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Bifoldable.purs", + "vendored_sha256": "ba0f8f91040f1e49a0ef10ad012eb9be475fa820332eb3d6bca9345c1a3c4446", + "vendored_lines": 198, + "self_recursions": [], + "package": "purescript-foldable-traversable", + "tag": "v6.0.0", + "commit": "b3926f870532d287ea59e2d5cd3873b81ef2a93a", + "upstream_sha256": "ba0f8f91040f1e49a0ef10ad012eb9be475fa820332eb3d6bca9345c1a3c4446", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/Bifoldable.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Bifunctor/Join.purs", + "vendored_sha256": "5db9d1533e678809804465de95637fa980d264314428d25f18e10b673bba1308", + "vendored_lines": 31, + "self_recursions": [], + "package": "purescript-bifunctors", + "tag": "v6.1.0", + "commit": "d35e0f1e5a33d59a226859b6bc53075fb58ced87", + "upstream_sha256": "5db9d1533e678809804465de95637fa980d264314428d25f18e10b673bba1308", + "upstream_url": "https://github.com/purescript/purescript-bifunctors/blob/d35e0f1e5a33d59a226859b6bc53075fb58ced87/src/Data/Bifunctor/Join.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Bifunctor.purs", + "vendored_sha256": "00c91e7e32934e59b1196a793247218670ed5ad6c69acb8ef4eec27db63a8e1a", + "vendored_lines": 46, + "self_recursions": [], + "package": "purescript-bifunctors", + "tag": "v6.1.0", + "commit": "d35e0f1e5a33d59a226859b6bc53075fb58ced87", + "upstream_sha256": "00c91e7e32934e59b1196a793247218670ed5ad6c69acb8ef4eec27db63a8e1a", + "upstream_url": "https://github.com/purescript/purescript-bifunctors/blob/d35e0f1e5a33d59a226859b6bc53075fb58ced87/src/Data/Bifunctor.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Bitraversable.purs", + "vendored_sha256": "1f6eae4a9fb4457664d7fa4c822b451d15a1d320a28b12f81010622885e2cb84", + "vendored_lines": 136, + "self_recursions": [], + "package": "purescript-foldable-traversable", + "tag": "v6.0.0", + "commit": "b3926f870532d287ea59e2d5cd3873b81ef2a93a", + "upstream_sha256": "1f6eae4a9fb4457664d7fa4c822b451d15a1d320a28b12f81010622885e2cb84", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/Bitraversable.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Boolean.purs", + "vendored_sha256": "cbffb99db81cebc7626ab43b43f785f62ffe8c59b3781174907b05d8fb171eb2", + "vendored_lines": 10, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "cbffb99db81cebc7626ab43b43f785f62ffe8c59b3781174907b05d8fb171eb2", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Boolean.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/BooleanAlgebra.purs", + "vendored_sha256": "cc5bcc84af3a96675d3b8ee03b7754a818dd0dd49d20248cf0e17c4793f5ba96", + "vendored_lines": 43, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "cc5bcc84af3a96675d3b8ee03b7754a818dd0dd49d20248cf0e17c4793f5ba96", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/BooleanAlgebra.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Bounded/Generic.purs", + "vendored_sha256": "110cac55f2eadb6a6614397a31b5c167e20983d3ccdce89647ee975e24058f3f", + "vendored_lines": 56, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "110cac55f2eadb6a6614397a31b5c167e20983d3ccdce89647ee975e24058f3f", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Bounded/Generic.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Bounded.purs", + "vendored_sha256": "eae601093ee50d2d6134073077d098b8d87a141a9bc5237bad185107fc5a4c10", + "vendored_lines": 111, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "f67cb72dce353b0eecdd53214a76f879e52eba6335f1305c2ea744063c913d0a", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Bounded.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "topInt", + "line": 40, + "declaration": "foreign import topInt :: Int", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "bottomInt", + "line": 41, + "declaration": "foreign import bottomInt :: Int", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "topChar", + "line": 48, + "declaration": "foreign import topChar :: Char", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "bottomChar", + "line": 49, + "declaration": "foreign import bottomChar :: Char", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "topNumber", + "line": 59, + "declaration": "foreign import topNumber :: Number", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "bottomNumber", + "line": 60, + "declaration": "foreign import bottomNumber :: Number", + "vendored_status": "nonrecursive_replacement" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Char/Gen.purs", + "vendored_sha256": "3ddf70117671935799e26aade08e1f932221200ab98a1b50c90ad50c090a6ec2", + "vendored_lines": 35, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "3ddf70117671935799e26aade08e1f932221200ab98a1b50c90ad50c090a6ec2", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/Char/Gen.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Char.purs", + "vendored_sha256": "3923ba1cf2abddfc3d4bebb1a2f84f198aa9b23b1fae8e68a281f2188cc79ee7", + "vendored_lines": 16, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "3923ba1cf2abddfc3d4bebb1a2f84f198aa9b23b1fae8e68a281f2188cc79ee7", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/Char.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/CommutativeRing.purs", + "vendored_sha256": "bcb4f94840a1544d53287ef6d44e51f8af66119f12910d3678f101a78a7fea05", + "vendored_lines": 44, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "bcb4f94840a1544d53287ef6d44e51f8af66119f12910d3678f101a78a7fea05", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/CommutativeRing.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Compactable.purs", + "vendored_sha256": "431fbe83796ed62eecb1e3311a7f4a7e5601aee50c611fcb11eff5f29bfe143a", + "vendored_lines": 164, + "self_recursions": [], + "package": "purescript-filterable", + "tag": "v5.0.0", + "commit": "7c5b8c72779997f2b17d12ce478ff81e7ddda285", + "upstream_sha256": "431fbe83796ed62eecb1e3311a7f4a7e5601aee50c611fcb11eff5f29bfe143a", + "upstream_url": "https://github.com/purescript/purescript-filterable/blob/7c5b8c72779997f2b17d12ce478ff81e7ddda285/src/Data/Compactable.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Comparison.purs", + "vendored_sha256": "b94e588dbd3b667ef023394842ed4d0e2afb10b8b315173f5e1095fc3094fc02", + "vendored_lines": 25, + "self_recursions": [], + "package": "purescript-contravariant", + "tag": "v6.0.0", + "commit": "9ad3e105b8855bcc25f4e0893c784789d05a58de", + "upstream_sha256": "b94e588dbd3b667ef023394842ed4d0e2afb10b8b315173f5e1095fc3094fc02", + "upstream_url": "https://github.com/purescript/purescript-contravariant/blob/9ad3e105b8855bcc25f4e0893c784789d05a58de/src/Data/Comparison.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Const.purs", + "vendored_sha256": "a6e5d9429e810f5b834720a35e3103c4f271ca6401b19e7f7f8250e96cb04828", + "vendored_lines": 63, + "self_recursions": [], + "package": "purescript-const", + "tag": "v6.0.0", + "commit": "ab9570cf2b6e67f7e441178211db1231cfd75c37", + "upstream_sha256": "a6e5d9429e810f5b834720a35e3103c4f271ca6401b19e7f7f8250e96cb04828", + "upstream_url": "https://github.com/purescript/purescript-const/blob/ab9570cf2b6e67f7e441178211db1231cfd75c37/src/Data/Const.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Decidable.purs", + "vendored_sha256": "d3260262715019947e3db7351810c3af103bd86cc4e94dcca3a28c22a3d3072a", + "vendored_lines": 29, + "self_recursions": [], + "package": "purescript-contravariant", + "tag": "v6.0.0", + "commit": "9ad3e105b8855bcc25f4e0893c784789d05a58de", + "upstream_sha256": "d3260262715019947e3db7351810c3af103bd86cc4e94dcca3a28c22a3d3072a", + "upstream_url": "https://github.com/purescript/purescript-contravariant/blob/9ad3e105b8855bcc25f4e0893c784789d05a58de/src/Data/Decidable.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Decide.purs", + "vendored_sha256": "0301661ba4052b57c193b57cac94dd78ebdb40ae91eb1d182637e6a68d8c9718", + "vendored_lines": 42, + "self_recursions": [], + "package": "purescript-contravariant", + "tag": "v6.0.0", + "commit": "9ad3e105b8855bcc25f4e0893c784789d05a58de", + "upstream_sha256": "0301661ba4052b57c193b57cac94dd78ebdb40ae91eb1d182637e6a68d8c9718", + "upstream_url": "https://github.com/purescript/purescript-contravariant/blob/9ad3e105b8855bcc25f4e0893c784789d05a58de/src/Data/Decide.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Distributive.purs", + "vendored_sha256": "47bf3b62d71dbeef667007924b75ea18cc4179ef1f111a359d19463e1fa49279", + "vendored_lines": 67, + "self_recursions": [], + "package": "purescript-distributive", + "tag": "v6.0.0", + "commit": "6005e513642e855ebf6f884d24a35c2803ca252a", + "upstream_sha256": "47bf3b62d71dbeef667007924b75ea18cc4179ef1f111a359d19463e1fa49279", + "upstream_url": "https://github.com/purescript/purescript-distributive/blob/6005e513642e855ebf6f884d24a35c2803ca252a/src/Data/Distributive.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Divide.purs", + "vendored_sha256": "cc9f771f056da1b686569a299b46fd0d09be4544bda5e95a01551ce4e0f2d021", + "vendored_lines": 46, + "self_recursions": [], + "package": "purescript-contravariant", + "tag": "v6.0.0", + "commit": "9ad3e105b8855bcc25f4e0893c784789d05a58de", + "upstream_sha256": "cc9f771f056da1b686569a299b46fd0d09be4544bda5e95a01551ce4e0f2d021", + "upstream_url": "https://github.com/purescript/purescript-contravariant/blob/9ad3e105b8855bcc25f4e0893c784789d05a58de/src/Data/Divide.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Divisible.purs", + "vendored_sha256": "fe939175ea5070c0233b11fbf6dd035bae54a6e590dc47253977bbfba7beae82", + "vendored_lines": 25, + "self_recursions": [], + "package": "purescript-contravariant", + "tag": "v6.0.0", + "commit": "9ad3e105b8855bcc25f4e0893c784789d05a58de", + "upstream_sha256": "fe939175ea5070c0233b11fbf6dd035bae54a6e590dc47253977bbfba7beae82", + "upstream_url": "https://github.com/purescript/purescript-contravariant/blob/9ad3e105b8855bcc25f4e0893c784789d05a58de/src/Data/Divisible.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/DivisionRing.purs", + "vendored_sha256": "be3721f83db50d4d1bcc6f2cba1cb30e2be39b376440e215c79ab0e0d7b688f5", + "vendored_lines": 55, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "be3721f83db50d4d1bcc6f2cba1cb30e2be39b376440e215c79ab0e0d7b688f5", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/DivisionRing.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Either/Inject.purs", + "vendored_sha256": "95f071e80bf9658d34b08093bc03ee3fbbb9ef6af3457ad4f203c396ea507e41", + "vendored_lines": 23, + "self_recursions": [], + "package": "purescript-either", + "tag": "v6.1.0", + "commit": "af655a04ed2fd694b6688af39ee20d7907ad0763", + "upstream_sha256": "95f071e80bf9658d34b08093bc03ee3fbbb9ef6af3457ad4f203c396ea507e41", + "upstream_url": "https://github.com/purescript/purescript-either/blob/af655a04ed2fd694b6688af39ee20d7907ad0763/src/Data/Either/Inject.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Either/Nested.purs", + "vendored_sha256": "fa22a911e6c9f62dd7f1bfabfb98962824f75a7f5291e6fafc628dba658cb274", + "vendored_lines": 278, + "self_recursions": [], + "package": "purescript-either", + "tag": "v6.1.0", + "commit": "af655a04ed2fd694b6688af39ee20d7907ad0763", + "upstream_sha256": "fa22a911e6c9f62dd7f1bfabfb98962824f75a7f5291e6fafc628dba658cb274", + "upstream_url": "https://github.com/purescript/purescript-either/blob/af655a04ed2fd694b6688af39ee20d7907ad0763/src/Data/Either/Nested.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Either.purs", + "vendored_sha256": "047a8ae72c32cd064fddb773a739dc130c0cce615270d403491898fbf6c6f5d9", + "vendored_lines": 294, + "self_recursions": [], + "package": "purescript-either", + "tag": "v6.1.0", + "commit": "af655a04ed2fd694b6688af39ee20d7907ad0763", + "upstream_sha256": "047a8ae72c32cd064fddb773a739dc130c0cce615270d403491898fbf6c6f5d9", + "upstream_url": "https://github.com/purescript/purescript-either/blob/af655a04ed2fd694b6688af39ee20d7907ad0763/src/Data/Either.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Enum/Gen.purs", + "vendored_sha256": "3d7234b9dd417d2a31510cf22c691da3cfd0bb4a7bc2265c68cbae019dbcdc13", + "vendored_lines": 18, + "self_recursions": [], + "package": "purescript-enums", + "tag": "v6.0.1", + "commit": "cd373c580b69fdc00e412bddbc299adabe242cc5", + "upstream_sha256": "3d7234b9dd417d2a31510cf22c691da3cfd0bb4a7bc2265c68cbae019dbcdc13", + "upstream_url": "https://github.com/purescript/purescript-enums/blob/cd373c580b69fdc00e412bddbc299adabe242cc5/src/Data/Enum/Gen.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Enum/Generic.purs", + "vendored_sha256": "1c9eea073e25c24c7cca9455487ebec47f0072b1ecba7e2076e7da56fa473846", + "vendored_lines": 118, + "self_recursions": [], + "package": "purescript-enums", + "tag": "v6.0.1", + "commit": "cd373c580b69fdc00e412bddbc299adabe242cc5", + "upstream_sha256": "1c9eea073e25c24c7cca9455487ebec47f0072b1ecba7e2076e7da56fa473846", + "upstream_url": "https://github.com/purescript/purescript-enums/blob/cd373c580b69fdc00e412bddbc299adabe242cc5/src/Data/Enum/Generic.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Enum.purs", + "vendored_sha256": "ce04da7763ab6d7e6a00d63702f6dfd38f0b9101d101b04851281badc8445dfa", + "vendored_lines": 323, + "self_recursions": [ + { + "name": "toCharCode", + "line": 321, + "equation": "toCharCode a0 = toCharCode a0", + "replaces_upstream_foreign": true + }, + { + "name": "fromCharCode", + "line": 323, + "equation": "fromCharCode a0 = fromCharCode a0", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-enums", + "tag": "v6.0.1", + "commit": "cd373c580b69fdc00e412bddbc299adabe242cc5", + "upstream_sha256": "a7ffaa7a8cc15c362852409f1aba80a5b7792d89353635d54317d43e864e5e9c", + "upstream_url": "https://github.com/purescript/purescript-enums/blob/cd373c580b69fdc00e412bddbc299adabe242cc5/src/Data/Enum.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "toCharCode", + "line": 320, + "declaration": "foreign import toCharCode :: Char -> Int", + "vendored_status": "direct_self_recursion" + }, + { + "name": "fromCharCode", + "line": 321, + "declaration": "foreign import fromCharCode :: Int -> Char", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Eq/Generic.purs", + "vendored_sha256": "9cd6184fb35329e232a2b593e1e3404e89692b36b99104f800bfe23894627373", + "vendored_lines": 35, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "9cd6184fb35329e232a2b593e1e3404e89692b36b99104f800bfe23894627373", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Eq/Generic.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Eq.purs", + "vendored_sha256": "fdc7df1e5faaa15fdbb1a57c247592f4bc31ca8c9b794edf3fd42280f6270cb7", + "vendored_lines": 134, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "53036679ea014e3c452fce0ee8ce26f58de222cd88eed2586d6dd7dcea3fd872", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Eq.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "eqBooleanImpl", + "line": 77, + "declaration": "foreign import eqBooleanImpl :: Boolean -> Boolean -> Boolean", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "eqIntImpl", + "line": 78, + "declaration": "foreign import eqIntImpl :: Int -> Int -> Boolean", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "eqNumberImpl", + "line": 79, + "declaration": "foreign import eqNumberImpl :: Number -> Number -> Boolean", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "eqCharImpl", + "line": 80, + "declaration": "foreign import eqCharImpl :: Char -> Char -> Boolean", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "eqStringImpl", + "line": 81, + "declaration": "foreign import eqStringImpl :: String -> String -> Boolean", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "eqArrayImpl", + "line": 83, + "declaration": "foreign import eqArrayImpl :: forall a. (a -> a -> Boolean) -> Array a -> Array a -> Boolean", + "vendored_status": "nonrecursive_replacement" + } + ], + "changed_original_code_outside_value_ffi": [ + { + "line": 48, + "text": " eq = eqBooleanImpl" + }, + { + "line": 51, + "text": " eq = eqIntImpl" + }, + { + "line": 54, + "text": " eq = eqNumberImpl" + }, + { + "line": 57, + "text": " eq = eqCharImpl" + }, + { + "line": 60, + "text": " eq = eqStringImpl" + }, + { + "line": 69, + "text": " eq = eqArrayImpl eq" + } + ] + }, + { + "path": "Data/Equivalence.purs", + "vendored_sha256": "82d547469f77f4be0ba5c8f151d623a12a2a3edaa3d7b65c7f01110ed696932f", + "vendored_lines": 31, + "self_recursions": [], + "package": "purescript-contravariant", + "tag": "v6.0.0", + "commit": "9ad3e105b8855bcc25f4e0893c784789d05a58de", + "upstream_sha256": "82d547469f77f4be0ba5c8f151d623a12a2a3edaa3d7b65c7f01110ed696932f", + "upstream_url": "https://github.com/purescript/purescript-contravariant/blob/9ad3e105b8855bcc25f4e0893c784789d05a58de/src/Data/Equivalence.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/EuclideanRing.purs", + "vendored_sha256": "45b6572552d2728c43d0c00668239ca748f6522512d9d093aeefb719a3bb927f", + "vendored_lines": 105, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "15d83e118a173ae91fff5b12152afdacc549c12da0a541602d0cf5d702fae863", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/EuclideanRing.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "intDegree", + "line": 84, + "declaration": "foreign import intDegree :: Int -> Int", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "intDiv", + "line": 85, + "declaration": "foreign import intDiv :: Int -> Int -> Int", + "vendored_status": "declaration_removed" + }, + { + "name": "intMod", + "line": 86, + "declaration": "foreign import intMod :: Int -> Int -> Int", + "vendored_status": "declaration_removed" + }, + { + "name": "numDiv", + "line": 88, + "declaration": "foreign import numDiv :: Number -> Number -> Number", + "vendored_status": "nonrecursive_replacement" + } + ], + "changed_original_code_outside_value_ffi": [ + { + "line": 75, + "text": " degree = intDegree" + }, + { + "line": 76, + "text": " div = intDiv" + }, + { + "line": 77, + "text": " mod = intMod" + }, + { + "line": 81, + "text": " div = numDiv" + } + ] + }, + { + "path": "Data/Exists.purs", + "vendored_sha256": "74aec7e155ecd0bac49cacb3f87506223193a5f0ff4ae1dd0a8e134775d7794d", + "vendored_lines": 57, + "self_recursions": [], + "package": "purescript-exists", + "tag": "v6.0.0", + "commit": "f765b4ace7869c27b9c05949e18c843881f9173b", + "upstream_sha256": "74aec7e155ecd0bac49cacb3f87506223193a5f0ff4ae1dd0a8e134775d7794d", + "upstream_url": "https://github.com/purescript/purescript-exists/blob/f765b4ace7869c27b9c05949e18c843881f9173b/src/Data/Exists.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Field.purs", + "vendored_sha256": "a27a8bb18ff139cd8eb5c7512c9aa999050faad79f02727e285360b2cda5f06d", + "vendored_lines": 41, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "a27a8bb18ff139cd8eb5c7512c9aa999050faad79f02727e285360b2cda5f06d", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Field.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Filterable.purs", + "vendored_sha256": "459ef72163c124e3ea0f24a4682773ba8562dc9a1e8709a994eb3bd0aa3895bf", + "vendored_lines": 229, + "self_recursions": [], + "package": "purescript-filterable", + "tag": "v5.0.0", + "commit": "7c5b8c72779997f2b17d12ce478ff81e7ddda285", + "upstream_sha256": "459ef72163c124e3ea0f24a4682773ba8562dc9a1e8709a994eb3bd0aa3895bf", + "upstream_url": "https://github.com/purescript/purescript-filterable/blob/7c5b8c72779997f2b17d12ce478ff81e7ddda285/src/Data/Filterable.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Foldable.purs", + "vendored_sha256": "3b833665daa379ff65f822737d6dccef23c1c068b4f813d0519e765285095ae0", + "vendored_lines": 473, + "self_recursions": [ + { + "name": "foldrArray", + "line": 136, + "equation": "foldrArray a0 a1 a2 = foldrArray a0 a1 a2", + "replaces_upstream_foreign": true + }, + { + "name": "foldlArray", + "line": 138, + "equation": "foldlArray a0 a1 a2 = foldlArray a0 a1 a2", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-foldable-traversable", + "tag": "v6.0.0", + "commit": "b3926f870532d287ea59e2d5cd3873b81ef2a93a", + "upstream_sha256": "5557319b5a97cf265d53cd68266d721b31f28de3240b10e7728adae8a2070052", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/Foldable.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "foldrArray", + "line": 135, + "declaration": "foreign import foldrArray :: forall a b. (a -> b -> b) -> b -> Array a -> b", + "vendored_status": "direct_self_recursion" + }, + { + "name": "foldlArray", + "line": 136, + "declaration": "foreign import foldlArray :: forall a b. (b -> a -> b) -> b -> Array a -> b", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [ + { + "line": 226, + "text": "fold = foldMap identity" + } + ] + }, + { + "path": "Data/FoldableWithIndex.purs", + "vendored_sha256": "2e10b31b454a84f9597ddb15e43fcfa5bfa84cae8cc5e8b6bbbc5a2018755944", + "vendored_lines": 370, + "self_recursions": [], + "package": "purescript-foldable-traversable", + "tag": "v6.0.0", + "commit": "b3926f870532d287ea59e2d5cd3873b81ef2a93a", + "upstream_sha256": "2e10b31b454a84f9597ddb15e43fcfa5bfa84cae8cc5e8b6bbbc5a2018755944", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/FoldableWithIndex.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Function/Uncurried.purs", + "vendored_sha256": "2b7328f13a13d5cf244fd08aba8c6e35027ac514de02b8a92ce13c0232141eaa", + "vendored_lines": 144, + "self_recursions": [ + { + "name": "mkFn0", + "line": 60, + "equation": "mkFn0 a0 = mkFn0 a0", + "replaces_upstream_foreign": true + }, + { + "name": "mkFn2", + "line": 68, + "equation": "mkFn2 a0 = mkFn2 a0", + "replaces_upstream_foreign": true + }, + { + "name": "mkFn3", + "line": 72, + "equation": "mkFn3 a0 = mkFn3 a0", + "replaces_upstream_foreign": true + }, + { + "name": "mkFn4", + "line": 76, + "equation": "mkFn4 a0 = mkFn4 a0", + "replaces_upstream_foreign": true + }, + { + "name": "mkFn5", + "line": 80, + "equation": "mkFn5 a0 = mkFn5 a0", + "replaces_upstream_foreign": true + }, + { + "name": "mkFn6", + "line": 84, + "equation": "mkFn6 a0 = mkFn6 a0", + "replaces_upstream_foreign": true + }, + { + "name": "mkFn7", + "line": 88, + "equation": "mkFn7 a0 = mkFn7 a0", + "replaces_upstream_foreign": true + }, + { + "name": "mkFn8", + "line": 92, + "equation": "mkFn8 a0 = mkFn8 a0", + "replaces_upstream_foreign": true + }, + { + "name": "mkFn9", + "line": 96, + "equation": "mkFn9 a0 = mkFn9 a0", + "replaces_upstream_foreign": true + }, + { + "name": "mkFn10", + "line": 100, + "equation": "mkFn10 a0 = mkFn10 a0", + "replaces_upstream_foreign": true + }, + { + "name": "runFn0", + "line": 104, + "equation": "runFn0 a0 = runFn0 a0", + "replaces_upstream_foreign": true + }, + { + "name": "runFn2", + "line": 112, + "equation": "runFn2 a0 a1 a2 = runFn2 a0 a1 a2", + "replaces_upstream_foreign": true + }, + { + "name": "runFn3", + "line": 116, + "equation": "runFn3 a0 a1 a2 a3 = runFn3 a0 a1 a2 a3", + "replaces_upstream_foreign": true + }, + { + "name": "runFn4", + "line": 120, + "equation": "runFn4 a0 a1 a2 a3 a4 = runFn4 a0 a1 a2 a3 a4", + "replaces_upstream_foreign": true + }, + { + "name": "runFn5", + "line": 124, + "equation": "runFn5 a0 a1 a2 a3 a4 a5 = runFn5 a0 a1 a2 a3 a4 a5", + "replaces_upstream_foreign": true + }, + { + "name": "runFn6", + "line": 128, + "equation": "runFn6 a0 a1 a2 a3 a4 a5 a6 = runFn6 a0 a1 a2 a3 a4 a5 a6", + "replaces_upstream_foreign": true + }, + { + "name": "runFn7", + "line": 132, + "equation": "runFn7 a0 a1 a2 a3 a4 a5 a6 a7 = runFn7 a0 a1 a2 a3 a4 a5 a6 a7", + "replaces_upstream_foreign": true + }, + { + "name": "runFn8", + "line": 136, + "equation": "runFn8 a0 a1 a2 a3 a4 a5 a6 a7 a8 = runFn8 a0 a1 a2 a3 a4 a5 a6 a7 a8", + "replaces_upstream_foreign": true + }, + { + "name": "runFn9", + "line": 140, + "equation": "runFn9 a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 = runFn9 a0 a1 a2 a3 a4 a5 a6 a7 a8 a9", + "replaces_upstream_foreign": true + }, + { + "name": "runFn10", + "line": 144, + "equation": "runFn10 a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 = runFn10 a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-functions", + "tag": "v6.0.0", + "commit": "f626f20580483977c5b27a01aac6471e28aff367", + "upstream_sha256": "4e210a49f64e1862cc8530bcbe661c350c964568bbfa2f79bbb1fbe394152d02", + "upstream_url": "https://github.com/purescript/purescript-functions/blob/f626f20580483977c5b27a01aac6471e28aff367/src/Data/Function/Uncurried.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "mkFn0", + "line": 59, + "declaration": "foreign import mkFn0 :: forall a. (Unit -> a) -> Fn0 a", + "vendored_status": "direct_self_recursion" + }, + { + "name": "mkFn2", + "line": 66, + "declaration": "foreign import mkFn2 :: forall a b c. (a -> b -> c) -> Fn2 a b c", + "vendored_status": "direct_self_recursion" + }, + { + "name": "mkFn3", + "line": 69, + "declaration": "foreign import mkFn3 :: forall a b c d. (a -> b -> c -> d) -> Fn3 a b c d", + "vendored_status": "direct_self_recursion" + }, + { + "name": "mkFn4", + "line": 72, + "declaration": "foreign import mkFn4 :: forall a b c d e. (a -> b -> c -> d -> e) -> Fn4 a b c d e", + "vendored_status": "direct_self_recursion" + }, + { + "name": "mkFn5", + "line": 75, + "declaration": "foreign import mkFn5 :: forall a b c d e f. (a -> b -> c -> d -> e -> f) -> Fn5 a b c d e f", + "vendored_status": "direct_self_recursion" + }, + { + "name": "mkFn6", + "line": 78, + "declaration": "foreign import mkFn6 :: forall a b c d e f g. (a -> b -> c -> d -> e -> f -> g) -> Fn6 a b c d e f g", + "vendored_status": "direct_self_recursion" + }, + { + "name": "mkFn7", + "line": 81, + "declaration": "foreign import mkFn7 :: forall a b c d e f g h. (a -> b -> c -> d -> e -> f -> g -> h) -> Fn7 a b c d e f g h", + "vendored_status": "direct_self_recursion" + }, + { + "name": "mkFn8", + "line": 84, + "declaration": "foreign import mkFn8 :: forall a b c d e f g h i. (a -> b -> c -> d -> e -> f -> g -> h -> i) -> Fn8 a b c d e f g h i", + "vendored_status": "direct_self_recursion" + }, + { + "name": "mkFn9", + "line": 87, + "declaration": "foreign import mkFn9 :: forall a b c d e f g h i j. (a -> b -> c -> d -> e -> f -> g -> h -> i -> j) -> Fn9 a b c d e f g h i j", + "vendored_status": "direct_self_recursion" + }, + { + "name": "mkFn10", + "line": 90, + "declaration": "foreign import mkFn10 :: forall a b c d e f g h i j k. (a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> k) -> Fn10 a b c d e f g h i j k", + "vendored_status": "direct_self_recursion" + }, + { + "name": "runFn0", + "line": 93, + "declaration": "foreign import runFn0 :: forall a. Fn0 a -> a", + "vendored_status": "direct_self_recursion" + }, + { + "name": "runFn2", + "line": 100, + "declaration": "foreign import runFn2 :: forall a b c. Fn2 a b c -> a -> b -> c", + "vendored_status": "direct_self_recursion" + }, + { + "name": "runFn3", + "line": 103, + "declaration": "foreign import runFn3 :: forall a b c d. Fn3 a b c d -> a -> b -> c -> d", + "vendored_status": "direct_self_recursion" + }, + { + "name": "runFn4", + "line": 106, + "declaration": "foreign import runFn4 :: forall a b c d e. Fn4 a b c d e -> a -> b -> c -> d -> e", + "vendored_status": "direct_self_recursion" + }, + { + "name": "runFn5", + "line": 109, + "declaration": "foreign import runFn5 :: forall a b c d e f. Fn5 a b c d e f -> a -> b -> c -> d -> e -> f", + "vendored_status": "direct_self_recursion" + }, + { + "name": "runFn6", + "line": 112, + "declaration": "foreign import runFn6 :: forall a b c d e f g. Fn6 a b c d e f g -> a -> b -> c -> d -> e -> f -> g", + "vendored_status": "direct_self_recursion" + }, + { + "name": "runFn7", + "line": 115, + "declaration": "foreign import runFn7 :: forall a b c d e f g h. Fn7 a b c d e f g h -> a -> b -> c -> d -> e -> f -> g -> h", + "vendored_status": "direct_self_recursion" + }, + { + "name": "runFn8", + "line": 118, + "declaration": "foreign import runFn8 :: forall a b c d e f g h i. Fn8 a b c d e f g h i -> a -> b -> c -> d -> e -> f -> g -> h -> i", + "vendored_status": "direct_self_recursion" + }, + { + "name": "runFn9", + "line": 121, + "declaration": "foreign import runFn9 :: forall a b c d e f g h i j. Fn9 a b c d e f g h i j -> a -> b -> c -> d -> e -> f -> g -> h -> i -> j", + "vendored_status": "direct_self_recursion" + }, + { + "name": "runFn10", + "line": 124, + "declaration": "foreign import 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", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Function.purs", + "vendored_sha256": "2142b200f60953ac4fe648b765da7cf6cfb046e8600ffef3484a7f3e4f8f03b7", + "vendored_lines": 120, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "2142b200f60953ac4fe648b765da7cf6cfb046e8600ffef3484a7f3e4f8f03b7", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Function.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/App.purs", + "vendored_sha256": "2c90077463cc2deb82dc888dd7c6fc10fa465d444c7b48fa042b4a2d4e834711", + "vendored_lines": 56, + "self_recursions": [], + "package": "purescript-functors", + "tag": "v5.0.0", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "upstream_sha256": "2c90077463cc2deb82dc888dd7c6fc10fa465d444c7b48fa042b4a2d4e834711", + "upstream_url": "https://github.com/purescript/purescript-functors/blob/022ffd7a2a7ec12080314f3d217b400674a247b4/src/Data/Functor/App.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Clown.purs", + "vendored_sha256": "9e0ee4050d69d24a0965026576288b9742f1e77941b819d510e1317bbe1ca1c2", + "vendored_lines": 44, + "self_recursions": [], + "package": "purescript-functors", + "tag": "v5.0.0", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "upstream_sha256": "9e0ee4050d69d24a0965026576288b9742f1e77941b819d510e1317bbe1ca1c2", + "upstream_url": "https://github.com/purescript/purescript-functors/blob/022ffd7a2a7ec12080314f3d217b400674a247b4/src/Data/Functor/Clown.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Compose.purs", + "vendored_sha256": "1ea6725c558c1d8424b8e8bf03331879bcc589ec95ef0208dea7dcb90861d202", + "vendored_lines": 58, + "self_recursions": [], + "package": "purescript-functors", + "tag": "v5.0.0", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "upstream_sha256": "1ea6725c558c1d8424b8e8bf03331879bcc589ec95ef0208dea7dcb90861d202", + "upstream_url": "https://github.com/purescript/purescript-functors/blob/022ffd7a2a7ec12080314f3d217b400674a247b4/src/Data/Functor/Compose.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Contravariant.purs", + "vendored_sha256": "ecb01e255618d9b446bc72d042d10c44a84ce7b10df14f838e50fa49a8ddd4ab", + "vendored_lines": 35, + "self_recursions": [], + "package": "purescript-contravariant", + "tag": "v6.0.0", + "commit": "9ad3e105b8855bcc25f4e0893c784789d05a58de", + "upstream_sha256": "ecb01e255618d9b446bc72d042d10c44a84ce7b10df14f838e50fa49a8ddd4ab", + "upstream_url": "https://github.com/purescript/purescript-contravariant/blob/9ad3e105b8855bcc25f4e0893c784789d05a58de/src/Data/Functor/Contravariant.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Coproduct/Inject.purs", + "vendored_sha256": "dfccf4433db792b3a1246b72b7aab69d39a593750c242a741a4adc92a012b743", + "vendored_lines": 24, + "self_recursions": [], + "package": "purescript-functors", + "tag": "v5.0.0", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "upstream_sha256": "dfccf4433db792b3a1246b72b7aab69d39a593750c242a741a4adc92a012b743", + "upstream_url": "https://github.com/purescript/purescript-functors/blob/022ffd7a2a7ec12080314f3d217b400674a247b4/src/Data/Functor/Coproduct/Inject.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Coproduct/Nested.purs", + "vendored_sha256": "efd0e04c36996fd958425c21f3c6748cf3c08abf4ff014b09eca3687937e193c", + "vendored_lines": 273, + "self_recursions": [], + "package": "purescript-functors", + "tag": "v5.0.0", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "upstream_sha256": "efd0e04c36996fd958425c21f3c6748cf3c08abf4ff014b09eca3687937e193c", + "upstream_url": "https://github.com/purescript/purescript-functors/blob/022ffd7a2a7ec12080314f3d217b400674a247b4/src/Data/Functor/Coproduct/Nested.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Coproduct.purs", + "vendored_sha256": "bdef394de4a506431d3397ed765e861daf47989882f1f8d97419edb312a62637", + "vendored_lines": 76, + "self_recursions": [], + "package": "purescript-functors", + "tag": "v5.0.0", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "upstream_sha256": "bdef394de4a506431d3397ed765e861daf47989882f1f8d97419edb312a62637", + "upstream_url": "https://github.com/purescript/purescript-functors/blob/022ffd7a2a7ec12080314f3d217b400674a247b4/src/Data/Functor/Coproduct.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Costar.purs", + "vendored_sha256": "3bde423fa6f47a635e62c4a55fa4eb1ebbd32dcb5776b748bb4bc59b892c77b5", + "vendored_lines": 66, + "self_recursions": [], + "package": "purescript-functors", + "tag": "v5.0.0", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "upstream_sha256": "3bde423fa6f47a635e62c4a55fa4eb1ebbd32dcb5776b748bb4bc59b892c77b5", + "upstream_url": "https://github.com/purescript/purescript-functors/blob/022ffd7a2a7ec12080314f3d217b400674a247b4/src/Data/Functor/Costar.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Flip.purs", + "vendored_sha256": "b3d31bfa23a05a6ed6c4185775bb45ff2e0c562cd9c95c711b1cf636459c153b", + "vendored_lines": 44, + "self_recursions": [], + "package": "purescript-functors", + "tag": "v5.0.0", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "upstream_sha256": "b3d31bfa23a05a6ed6c4185775bb45ff2e0c562cd9c95c711b1cf636459c153b", + "upstream_url": "https://github.com/purescript/purescript-functors/blob/022ffd7a2a7ec12080314f3d217b400674a247b4/src/Data/Functor/Flip.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Invariant.purs", + "vendored_sha256": "070d432e594b5423ebd865d101dbddaf1bd64690917a6ebeabd559393c152f26", + "vendored_lines": 57, + "self_recursions": [], + "package": "purescript-invariant", + "tag": "v6.0.0", + "commit": "1d2a196d51e90623adb88496c2cfd759c6736894", + "upstream_sha256": "070d432e594b5423ebd865d101dbddaf1bd64690917a6ebeabd559393c152f26", + "upstream_url": "https://github.com/purescript/purescript-invariant/blob/1d2a196d51e90623adb88496c2cfd759c6736894/src/Data/Functor/Invariant.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Joker.purs", + "vendored_sha256": "3c32235123a30b6e38273bbfcf6cf50cef94d9a11e7da16e7ee3d7e0ed98cac4", + "vendored_lines": 60, + "self_recursions": [], + "package": "purescript-functors", + "tag": "v5.0.0", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "upstream_sha256": "3c32235123a30b6e38273bbfcf6cf50cef94d9a11e7da16e7ee3d7e0ed98cac4", + "upstream_url": "https://github.com/purescript/purescript-functors/blob/022ffd7a2a7ec12080314f3d217b400674a247b4/src/Data/Functor/Joker.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Product/Nested.purs", + "vendored_sha256": "eabec66003663a5e64b64e6abf317a27e8660d5c09475bbd38d9219db0834580", + "vendored_lines": 112, + "self_recursions": [], + "package": "purescript-functors", + "tag": "v5.0.0", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "upstream_sha256": "eabec66003663a5e64b64e6abf317a27e8660d5c09475bbd38d9219db0834580", + "upstream_url": "https://github.com/purescript/purescript-functors/blob/022ffd7a2a7ec12080314f3d217b400674a247b4/src/Data/Functor/Product/Nested.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Product.purs", + "vendored_sha256": "c43e912ffe484cfd0e01cafa143474e140f06a85c7177c9f2cc6b59b51d18a37", + "vendored_lines": 60, + "self_recursions": [], + "package": "purescript-functors", + "tag": "v5.0.0", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "upstream_sha256": "c43e912ffe484cfd0e01cafa143474e140f06a85c7177c9f2cc6b59b51d18a37", + "upstream_url": "https://github.com/purescript/purescript-functors/blob/022ffd7a2a7ec12080314f3d217b400674a247b4/src/Data/Functor/Product.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Product2.purs", + "vendored_sha256": "80763924484595b34488d8cca7bd8e5d399aebc7a79442e1625d7ddbebec9c7c", + "vendored_lines": 40, + "self_recursions": [], + "package": "purescript-functors", + "tag": "v5.0.0", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "upstream_sha256": "80763924484595b34488d8cca7bd8e5d399aebc7a79442e1625d7ddbebec9c7c", + "upstream_url": "https://github.com/purescript/purescript-functors/blob/022ffd7a2a7ec12080314f3d217b400674a247b4/src/Data/Functor/Product2.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor.purs", + "vendored_sha256": "4756955051839c40795329d7b32a26eec0db5425141731da22ed0fbc6723ab1d", + "vendored_lines": 114, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "fc5b0cc22a6c4d96a5779eb629cb304cebaa910f03a77192628c131a867bbeb8", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Functor.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "arrayMap", + "line": 55, + "declaration": "foreign import arrayMap :: forall a b. (a -> b) -> Array a -> Array b", + "vendored_status": "nonrecursive_replacement" + } + ], + "changed_original_code_outside_value_ffi": [ + { + "line": 50, + "text": " map = arrayMap" + }, + { + "line": 70, + "text": "void = map (const unit)" + }, + { + "line": 75, + "text": "voidRight x = map (const x)" + }, + { + "line": 81, + "text": "voidLeft f x = const x <$> f" + } + ] + }, + { + "path": "Data/FunctorWithIndex.purs", + "vendored_sha256": "41d57b2f4a7b3d785a1c0976351b4faf0dc0ce92717bf69f2b94e7e54c87942c", + "vendored_lines": 94, + "self_recursions": [ + { + "name": "mapWithIndexArray", + "line": 39, + "equation": "mapWithIndexArray a0 a1 = mapWithIndexArray a0 a1", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-foldable-traversable", + "tag": "v6.0.0", + "commit": "b3926f870532d287ea59e2d5cd3873b81ef2a93a", + "upstream_sha256": "cdffaeb5485f7a5a12796b5db2a9a81b7212d50dacd6405a87bc3f57fbc15f6c", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/FunctorWithIndex.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "mapWithIndexArray", + "line": 38, + "declaration": "foreign import mapWithIndexArray :: forall a b. (Int -> a -> b) -> Array a -> Array b", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Generic/Rep.purs", + "vendored_sha256": "fc1e17e76bf53bf8dc86593ca2d27a77f921e8aa67bffcb6265fdc473736862d", + "vendored_lines": 62, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "fc1e17e76bf53bf8dc86593ca2d27a77f921e8aa67bffcb6265fdc473736862d", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Generic/Rep.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/HeytingAlgebra/Generic.purs", + "vendored_sha256": "e26f345cfc1d84df08677300132cfc93187e80a285e2f1228a45ae272e7f3260", + "vendored_lines": 70, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "c5022ab44881b51e4b62d06994d92f04b037ce97ff2c50054e4b90d91a463bbe", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/HeytingAlgebra/Generic.purs", + "status": "newline_only", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/HeytingAlgebra.purs", + "vendored_sha256": "90b66ffc82855d35d43d568d5015bbf6b33917bb8e171db624467a6e9c35f7fc", + "vendored_lines": 174, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "797b9e8358e10691e93c8b1552723507fd12647441f90db11b01e35850bce75b", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/HeytingAlgebra.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "boolConj", + "line": 103, + "declaration": "foreign import boolConj :: Boolean -> Boolean -> Boolean", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "boolDisj", + "line": 104, + "declaration": "foreign import boolDisj :: Boolean -> Boolean -> Boolean", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "boolNot", + "line": 105, + "declaration": "foreign import boolNot :: Boolean -> Boolean", + "vendored_status": "nonrecursive_replacement" + } + ], + "changed_original_code_outside_value_ffi": [ + { + "line": 67, + "text": " conj = boolConj" + }, + { + "line": 68, + "text": " disj = boolDisj" + }, + { + "line": 69, + "text": " not = boolNot" + } + ] + }, + { + "path": "Data/Identity.purs", + "vendored_sha256": "65f7f8ca763a00e78b46453c480a22e41e43dcda5a582cfea9eecd4b4448f87d", + "vendored_lines": 72, + "self_recursions": [], + "package": "purescript-identity", + "tag": "v6.0.0", + "commit": "ef6768f8a52ab0bc943a85f5761ba07c257f639f", + "upstream_sha256": "65f7f8ca763a00e78b46453c480a22e41e43dcda5a582cfea9eecd4b4448f87d", + "upstream_url": "https://github.com/purescript/purescript-identity/blob/ef6768f8a52ab0bc943a85f5761ba07c257f639f/src/Data/Identity.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Int/Bits.purs", + "vendored_sha256": "e0799d3e2f4611638dd57693399300590518cd45387e5ea014aa556af32a72ec", + "vendored_lines": 44, + "self_recursions": [], + "package": "purescript-integers", + "tag": "v6.0.0", + "commit": "54d712b25c594833083d15dc9ff2418eb9c52822", + "upstream_sha256": "6377bce107895b0021264a30a35c8e50867b084349407009061728bb42c436de", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int/Bits.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "and", + "line": 13, + "declaration": "foreign import and :: Int -> Int -> Int", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "or", + "line": 18, + "declaration": "foreign import or :: Int -> Int -> Int", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "xor", + "line": 23, + "declaration": "foreign import xor :: Int -> Int -> Int", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "shl", + "line": 28, + "declaration": "foreign import shl :: Int -> Int -> Int", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "shr", + "line": 31, + "declaration": "foreign import shr :: Int -> Int -> Int", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "zshr", + "line": 34, + "declaration": "foreign import zshr :: Int -> Int -> Int", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "complement", + "line": 37, + "declaration": "foreign import complement :: Int -> Int", + "vendored_status": "nonrecursive_replacement" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Int.purs", + "vendored_sha256": "d4ce869235bc1d0b0b8cb1415c51fbcf499319f3f332b258940f8ef9cc786b5d", + "vendored_lines": 255, + "self_recursions": [ + { + "name": "fromNumberImpl", + "line": 41, + "equation": "fromNumberImpl a0 a1 a2 = fromNumberImpl a0 a1 a2", + "replaces_upstream_foreign": true + }, + { + "name": "toNumber", + "line": 79, + "equation": "toNumber a0 = toNumber a0", + "replaces_upstream_foreign": true + }, + { + "name": "quot", + "line": 226, + "equation": "quot a0 a1 = quot a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "rem", + "line": 245, + "equation": "rem a0 a1 = rem a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "pow", + "line": 249, + "equation": "pow a0 a1 = pow a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "fromStringAsImpl", + "line": 252, + "equation": "fromStringAsImpl a0 a1 a2 a3 = fromStringAsImpl a0 a1 a2 a3", + "replaces_upstream_foreign": true + }, + { + "name": "toStringAs", + "line": 255, + "equation": "toStringAs a0 a1 = toStringAs a0 a1", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-integers", + "tag": "v6.0.0", + "commit": "54d712b25c594833083d15dc9ff2418eb9c52822", + "upstream_sha256": "53a7c199f384719a5f615faf6141e9b0ffa2fa9f66b9806d30a0f196be074eec", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "fromNumberImpl", + "line": 40, + "declaration": "foreign import fromNumberImpl :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Number -> Maybe Int", + "vendored_status": "direct_self_recursion" + }, + { + "name": "toNumber", + "line": 81, + "declaration": "foreign import toNumber :: Int -> Number", + "vendored_status": "direct_self_recursion" + }, + { + "name": "quot", + "line": 227, + "declaration": "foreign import quot :: Int -> Int -> Int", + "vendored_status": "direct_self_recursion" + }, + { + "name": "rem", + "line": 245, + "declaration": "foreign import rem :: Int -> Int -> Int", + "vendored_status": "direct_self_recursion" + }, + { + "name": "pow", + "line": 248, + "declaration": "foreign import pow :: Int -> Int -> Int", + "vendored_status": "direct_self_recursion" + }, + { + "name": "fromStringAsImpl", + "line": 250, + "declaration": "foreign import fromStringAsImpl :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Radix -> String -> Maybe Int", + "vendored_status": "direct_self_recursion" + }, + { + "name": "toStringAs", + "line": 257, + "declaration": "foreign import toStringAs :: Radix -> Int -> String", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Lazy.purs", + "vendored_sha256": "1a7a6fd1ac16db56608653d335d20573f0ff1861d3920990ea3a9ee4c49befcc", + "vendored_lines": 144, + "self_recursions": [ + { + "name": "defer", + "line": 36, + "equation": "defer a0 = defer a0", + "replaces_upstream_foreign": true + }, + { + "name": "force", + "line": 40, + "equation": "force a0 = force a0", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-lazy", + "tag": "v6.0.0", + "commit": "48347841226b27af5205a1a8ec71e27a93ce86fd", + "upstream_sha256": "9da5e2562a1a05764552d20dc3dc7119142550350a1f290d1919f502ebe54202", + "upstream_url": "https://github.com/purescript/purescript-lazy/blob/48347841226b27af5205a1a8ec71e27a93ce86fd/src/Data/Lazy.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "defer", + "line": 35, + "declaration": "foreign import defer :: forall a. (Unit -> a) -> Lazy a", + "vendored_status": "direct_self_recursion" + }, + { + "name": "force", + "line": 38, + "declaration": "foreign import force :: forall a. Lazy a -> a", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/List/Internal.purs", + "vendored_sha256": "9b542dbc4823476b26b6912cd0dbd47c3af97bc35f66f11b421aa59f28d6fc42", + "vendored_lines": 63, + "self_recursions": [], + "package": "purescript-lists", + "tag": "v7.0.0", + "commit": "b113451e5b41cad87d669a3165f955c71cd863e2", + "upstream_sha256": "9b542dbc4823476b26b6912cd0dbd47c3af97bc35f66f11b421aa59f28d6fc42", + "upstream_url": "https://github.com/purescript/purescript-lists/blob/b113451e5b41cad87d669a3165f955c71cd863e2/src/Data/List/Internal.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/List/Lazy/NonEmpty.purs", + "vendored_sha256": "5b06d7bdbb485fa4766134635b43d22f1e148f0e4bbc61f7c4e78710686b2586", + "vendored_lines": 88, + "self_recursions": [], + "package": "purescript-lists", + "tag": "v7.0.0", + "commit": "b113451e5b41cad87d669a3165f955c71cd863e2", + "upstream_sha256": "5b06d7bdbb485fa4766134635b43d22f1e148f0e4bbc61f7c4e78710686b2586", + "upstream_url": "https://github.com/purescript/purescript-lists/blob/b113451e5b41cad87d669a3165f955c71cd863e2/src/Data/List/Lazy/NonEmpty.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/List/Lazy/Types.purs", + "vendored_sha256": "68fa44b813560d340230fd122e965568c2920600c8153fd22219e27ab7307d64", + "vendored_lines": 295, + "self_recursions": [], + "package": "purescript-lists", + "tag": "v7.0.0", + "commit": "b113451e5b41cad87d669a3165f955c71cd863e2", + "upstream_sha256": "68fa44b813560d340230fd122e965568c2920600c8153fd22219e27ab7307d64", + "upstream_url": "https://github.com/purescript/purescript-lists/blob/b113451e5b41cad87d669a3165f955c71cd863e2/src/Data/List/Lazy/Types.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/List/Lazy.purs", + "vendored_sha256": "69f373e427611ef8124b9a49f4c02638aca287bc48bf095371da8afe090fe9b8", + "vendored_lines": 780, + "self_recursions": [], + "package": "purescript-lists", + "tag": "v7.0.0", + "commit": "b113451e5b41cad87d669a3165f955c71cd863e2", + "upstream_sha256": "69f373e427611ef8124b9a49f4c02638aca287bc48bf095371da8afe090fe9b8", + "upstream_url": "https://github.com/purescript/purescript-lists/blob/b113451e5b41cad87d669a3165f955c71cd863e2/src/Data/List/Lazy.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/List/NonEmpty.purs", + "vendored_sha256": "2b0911320ac74ddc1a80957592c965b370ee81de740f9c24f048d4f7d5c4b0d7", + "vendored_lines": 307, + "self_recursions": [], + "package": "purescript-lists", + "tag": "v7.0.0", + "commit": "b113451e5b41cad87d669a3165f955c71cd863e2", + "upstream_sha256": "2b0911320ac74ddc1a80957592c965b370ee81de740f9c24f048d4f7d5c4b0d7", + "upstream_url": "https://github.com/purescript/purescript-lists/blob/b113451e5b41cad87d669a3165f955c71cd863e2/src/Data/List/NonEmpty.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/List/Partial.purs", + "vendored_sha256": "95e40bb55ef84403901b6a19ac9367bae5cce79330703a8d4db60d57bf6fab4b", + "vendored_lines": 30, + "self_recursions": [], + "package": "purescript-lists", + "tag": "v7.0.0", + "commit": "b113451e5b41cad87d669a3165f955c71cd863e2", + "upstream_sha256": "95e40bb55ef84403901b6a19ac9367bae5cce79330703a8d4db60d57bf6fab4b", + "upstream_url": "https://github.com/purescript/purescript-lists/blob/b113451e5b41cad87d669a3165f955c71cd863e2/src/Data/List/Partial.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/List/Types.purs", + "vendored_sha256": "8a7d87397d8edc746e7fa1459d67e90ef9e2dd09bf7bd55baa9a4b549e316dd3", + "vendored_lines": 264, + "self_recursions": [], + "package": "purescript-lists", + "tag": "v7.0.0", + "commit": "b113451e5b41cad87d669a3165f955c71cd863e2", + "upstream_sha256": "8a7d87397d8edc746e7fa1459d67e90ef9e2dd09bf7bd55baa9a4b549e316dd3", + "upstream_url": "https://github.com/purescript/purescript-lists/blob/b113451e5b41cad87d669a3165f955c71cd863e2/src/Data/List/Types.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/List/ZipList.purs", + "vendored_sha256": "09acb25d60dd823ee7ce94d33e5ac9bb46aee527638dd69ccf82f1532da13719", + "vendored_lines": 66, + "self_recursions": [], + "package": "purescript-lists", + "tag": "v7.0.0", + "commit": "b113451e5b41cad87d669a3165f955c71cd863e2", + "upstream_sha256": "09acb25d60dd823ee7ce94d33e5ac9bb46aee527638dd69ccf82f1532da13719", + "upstream_url": "https://github.com/purescript/purescript-lists/blob/b113451e5b41cad87d669a3165f955c71cd863e2/src/Data/List/ZipList.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/List.purs", + "vendored_sha256": "fde72dc9da24f67efb446f57806890e95b3b40a4f0389ee313072de4cc2d56aa", + "vendored_lines": 826, + "self_recursions": [], + "package": "purescript-lists", + "tag": "v7.0.0", + "commit": "b113451e5b41cad87d669a3165f955c71cd863e2", + "upstream_sha256": "fde72dc9da24f67efb446f57806890e95b3b40a4f0389ee313072de4cc2d56aa", + "upstream_url": "https://github.com/purescript/purescript-lists/blob/b113451e5b41cad87d669a3165f955c71cd863e2/src/Data/List.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Map/Gen.purs", + "vendored_sha256": "9f2671c0a0a1efbad92a52b4e02517c06e08df9f741f59a9d8ae290d002a19e9", + "vendored_lines": 24, + "self_recursions": [], + "package": "purescript-ordered-collections", + "tag": "v3.2.0", + "commit": "b9c8ec72b53ee56d6cb6dc962d5ad7ea4708b438", + "upstream_sha256": "9f2671c0a0a1efbad92a52b4e02517c06e08df9f741f59a9d8ae290d002a19e9", + "upstream_url": "https://github.com/purescript/purescript-ordered-collections/blob/b9c8ec72b53ee56d6cb6dc962d5ad7ea4708b438/src/Data/Map/Gen.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Map/Internal.purs", + "vendored_sha256": "03608bed5db4804aa898eff034bb5b2feab0676948b19aefa606dc927eeb5cc9", + "vendored_lines": 988, + "self_recursions": [], + "package": "purescript-ordered-collections", + "tag": "v3.2.0", + "commit": "b9c8ec72b53ee56d6cb6dc962d5ad7ea4708b438", + "upstream_sha256": "03608bed5db4804aa898eff034bb5b2feab0676948b19aefa606dc927eeb5cc9", + "upstream_url": "https://github.com/purescript/purescript-ordered-collections/blob/b9c8ec72b53ee56d6cb6dc962d5ad7ea4708b438/src/Data/Map/Internal.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Map.purs", + "vendored_sha256": "8a999f683cf7f64950707e6cdc8e9a735974725e98b72c7a6d891f5f801e6242", + "vendored_lines": 65, + "self_recursions": [], + "package": "purescript-ordered-collections", + "tag": "v3.2.0", + "commit": "b9c8ec72b53ee56d6cb6dc962d5ad7ea4708b438", + "upstream_sha256": "8a999f683cf7f64950707e6cdc8e9a735974725e98b72c7a6d891f5f801e6242", + "upstream_url": "https://github.com/purescript/purescript-ordered-collections/blob/b9c8ec72b53ee56d6cb6dc962d5ad7ea4708b438/src/Data/Map.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Maybe/First.purs", + "vendored_sha256": "7cfcf5744a424bc512fd7e7e04a71484c609a52cf94ef5552bcf7afa8f967abf", + "vendored_lines": 68, + "self_recursions": [], + "package": "purescript-maybe", + "tag": "v6.0.0", + "commit": "c6f98ac1088766287106c5d9c8e30e7648d36786", + "upstream_sha256": "7cfcf5744a424bc512fd7e7e04a71484c609a52cf94ef5552bcf7afa8f967abf", + "upstream_url": "https://github.com/purescript/purescript-maybe/blob/c6f98ac1088766287106c5d9c8e30e7648d36786/src/Data/Maybe/First.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Maybe/Last.purs", + "vendored_sha256": "cc86da8e91615e04b7973bdc5f7467db4adc8df722eb509bc2cf0e735cbb93ab", + "vendored_lines": 67, + "self_recursions": [], + "package": "purescript-maybe", + "tag": "v6.0.0", + "commit": "c6f98ac1088766287106c5d9c8e30e7648d36786", + "upstream_sha256": "cc86da8e91615e04b7973bdc5f7467db4adc8df722eb509bc2cf0e735cbb93ab", + "upstream_url": "https://github.com/purescript/purescript-maybe/blob/c6f98ac1088766287106c5d9c8e30e7648d36786/src/Data/Maybe/Last.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Maybe.purs", + "vendored_sha256": "dd49326b74a5ba9c5d0e0aaca7c05226aadb9b9bf851d9a3fdbc34df23319fa8", + "vendored_lines": 312, + "self_recursions": [], + "package": "purescript-maybe", + "tag": "v6.0.0", + "commit": "c6f98ac1088766287106c5d9c8e30e7648d36786", + "upstream_sha256": "dd49326b74a5ba9c5d0e0aaca7c05226aadb9b9bf851d9a3fdbc34df23319fa8", + "upstream_url": "https://github.com/purescript/purescript-maybe/blob/c6f98ac1088766287106c5d9c8e30e7648d36786/src/Data/Maybe.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Monoid/Additive.purs", + "vendored_sha256": "9b44eba5a733dbd6f7e908ea1c4eb44b61694f5a3d1d1e157c7c81300eb7417a", + "vendored_lines": 44, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "9b44eba5a733dbd6f7e908ea1c4eb44b61694f5a3d1d1e157c7c81300eb7417a", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Monoid/Additive.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Monoid/Alternate.purs", + "vendored_sha256": "fb6b0d43a0c82f6028d64d957fad49d9ce48eb8a213ea8d2f0498d4be8f2de8d", + "vendored_lines": 60, + "self_recursions": [], + "package": "purescript-control", + "tag": "v6.0.0", + "commit": "a6033808790879a17b2729e73747a9ed3fb2264e", + "upstream_sha256": "fb6b0d43a0c82f6028d64d957fad49d9ce48eb8a213ea8d2f0498d4be8f2de8d", + "upstream_url": "https://github.com/purescript/purescript-control/blob/a6033808790879a17b2729e73747a9ed3fb2264e/src/Data/Monoid/Alternate.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Monoid/Conj.purs", + "vendored_sha256": "0e3bedf55635d1c04225ff1a756bc55b94afafa8fb8d327f118e4c1f58f40858", + "vendored_lines": 51, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "0e3bedf55635d1c04225ff1a756bc55b94afafa8fb8d327f118e4c1f58f40858", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Monoid/Conj.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Monoid/Disj.purs", + "vendored_sha256": "e1eb141917201b0b866c433e19e68a6b0d80705d8fb6ea3d61d103d4a01c735f", + "vendored_lines": 51, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "e1eb141917201b0b866c433e19e68a6b0d80705d8fb6ea3d61d103d4a01c735f", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Monoid/Disj.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Monoid/Dual.purs", + "vendored_sha256": "83c40da0d7125aa11225efe267713892ecaa044d66da05d048a1194ac0c48f6c", + "vendored_lines": 44, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "83c40da0d7125aa11225efe267713892ecaa044d66da05d048a1194ac0c48f6c", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Monoid/Dual.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Monoid/Endo.purs", + "vendored_sha256": "aa5b2bc60841651e0bf54a683e6d6d99f4aca932bf583691b3fdd71a32aa5594", + "vendored_lines": 30, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "aa5b2bc60841651e0bf54a683e6d6d99f4aca932bf583691b3fdd71a32aa5594", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Monoid/Endo.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Monoid/Generic.purs", + "vendored_sha256": "a3df423b6bc60580e9e80612ae7c1682445853c55086363ceed129e43c72bf70", + "vendored_lines": 27, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "a3df423b6bc60580e9e80612ae7c1682445853c55086363ceed129e43c72bf70", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Monoid/Generic.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Monoid/Multiplicative.purs", + "vendored_sha256": "0d01fac2de65e733c640b045e00f60422e1b776010ee69e7714dd659bb8c7796", + "vendored_lines": 44, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "0d01fac2de65e733c640b045e00f60422e1b776010ee69e7714dd659bb8c7796", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Monoid/Multiplicative.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Monoid.purs", + "vendored_sha256": "a1bf093434dc1d8f162a19369f3376516f3f899e17875711be3d7164fb20e605", + "vendored_lines": 120, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "a1bf093434dc1d8f162a19369f3376516f3f899e17875711be3d7164fb20e605", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Monoid.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/NaturalTransformation.purs", + "vendored_sha256": "3196072783cdc7135047788ad6e58073e1cfeb50d284f038b79b65db2df9eb7e", + "vendored_lines": 20, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "3196072783cdc7135047788ad6e58073e1cfeb50d284f038b79b65db2df9eb7e", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/NaturalTransformation.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Newtype.purs", + "vendored_sha256": "572911ec613a337d30004c92120afea496af97d34b63617d884d4a96f03d8304", + "vendored_lines": 308, + "self_recursions": [], + "package": "purescript-newtype", + "tag": "v5.0.0", + "commit": "29d8e6dd77aec2c975c948364ec3faf26e14ee7b", + "upstream_sha256": "572911ec613a337d30004c92120afea496af97d34b63617d884d4a96f03d8304", + "upstream_url": "https://github.com/purescript/purescript-newtype/blob/29d8e6dd77aec2c975c948364ec3faf26e14ee7b/src/Data/Newtype.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/NonEmpty.purs", + "vendored_sha256": "c48366ad44cc7b4311d5032ef27ae8d2ea39f603d7bb33f03d51536758976d7e", + "vendored_lines": 174, + "self_recursions": [], + "package": "purescript-nonempty", + "tag": "v7.0.0", + "commit": "28150ecc7419238b187abd609a92a645273348bb", + "upstream_sha256": "c48366ad44cc7b4311d5032ef27ae8d2ea39f603d7bb33f03d51536758976d7e", + "upstream_url": "https://github.com/purescript/purescript-nonempty/blob/28150ecc7419238b187abd609a92a645273348bb/src/Data/NonEmpty.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Number/Approximate.purs", + "vendored_sha256": "fbd544b15b42845be1a3fa11f1ec3c61d290baa300d41b7e41e3aacf707eacff", + "vendored_lines": 95, + "self_recursions": [], + "package": "purescript-numbers", + "tag": "v9.0.1", + "commit": "27d54effdd2c0e7a86fe356b1cd813dca5981c2d", + "upstream_sha256": "fbd544b15b42845be1a3fa11f1ec3c61d290baa300d41b7e41e3aacf707eacff", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number/Approximate.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Number/Format.purs", + "vendored_sha256": "e6043544d5a1d545e807d200cfc0b99c269b797f046ad25aba51527ff7053924", + "vendored_lines": 80, + "self_recursions": [ + { + "name": "toPrecisionNative", + "line": 34, + "equation": "toPrecisionNative a0 a1 = toPrecisionNative a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "toFixedNative", + "line": 36, + "equation": "toFixedNative a0 a1 = toFixedNative a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "toExponentialNative", + "line": 38, + "equation": "toExponentialNative a0 a1 = toExponentialNative a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "toString", + "line": 80, + "equation": "toString a0 = toString a0", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-numbers", + "tag": "v9.0.1", + "commit": "27d54effdd2c0e7a86fe356b1cd813dca5981c2d", + "upstream_sha256": "0922126e6d31ecc361a00b265ecf302fb1f6449ca11d2cc2331a0ae174d9f83f", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number/Format.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "toPrecisionNative", + "line": 33, + "declaration": "foreign import toPrecisionNative :: Int -> Number -> String", + "vendored_status": "direct_self_recursion" + }, + { + "name": "toFixedNative", + "line": 34, + "declaration": "foreign import toFixedNative :: Int -> Number -> String", + "vendored_status": "direct_self_recursion" + }, + { + "name": "toExponentialNative", + "line": 35, + "declaration": "foreign import toExponentialNative :: Int -> Number -> String", + "vendored_status": "direct_self_recursion" + }, + { + "name": "toString", + "line": 76, + "declaration": "foreign import toString :: Number -> String", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Number.purs", + "vendored_sha256": "eafedeed553989c8542ec41fdc7216a75d3195b235446448dc0bc69b88c4748b", + "vendored_lines": 388, + "self_recursions": [ + { + "name": "nan", + "line": 48, + "equation": "nan = nan", + "replaces_upstream_foreign": true + }, + { + "name": "isNaN", + "line": 59, + "equation": "isNaN a0 = isNaN a0", + "replaces_upstream_foreign": true + }, + { + "name": "infinity", + "line": 70, + "equation": "infinity = infinity", + "replaces_upstream_foreign": true + }, + { + "name": "isFinite", + "line": 87, + "equation": "isFinite a0 = isFinite a0", + "replaces_upstream_foreign": true + }, + { + "name": "fromStringImpl", + "line": 120, + "equation": "fromStringImpl = fromStringImpl", + "replaces_upstream_foreign": true + }, + { + "name": "abs", + "line": 129, + "equation": "abs a0 = abs a0", + "replaces_upstream_foreign": true + }, + { + "name": "acos", + "line": 137, + "equation": "acos a0 = acos a0", + "replaces_upstream_foreign": true + }, + { + "name": "asin", + "line": 145, + "equation": "asin a0 = asin a0", + "replaces_upstream_foreign": true + }, + { + "name": "atan", + "line": 153, + "equation": "atan a0 = atan a0", + "replaces_upstream_foreign": true + }, + { + "name": "atan2", + "line": 167, + "equation": "atan2 a0 a1 = atan2 a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "ceil", + "line": 175, + "equation": "ceil a0 = ceil a0", + "replaces_upstream_foreign": true + }, + { + "name": "cos", + "line": 183, + "equation": "cos a0 = cos a0", + "replaces_upstream_foreign": true + }, + { + "name": "exp", + "line": 191, + "equation": "exp a0 = exp a0", + "replaces_upstream_foreign": true + }, + { + "name": "floor", + "line": 199, + "equation": "floor a0 = floor a0", + "replaces_upstream_foreign": true + }, + { + "name": "log", + "line": 206, + "equation": "log a0 = log a0", + "replaces_upstream_foreign": true + }, + { + "name": "max", + "line": 211, + "equation": "max a0 a1 = max a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "min", + "line": 216, + "equation": "min a0 a1 = min a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "pow", + "line": 227, + "equation": "pow a0 a1 = pow a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "remainder", + "line": 235, + "equation": "remainder a0 a1 = remainder a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "round", + "line": 245, + "equation": "round a0 = round a0", + "replaces_upstream_foreign": true + }, + { + "name": "sign", + "line": 256, + "equation": "sign a0 = sign a0", + "replaces_upstream_foreign": true + }, + { + "name": "sin", + "line": 264, + "equation": "sin a0 = sin a0", + "replaces_upstream_foreign": true + }, + { + "name": "sqrt", + "line": 272, + "equation": "sqrt a0 = sqrt a0", + "replaces_upstream_foreign": true + }, + { + "name": "tan", + "line": 280, + "equation": "tan a0 = tan a0", + "replaces_upstream_foreign": true + }, + { + "name": "trunc", + "line": 289, + "equation": "trunc a0 = trunc a0", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-numbers", + "tag": "v9.0.1", + "commit": "27d54effdd2c0e7a86fe356b1cd813dca5981c2d", + "upstream_sha256": "13c04cf0d293c54825fc95b5b3d543379a8d913e110d9e5056ca5388a31afc87", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "nan", + "line": 47, + "declaration": "foreign import nan :: Number", + "vendored_status": "direct_self_recursion" + }, + { + "name": "isNaN", + "line": 57, + "declaration": "foreign import isNaN :: Number -> Boolean", + "vendored_status": "direct_self_recursion" + }, + { + "name": "infinity", + "line": 67, + "declaration": "foreign import infinity :: Number", + "vendored_status": "direct_self_recursion" + }, + { + "name": "isFinite", + "line": 83, + "declaration": "foreign import isFinite :: Number -> Boolean", + "vendored_status": "direct_self_recursion" + }, + { + "name": "fromStringImpl", + "line": 115, + "declaration": "foreign import fromStringImpl :: Fn4 String (Number -> Boolean) (forall a. a -> Maybe a) (forall a. Maybe a) (Maybe Number)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "abs", + "line": 123, + "declaration": "foreign import abs :: Number -> Number", + "vendored_status": "direct_self_recursion" + }, + { + "name": "acos", + "line": 130, + "declaration": "foreign import acos :: Number -> Number", + "vendored_status": "direct_self_recursion" + }, + { + "name": "asin", + "line": 137, + "declaration": "foreign import asin :: Number -> Number", + "vendored_status": "direct_self_recursion" + }, + { + "name": "atan", + "line": 144, + "declaration": "foreign import atan :: Number -> Number", + "vendored_status": "direct_self_recursion" + }, + { + "name": "atan2", + "line": 157, + "declaration": "foreign import atan2 :: Number -> Number -> Number", + "vendored_status": "direct_self_recursion" + }, + { + "name": "ceil", + "line": 164, + "declaration": "foreign import ceil :: Number -> Number", + "vendored_status": "direct_self_recursion" + }, + { + "name": "cos", + "line": 171, + "declaration": "foreign import cos :: Number -> Number", + "vendored_status": "direct_self_recursion" + }, + { + "name": "exp", + "line": 178, + "declaration": "foreign import exp :: Number -> Number", + "vendored_status": "direct_self_recursion" + }, + { + "name": "floor", + "line": 185, + "declaration": "foreign import floor :: Number -> Number", + "vendored_status": "direct_self_recursion" + }, + { + "name": "log", + "line": 191, + "declaration": "foreign import log :: Number -> Number", + "vendored_status": "direct_self_recursion" + }, + { + "name": "max", + "line": 195, + "declaration": "foreign import max :: Number -> Number -> Number", + "vendored_status": "direct_self_recursion" + }, + { + "name": "min", + "line": 199, + "declaration": "foreign import min :: Number -> Number -> Number", + "vendored_status": "direct_self_recursion" + }, + { + "name": "pow", + "line": 209, + "declaration": "foreign import pow :: Number -> Number -> Number", + "vendored_status": "direct_self_recursion" + }, + { + "name": "remainder", + "line": 216, + "declaration": "foreign import remainder :: Number -> Number -> Number", + "vendored_status": "direct_self_recursion" + }, + { + "name": "round", + "line": 225, + "declaration": "foreign import round :: Number -> Number", + "vendored_status": "direct_self_recursion" + }, + { + "name": "sign", + "line": 235, + "declaration": "foreign import sign :: Number -> Number", + "vendored_status": "direct_self_recursion" + }, + { + "name": "sin", + "line": 242, + "declaration": "foreign import sin :: Number -> Number", + "vendored_status": "direct_self_recursion" + }, + { + "name": "sqrt", + "line": 249, + "declaration": "foreign import sqrt :: Number -> Number", + "vendored_status": "direct_self_recursion" + }, + { + "name": "tan", + "line": 256, + "declaration": "foreign import tan :: Number -> Number", + "vendored_status": "direct_self_recursion" + }, + { + "name": "trunc", + "line": 264, + "declaration": "foreign import trunc :: Number -> Number", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Op.purs", + "vendored_sha256": "400182c9f9c76ef9eaab32377405195986d70d1f4a4a04cc4833fba0e035cec6", + "vendored_lines": 22, + "self_recursions": [], + "package": "purescript-contravariant", + "tag": "v6.0.0", + "commit": "9ad3e105b8855bcc25f4e0893c784789d05a58de", + "upstream_sha256": "400182c9f9c76ef9eaab32377405195986d70d1f4a4a04cc4833fba0e035cec6", + "upstream_url": "https://github.com/purescript/purescript-contravariant/blob/9ad3e105b8855bcc25f4e0893c784789d05a58de/src/Data/Op.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Ord/Down.purs", + "vendored_sha256": "1c1ac8c739112884f86076eaae7ebe1a24c533892c044940813ceee796d840bc", + "vendored_lines": 26, + "self_recursions": [], + "package": "purescript-orders", + "tag": "v6.0.0", + "commit": "f86db621ec5eef1274145f8b1fd8ebbfe0ed4a2c", + "upstream_sha256": "1c1ac8c739112884f86076eaae7ebe1a24c533892c044940813ceee796d840bc", + "upstream_url": "https://github.com/purescript/purescript-orders/blob/f86db621ec5eef1274145f8b1fd8ebbfe0ed4a2c/src/Data/Ord/Down.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Ord/Generic.purs", + "vendored_sha256": "f1a8a7f58a29e0d22087de7acb3e7eb4a7e159fa5cb589d127274efcb91246ca", + "vendored_lines": 39, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "f1a8a7f58a29e0d22087de7acb3e7eb4a7e159fa5cb589d127274efcb91246ca", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Ord/Generic.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Ord/Max.purs", + "vendored_sha256": "0ba5b99631c3d19685c2ff3141d8e7701933e53828ca4277c0060cdb010b6255", + "vendored_lines": 29, + "self_recursions": [], + "package": "purescript-orders", + "tag": "v6.0.0", + "commit": "f86db621ec5eef1274145f8b1fd8ebbfe0ed4a2c", + "upstream_sha256": "0ba5b99631c3d19685c2ff3141d8e7701933e53828ca4277c0060cdb010b6255", + "upstream_url": "https://github.com/purescript/purescript-orders/blob/f86db621ec5eef1274145f8b1fd8ebbfe0ed4a2c/src/Data/Ord/Max.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Ord/Min.purs", + "vendored_sha256": "2ff841d2cdab3ddacb446d4562a49b7e17a76bfa23441188f02a77a65db4a6f1", + "vendored_lines": 29, + "self_recursions": [], + "package": "purescript-orders", + "tag": "v6.0.0", + "commit": "f86db621ec5eef1274145f8b1fd8ebbfe0ed4a2c", + "upstream_sha256": "2ff841d2cdab3ddacb446d4562a49b7e17a76bfa23441188f02a77a65db4a6f1", + "upstream_url": "https://github.com/purescript/purescript-orders/blob/f86db621ec5eef1274145f8b1fd8ebbfe0ed4a2c/src/Data/Ord/Min.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Ord.purs", + "vendored_sha256": "c9d3cfd012a36939b4f10243aa9bf9db0b70bbb8901270eab314e1d9e5f46ff9", + "vendored_lines": 263, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "ba132ffbd2784283842f3e5c0b8a47be006968229bcd70abce0764a21b8a47ae", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Ord.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "ordBooleanImpl", + "line": 84, + "declaration": "foreign import ordBooleanImpl :: Ordering -> Ordering -> Ordering -> Boolean -> Boolean -> Ordering", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "ordIntImpl", + "line": 92, + "declaration": "foreign import ordIntImpl :: Ordering -> Ordering -> Ordering -> Int -> Int -> Ordering", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "ordNumberImpl", + "line": 100, + "declaration": "foreign import ordNumberImpl :: Ordering -> Ordering -> Ordering -> Number -> Number -> Ordering", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "ordStringImpl", + "line": 108, + "declaration": "foreign import ordStringImpl :: Ordering -> Ordering -> Ordering -> String -> String -> Ordering", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "ordCharImpl", + "line": 116, + "declaration": "foreign import ordCharImpl :: Ordering -> Ordering -> Ordering -> Char -> Char -> Ordering", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "ordArrayImpl", + "line": 124, + "declaration": "foreign import ordArrayImpl :: forall a. (a -> a -> Int) -> Array a -> Array a -> Int", + "vendored_status": "nonrecursive_replacement" + } + ], + "changed_original_code_outside_value_ffi": [ + { + "line": 52, + "text": " compare = ordBooleanImpl LT EQ GT" + }, + { + "line": 55, + "text": " compare = ordIntImpl LT EQ GT" + }, + { + "line": 58, + "text": " compare = ordNumberImpl LT EQ GT" + }, + { + "line": 61, + "text": " compare = ordStringImpl LT EQ GT" + }, + { + "line": 64, + "text": " compare = ordCharImpl LT EQ GT" + } + ] + }, + { + "path": "Data/Ordering.purs", + "vendored_sha256": "9a53fe2e7bd7afb11b8db093b1511bc210fc88c1661a78fe4d89800b20cae0a1", + "vendored_lines": 36, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "9a53fe2e7bd7afb11b8db093b1511bc210fc88c1661a78fe4d89800b20cae0a1", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Ordering.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Predicate.purs", + "vendored_sha256": "75e1d24c5f0eecc0d11f7664bd38edd641f5fcb039577d11213eac1a819a2d1c", + "vendored_lines": 18, + "self_recursions": [], + "package": "purescript-contravariant", + "tag": "v6.0.0", + "commit": "9ad3e105b8855bcc25f4e0893c784789d05a58de", + "upstream_sha256": "75e1d24c5f0eecc0d11f7664bd38edd641f5fcb039577d11213eac1a819a2d1c", + "upstream_url": "https://github.com/purescript/purescript-contravariant/blob/9ad3e105b8855bcc25f4e0893c784789d05a58de/src/Data/Predicate.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Profunctor/Choice.purs", + "vendored_sha256": "687391e4819de27fda58acb18be86fdd99bf52166b1ca736525fa4c3310d752a", + "vendored_lines": 83, + "self_recursions": [], + "package": "purescript-profunctor", + "tag": "v6.0.1", + "commit": "17f927ce69710085f2bbefc9a37506828d1ebc5a", + "upstream_sha256": "687391e4819de27fda58acb18be86fdd99bf52166b1ca736525fa4c3310d752a", + "upstream_url": "https://github.com/purescript/purescript-profunctor/blob/17f927ce69710085f2bbefc9a37506828d1ebc5a/src/Data/Profunctor/Choice.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Profunctor/Closed.purs", + "vendored_sha256": "2ebbae569682f0bb828e69417c6b991eb6ac54ae0c439e1da9a44064a613d01d", + "vendored_lines": 12, + "self_recursions": [], + "package": "purescript-profunctor", + "tag": "v6.0.1", + "commit": "17f927ce69710085f2bbefc9a37506828d1ebc5a", + "upstream_sha256": "2ebbae569682f0bb828e69417c6b991eb6ac54ae0c439e1da9a44064a613d01d", + "upstream_url": "https://github.com/purescript/purescript-profunctor/blob/17f927ce69710085f2bbefc9a37506828d1ebc5a/src/Data/Profunctor/Closed.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Profunctor/Cochoice.purs", + "vendored_sha256": "d04b01c617f374356eba5358777bf945f6e41c274b7c8c58cd4327d2d6cfe117", + "vendored_lines": 9, + "self_recursions": [], + "package": "purescript-profunctor", + "tag": "v6.0.1", + "commit": "17f927ce69710085f2bbefc9a37506828d1ebc5a", + "upstream_sha256": "d04b01c617f374356eba5358777bf945f6e41c274b7c8c58cd4327d2d6cfe117", + "upstream_url": "https://github.com/purescript/purescript-profunctor/blob/17f927ce69710085f2bbefc9a37506828d1ebc5a/src/Data/Profunctor/Cochoice.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Profunctor/Costrong.purs", + "vendored_sha256": "7d7eb115e194c2d8518b070a3b39809b1b9bc0bdcfe3e48a29b40e9bd6748513", + "vendored_lines": 9, + "self_recursions": [], + "package": "purescript-profunctor", + "tag": "v6.0.1", + "commit": "17f927ce69710085f2bbefc9a37506828d1ebc5a", + "upstream_sha256": "7d7eb115e194c2d8518b070a3b39809b1b9bc0bdcfe3e48a29b40e9bd6748513", + "upstream_url": "https://github.com/purescript/purescript-profunctor/blob/17f927ce69710085f2bbefc9a37506828d1ebc5a/src/Data/Profunctor/Costrong.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Profunctor/Join.purs", + "vendored_sha256": "acbfbd68ca96516e42a4416e1c587b8db4cfae572f7814fa9784cbfeaf286c9f", + "vendored_lines": 28, + "self_recursions": [], + "package": "purescript-profunctor", + "tag": "v6.0.1", + "commit": "17f927ce69710085f2bbefc9a37506828d1ebc5a", + "upstream_sha256": "acbfbd68ca96516e42a4416e1c587b8db4cfae572f7814fa9784cbfeaf286c9f", + "upstream_url": "https://github.com/purescript/purescript-profunctor/blob/17f927ce69710085f2bbefc9a37506828d1ebc5a/src/Data/Profunctor/Join.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Profunctor/Split.purs", + "vendored_sha256": "32b0f17f7d2140c6f7ed3e536d0ec5220b504017deedcb798df586e4c61ca909", + "vendored_lines": 39, + "self_recursions": [], + "package": "purescript-profunctor", + "tag": "v6.0.1", + "commit": "17f927ce69710085f2bbefc9a37506828d1ebc5a", + "upstream_sha256": "32b0f17f7d2140c6f7ed3e536d0ec5220b504017deedcb798df586e4c61ca909", + "upstream_url": "https://github.com/purescript/purescript-profunctor/blob/17f927ce69710085f2bbefc9a37506828d1ebc5a/src/Data/Profunctor/Split.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Profunctor/Star.purs", + "vendored_sha256": "52953a97b7701dbe8619f61bf2bc0b13d2606efbf85d201527dbda5348c05b40", + "vendored_lines": 80, + "self_recursions": [], + "package": "purescript-profunctor", + "tag": "v6.0.1", + "commit": "17f927ce69710085f2bbefc9a37506828d1ebc5a", + "upstream_sha256": "52953a97b7701dbe8619f61bf2bc0b13d2606efbf85d201527dbda5348c05b40", + "upstream_url": "https://github.com/purescript/purescript-profunctor/blob/17f927ce69710085f2bbefc9a37506828d1ebc5a/src/Data/Profunctor/Star.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Profunctor/Strong.purs", + "vendored_sha256": "7ce15c8be67cd563c7d611891e73be2018fae763e26b9b10d790cab4110cc1cb", + "vendored_lines": 80, + "self_recursions": [], + "package": "purescript-profunctor", + "tag": "v6.0.1", + "commit": "17f927ce69710085f2bbefc9a37506828d1ebc5a", + "upstream_sha256": "7ce15c8be67cd563c7d611891e73be2018fae763e26b9b10d790cab4110cc1cb", + "upstream_url": "https://github.com/purescript/purescript-profunctor/blob/17f927ce69710085f2bbefc9a37506828d1ebc5a/src/Data/Profunctor/Strong.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Profunctor.purs", + "vendored_sha256": "f0e9030ebb6252131618dcbc5e181d98dab01020126bbcedd0344d78cea8c9e1", + "vendored_lines": 44, + "self_recursions": [], + "package": "purescript-profunctor", + "tag": "v6.0.1", + "commit": "17f927ce69710085f2bbefc9a37506828d1ebc5a", + "upstream_sha256": "f0e9030ebb6252131618dcbc5e181d98dab01020126bbcedd0344d78cea8c9e1", + "upstream_url": "https://github.com/purescript/purescript-profunctor/blob/17f927ce69710085f2bbefc9a37506828d1ebc5a/src/Data/Profunctor.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Reflectable.purs", + "vendored_sha256": "1e6690c68fc625587e7ec7f8417d4dffe153cc499c68220dc5b91aea6eb07807", + "vendored_lines": 58, + "self_recursions": [ + { + "name": "unsafeCoerce", + "line": 37, + "equation": "unsafeCoerce a0 = unsafeCoerce a0", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "3335b7b50707e01e8fe217afa89c1ccc3d9b1c9323329bd153094079a6890bfc", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Reflectable.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "unsafeCoerce", + "line": 36, + "declaration": "foreign import unsafeCoerce :: forall a b. a -> b", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Ring/Generic.purs", + "vendored_sha256": "931e3701f06b90079c67160b02a3e8fe78703b68da7f0de98fab1efb88a1f99a", + "vendored_lines": 24, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "4aec6adf51e6cb9e63f3004968d2bda3801d31cbc60e8ffff183307393eabee4", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Ring/Generic.purs", + "status": "newline_only", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Ring.purs", + "vendored_sha256": "63acb4da5368de6b5b30da20b01c17684e3d925a4c4232de6b99d3ddf1cf8fb7", + "vendored_lines": 79, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "1abcaa302136c9731ec2d89ee1fe29f3acbec0dd32da45d30a8d46cc6fb87b7d", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Ring.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "intSub", + "line": 54, + "declaration": "foreign import intSub :: Int -> Int -> Int", + "vendored_status": "declaration_removed" + }, + { + "name": "numSub", + "line": 55, + "declaration": "foreign import numSub :: Number -> Number -> Number", + "vendored_status": "nonrecursive_replacement" + } + ], + "changed_original_code_outside_value_ffi": [ + { + "line": 33, + "text": " sub = intSub" + }, + { + "line": 36, + "text": " sub = numSub" + } + ] + }, + { + "path": "Data/Semigroup/First.purs", + "vendored_sha256": "9df1dca916c4a4d344ddf580f2a3d3a1cb270dc8df91044147d590e8bc315347", + "vendored_lines": 40, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "9df1dca916c4a4d344ddf580f2a3d3a1cb270dc8df91044147d590e8bc315347", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Semigroup/First.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Semigroup/Foldable.purs", + "vendored_sha256": "7bbb43d0a9354e63d409e6b01f4ee634f0e900c6516ff17734eee0e0cbb03629", + "vendored_lines": 178, + "self_recursions": [], + "package": "purescript-foldable-traversable", + "tag": "v6.0.0", + "commit": "b3926f870532d287ea59e2d5cd3873b81ef2a93a", + "upstream_sha256": "7bbb43d0a9354e63d409e6b01f4ee634f0e900c6516ff17734eee0e0cbb03629", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/Semigroup/Foldable.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Semigroup/Generic.purs", + "vendored_sha256": "c33f0a8dea5d46a2c7ac0b7805840a443b2ed3d82d226319c243ff0b29ded7bd", + "vendored_lines": 31, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "c33f0a8dea5d46a2c7ac0b7805840a443b2ed3d82d226319c243ff0b29ded7bd", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Semigroup/Generic.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Semigroup/Last.purs", + "vendored_sha256": "f3aadefc2e6687234ad3d4690dd61e436bfa3aca9b57e0a1a4da7328738f7c11", + "vendored_lines": 40, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "f3aadefc2e6687234ad3d4690dd61e436bfa3aca9b57e0a1a4da7328738f7c11", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Semigroup/Last.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Semigroup/Traversable.purs", + "vendored_sha256": "361841e1f1960110c9bdaebdbdd8637742f626a26942e61fe7a8ee91fcf8b447", + "vendored_lines": 72, + "self_recursions": [], + "package": "purescript-foldable-traversable", + "tag": "v6.0.0", + "commit": "b3926f870532d287ea59e2d5cd3873b81ef2a93a", + "upstream_sha256": "361841e1f1960110c9bdaebdbdd8637742f626a26942e61fe7a8ee91fcf8b447", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/Semigroup/Traversable.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Semigroup.purs", + "vendored_sha256": "fdb73654fa6881c3e3d7c5c110ba5a5a59f4b59f6343d0af868049a154a87c4d", + "vendored_lines": 86, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "e14ab5200a3ba533334c3d06cbeac9bd9fdb3428c64a9d59926f69284ce4f9a8", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Semigroup.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "concatString", + "line": 60, + "declaration": "foreign import concatString :: String -> String -> String", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "concatArray", + "line": 61, + "declaration": "foreign import concatArray :: forall a. Array a -> Array a -> Array a", + "vendored_status": "nonrecursive_replacement" + } + ], + "changed_original_code_outside_value_ffi": [ + { + "line": 40, + "text": " append = concatString" + }, + { + "line": 52, + "text": " append = concatArray" + } + ] + }, + { + "path": "Data/Semiring/Generic.purs", + "vendored_sha256": "48598a49eeb1bae16eb02308dfb46ff9dd096b30d0100efcaee5b91232f61d88", + "vendored_lines": 51, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "13e146664e64d99ebe5d942ef3bd897ed78c85e2ab737374d1976b37c36b8601", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Semiring/Generic.purs", + "status": "newline_only", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Semiring.purs", + "vendored_sha256": "ec5df1607dee317beb962fda9b5165404d64336ce0a9d1b7f35fd45781582e36", + "vendored_lines": 142, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "6e588bb9736b230b5bd6410ce86f9e9e4d4326ab250a66fa239a7c5f822e7072", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Semiring.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "intAdd", + "line": 89, + "declaration": "foreign import intAdd :: Int -> Int -> Int", + "vendored_status": "declaration_removed" + }, + { + "name": "intMul", + "line": 90, + "declaration": "foreign import intMul :: Int -> Int -> Int", + "vendored_status": "declaration_removed" + }, + { + "name": "numAdd", + "line": 91, + "declaration": "foreign import numAdd :: Number -> Number -> Number", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "numMul", + "line": 92, + "declaration": "foreign import numMul :: Number -> Number -> Number", + "vendored_status": "nonrecursive_replacement" + } + ], + "changed_original_code_outside_value_ffi": [ + { + "line": 54, + "text": " add = intAdd" + }, + { + "line": 56, + "text": " mul = intMul" + }, + { + "line": 60, + "text": " add = numAdd" + }, + { + "line": 62, + "text": " mul = numMul" + } + ] + }, + { + "path": "Data/Set/NonEmpty.purs", + "vendored_sha256": "45a49dc49087f55b9c3c315a1fa68e62313a55bf6aab9f8df2bba52ccf7a4dd5", + "vendored_lines": 163, + "self_recursions": [], + "package": "purescript-ordered-collections", + "tag": "v3.2.0", + "commit": "b9c8ec72b53ee56d6cb6dc962d5ad7ea4708b438", + "upstream_sha256": "45a49dc49087f55b9c3c315a1fa68e62313a55bf6aab9f8df2bba52ccf7a4dd5", + "upstream_url": "https://github.com/purescript/purescript-ordered-collections/blob/b9c8ec72b53ee56d6cb6dc962d5ad7ea4708b438/src/Data/Set/NonEmpty.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Set.purs", + "vendored_sha256": "8281a67c595c3948a64b00cb3cb21d26c8b5f7a80c686e77320c85fdea46aee7", + "vendored_lines": 188, + "self_recursions": [], + "package": "purescript-ordered-collections", + "tag": "v3.2.0", + "commit": "b9c8ec72b53ee56d6cb6dc962d5ad7ea4708b438", + "upstream_sha256": "8281a67c595c3948a64b00cb3cb21d26c8b5f7a80c686e77320c85fdea46aee7", + "upstream_url": "https://github.com/purescript/purescript-ordered-collections/blob/b9c8ec72b53ee56d6cb6dc962d5ad7ea4708b438/src/Data/Set.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Show/Generic.purs", + "vendored_sha256": "b6079dad4ee250ff2ad3b1369f509f0d28a8d05e05af6cdc88eb2950f5ac1c70", + "vendored_lines": 64, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "bd4a82c3cb1e734f775ff6a40fae47b053d336313faf47efb8b4b5d78895376f", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Show/Generic.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "intercalate", + "line": 57, + "declaration": "foreign import intercalate :: String -> Array String -> String", + "vendored_status": "nonrecursive_replacement" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Show.purs", + "vendored_sha256": "1abf3b5f105c2e441d7f32e7fd3be9321b5fbfaefdb814299cae7e0d27a83f7d", + "vendored_lines": 280, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "4422b4bbe3d67f1f85575937fb7b9127af4443fe951926a95f6ecf0822032e6c", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Show.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "showIntImpl", + "line": 93, + "declaration": "foreign import showIntImpl :: Int -> String", + "vendored_status": "declaration_removed" + }, + { + "name": "showNumberImpl", + "line": 94, + "declaration": "foreign import showNumberImpl :: Number -> String", + "vendored_status": "declaration_removed" + }, + { + "name": "showCharImpl", + "line": 95, + "declaration": "foreign import showCharImpl :: Char -> String", + "vendored_status": "declaration_removed" + }, + { + "name": "showStringImpl", + "line": 96, + "declaration": "foreign import showStringImpl :: String -> String", + "vendored_status": "declaration_removed" + }, + { + "name": "showArrayImpl", + "line": 97, + "declaration": "foreign import showArrayImpl :: forall a. (a -> String) -> Array a -> String", + "vendored_status": "declaration_removed" + } + ], + "changed_original_code_outside_value_ffi": [ + { + "line": 4, + "text": " , class ShowRecordFields" + }, + { + "line": 5, + "text": " , showRecordFields" + }, + { + "line": 9, + "text": "import Data.Symbol (class IsSymbol, reflectSymbol)" + }, + { + "line": 10, + "text": "import Data.Unit (Unit)" + }, + { + "line": 11, + "text": "import Data.Void (Void, absurd)" + }, + { + "line": 12, + "text": "import Prim.Row (class Nub)" + }, + { + "line": 13, + "text": "import Prim.RowList as RL" + }, + { + "line": 14, + "text": "import Record.Unsafe (unsafeGet)" + }, + { + "line": 15, + "text": "import Type.Proxy (Proxy(..))" + }, + { + "line": 29, + "text": "instance showBoolean :: Show Boolean where" + }, + { + "line": 30, + "text": " show true = \"true\"" + }, + { + "line": 31, + "text": " show false = \"false\"" + }, + { + "line": 33, + "text": "instance showInt :: Show Int where" + }, + { + "line": 34, + "text": " show = showIntImpl" + }, + { + "line": 36, + "text": "instance showNumber :: Show Number where" + }, + { + "line": 37, + "text": " show = showNumberImpl" + }, + { + "line": 39, + "text": "instance showChar :: Show Char where" + }, + { + "line": 40, + "text": " show = showCharImpl" + }, + { + "line": 42, + "text": "instance showString :: Show String where" + }, + { + "line": 43, + "text": " show = showStringImpl" + }, + { + "line": 45, + "text": "instance showArray :: Show a => Show (Array a) where" + }, + { + "line": 46, + "text": " show = showArrayImpl show" + }, + { + "line": 48, + "text": "instance showProxy :: Show (Proxy a) where" + }, + { + "line": 49, + "text": " show _ = \"Proxy\"" + }, + { + "line": 51, + "text": "instance showVoid :: Show Void where" + }, + { + "line": 52, + "text": " show = absurd" + }, + { + "line": 54, + "text": "instance showRecord ::" + }, + { + "line": 55, + "text": " ( Nub rs rs" + }, + { + "line": 56, + "text": " , RL.RowToList rs ls" + }, + { + "line": 57, + "text": " , ShowRecordFields ls rs" + }, + { + "line": 58, + "text": " ) =>" + }, + { + "line": 59, + "text": " Show (Record rs) where" + }, + { + "line": 60, + "text": " show record = \"{\" <> showRecordFields (Proxy :: Proxy ls) record <> \"}\"" + }, + { + "line": 64, + "text": "class ShowRecordFields :: RL.RowList Type -> Row Type -> Constraint" + }, + { + "line": 65, + "text": "class ShowRecordFields rowlist row where" + }, + { + "line": 66, + "text": " showRecordFields :: Proxy rowlist -> Record row -> String" + }, + { + "line": 68, + "text": "instance showRecordFieldsNil :: ShowRecordFields RL.Nil row where" + }, + { + "line": 69, + "text": " showRecordFields _ _ = \"\"" + }, + { + "line": 70, + "text": "else" + }, + { + "line": 71, + "text": "instance showRecordFieldsConsNil ::" + }, + { + "line": 72, + "text": " ( IsSymbol key" + }, + { + "line": 73, + "text": " , Show focus" + }, + { + "line": 74, + "text": " ) =>" + }, + { + "line": 75, + "text": " ShowRecordFields (RL.Cons key focus RL.Nil) row where" + }, + { + "line": 76, + "text": " showRecordFields _ record = \" \" <> key <> \": \" <> show focus <> \" \"" + }, + { + "line": 77, + "text": " where" + }, + { + "line": 78, + "text": " key = reflectSymbol (Proxy :: Proxy key)" + }, + { + "line": 79, + "text": " focus = unsafeGet key record :: focus" + }, + { + "line": 80, + "text": "else" + }, + { + "line": 81, + "text": "instance showRecordFieldsCons ::" + }, + { + "line": 82, + "text": " ( IsSymbol key" + }, + { + "line": 83, + "text": " , ShowRecordFields rowlistTail row" + }, + { + "line": 84, + "text": " , Show focus" + }, + { + "line": 85, + "text": " ) =>" + }, + { + "line": 86, + "text": " ShowRecordFields (RL.Cons key focus rowlistTail) row where" + }, + { + "line": 87, + "text": " showRecordFields _ record = \" \" <> key <> \": \" <> show focus <> \",\" <> tail" + }, + { + "line": 88, + "text": " where" + }, + { + "line": 89, + "text": " key = reflectSymbol (Proxy :: Proxy key)" + }, + { + "line": 90, + "text": " focus = unsafeGet key record :: focus" + }, + { + "line": 91, + "text": " tail = showRecordFields (Proxy :: Proxy rowlistTail) record" + } + ] + }, + { + "path": "Data/String/CaseInsensitive.purs", + "vendored_sha256": "1d889da73589b5bed249153cc55a01654a912756bc062ae1f12df3357c2ca9ed", + "vendored_lines": 22, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "1d889da73589b5bed249153cc55a01654a912756bc062ae1f12df3357c2ca9ed", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CaseInsensitive.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/CodePoints.purs", + "vendored_sha256": "5f4619365a32a93f2adf79e63dd58d292781520ba5da3227d46736ac031a4d50", + "vendored_lines": 418, + "self_recursions": [ + { + "name": "_singleton", + "line": 92, + "equation": "_singleton a0 a1 = _singleton a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "_fromCodePointArray", + "line": 116, + "equation": "_fromCodePointArray a0 a1 = _fromCodePointArray a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "_toCodePointArray", + "line": 133, + "equation": "_toCodePointArray a0 a1 a2 = _toCodePointArray a0 a1 a2", + "replaces_upstream_foreign": true + }, + { + "name": "_codePointAt", + "line": 160, + "equation": "_codePointAt a0 a1 a2 a3 a4 a5 = _codePointAt a0 a1 a2 a3 a4 a5", + "replaces_upstream_foreign": true + }, + { + "name": "_countPrefix", + "line": 218, + "equation": "_countPrefix a0 a1 a2 a3 = _countPrefix a0 a1 a2 a3", + "replaces_upstream_foreign": true + }, + { + "name": "_take", + "line": 315, + "equation": "_take a0 a1 a2 = _take a0 a1 a2", + "replaces_upstream_foreign": true + }, + { + "name": "_unsafeCodePointAt0", + "line": 406, + "equation": "_unsafeCodePointAt0 a0 a1 = _unsafeCodePointAt0 a0 a1", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "a8415ac45b14a0d78334af6d0fb533c7bfc3cfb3ed90e28113c453c9b15f170b", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodePoints.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "_singleton", + "line": 91, + "declaration": "foreign import _singleton :: (CodePoint -> String) -> CodePoint -> String", + "vendored_status": "direct_self_recursion" + }, + { + "name": "_fromCodePointArray", + "line": 117, + "declaration": "foreign import _fromCodePointArray :: (CodePoint -> String) -> Array CodePoint -> String", + "vendored_status": "direct_self_recursion" + }, + { + "name": "_toCodePointArray", + "line": 136, + "declaration": "foreign import _toCodePointArray :: (String -> Array CodePoint) -> (String -> CodePoint) -> String -> Array CodePoint", + "vendored_status": "direct_self_recursion" + }, + { + "name": "_codePointAt", + "line": 166, + "declaration": "foreign import _codePointAt :: (Int -> String -> Maybe CodePoint) -> (forall a. a -> Maybe a) -> (forall a. Maybe a) -> (String -> CodePoint) -> Int -> String -> Maybe CodePoint", + "vendored_status": "direct_self_recursion" + }, + { + "name": "_countPrefix", + "line": 230, + "declaration": "foreign import _countPrefix :: ((CodePoint -> Boolean) -> String -> Int) -> (String -> CodePoint) -> (CodePoint -> Boolean) -> String -> Int", + "vendored_status": "direct_self_recursion" + }, + { + "name": "_take", + "line": 331, + "declaration": "foreign import _take :: (Int -> String -> String) -> Int -> String -> String", + "vendored_status": "direct_self_recursion" + }, + { + "name": "_unsafeCodePointAt0", + "line": 421, + "declaration": "foreign import _unsafeCodePointAt0 :: (String -> CodePoint) -> String -> CodePoint", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/CodeUnits.purs", + "vendored_sha256": "6fad18f94ea9b6310d9287777ebf95e85a09d30c12d7e1b1bd30abc56665dbce", + "vendored_lines": 316, + "self_recursions": [ + { + "name": "singleton", + "line": 84, + "equation": "singleton a0 = singleton a0", + "replaces_upstream_foreign": true + }, + { + "name": "fromCharArray", + "line": 92, + "equation": "fromCharArray a0 = fromCharArray a0", + "replaces_upstream_foreign": true + }, + { + "name": "toCharArray", + "line": 100, + "equation": "toCharArray a0 = toCharArray a0", + "replaces_upstream_foreign": true + }, + { + "name": "_charAt", + "line": 113, + "equation": "_charAt a0 a1 a2 a3 = _charAt a0 a1 a2 a3", + "replaces_upstream_foreign": true + }, + { + "name": "_toChar", + "line": 126, + "equation": "_toChar a0 a1 a2 = _toChar a0 a1 a2", + "replaces_upstream_foreign": true + }, + { + "name": "length", + "line": 147, + "equation": "length a0 = length a0", + "replaces_upstream_foreign": true + }, + { + "name": "countPrefix", + "line": 157, + "equation": "countPrefix a0 a1 = countPrefix a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "_indexOf", + "line": 171, + "equation": "_indexOf a0 a1 a2 a3 = _indexOf a0 a1 a2 a3", + "replaces_upstream_foreign": true + }, + { + "name": "_indexOfStartingAt", + "line": 186, + "equation": "_indexOfStartingAt a0 a1 a2 a3 a4 = _indexOfStartingAt a0 a1 a2 a3 a4", + "replaces_upstream_foreign": true + }, + { + "name": "_lastIndexOf", + "line": 200, + "equation": "_lastIndexOf a0 a1 a2 a3 = _lastIndexOf a0 a1 a2 a3", + "replaces_upstream_foreign": true + }, + { + "name": "_lastIndexOfStartingAt", + "line": 224, + "equation": "_lastIndexOfStartingAt a0 a1 a2 a3 a4 = _lastIndexOfStartingAt a0 a1 a2 a3 a4", + "replaces_upstream_foreign": true + }, + { + "name": "take", + "line": 233, + "equation": "take a0 a1 = take a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "drop", + "line": 261, + "equation": "drop a0 a1 = drop a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "slice", + "line": 294, + "equation": "slice a0 a1 a2 = slice a0 a1 a2", + "replaces_upstream_foreign": true + }, + { + "name": "splitAt", + "line": 316, + "equation": "splitAt a0 a1 = splitAt a0 a1", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "801f13b0d180a49a454d6456c1c5a7a235a2a7d1827ab31537506e9b279a0648", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodeUnits.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "singleton", + "line": 83, + "declaration": "foreign import singleton :: Char -> String", + "vendored_status": "direct_self_recursion" + }, + { + "name": "fromCharArray", + "line": 90, + "declaration": "foreign import fromCharArray :: Array Char -> String", + "vendored_status": "direct_self_recursion" + }, + { + "name": "toCharArray", + "line": 97, + "declaration": "foreign import toCharArray :: String -> Array Char", + "vendored_status": "direct_self_recursion" + }, + { + "name": "_charAt", + "line": 109, + "declaration": "foreign import _charAt :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Int -> String -> Maybe Char", + "vendored_status": "direct_self_recursion" + }, + { + "name": "_toChar", + "line": 126, + "declaration": "foreign import _toChar :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> String -> Maybe Char", + "vendored_status": "direct_self_recursion" + }, + { + "name": "length", + "line": 150, + "declaration": "foreign import length :: String -> Int", + "vendored_status": "direct_self_recursion" + }, + { + "name": "countPrefix", + "line": 159, + "declaration": "foreign import countPrefix :: (Char -> Boolean) -> String -> Int", + "vendored_status": "direct_self_recursion" + }, + { + "name": "_indexOf", + "line": 172, + "declaration": "foreign import _indexOf :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Pattern -> String -> Maybe Int", + "vendored_status": "direct_self_recursion" + }, + { + "name": "_indexOfStartingAt", + "line": 191, + "declaration": "foreign import _indexOfStartingAt :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Pattern -> Int -> String -> Maybe Int", + "vendored_status": "direct_self_recursion" + }, + { + "name": "_lastIndexOf", + "line": 210, + "declaration": "foreign import _lastIndexOf :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Pattern -> String -> Maybe Int", + "vendored_status": "direct_self_recursion" + }, + { + "name": "_lastIndexOfStartingAt", + "line": 238, + "declaration": "foreign import _lastIndexOfStartingAt :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Pattern -> Int -> String -> Maybe Int", + "vendored_status": "direct_self_recursion" + }, + { + "name": "take", + "line": 252, + "declaration": "foreign import take :: Int -> String -> String", + "vendored_status": "direct_self_recursion" + }, + { + "name": "drop", + "line": 279, + "declaration": "foreign import drop :: Int -> String -> String", + "vendored_status": "direct_self_recursion" + }, + { + "name": "slice", + "line": 311, + "declaration": "foreign import slice :: Int -> Int -> String -> String", + "vendored_status": "direct_self_recursion" + }, + { + "name": "splitAt", + "line": 332, + "declaration": "foreign import splitAt :: Int -> String -> { before :: String, after :: String }", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/Common.purs", + "vendored_sha256": "d6bc291322f005e1aef6fe4186037d484587b22704c285330130c7cf94054fdc", + "vendored_lines": 98, + "self_recursions": [ + { + "name": "_localeCompare", + "line": 38, + "equation": "_localeCompare a0 a1 a2 a3 a4 = _localeCompare a0 a1 a2 a3 a4", + "replaces_upstream_foreign": true + }, + { + "name": "replace", + "line": 46, + "equation": "replace a0 a1 a2 = replace a0 a1 a2", + "replaces_upstream_foreign": true + }, + { + "name": "replaceAll", + "line": 54, + "equation": "replaceAll a0 a1 a2 = replaceAll a0 a1 a2", + "replaces_upstream_foreign": true + }, + { + "name": "split", + "line": 63, + "equation": "split a0 a1 = split a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "toLower", + "line": 71, + "equation": "toLower a0 = toLower a0", + "replaces_upstream_foreign": true + }, + { + "name": "toUpper", + "line": 79, + "equation": "toUpper a0 = toUpper a0", + "replaces_upstream_foreign": true + }, + { + "name": "trim", + "line": 89, + "equation": "trim a0 = trim a0", + "replaces_upstream_foreign": true + }, + { + "name": "joinWith", + "line": 98, + "equation": "joinWith a0 a1 = joinWith a0 a1", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "286d269bba5d0010924bc62a45188f664d93f7486707f9a3d5038ad7699a2f84", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Common.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "_localeCompare", + "line": 37, + "declaration": "foreign import _localeCompare :: Ordering -> Ordering -> Ordering -> String -> String -> Ordering", + "vendored_status": "direct_self_recursion" + }, + { + "name": "replace", + "line": 50, + "declaration": "foreign import replace :: Pattern -> Replacement -> String -> String", + "vendored_status": "direct_self_recursion" + }, + { + "name": "replaceAll", + "line": 57, + "declaration": "foreign import replaceAll :: Pattern -> Replacement -> String -> String", + "vendored_status": "direct_self_recursion" + }, + { + "name": "split", + "line": 65, + "declaration": "foreign import split :: Pattern -> String -> Array String", + "vendored_status": "direct_self_recursion" + }, + { + "name": "toLower", + "line": 72, + "declaration": "foreign import toLower :: String -> String", + "vendored_status": "direct_self_recursion" + }, + { + "name": "toUpper", + "line": 79, + "declaration": "foreign import toUpper :: String -> String", + "vendored_status": "direct_self_recursion" + }, + { + "name": "trim", + "line": 88, + "declaration": "foreign import trim :: String -> String", + "vendored_status": "direct_self_recursion" + }, + { + "name": "joinWith", + "line": 96, + "declaration": "foreign import joinWith :: String -> Array String -> String", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/Gen.purs", + "vendored_sha256": "fdc6f113d91213f5571aaef42726fd6966eba12c56cb30c40736ff9398a6585b", + "vendored_lines": 43, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "fdc6f113d91213f5571aaef42726fd6966eba12c56cb30c40736ff9398a6585b", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Gen.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/NonEmpty/CaseInsensitive.purs", + "vendored_sha256": "73160accd99f00200e60a5431d70f197aded453aac143f15d842d8fcb984d803", + "vendored_lines": 22, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "73160accd99f00200e60a5431d70f197aded453aac143f15d842d8fcb984d803", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/NonEmpty/CaseInsensitive.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/NonEmpty/CodePoints.purs", + "vendored_sha256": "26f6c829cda09064888b07ada1989e44d5781c81d9927f9b3c2351e82b3077b0", + "vendored_lines": 138, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "26f6c829cda09064888b07ada1989e44d5781c81d9927f9b3c2351e82b3077b0", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/NonEmpty/CodePoints.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/NonEmpty/CodeUnits.purs", + "vendored_sha256": "305c39d652a011b9dbb723e22be8e87767bd91fb3f962cb511666828647fafc8", + "vendored_lines": 308, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "305c39d652a011b9dbb723e22be8e87767bd91fb3f962cb511666828647fafc8", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/NonEmpty/CodeUnits.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/NonEmpty/Internal.purs", + "vendored_sha256": "1efe2c4ec248665e11f8978bf5f44d6eec2c2c248586c47a6137e652d452d966", + "vendored_lines": 232, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "1efe2c4ec248665e11f8978bf5f44d6eec2c2c248586c47a6137e652d452d966", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/NonEmpty/Internal.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/NonEmpty.purs", + "vendored_sha256": "b369bd4f0f9ee7f1d8ced366cd82f0c8b17b99365517d9f305b2fe980c172134", + "vendored_lines": 9, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "b369bd4f0f9ee7f1d8ced366cd82f0c8b17b99365517d9f305b2fe980c172134", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/NonEmpty.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/Pattern.purs", + "vendored_sha256": "cd63d7bb03364c5f5e0b2fbe44746c81b2965300bbf890702331d146a5f3b90f", + "vendored_lines": 33, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "cd63d7bb03364c5f5e0b2fbe44746c81b2965300bbf890702331d146a5f3b90f", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Pattern.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/Regex/Flags.purs", + "vendored_sha256": "c764c8d36ccd74d5603dbfef8b62976788369c9844bceab91c319d61dea16c16", + "vendored_lines": 129, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "c764c8d36ccd74d5603dbfef8b62976788369c9844bceab91c319d61dea16c16", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Regex/Flags.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/Regex/Unsafe.purs", + "vendored_sha256": "9a0bf4c37a2ac844622b29a72d24c3b3aa1e0ad04bcfe1186962f6cd2e433e59", + "vendored_lines": 14, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "9a0bf4c37a2ac844622b29a72d24c3b3aa1e0ad04bcfe1186962f6cd2e433e59", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Regex/Unsafe.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/Regex.purs", + "vendored_sha256": "224d33bddffeccad9347c2d4481752d3a5eb8ac29e0913bb7328b5e2faf1d58d", + "vendored_lines": 120, + "self_recursions": [ + { + "name": "showRegexImpl", + "line": 32, + "equation": "showRegexImpl a0 = showRegexImpl a0", + "replaces_upstream_foreign": true + }, + { + "name": "regexImpl", + "line": 38, + "equation": "regexImpl a0 a1 a2 a3 = regexImpl a0 a1 a2 a3", + "replaces_upstream_foreign": true + }, + { + "name": "source", + "line": 47, + "equation": "source a0 = source a0", + "replaces_upstream_foreign": true + }, + { + "name": "flagsImpl", + "line": 55, + "equation": "flagsImpl a0 = flagsImpl a0", + "replaces_upstream_foreign": true + }, + { + "name": "test", + "line": 82, + "equation": "test a0 a1 = test a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "_match", + "line": 85, + "equation": "_match a0 a1 a2 a3 = _match a0 a1 a2 a3", + "replaces_upstream_foreign": true + }, + { + "name": "replace", + "line": 98, + "equation": "replace a0 a1 a2 = replace a0 a1 a2", + "replaces_upstream_foreign": true + }, + { + "name": "_replaceBy", + "line": 101, + "equation": "_replaceBy a0 a1 a2 a3 a4 = _replaceBy a0 a1 a2 a3 a4", + "replaces_upstream_foreign": true + }, + { + "name": "_search", + "line": 111, + "equation": "_search a0 a1 a2 a3 = _search a0 a1 a2 a3", + "replaces_upstream_foreign": true + }, + { + "name": "split", + "line": 120, + "equation": "split a0 a1 = split a0 a1", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "101f80a5bcba3f3bf1b6ec1ec68f48c18d149d9e4b7808eddf8e753eb15d1d88", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Regex.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "showRegexImpl", + "line": 31, + "declaration": "foreign import showRegexImpl :: Regex -> String", + "vendored_status": "direct_self_recursion" + }, + { + "name": "regexImpl", + "line": 36, + "declaration": "foreign import regexImpl :: (String -> Either String Regex) -> (Regex -> Either String Regex) -> String -> String -> Either String Regex", + "vendored_status": "direct_self_recursion" + }, + { + "name": "source", + "line": 49, + "declaration": "foreign import source :: Regex -> String", + "vendored_status": "direct_self_recursion" + }, + { + "name": "flagsImpl", + "line": 56, + "declaration": "foreign import flagsImpl :: Regex -> RegexFlagsRec", + "vendored_status": "direct_self_recursion" + }, + { + "name": "test", + "line": 82, + "declaration": "foreign import test :: Regex -> String -> Boolean", + "vendored_status": "direct_self_recursion" + }, + { + "name": "_match", + "line": 84, + "declaration": "foreign import _match :: (forall r. r -> Maybe r) -> (forall r. Maybe r) -> Regex -> String -> Maybe (NonEmptyArray (Maybe String))", + "vendored_status": "direct_self_recursion" + }, + { + "name": "replace", + "line": 101, + "declaration": "foreign import replace :: Regex -> String -> String -> String", + "vendored_status": "direct_self_recursion" + }, + { + "name": "_replaceBy", + "line": 103, + "declaration": "foreign import _replaceBy :: (forall r. r -> Maybe r) -> (forall r. Maybe r) -> Regex -> (String -> Array (Maybe String) -> String) -> String -> String", + "vendored_status": "direct_self_recursion" + }, + { + "name": "_search", + "line": 118, + "declaration": "foreign import _search :: (forall r. r -> Maybe r) -> (forall r. Maybe r) -> Regex -> String -> Maybe Int", + "vendored_status": "direct_self_recursion" + }, + { + "name": "split", + "line": 131, + "declaration": "foreign import split :: Regex -> String -> Array String", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/Unsafe.purs", + "vendored_sha256": "c737041c3238223555c325d8a30db5f590ea817b54f243be91fd968777f165f5", + "vendored_lines": 17, + "self_recursions": [ + { + "name": "charAt", + "line": 11, + "equation": "charAt a0 a1 = charAt a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "char", + "line": 17, + "equation": "char a0 = char a0", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "b361c98410d61e2d110a4ae54cda75012679a6733b101dde378d3307ec69c702", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Unsafe.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "charAt", + "line": 10, + "declaration": "foreign import charAt :: Int -> String -> Char", + "vendored_status": "direct_self_recursion" + }, + { + "name": "char", + "line": 15, + "declaration": "foreign import char :: String -> Char", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String.purs", + "vendored_sha256": "0cf63fa0bccd027bededff0647c1a4251d235be02c1df8a8310be2aea2eb367f", + "vendored_lines": 10, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "0cf63fa0bccd027bededff0647c1a4251d235be02c1df8a8310be2aea2eb367f", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Symbol.purs", + "vendored_sha256": "ed7c86cc83440e529dec16a2a4a7a62e1e5f1f60a31e4ff56d3146489cd00946", + "vendored_lines": 25, + "self_recursions": [ + { + "name": "unsafeCoerce", + "line": 15, + "equation": "unsafeCoerce a0 = unsafeCoerce a0", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "8302d378edfcd58fbe1aadcf95c22f8637533de188b99ca2a0e43147e8957ec4", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Symbol.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "unsafeCoerce", + "line": 14, + "declaration": "foreign import unsafeCoerce :: forall a b. a -> b", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Traversable/Accum/Internal.purs", + "vendored_sha256": "03a2322dd8920092618ad5a9ef8bab31e740b384af88b2e9816abd25d34d1755", + "vendored_lines": 44, + "self_recursions": [], + "package": "purescript-foldable-traversable", + "tag": "v6.0.0", + "commit": "b3926f870532d287ea59e2d5cd3873b81ef2a93a", + "upstream_sha256": "03a2322dd8920092618ad5a9ef8bab31e740b384af88b2e9816abd25d34d1755", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/Traversable/Accum/Internal.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Traversable/Accum.purs", + "vendored_sha256": "a37609f85d2dc1e527a2cd969302f0bf0bd5c8735226f5fbc874f67a6f4b646d", + "vendored_lines": 5, + "self_recursions": [], + "package": "purescript-foldable-traversable", + "tag": "v6.0.0", + "commit": "b3926f870532d287ea59e2d5cd3873b81ef2a93a", + "upstream_sha256": "a37609f85d2dc1e527a2cd969302f0bf0bd5c8735226f5fbc874f67a6f4b646d", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/Traversable/Accum.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Traversable.purs", + "vendored_sha256": "b7410ac276078ed6a821fac77137bf631e6c9ba31b438e5d5d04b4eb33129675", + "vendored_lines": 251, + "self_recursions": [ + { + "name": "traverseArrayImpl", + "line": 107, + "equation": "traverseArrayImpl a0 a1 a2 a3 a4 = traverseArrayImpl a0 a1 a2 a3 a4", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-foldable-traversable", + "tag": "v6.0.0", + "commit": "b3926f870532d287ea59e2d5cd3873b81ef2a93a", + "upstream_sha256": "605c2e3c31d3531ce3674d5fb48ab608981b1553f9272c234ad9ca792108bf77", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/Traversable.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "traverseArrayImpl", + "line": 106, + "declaration": "foreign import traverseArrayImpl :: forall m a b . (forall x y. m (x -> y) -> m x -> m y) -> (forall x y. (x -> y) -> m x -> m y) -> (forall x. x -> m x) -> (a -> m b) -> Array a -> m (Array b)", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/TraversableWithIndex.purs", + "vendored_sha256": "aa2f2406e9ffbf49bb8b07a74b5c044eef9f4e3cef21b8cf7ba18a0edddc23ac", + "vendored_lines": 213, + "self_recursions": [], + "package": "purescript-foldable-traversable", + "tag": "v6.0.0", + "commit": "b3926f870532d287ea59e2d5cd3873b81ef2a93a", + "upstream_sha256": "aa2f2406e9ffbf49bb8b07a74b5c044eef9f4e3cef21b8cf7ba18a0edddc23ac", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/TraversableWithIndex.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Tuple/Nested.purs", + "vendored_sha256": "6747d4c125c506fb22b831142513c54112fb3bf5908697534b540ce8e4cb0a4e", + "vendored_lines": 294, + "self_recursions": [], + "package": "purescript-tuples", + "tag": "v7.0.0", + "commit": "4f52da2729b448c8564369378f1232d8d2dc1d8b", + "upstream_sha256": "6747d4c125c506fb22b831142513c54112fb3bf5908697534b540ce8e4cb0a4e", + "upstream_url": "https://github.com/purescript/purescript-tuples/blob/4f52da2729b448c8564369378f1232d8d2dc1d8b/src/Data/Tuple/Nested.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Tuple.purs", + "vendored_sha256": "dbeb961cc0b8d3b9b29d2377d2d961bba227087e6b324c19cef818c638dc1f06", + "vendored_lines": 47, + "self_recursions": [], + "package": "purescript-tuples", + "tag": "v7.0.0", + "commit": "4f52da2729b448c8564369378f1232d8d2dc1d8b", + "upstream_sha256": "dc2ef1a90e52e851e02de1e8bb3bbbe9bfdbff8fe821473cbfda1998a82bc0d7", + "upstream_url": "https://github.com/purescript/purescript-tuples/blob/4f52da2729b448c8564369378f1232d8d2dc1d8b/src/Data/Tuple.purs", + "status": "modified", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [ + { + "line": 2, + "text": "module Data.Tuple where" + }, + { + "line": 4, + "text": "import Prelude" + }, + { + "line": 6, + "text": "import Control.Comonad (class Comonad)" + }, + { + "line": 7, + "text": "import Control.Extend (class Extend)" + }, + { + "line": 8, + "text": "import Control.Lazy (class Lazy, defer)" + }, + { + "line": 9, + "text": "import Data.Eq (class Eq1)" + }, + { + "line": 10, + "text": "import Data.Functor.Invariant (class Invariant, imapF)" + }, + { + "line": 11, + "text": "import Data.Generic.Rep (class Generic)" + }, + { + "line": 12, + "text": "import Data.HeytingAlgebra (implies, ff, tt)" + }, + { + "line": 13, + "text": "import Data.Ord (class Ord1)" + }, + { + "line": 20, + "text": "instance showTuple :: (Show a, Show b) => Show (Tuple a b) where" + }, + { + "line": 21, + "text": " show (Tuple a b) = \"(Tuple \" <> show a <> \" \" <> show b <> \")\"" + }, + { + "line": 27, + "text": "derive instance eq1Tuple :: Eq a => Eq1 (Tuple a)" + }, + { + "line": 35, + "text": "derive instance ord1Tuple :: Ord a => Ord1 (Tuple a)" + }, + { + "line": 37, + "text": "instance boundedTuple :: (Bounded a, Bounded b) => Bounded (Tuple a b) where" + }, + { + "line": 38, + "text": " top = Tuple top top" + }, + { + "line": 39, + "text": " bottom = Tuple bottom bottom" + }, + { + "line": 41, + "text": "instance semigroupoidTuple :: Semigroupoid Tuple where" + }, + { + "line": 42, + "text": " compose (Tuple _ c) (Tuple a _) = Tuple a c" + }, + { + "line": 50, + "text": "instance semigroupTuple :: (Semigroup a, Semigroup b) => Semigroup (Tuple a b) where" + }, + { + "line": 51, + "text": " append (Tuple a1 b1) (Tuple a2 b2) = Tuple (a1 <> a2) (b1 <> b2)" + }, + { + "line": 53, + "text": "instance monoidTuple :: (Monoid a, Monoid b) => Monoid (Tuple a b) where" + }, + { + "line": 54, + "text": " mempty = Tuple mempty mempty" + }, + { + "line": 56, + "text": "instance semiringTuple :: (Semiring a, Semiring b) => Semiring (Tuple a b) where" + }, + { + "line": 57, + "text": " add (Tuple x1 y1) (Tuple x2 y2) = Tuple (add x1 x2) (add y1 y2)" + }, + { + "line": 58, + "text": " one = Tuple one one" + }, + { + "line": 59, + "text": " mul (Tuple x1 y1) (Tuple x2 y2) = Tuple (mul x1 x2) (mul y1 y2)" + }, + { + "line": 60, + "text": " zero = Tuple zero zero" + }, + { + "line": 62, + "text": "instance ringTuple :: (Ring a, Ring b) => Ring (Tuple a b) where" + }, + { + "line": 63, + "text": " sub (Tuple x1 y1) (Tuple x2 y2) = Tuple (sub x1 x2) (sub y1 y2)" + }, + { + "line": 65, + "text": "instance commutativeRingTuple :: (CommutativeRing a, CommutativeRing b) => CommutativeRing (Tuple a b)" + }, + { + "line": 67, + "text": "instance heytingAlgebraTuple :: (HeytingAlgebra a, HeytingAlgebra b) => HeytingAlgebra (Tuple a b) where" + }, + { + "line": 68, + "text": " tt = Tuple tt tt" + }, + { + "line": 69, + "text": " ff = Tuple ff ff" + }, + { + "line": 70, + "text": " implies (Tuple x1 y1) (Tuple x2 y2) = Tuple (x1 `implies` x2) (y1 `implies` y2)" + }, + { + "line": 71, + "text": " conj (Tuple x1 y1) (Tuple x2 y2) = Tuple (conj x1 x2) (conj y1 y2)" + }, + { + "line": 72, + "text": " disj (Tuple x1 y1) (Tuple x2 y2) = Tuple (disj x1 x2) (disj y1 y2)" + }, + { + "line": 73, + "text": " not (Tuple x y) = Tuple (not x) (not y)" + }, + { + "line": 75, + "text": "instance booleanAlgebraTuple :: (BooleanAlgebra a, BooleanAlgebra b) => BooleanAlgebra (Tuple a b)" + }, + { + "line": 85, + "text": "derive instance genericTuple :: Generic (Tuple a b) _" + }, + { + "line": 87, + "text": "instance invariantTuple :: Invariant (Tuple a) where" + }, + { + "line": 88, + "text": " imap = imapF" + }, + { + "line": 96, + "text": "instance applyTuple :: (Semigroup a) => Apply (Tuple a) where" + }, + { + "line": 97, + "text": " apply (Tuple a1 f) (Tuple a2 x) = Tuple (a1 <> a2) (f x)" + }, + { + "line": 99, + "text": "instance applicativeTuple :: (Monoid a) => Applicative (Tuple a) where" + }, + { + "line": 100, + "text": " pure = Tuple mempty" + }, + { + "line": 102, + "text": "instance bindTuple :: (Semigroup a) => Bind (Tuple a) where" + }, + { + "line": 103, + "text": " bind (Tuple a1 b) f = case f b of" + }, + { + "line": 104, + "text": " Tuple a2 c -> Tuple (a1 <> a2) c" + }, + { + "line": 106, + "text": "instance monadTuple :: (Monoid a) => Monad (Tuple a)" + }, + { + "line": 108, + "text": "instance extendTuple :: Extend (Tuple a) where" + }, + { + "line": 109, + "text": " extend f t@(Tuple a _) = Tuple a (f t)" + }, + { + "line": 111, + "text": "instance comonadTuple :: Comonad (Tuple a) where" + }, + { + "line": 112, + "text": " extract = snd" + }, + { + "line": 114, + "text": "instance lazyTuple :: (Lazy a, Lazy b) => Lazy (Tuple a b) where" + }, + { + "line": 115, + "text": " defer f = Tuple (defer $ \\_ -> fst (f unit)) (defer $ \\_ -> snd (f unit))" + }, + { + "line": 118, + "text": "fst :: forall a b. Tuple a b -> a" + }, + { + "line": 119, + "text": "fst (Tuple a _) = a" + }, + { + "line": 122, + "text": "snd :: forall a b. Tuple a b -> b" + }, + { + "line": 123, + "text": "snd (Tuple _ b) = b" + }, + { + "line": 126, + "text": "curry :: forall a b c. (Tuple a b -> c) -> a -> b -> c" + }, + { + "line": 127, + "text": "curry f a b = f (Tuple a b)" + }, + { + "line": 130, + "text": "uncurry :: forall a b c. (a -> b -> c) -> Tuple a b -> c" + }, + { + "line": 131, + "text": "uncurry f (Tuple a b) = f a b" + }, + { + "line": 135, + "text": "swap (Tuple a b) = Tuple b a" + } + ] + }, + { + "path": "Data/Unfoldable.purs", + "vendored_sha256": "c9e3dc6b977c57614e65fe0d20ab7b13d9358f22dbbef0367558cf2e2602001c", + "vendored_lines": 96, + "self_recursions": [ + { + "name": "unfoldrArrayImpl", + "line": 48, + "equation": "unfoldrArrayImpl a0 a1 a2 a3 a4 a5 = unfoldrArrayImpl a0 a1 a2 a3 a4 a5", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-unfoldable", + "tag": "v6.0.0", + "commit": "493dfe04ed590e20d8f69079df2f58486882748d", + "upstream_sha256": "c36722cb427b94e8d20b6a4df3afd8163ebf71cf88502cc20e68d8e3cad773f7", + "upstream_url": "https://github.com/purescript/purescript-unfoldable/blob/493dfe04ed590e20d8f69079df2f58486882748d/src/Data/Unfoldable.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "unfoldrArrayImpl", + "line": 47, + "declaration": "foreign import unfoldrArrayImpl :: forall a b . (forall x. Maybe x -> Boolean) -> (forall x. Maybe x -> x) -> (forall x y. Tuple x y -> x) -> (forall x y. Tuple x y -> y) -> (b -> Maybe (Tuple a b)) -> b -> Array a", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Unfoldable1.purs", + "vendored_sha256": "f609c34c7ec0e075fed9e31834461b445fa1031bbcf492dd30f04945b3bcc414", + "vendored_lines": 124, + "self_recursions": [ + { + "name": "unfoldr1ArrayImpl", + "line": 49, + "equation": "unfoldr1ArrayImpl a0 a1 a2 a3 a4 a5 = unfoldr1ArrayImpl a0 a1 a2 a3 a4 a5", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-unfoldable", + "tag": "v6.0.0", + "commit": "493dfe04ed590e20d8f69079df2f58486882748d", + "upstream_sha256": "7e5bf042a5c7d47bc6b5c57bcbfec7eadfd72d2f33979fa0d5ab7c4605f8edc1", + "upstream_url": "https://github.com/purescript/purescript-unfoldable/blob/493dfe04ed590e20d8f69079df2f58486882748d/src/Data/Unfoldable1.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "unfoldr1ArrayImpl", + "line": 48, + "declaration": "foreign import unfoldr1ArrayImpl :: forall a b . (forall x. Maybe x -> Boolean) -> (forall x. Maybe x -> x) -> (forall x y. Tuple x y -> x) -> (forall x y. Tuple x y -> y) -> (b -> Tuple a (Maybe b)) -> b -> Array a", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Unit.purs", + "vendored_sha256": "9da429dc5a86e009ed827b12b3b98106668d6d744c6f6ab7ee8d71414eb6ca0d", + "vendored_lines": 5, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "ce9f2d8fba25b6b392e5e9e81afe0fe33968dc15420765ab19bac8adb948e64f", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Unit.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "unit", + "line": 14, + "declaration": "foreign import unit :: Unit", + "vendored_status": "declaration_removed" + } + ], + "changed_original_code_outside_value_ffi": [ + { + "line": 1, + "text": "module Data.Unit where" + }, + { + "line": 11, + "text": "foreign import data Unit :: Type" + } + ] + }, + { + "path": "Data/Void.purs", + "vendored_sha256": "bf2d7931a857b5aeafc4e3a27c1425010c8c7710772b4e3ff8c102f5c3f84564", + "vendored_lines": 34, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "bf2d7931a857b5aeafc4e3a27c1425010c8c7710772b4e3ff8c102f5c3f84564", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Void.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Witherable.purs", + "vendored_sha256": "3250d0c8465691cc772887e09ef48f4b9554a236f614da7346abed3c0f5cfb93", + "vendored_lines": 162, + "self_recursions": [], + "package": "purescript-filterable", + "tag": "v5.0.0", + "commit": "7c5b8c72779997f2b17d12ce478ff81e7ddda285", + "upstream_sha256": "3250d0c8465691cc772887e09ef48f4b9554a236f614da7346abed3c0f5cfb93", + "upstream_url": "https://github.com/purescript/purescript-filterable/blob/7c5b8c72779997f2b17d12ce478ff81e7ddda285/src/Data/Witherable.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Effect/Console.purs", + "vendored_sha256": "c2a5ee927ddd5adb8e9ff0a78ad4937527f170e2cdbeaa906c163f6f9ce2ad0b", + "vendored_lines": 23, + "self_recursions": [], + "package": "purescript-console", + "tag": "v6.0.0", + "commit": "3b83d7b792d03872afeea5e62b4f686ab0f09842", + "upstream_sha256": "14a1820f28867485552db40f58826ceafb63fc899802a38a15ea82dd6974cce7", + "upstream_url": "https://github.com/purescript/purescript-console/blob/3b83d7b792d03872afeea5e62b4f686ab0f09842/src/Effect/Console.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "log", + "line": 9, + "declaration": "foreign import log :: String -> Effect Unit", + "vendored_status": "declaration_removed" + }, + { + "name": "warn", + "line": 19, + "declaration": "foreign import warn :: String -> Effect Unit", + "vendored_status": "declaration_removed" + }, + { + "name": "error", + "line": 29, + "declaration": "foreign import error :: String -> Effect Unit", + "vendored_status": "declaration_removed" + }, + { + "name": "info", + "line": 39, + "declaration": "foreign import info :: String -> Effect Unit", + "vendored_status": "declaration_removed" + }, + { + "name": "debug", + "line": 49, + "declaration": "foreign import debug :: String -> Effect Unit", + "vendored_status": "declaration_removed" + }, + { + "name": "time", + "line": 59, + "declaration": "foreign import time :: String -> Effect Unit", + "vendored_status": "declaration_removed" + }, + { + "name": "timeLog", + "line": 62, + "declaration": "foreign import timeLog :: String -> Effect Unit", + "vendored_status": "declaration_removed" + }, + { + "name": "timeEnd", + "line": 65, + "declaration": "foreign import timeEnd :: String -> Effect Unit", + "vendored_status": "declaration_removed" + }, + { + "name": "clear", + "line": 68, + "declaration": "foreign import clear :: Effect Unit", + "vendored_status": "declaration_removed" + } + ], + "changed_original_code_outside_value_ffi": [ + { + "line": 1, + "text": "module Effect.Console where" + }, + { + "line": 3, + "text": "import Effect (Effect)" + }, + { + "line": 5, + "text": "import Data.Show (class Show, show)" + }, + { + "line": 6, + "text": "import Data.Unit (Unit)" + }, + { + "line": 16, + "text": "logShow a = log (show a)" + }, + { + "line": 25, + "text": "warnShow :: forall a. Show a => a -> Effect Unit" + }, + { + "line": 26, + "text": "warnShow a = warn (show a)" + }, + { + "line": 35, + "text": "errorShow :: forall a. Show a => a -> Effect Unit" + }, + { + "line": 36, + "text": "errorShow a = error (show a)" + }, + { + "line": 45, + "text": "infoShow :: forall a. Show a => a -> Effect Unit" + }, + { + "line": 46, + "text": "infoShow a = info (show a)" + }, + { + "line": 55, + "text": "debugShow :: forall a. Show a => a -> Effect Unit" + }, + { + "line": 56, + "text": "debugShow a = debug (show a)" + } + ] + }, + { + "path": "Effect/Ref.purs", + "vendored_sha256": "d86b26af62019f18a830bab785419eca0fa016a366a7aed267bcf8967ca7c27b", + "vendored_lines": 78, + "self_recursions": [ + { + "name": "_new", + "line": 45, + "equation": "_new a0 = _new a0", + "replaces_upstream_foreign": true + }, + { + "name": "newWithSelf", + "line": 53, + "equation": "newWithSelf a0 = newWithSelf a0", + "replaces_upstream_foreign": true + }, + { + "name": "read", + "line": 57, + "equation": "read a0 = read a0", + "replaces_upstream_foreign": true + }, + { + "name": "modifyImpl", + "line": 65, + "equation": "modifyImpl a0 a1 = modifyImpl a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "write", + "line": 78, + "equation": "write a0 a1 = write a0 a1", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-refs", + "tag": "v6.0.0", + "commit": "f8e6216da4cb9309fde1f20cd6f69ac3a3b7f9e8", + "upstream_sha256": "d1e4e9c7037152b73f39a9a1369e9d6c00ca0d6ce6c437abe7ea457adb6d8774", + "upstream_url": "https://github.com/purescript/purescript-refs/blob/f8e6216da4cb9309fde1f20cd6f69ac3a3b7f9e8/src/Effect/Ref.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "_new", + "line": 44, + "declaration": "foreign import _new :: forall s. s -> Effect (Ref s)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "newWithSelf", + "line": 51, + "declaration": "foreign import newWithSelf :: forall s. (Ref s -> s) -> Effect (Ref s)", + "vendored_status": "direct_self_recursion" + }, + { + "name": "read", + "line": 54, + "declaration": "foreign import read :: forall s. Ref s -> Effect s", + "vendored_status": "direct_self_recursion" + }, + { + "name": "modifyImpl", + "line": 61, + "declaration": "foreign import modifyImpl :: forall s b. (s -> { state :: s, value :: b }) -> Ref s -> Effect b", + "vendored_status": "direct_self_recursion" + }, + { + "name": "write", + "line": 73, + "declaration": "foreign import write :: forall s. s -> Ref s -> Effect Unit", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Effect.purs", + "vendored_sha256": "5fc8b5fcab7455eb0e8b465d2132e6d9661cda2234f8c33ba0786df943ea97a1", + "vendored_lines": 29, + "self_recursions": [], + "package": "purescript-effect", + "tag": "v4.0.0", + "commit": "a192ddb923027d426d6ea3d8deb030c9aa7c7dda", + "upstream_sha256": "f5738a67511e72894c3784e44a334c94a6aa8e504fd3a29ddccc3617416b6f40", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "pureE", + "line": 29, + "declaration": "foreign import pureE :: forall a. a -> Effect a", + "vendored_status": "declaration_removed" + }, + { + "name": "bindE", + "line": 34, + "declaration": "foreign import bindE :: forall a b. Effect a -> (a -> Effect b) -> Effect b", + "vendored_status": "declaration_removed" + }, + { + "name": "untilE", + "line": 53, + "declaration": "foreign import untilE :: Effect Boolean -> Effect Unit", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "whileE", + "line": 60, + "declaration": "foreign import whileE :: forall a. Effect Boolean -> Effect a -> Effect Unit", + "vendored_status": "declaration_removed" + }, + { + "name": "forE", + "line": 66, + "declaration": "foreign import forE :: Int -> Int -> (Int -> Effect Unit) -> Effect Unit", + "vendored_status": "declaration_removed" + }, + { + "name": "foreachE", + "line": 72, + "declaration": "foreign import foreachE :: forall a. Array a -> (a -> Effect Unit) -> Effect Unit", + "vendored_status": "declaration_removed" + } + ], + "changed_original_code_outside_value_ffi": [ + { + "line": 6, + "text": " , untilE, whileE, forE, foreachE" + }, + { + "line": 9, + "text": "import Prelude" + }, + { + "line": 11, + "text": "import Control.Apply (lift2)" + }, + { + "line": 16, + "text": "foreign import data Effect :: Type -> Type" + }, + { + "line": 18, + "text": "type role Effect representational" + }, + { + "line": 20, + "text": "instance functorEffect :: Functor Effect where" + }, + { + "line": 21, + "text": " map = liftA1" + }, + { + "line": 23, + "text": "instance applyEffect :: Apply Effect where" + }, + { + "line": 24, + "text": " apply = ap" + }, + { + "line": 26, + "text": "instance applicativeEffect :: Applicative Effect where" + }, + { + "line": 27, + "text": " pure = pureE" + }, + { + "line": 31, + "text": "instance bindEffect :: Bind Effect where" + }, + { + "line": 32, + "text": " bind = bindE" + }, + { + "line": 36, + "text": "instance monadEffect :: Monad Effect" + }, + { + "line": 41, + "text": "instance semigroupEffect :: Semigroup a => Semigroup (Effect a) where" + }, + { + "line": 42, + "text": " append = lift2 append" + }, + { + "line": 46, + "text": "instance monoidEffect :: Monoid a => Monoid (Effect a) where" + }, + { + "line": 47, + "text": " mempty = pureE mempty" + } + ] + }, + { + "path": "Partial/Unsafe.purs", + "vendored_sha256": "022f9f5be76786175aad6bd61195e5ee73de6de35d73ee358456052983868ff0", + "vendored_lines": 25, + "self_recursions": [ + { + "name": "_unsafePartial", + "line": 17, + "equation": "_unsafePartial a0 = _unsafePartial a0", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-partial", + "tag": "v4.0.0", + "commit": "0fa0646f5ea1ec5f0c46dcbd770c705a6c9ad3ec", + "upstream_sha256": "d3d70727652d25459d19f812d186cf24583de1be5eb1decd87b6bf4afc36a4bd", + "upstream_url": "https://github.com/purescript/purescript-partial/blob/0fa0646f5ea1ec5f0c46dcbd770c705a6c9ad3ec/src/Partial/Unsafe.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "_unsafePartial", + "line": 16, + "declaration": "foreign import _unsafePartial :: forall a b. a -> b", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Partial.purs", + "vendored_sha256": "565d0df86ee74a5e8019f9b512a396accb6576a5b3d4d67eff8a594831d94c97", + "vendored_lines": 16, + "self_recursions": [ + { + "name": "_crashWith", + "line": 16, + "equation": "_crashWith a0 = _crashWith a0", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-partial", + "tag": "v4.0.0", + "commit": "0fa0646f5ea1ec5f0c46dcbd770c705a6c9ad3ec", + "upstream_sha256": "33ec6baf7de02292b40b97ed63f91402ec09ebf49c97392bb77ea78680177cd6", + "upstream_url": "https://github.com/purescript/purescript-partial/blob/0fa0646f5ea1ec5f0c46dcbd770c705a6c9ad3ec/src/Partial.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "_crashWith", + "line": 15, + "declaration": "foreign import _crashWith :: forall a. String -> a", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Prelude.purs", + "vendored_sha256": "925d3e4db03f42b08f79538d254b9c453a096330101768f8e425860b6e3c09d2", + "vendored_lines": 104, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "af6b069885f179a179cc805e6ff7920e1c791dba6fc4d07cd2f52c5e7b91e59f", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Prelude.purs", + "status": "modified", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [ + { + "line": 14, + "text": " ( module Control.Applicative" + } + ] + }, + { + "path": "Record/Unsafe.purs", + "vendored_sha256": "5cc9d89e59a814db34295bbf35f1609d7e0b553cdc26ad2d03fc4aae9d0b59da", + "vendored_lines": 31, + "self_recursions": [ + { + "name": "unsafeHas", + "line": 11, + "equation": "unsafeHas a0 a1 = unsafeHas a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "unsafeGet", + "line": 17, + "equation": "unsafeGet a0 a1 = unsafeGet a0 a1", + "replaces_upstream_foreign": true + }, + { + "name": "unsafeSet", + "line": 24, + "equation": "unsafeSet a0 a1 a2 = unsafeSet a0 a1 a2", + "replaces_upstream_foreign": true + }, + { + "name": "unsafeDelete", + "line": 31, + "equation": "unsafeDelete a0 a1 = unsafeDelete a0 a1", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "21acab8a811f760d56be569f4e8c2ae30705b34711b26de1cc39b16f96ba3ac1", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Record/Unsafe.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "unsafeHas", + "line": 10, + "declaration": "foreign import unsafeHas :: forall r1. String -> Record r1 -> Boolean", + "vendored_status": "direct_self_recursion" + }, + { + "name": "unsafeGet", + "line": 15, + "declaration": "foreign import unsafeGet :: forall r a. String -> Record r -> a", + "vendored_status": "direct_self_recursion" + }, + { + "name": "unsafeSet", + "line": 21, + "declaration": "foreign import unsafeSet :: forall r1 r2 a. String -> a -> Record r1 -> Record r2", + "vendored_status": "direct_self_recursion" + }, + { + "name": "unsafeDelete", + "line": 27, + "declaration": "foreign import unsafeDelete :: forall r1 r2. String -> Record r1 -> Record r2", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Safe/Coerce.purs", + "vendored_sha256": "67ab89bd8194231b886591fb03a5d8cd1cbb91e68e384a7e768c4405fa6d959e", + "vendored_lines": 27, + "self_recursions": [], + "package": "purescript-safe-coerce", + "tag": "v2.0.0", + "commit": "7fa799ae80a38b8d948efcb52608e58e198b3da7", + "upstream_sha256": "67ab89bd8194231b886591fb03a5d8cd1cbb91e68e384a7e768c4405fa6d959e", + "upstream_url": "https://github.com/purescript/purescript-safe-coerce/blob/7fa799ae80a38b8d948efcb52608e58e198b3da7/src/Safe/Coerce.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Test/Assert.purs", + "vendored_sha256": "1bb4d996770bed380779a280c1c663703f151e4e89c146daec0b6b342be4f53b", + "vendored_lines": 70, + "self_recursions": [], + "package": "purescript-assert", + "tag": "v6.0.0", + "commit": "27c0edb57d2ee497eb5fab664f5601c35b613eda", + "upstream_sha256": "666601c3d459503802027dca698b9bd05bacd42f48f7d320a834196ed4cb4127", + "upstream_url": "https://github.com/purescript/purescript-assert/blob/27c0edb57d2ee497eb5fab664f5601c35b613eda/src/Test/Assert.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "assertImpl", + "line": 29, + "declaration": "foreign import assertImpl :: String -> Boolean -> Effect Unit", + "vendored_status": "declaration_removed" + }, + { + "name": "checkThrows", + "line": 59, + "declaration": "foreign import checkThrows :: forall a . (Unit -> a) -> Effect Boolean", + "vendored_status": "declaration_removed" + } + ], + "changed_original_code_outside_value_ffi": [ + { + "line": 1, + "text": "module Test.Assert" + }, + { + "line": 2, + "text": " ( assert" + }, + { + "line": 3, + "text": " , assert'" + }, + { + "line": 4, + "text": " , assertEqual" + }, + { + "line": 5, + "text": " , assertEqual'" + }, + { + "line": 6, + "text": " , assertFalse" + }, + { + "line": 7, + "text": " , assertFalse'" + }, + { + "line": 8, + "text": " , assertThrows" + }, + { + "line": 9, + "text": " , assertThrows'" + }, + { + "line": 10, + "text": " , assertTrue" + }, + { + "line": 11, + "text": " , assertTrue'" + }, + { + "line": 12, + "text": " ) where" + }, + { + "line": 26, + "text": "assert' :: String -> Boolean -> Effect Unit" + }, + { + "line": 27, + "text": "assert' = assertImpl" + }, + { + "line": 41, + "text": "assertThrows :: forall a. (Unit -> a) -> Effect Unit" + }, + { + "line": 42, + "text": "assertThrows =" + }, + { + "line": 43, + "text": " assertThrows' \"Assertion failed: An error should have been thrown\"" + }, + { + "line": 52, + "text": "assertThrows'" + }, + { + "line": 53, + "text": " :: forall a" + }, + { + "line": 54, + "text": " . String" + }, + { + "line": 55, + "text": " -> (Unit -> a)" + }, + { + "line": 56, + "text": " -> Effect Unit" + }, + { + "line": 57, + "text": "assertThrows' msg fn = assert' msg =<< checkThrows fn" + }, + { + "line": 68, + "text": "assertEqual" + }, + { + "line": 69, + "text": " :: forall a" + }, + { + "line": 70, + "text": " . Eq a" + }, + { + "line": 71, + "text": " => Show a" + }, + { + "line": 72, + "text": " => { actual :: a, expected :: a }" + }, + { + "line": 73, + "text": " -> Effect Unit" + }, + { + "line": 74, + "text": "assertEqual = assertEqual' \"\"" + }, + { + "line": 80, + "text": "assertEqual'" + }, + { + "line": 81, + "text": " :: forall a" + }, + { + "line": 82, + "text": " . Eq a" + }, + { + "line": 83, + "text": " => Show a" + }, + { + "line": 84, + "text": " => String" + }, + { + "line": 85, + "text": " -> { actual :: a, expected :: a }" + }, + { + "line": 86, + "text": " -> Effect Unit" + }, + { + "line": 87, + "text": "assertEqual' userMessage {actual, expected} = do" + }, + { + "line": 88, + "text": " unless result $ error message" + }, + { + "line": 89, + "text": " assert' message result" + }, + { + "line": 90, + "text": " where" + }, + { + "line": 91, + "text": " message = (if userMessage == \"\" then \"\" else userMessage <> \"\\n\")" + }, + { + "line": 92, + "text": " <> \"Expected: \" <> show expected" + }, + { + "line": 93, + "text": " <> \"\\nActual: \" <> show actual" + }, + { + "line": 94, + "text": " result = actual == expected" + }, + { + "line": 100, + "text": "assertTrue" + }, + { + "line": 101, + "text": " :: Boolean" + }, + { + "line": 102, + "text": " -> Effect Unit" + }, + { + "line": 103, + "text": "assertTrue actual = assertEqual { actual, expected: true }" + }, + { + "line": 110, + "text": "assertTrue'" + }, + { + "line": 111, + "text": " :: String" + }, + { + "line": 112, + "text": " -> Boolean" + }, + { + "line": 113, + "text": " -> Effect Unit" + }, + { + "line": 114, + "text": "assertTrue' message actual = assertEqual' message { actual, expected: true }" + }, + { + "line": 120, + "text": "assertFalse" + }, + { + "line": 121, + "text": " :: Boolean" + }, + { + "line": 122, + "text": " -> Effect Unit" + }, + { + "line": 123, + "text": "assertFalse actual = assertEqual { actual, expected: false }" + }, + { + "line": 130, + "text": "assertFalse'" + }, + { + "line": 131, + "text": " :: String" + }, + { + "line": 132, + "text": " -> Boolean" + }, + { + "line": 133, + "text": " -> Effect Unit" + }, + { + "line": 134, + "text": "assertFalse' message actual = assertEqual' message { actual, expected: false }" + } + ] + }, + { + "path": "Type/Data/Boolean.purs", + "vendored_sha256": "5bb19aeaddc6ea478b96f06025e742284c85f413637de56cb78198b86aa5fc87", + "vendored_lines": 66, + "self_recursions": [], + "package": "purescript-typelevel-prelude", + "tag": "v7.0.0", + "commit": "dca2fe3c8cfd5527d4fe70c4bedfda30148405bf", + "upstream_sha256": "5bb19aeaddc6ea478b96f06025e742284c85f413637de56cb78198b86aa5fc87", + "upstream_url": "https://github.com/purescript/purescript-typelevel-prelude/blob/dca2fe3c8cfd5527d4fe70c4bedfda30148405bf/src/Type/Data/Boolean.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Type/Data/Ordering.purs", + "vendored_sha256": "fcc7c02e66257c06c4647b929e187d214d653789fee6afc49346d4990ed14997", + "vendored_lines": 69, + "self_recursions": [], + "package": "purescript-typelevel-prelude", + "tag": "v7.0.0", + "commit": "dca2fe3c8cfd5527d4fe70c4bedfda30148405bf", + "upstream_sha256": "fcc7c02e66257c06c4647b929e187d214d653789fee6afc49346d4990ed14997", + "upstream_url": "https://github.com/purescript/purescript-typelevel-prelude/blob/dca2fe3c8cfd5527d4fe70c4bedfda30148405bf/src/Type/Data/Ordering.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Type/Data/Symbol.purs", + "vendored_sha256": "4c3283d0804ca9d2a1ba9fdfb7cf85aacdf2d0c8f6472b383d4b162d8940817b", + "vendored_lines": 35, + "self_recursions": [], + "package": "purescript-typelevel-prelude", + "tag": "v7.0.0", + "commit": "dca2fe3c8cfd5527d4fe70c4bedfda30148405bf", + "upstream_sha256": "4c3283d0804ca9d2a1ba9fdfb7cf85aacdf2d0c8f6472b383d4b162d8940817b", + "upstream_url": "https://github.com/purescript/purescript-typelevel-prelude/blob/dca2fe3c8cfd5527d4fe70c4bedfda30148405bf/src/Type/Data/Symbol.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Type/Equality.purs", + "vendored_sha256": "ac672615fd3612b596eda3ab9f2472006e7ee954c7880cc91ac4865f7c120f56", + "vendored_lines": 35, + "self_recursions": [], + "package": "purescript-type-equality", + "tag": "v4.0.1", + "commit": "0525b7d39e0fbd81b4209518139fb8ab02695774", + "upstream_sha256": "ac672615fd3612b596eda3ab9f2472006e7ee954c7880cc91ac4865f7c120f56", + "upstream_url": "https://github.com/purescript/purescript-type-equality/blob/0525b7d39e0fbd81b4209518139fb8ab02695774/src/Type/Equality.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Type/Function.purs", + "vendored_sha256": "d134b0394685faa83528c92f64c6827194b4531c956dd7d669436c30b2fd9812", + "vendored_lines": 23, + "self_recursions": [], + "package": "purescript-typelevel-prelude", + "tag": "v7.0.0", + "commit": "dca2fe3c8cfd5527d4fe70c4bedfda30148405bf", + "upstream_sha256": "d134b0394685faa83528c92f64c6827194b4531c956dd7d669436c30b2fd9812", + "upstream_url": "https://github.com/purescript/purescript-typelevel-prelude/blob/dca2fe3c8cfd5527d4fe70c4bedfda30148405bf/src/Type/Function.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Type/Prelude.purs", + "vendored_sha256": "2293b8cfd59325e393d51a047dcbaf4aaaf81a131c3f16c9b91ad49bb8f9732d", + "vendored_lines": 17, + "self_recursions": [], + "package": "purescript-typelevel-prelude", + "tag": "v7.0.0", + "commit": "dca2fe3c8cfd5527d4fe70c4bedfda30148405bf", + "upstream_sha256": "2293b8cfd59325e393d51a047dcbaf4aaaf81a131c3f16c9b91ad49bb8f9732d", + "upstream_url": "https://github.com/purescript/purescript-typelevel-prelude/blob/dca2fe3c8cfd5527d4fe70c4bedfda30148405bf/src/Type/Prelude.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Type/Proxy.purs", + "vendored_sha256": "e823c48b15006447de763d42a9f6b8c9e6913a3bd4b49b5e3b7cdce78d7d9b7c", + "vendored_lines": 53, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "e823c48b15006447de763d42a9f6b8c9e6913a3bd4b49b5e3b7cdce78d7d9b7c", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Type/Proxy.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Type/Row/Homogeneous.purs", + "vendored_sha256": "a3db3ece627f2d2c1ef76b76a0e6699934115dd52c6e56ecf50d95868e4eaab8", + "vendored_lines": 23, + "self_recursions": [], + "package": "purescript-typelevel-prelude", + "tag": "v7.0.0", + "commit": "dca2fe3c8cfd5527d4fe70c4bedfda30148405bf", + "upstream_sha256": "a3db3ece627f2d2c1ef76b76a0e6699934115dd52c6e56ecf50d95868e4eaab8", + "upstream_url": "https://github.com/purescript/purescript-typelevel-prelude/blob/dca2fe3c8cfd5527d4fe70c4bedfda30148405bf/src/Type/Row/Homogeneous.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Type/Row.purs", + "vendored_sha256": "b504404c46678bdbe8c49567a4fbbc1ba3cdddc25a7834539c23a1e99001132b", + "vendored_lines": 22, + "self_recursions": [], + "package": "purescript-typelevel-prelude", + "tag": "v7.0.0", + "commit": "dca2fe3c8cfd5527d4fe70c4bedfda30148405bf", + "upstream_sha256": "b504404c46678bdbe8c49567a4fbbc1ba3cdddc25a7834539c23a1e99001132b", + "upstream_url": "https://github.com/purescript/purescript-typelevel-prelude/blob/dca2fe3c8cfd5527d4fe70c4bedfda30148405bf/src/Type/Row.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Type/RowList.purs", + "vendored_sha256": "8e5b0112718c7a42104d5904dc8ad26619b04d826493fc818e375469010a24dd", + "vendored_lines": 82, + "self_recursions": [], + "package": "purescript-typelevel-prelude", + "tag": "v7.0.0", + "commit": "dca2fe3c8cfd5527d4fe70c4bedfda30148405bf", + "upstream_sha256": "8e5b0112718c7a42104d5904dc8ad26619b04d826493fc818e375469010a24dd", + "upstream_url": "https://github.com/purescript/purescript-typelevel-prelude/blob/dca2fe3c8cfd5527d4fe70c4bedfda30148405bf/src/Type/RowList.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Unsafe/Coerce.purs", + "vendored_sha256": "ddd0085161aef5cf24f2b0970307043ac3ca2988878949cc38440e5c1ef38493", + "vendored_lines": 28, + "self_recursions": [ + { + "name": "unsafeCoerce", + "line": 28, + "equation": "unsafeCoerce a0 = unsafeCoerce a0", + "replaces_upstream_foreign": true + } + ], + "package": "purescript-unsafe-coerce", + "tag": "v6.0.0", + "commit": "ab956f82e66e633f647fb3098e8ddd3ec58d689f", + "upstream_sha256": "f0d052e7b2e2431860e6e995a91824c0d6437e2de773fd51c2324f626e10ed19", + "upstream_url": "https://github.com/purescript/purescript-unsafe-coerce/blob/ab956f82e66e633f647fb3098e8ddd3ec58d689f/src/Unsafe/Coerce.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "unsafeCoerce", + "line": 27, + "declaration": "foreign import unsafeCoerce :: forall a b. a -> b", + "vendored_status": "direct_self_recursion" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "WASI/Clock.purs", + "vendored_sha256": "222f07399b2b881674691eff41d296a943824435ab13e60556f0117e048a7d0a", + "vendored_lines": 14, + "self_recursions": [], + "status": "platform_addition" + }, + { + "path": "WASI/Console.purs", + "vendored_sha256": "f49f4c7ae2e7c70f21bc35ce5080d000637a569e561567470ebf360c32c4beb9", + "vendored_lines": 33, + "self_recursions": [], + "status": "platform_addition" + }, + { + "path": "WASI/FileSystem.purs", + "vendored_sha256": "66747fef3108642b01290cd8323b7ef9ff4aaf08703370ffcd2d1654d92f5a32", + "vendored_lines": 270, + "self_recursions": [], + "status": "platform_addition" + }, + { + "path": "WASI/IO.purs", + "vendored_sha256": "e3df53b3966841930e01f4c89505172ed64996c60aae37118bb5d0d03d725c85", + "vendored_lines": 67, + "self_recursions": [], + "status": "platform_addition" + }, + { + "path": "WASI/Network.purs", + "vendored_sha256": "c2b769cf39bf4ba879024c5bcae388a0ee16bec143be81fb4bd199d6f3fd80d4", + "vendored_lines": 130, + "self_recursions": [], + "status": "platform_addition" + }, + { + "path": "WASI/Process.purs", + "vendored_sha256": "4cfd83ff897a6b3a1cea44aa6d4d5a3fd6e3d4d3a8ad30061ba80636de0bca7b", + "vendored_lines": 13, + "self_recursions": [], + "status": "platform_addition" + }, + { + "path": "WASI/Random.purs", + "vendored_sha256": "c05d4bf338e66e0358d9c74a494d8d84c2c4c99e37750a54c909600834175ee9", + "vendored_lines": 16, + "self_recursions": [], + "status": "platform_addition" + }, + { + "path": "WASI/Resource.purs", + "vendored_sha256": "8da4a53234de3f7d1ec6ee6aaead124a27201edba261da22da38f171fb167acc", + "vendored_lines": 14, + "self_recursions": [], + "status": "platform_addition" + }, + { + "path": "WASI.purs", + "vendored_sha256": "a1f72de13e9676ec66a87080e631ed4e31bf4a4392b10b6fdeabfc39f8d2bb11", + "vendored_lines": 102, + "self_recursions": [], + "status": "platform_addition" + } + ], + "absent_modules": [ + { + "path": "Effect/Class.purs", + "package": "purescript-effect", + "tag": "v4.0.0", + "commit": "a192ddb923027d426d6ea3d8deb030c9aa7c7dda", + "upstream_sha256": "a78c5876b0aad1123e30ca8a37cd79ca2a837fbccc3a9e92bea3793033cc5a03", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Class.purs" + }, + { + "path": "Effect/Class/Console.purs", + "package": "purescript-console", + "tag": "v6.0.0", + "commit": "3b83d7b792d03872afeea5e62b4f686ab0f09842", + "upstream_sha256": "1aa9260f9b96c0d6b5721c5ad473dd0c518157d06da9c5d4cbbac6c28518558d", + "upstream_url": "https://github.com/purescript/purescript-console/blob/3b83d7b792d03872afeea5e62b4f686ab0f09842/src/Effect/Class/Console.purs" + }, + { + "path": "Effect/Uncurried.purs", + "package": "purescript-effect", + "tag": "v4.0.0", + "commit": "a192ddb923027d426d6ea3d8deb030c9aa7c7dda", + "upstream_sha256": "0c6db8591d2519d9b18bf2c5ec56f33b6c25ee6f69d3edfbce8482d48771214b", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Uncurried.purs" + }, + { + "path": "Effect/Unsafe.purs", + "package": "purescript-effect", + "tag": "v4.0.0", + "commit": "a192ddb923027d426d6ea3d8deb030c9aa7c7dda", + "upstream_sha256": "e20339c8aed22f342fa8d452dd186079b4324cd00933b9e9f4eeb59d3313cac0", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Unsafe.purs" + } + ], + "limits": [ + "Only supplied upstream checkouts are compared; baseline_unavailable is not a pass.", + "Self-recursion detection covers exact top-level same-argument equations only.", + "The script records differences without approving target adaptations or proving semantic equivalence.", + "The official compiler support dependency ranges do not uniquely pin package patch versions." + ] +} diff --git a/docs/implementation/stdlib/vendor-audit-2026-10-06/modules.md b/docs/implementation/stdlib/vendor-audit-2026-10-06/modules.md new file mode 100644 index 00000000..6b4fba99 --- /dev/null +++ b/docs/implementation/stdlib/vendor-audit-2026-10-06/modules.md @@ -0,0 +1,226 @@ +# Vendored module inventory + +Generated by `audit-stdlib-vendor.py`. Status is comparison evidence, not approval. + +| Module path | Official package/tag | Comparison | Direct self-recursions | +| --- | --- | --- | --- | +| `Control/Alt.purs` | purescript-control v6.0.0 | identical | 0 | +| `Control/Alternative.purs` | purescript-control v6.0.0 | identical | 0 | +| `Control/Applicative.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Control/Apply.purs` | purescript-prelude v6.0.1 | modified | 0 | +| `Control/Biapplicative.purs` | purescript-bifunctors v6.1.0 | identical | 0 | +| `Control/Biapply.purs` | purescript-bifunctors v6.1.0 | identical | 0 | +| `Control/Bind.purs` | purescript-prelude v6.0.1 | modified | 0 | +| `Control/Category.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Control/Comonad.purs` | purescript-control v6.0.0 | identical | 0 | +| `Control/Extend.purs` | purescript-control v6.0.0 | modified | 1 | +| `Control/Lazy.purs` | purescript-control v6.0.0 | identical | 0 | +| `Control/Monad/Gen/Class.purs` | purescript-gen v4.0.0 | identical | 0 | +| `Control/Monad/Gen/Common.purs` | purescript-gen v4.0.0 | identical | 0 | +| `Control/Monad/Gen.purs` | purescript-gen v4.0.0 | identical | 0 | +| `Control/Monad/Rec/Class.purs` | purescript-tailrec v6.1.0 | identical | 0 | +| `Control/Monad/ST/Class.purs` | purescript-st v6.2.0 | identical | 0 | +| `Control/Monad/ST/Global.purs` | purescript-st v6.2.0 | identical | 0 | +| `Control/Monad/ST/Internal.purs` | purescript-st v6.2.0 | modified | 11 | +| `Control/Monad/ST/Ref.purs` | purescript-st v6.2.0 | identical | 0 | +| `Control/Monad/ST/Uncurried.purs` | purescript-st v6.2.0 | modified | 20 | +| `Control/Monad/ST.purs` | purescript-st v6.2.0 | identical | 0 | +| `Control/Monad.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Control/MonadPlus.purs` | purescript-control v6.0.0 | identical | 0 | +| `Control/Plus.purs` | purescript-control v6.0.0 | identical | 0 | +| `Control/Semigroupoid.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Array/NonEmpty/Internal.purs` | purescript-arrays v7.3.0 | modified | 3 | +| `Data/Array/NonEmpty.purs` | purescript-arrays v7.3.0 | identical | 0 | +| `Data/Array/Partial.purs` | purescript-arrays v7.3.0 | identical | 0 | +| `Data/Array/ST/Iterator.purs` | purescript-arrays v7.3.0 | identical | 0 | +| `Data/Array/ST/Partial.purs` | purescript-arrays v7.3.0 | modified | 2 | +| `Data/Array/ST.purs` | purescript-arrays v7.3.0 | modified | 17 | +| `Data/Array.purs` | purescript-arrays v7.3.0 | modified | 24 | +| `Data/Bifoldable.purs` | purescript-foldable-traversable v6.0.0 | identical | 0 | +| `Data/Bifunctor/Join.purs` | purescript-bifunctors v6.1.0 | identical | 0 | +| `Data/Bifunctor.purs` | purescript-bifunctors v6.1.0 | identical | 0 | +| `Data/Bitraversable.purs` | purescript-foldable-traversable v6.0.0 | identical | 0 | +| `Data/Boolean.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/BooleanAlgebra.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Bounded/Generic.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Bounded.purs` | purescript-prelude v6.0.1 | modified | 0 | +| `Data/Char/Gen.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/Char.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/CommutativeRing.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Compactable.purs` | purescript-filterable v5.0.0 | identical | 0 | +| `Data/Comparison.purs` | purescript-contravariant v6.0.0 | identical | 0 | +| `Data/Const.purs` | purescript-const v6.0.0 | identical | 0 | +| `Data/Decidable.purs` | purescript-contravariant v6.0.0 | identical | 0 | +| `Data/Decide.purs` | purescript-contravariant v6.0.0 | identical | 0 | +| `Data/Distributive.purs` | purescript-distributive v6.0.0 | identical | 0 | +| `Data/Divide.purs` | purescript-contravariant v6.0.0 | identical | 0 | +| `Data/Divisible.purs` | purescript-contravariant v6.0.0 | identical | 0 | +| `Data/DivisionRing.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Either/Inject.purs` | purescript-either v6.1.0 | identical | 0 | +| `Data/Either/Nested.purs` | purescript-either v6.1.0 | identical | 0 | +| `Data/Either.purs` | purescript-either v6.1.0 | identical | 0 | +| `Data/Enum/Gen.purs` | purescript-enums v6.0.1 | identical | 0 | +| `Data/Enum/Generic.purs` | purescript-enums v6.0.1 | identical | 0 | +| `Data/Enum.purs` | purescript-enums v6.0.1 | modified | 2 | +| `Data/Eq/Generic.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Eq.purs` | purescript-prelude v6.0.1 | modified | 0 | +| `Data/Equivalence.purs` | purescript-contravariant v6.0.0 | identical | 0 | +| `Data/EuclideanRing.purs` | purescript-prelude v6.0.1 | modified | 0 | +| `Data/Exists.purs` | purescript-exists v6.0.0 | identical | 0 | +| `Data/Field.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Filterable.purs` | purescript-filterable v5.0.0 | identical | 0 | +| `Data/Foldable.purs` | purescript-foldable-traversable v6.0.0 | modified | 2 | +| `Data/FoldableWithIndex.purs` | purescript-foldable-traversable v6.0.0 | identical | 0 | +| `Data/Function/Uncurried.purs` | purescript-functions v6.0.0 | modified | 20 | +| `Data/Function.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Functor/App.purs` | purescript-functors v5.0.0 | identical | 0 | +| `Data/Functor/Clown.purs` | purescript-functors v5.0.0 | identical | 0 | +| `Data/Functor/Compose.purs` | purescript-functors v5.0.0 | identical | 0 | +| `Data/Functor/Contravariant.purs` | purescript-contravariant v6.0.0 | identical | 0 | +| `Data/Functor/Coproduct/Inject.purs` | purescript-functors v5.0.0 | identical | 0 | +| `Data/Functor/Coproduct/Nested.purs` | purescript-functors v5.0.0 | identical | 0 | +| `Data/Functor/Coproduct.purs` | purescript-functors v5.0.0 | identical | 0 | +| `Data/Functor/Costar.purs` | purescript-functors v5.0.0 | identical | 0 | +| `Data/Functor/Flip.purs` | purescript-functors v5.0.0 | identical | 0 | +| `Data/Functor/Invariant.purs` | purescript-invariant v6.0.0 | identical | 0 | +| `Data/Functor/Joker.purs` | purescript-functors v5.0.0 | identical | 0 | +| `Data/Functor/Product/Nested.purs` | purescript-functors v5.0.0 | identical | 0 | +| `Data/Functor/Product.purs` | purescript-functors v5.0.0 | identical | 0 | +| `Data/Functor/Product2.purs` | purescript-functors v5.0.0 | identical | 0 | +| `Data/Functor.purs` | purescript-prelude v6.0.1 | modified | 0 | +| `Data/FunctorWithIndex.purs` | purescript-foldable-traversable v6.0.0 | modified | 1 | +| `Data/Generic/Rep.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/HeytingAlgebra/Generic.purs` | purescript-prelude v6.0.1 | newline_only | 0 | +| `Data/HeytingAlgebra.purs` | purescript-prelude v6.0.1 | modified | 0 | +| `Data/Identity.purs` | purescript-identity v6.0.0 | identical | 0 | +| `Data/Int/Bits.purs` | purescript-integers v6.0.0 | modified | 0 | +| `Data/Int.purs` | purescript-integers v6.0.0 | modified | 7 | +| `Data/Lazy.purs` | purescript-lazy v6.0.0 | modified | 2 | +| `Data/List/Internal.purs` | purescript-lists v7.0.0 | identical | 0 | +| `Data/List/Lazy/NonEmpty.purs` | purescript-lists v7.0.0 | identical | 0 | +| `Data/List/Lazy/Types.purs` | purescript-lists v7.0.0 | identical | 0 | +| `Data/List/Lazy.purs` | purescript-lists v7.0.0 | identical | 0 | +| `Data/List/NonEmpty.purs` | purescript-lists v7.0.0 | identical | 0 | +| `Data/List/Partial.purs` | purescript-lists v7.0.0 | identical | 0 | +| `Data/List/Types.purs` | purescript-lists v7.0.0 | identical | 0 | +| `Data/List/ZipList.purs` | purescript-lists v7.0.0 | identical | 0 | +| `Data/List.purs` | purescript-lists v7.0.0 | identical | 0 | +| `Data/Map/Gen.purs` | purescript-ordered-collections v3.2.0 | identical | 0 | +| `Data/Map/Internal.purs` | purescript-ordered-collections v3.2.0 | identical | 0 | +| `Data/Map.purs` | purescript-ordered-collections v3.2.0 | identical | 0 | +| `Data/Maybe/First.purs` | purescript-maybe v6.0.0 | identical | 0 | +| `Data/Maybe/Last.purs` | purescript-maybe v6.0.0 | identical | 0 | +| `Data/Maybe.purs` | purescript-maybe v6.0.0 | identical | 0 | +| `Data/Monoid/Additive.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Monoid/Alternate.purs` | purescript-control v6.0.0 | identical | 0 | +| `Data/Monoid/Conj.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Monoid/Disj.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Monoid/Dual.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Monoid/Endo.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Monoid/Generic.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Monoid/Multiplicative.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Monoid.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/NaturalTransformation.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Newtype.purs` | purescript-newtype v5.0.0 | identical | 0 | +| `Data/NonEmpty.purs` | purescript-nonempty v7.0.0 | identical | 0 | +| `Data/Number/Approximate.purs` | purescript-numbers v9.0.1 | identical | 0 | +| `Data/Number/Format.purs` | purescript-numbers v9.0.1 | modified | 4 | +| `Data/Number.purs` | purescript-numbers v9.0.1 | modified | 25 | +| `Data/Op.purs` | purescript-contravariant v6.0.0 | identical | 0 | +| `Data/Ord/Down.purs` | purescript-orders v6.0.0 | identical | 0 | +| `Data/Ord/Generic.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Ord/Max.purs` | purescript-orders v6.0.0 | identical | 0 | +| `Data/Ord/Min.purs` | purescript-orders v6.0.0 | identical | 0 | +| `Data/Ord.purs` | purescript-prelude v6.0.1 | modified | 0 | +| `Data/Ordering.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Predicate.purs` | purescript-contravariant v6.0.0 | identical | 0 | +| `Data/Profunctor/Choice.purs` | purescript-profunctor v6.0.1 | identical | 0 | +| `Data/Profunctor/Closed.purs` | purescript-profunctor v6.0.1 | identical | 0 | +| `Data/Profunctor/Cochoice.purs` | purescript-profunctor v6.0.1 | identical | 0 | +| `Data/Profunctor/Costrong.purs` | purescript-profunctor v6.0.1 | identical | 0 | +| `Data/Profunctor/Join.purs` | purescript-profunctor v6.0.1 | identical | 0 | +| `Data/Profunctor/Split.purs` | purescript-profunctor v6.0.1 | identical | 0 | +| `Data/Profunctor/Star.purs` | purescript-profunctor v6.0.1 | identical | 0 | +| `Data/Profunctor/Strong.purs` | purescript-profunctor v6.0.1 | identical | 0 | +| `Data/Profunctor.purs` | purescript-profunctor v6.0.1 | identical | 0 | +| `Data/Reflectable.purs` | purescript-prelude v6.0.1 | modified | 1 | +| `Data/Ring/Generic.purs` | purescript-prelude v6.0.1 | newline_only | 0 | +| `Data/Ring.purs` | purescript-prelude v6.0.1 | modified | 0 | +| `Data/Semigroup/First.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Semigroup/Foldable.purs` | purescript-foldable-traversable v6.0.0 | identical | 0 | +| `Data/Semigroup/Generic.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Semigroup/Last.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Semigroup/Traversable.purs` | purescript-foldable-traversable v6.0.0 | identical | 0 | +| `Data/Semigroup.purs` | purescript-prelude v6.0.1 | modified | 0 | +| `Data/Semiring/Generic.purs` | purescript-prelude v6.0.1 | newline_only | 0 | +| `Data/Semiring.purs` | purescript-prelude v6.0.1 | modified | 0 | +| `Data/Set/NonEmpty.purs` | purescript-ordered-collections v3.2.0 | identical | 0 | +| `Data/Set.purs` | purescript-ordered-collections v3.2.0 | identical | 0 | +| `Data/Show/Generic.purs` | purescript-prelude v6.0.1 | modified | 0 | +| `Data/Show.purs` | purescript-prelude v6.0.1 | modified | 0 | +| `Data/String/CaseInsensitive.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String/CodePoints.purs` | purescript-strings v6.0.1 | modified | 7 | +| `Data/String/CodeUnits.purs` | purescript-strings v6.0.1 | modified | 15 | +| `Data/String/Common.purs` | purescript-strings v6.0.1 | modified | 8 | +| `Data/String/Gen.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String/NonEmpty/CaseInsensitive.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String/NonEmpty/CodePoints.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String/NonEmpty/CodeUnits.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String/NonEmpty/Internal.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String/NonEmpty.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String/Pattern.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String/Regex/Flags.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String/Regex/Unsafe.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String/Regex.purs` | purescript-strings v6.0.1 | modified | 10 | +| `Data/String/Unsafe.purs` | purescript-strings v6.0.1 | modified | 2 | +| `Data/String.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/Symbol.purs` | purescript-prelude v6.0.1 | modified | 1 | +| `Data/Traversable/Accum/Internal.purs` | purescript-foldable-traversable v6.0.0 | identical | 0 | +| `Data/Traversable/Accum.purs` | purescript-foldable-traversable v6.0.0 | identical | 0 | +| `Data/Traversable.purs` | purescript-foldable-traversable v6.0.0 | modified | 1 | +| `Data/TraversableWithIndex.purs` | purescript-foldable-traversable v6.0.0 | identical | 0 | +| `Data/Tuple/Nested.purs` | purescript-tuples v7.0.0 | identical | 0 | +| `Data/Tuple.purs` | purescript-tuples v7.0.0 | modified | 0 | +| `Data/Unfoldable.purs` | purescript-unfoldable v6.0.0 | modified | 1 | +| `Data/Unfoldable1.purs` | purescript-unfoldable v6.0.0 | modified | 1 | +| `Data/Unit.purs` | purescript-prelude v6.0.1 | modified | 0 | +| `Data/Void.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Witherable.purs` | purescript-filterable v5.0.0 | identical | 0 | +| `Effect/Console.purs` | purescript-console v6.0.0 | modified | 0 | +| `Effect/Ref.purs` | purescript-refs v6.0.0 | modified | 5 | +| `Effect.purs` | purescript-effect v4.0.0 | modified | 0 | +| `Partial/Unsafe.purs` | purescript-partial v4.0.0 | modified | 1 | +| `Partial.purs` | purescript-partial v4.0.0 | modified | 1 | +| `Prelude.purs` | purescript-prelude v6.0.1 | modified | 0 | +| `Record/Unsafe.purs` | purescript-prelude v6.0.1 | modified | 4 | +| `Safe/Coerce.purs` | purescript-safe-coerce v2.0.0 | identical | 0 | +| `Test/Assert.purs` | purescript-assert v6.0.0 | modified | 0 | +| `Type/Data/Boolean.purs` | purescript-typelevel-prelude v7.0.0 | identical | 0 | +| `Type/Data/Ordering.purs` | purescript-typelevel-prelude v7.0.0 | identical | 0 | +| `Type/Data/Symbol.purs` | purescript-typelevel-prelude v7.0.0 | identical | 0 | +| `Type/Equality.purs` | purescript-type-equality v4.0.1 | identical | 0 | +| `Type/Function.purs` | purescript-typelevel-prelude v7.0.0 | identical | 0 | +| `Type/Prelude.purs` | purescript-typelevel-prelude v7.0.0 | identical | 0 | +| `Type/Proxy.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Type/Row/Homogeneous.purs` | purescript-typelevel-prelude v7.0.0 | identical | 0 | +| `Type/Row.purs` | purescript-typelevel-prelude v7.0.0 | identical | 0 | +| `Type/RowList.purs` | purescript-typelevel-prelude v7.0.0 | identical | 0 | +| `Unsafe/Coerce.purs` | purescript-unsafe-coerce v6.0.0 | modified | 1 | +| `WASI/Clock.purs` | unavailable | platform_addition | 0 | +| `WASI/Console.purs` | unavailable | platform_addition | 0 | +| `WASI/FileSystem.purs` | unavailable | platform_addition | 0 | +| `WASI/IO.purs` | unavailable | platform_addition | 0 | +| `WASI/Network.purs` | unavailable | platform_addition | 0 | +| `WASI/Process.purs` | unavailable | platform_addition | 0 | +| `WASI/Random.purs` | unavailable | platform_addition | 0 | +| `WASI/Resource.purs` | unavailable | platform_addition | 0 | +| `WASI.purs` | unavailable | platform_addition | 0 | + +## Official modules absent from the vendored library + +| Module path | Official package/tag | +| --- | --- | +| `Effect/Class.purs` | purescript-effect v4.0.0 | +| `Effect/Class/Console.purs` | purescript-console v6.0.0 | +| `Effect/Uncurried.purs` | purescript-effect v4.0.0 | +| `Effect/Unsafe.purs` | purescript-effect v4.0.0 | diff --git a/docs/implementation/stdlib/vendor-audit-2026-10-06/official-vs-vendored.diff b/docs/implementation/stdlib/vendor-audit-2026-10-06/official-vs-vendored.diff new file mode 100644 index 00000000..3fc53ad5 --- /dev/null +++ b/docs/implementation/stdlib/vendor-audit-2026-10-06/official-vs-vendored.diff @@ -0,0 +1,3821 @@ +--- purescript-prelude@v6.0.1/src/Control/Apply.purs ++++ stdlib/lib/Control/Apply.purs +@@ -60,7 +60,22 @@ + instance applyArray :: Apply Array where + apply = arrayApply + +-foreign import arrayApply :: forall a b. Array (a -> b) -> Array a -> Array b ++arrayApply :: forall a b. Array (a -> b) -> Array a -> Array b ++arrayApply a0 a1 = applyArrayFrom a0 a1 0 ++ ++applyArrayFrom :: forall a b. Array (a -> b) -> Array a -> Int -> Array b ++applyArrayFrom fs xs index = ++ if intLt index (arrayLength fs) then ++ arrayAppend (mapArrayAll (arrayIndex fs index) xs 0) (applyArrayFrom fs xs (intAdd index 1)) ++ else ++ [] ++ ++mapArrayAll :: forall a b. (a -> b) -> Array a -> Int -> Array b ++mapArrayAll f xs index = ++ if intLt index (arrayLength xs) then ++ arrayAppend [f (arrayIndex xs index)] (mapArrayAll f xs (intAdd index 1)) ++ else ++ [] + + instance applyProxy :: Apply Proxy where + apply _ _ = Proxy +--- purescript-prelude@v6.0.1/src/Control/Bind.purs ++++ stdlib/lib/Control/Bind.purs +@@ -94,7 +94,15 @@ + instance bindArray :: Bind Array where + bind = arrayBind + +-foreign import arrayBind :: forall a b. Array a -> (a -> Array b) -> Array b ++arrayBind :: forall a b. Array a -> (a -> Array b) -> Array b ++arrayBind a0 a1 = bindArrayFrom a0 a1 0 ++ ++bindArrayFrom :: forall a b. Array a -> (a -> Array b) -> Int -> Array b ++bindArrayFrom xs f index = ++ if intLt index (arrayLength xs) then ++ arrayAppend (f (arrayIndex xs index)) (bindArrayFrom xs f (intAdd index 1)) ++ else ++ [] + + instance bindProxy :: Bind Proxy where + bind _ _ = Proxy +--- purescript-control@v6.0.0/src/Control/Extend.purs ++++ stdlib/lib/Control/Extend.purs +@@ -27,7 +27,8 @@ + instance extendFn :: Semigroup w => Extend ((->) w) where + extend f g w = f \w' -> g (w <> w') + +-foreign import arrayExtend :: forall a b. (Array a -> b) -> Array a -> Array b ++arrayExtend :: forall a b. (Array a -> b) -> Array a -> Array b ++arrayExtend a0 a1 = arrayExtend a0 a1 + + instance extendArray :: Extend Array where + extend = arrayExtend +--- purescript-st@v6.2.0/src/Control/Monad/ST/Internal.purs ++++ stdlib/lib/Control/Monad/ST/Internal.purs +@@ -35,11 +35,14 @@ + + type role ST nominal representational + +-foreign import map_ :: forall r a b. (a -> b) -> ST r a -> ST r b ++map_ :: forall r a b. (a -> b) -> ST r a -> ST r b ++map_ a0 a1 = map_ a0 a1 + +-foreign import pure_ :: forall r a. a -> ST r a ++pure_ :: forall r a. a -> ST r a ++pure_ a0 = pure_ a0 + +-foreign import bind_ :: forall r a b. ST r a -> (a -> ST r b) -> ST r b ++bind_ :: forall r a b. ST r a -> (a -> ST r b) -> ST r b ++bind_ a0 a1 = bind_ a0 a1 + + instance functorST :: Functor (ST r) where + map = map_ +@@ -86,26 +89,30 @@ + -- | to the surrounding computation. It may cause problems to apply this + -- | function using the `$` operator. The recommended approach is to use + -- | parentheses instead. +-foreign import run :: forall a. (forall r. ST r a) -> a ++run :: forall a. (forall r. ST r a) -> a ++run a0 = run a0 + + -- | Loop while a condition is `true`. + -- | + -- | `while b m` is ST computation which runs the ST computation `b`. If its + -- | result is `true`, it runs the ST computation `m` and loops. If not, the + -- | computation ends. +-foreign import while :: forall r a. ST r Boolean -> ST r a -> ST r Unit ++while :: forall r a. ST r Boolean -> ST r a -> ST r Unit ++while a0 a1 = while a0 a1 + + -- | Loop over a consecutive collection of numbers + -- | + -- | `ST.for lo hi f` runs the computation returned by the function `f` for each + -- | of the inputs between `lo` (inclusive) and `hi` (exclusive). +-foreign import for :: forall r a. Int -> Int -> (Int -> ST r a) -> ST r Unit ++for :: forall r a. Int -> Int -> (Int -> ST r a) -> ST r Unit ++for a0 a1 a2 = for a0 a1 a2 + + -- | Loop over an array of values. + -- | + -- | `ST.foreach xs f` runs the computation returned by the function `f` for each + -- | of the inputs `xs`. +-foreign import foreach :: forall r a. Array a -> (a -> ST r Unit) -> ST r Unit ++foreach :: forall r a. Array a -> (a -> ST r Unit) -> ST r Unit ++foreach a0 a1 = foreach a0 a1 + + -- | The type `STRef r a` represents a mutable reference holding a value of + -- | type `a`, which can be used with the `ST r` effect. +@@ -114,10 +121,12 @@ + type role STRef nominal representational + + -- | Create a new mutable reference. +-foreign import new :: forall a r. a -> ST r (STRef r a) ++new :: forall a r. a -> ST r (STRef r a) ++new a0 = new a0 + + -- | Read the current value of a mutable reference. +-foreign import read :: forall a r. STRef r a -> ST r a ++read :: forall a r. STRef r a -> ST r a ++read a0 = read a0 + + -- | Update the value of a mutable reference by applying a function + -- | to the current value, computing a new state value for the reference and +@@ -125,7 +134,8 @@ + modify' :: forall r a b. (a -> { state :: a, value :: b }) -> STRef r a -> ST r b + modify' = modifyImpl + +-foreign import modifyImpl :: forall r a b. (a -> { state :: a, value :: b }) -> STRef r a -> ST r b ++modifyImpl :: forall r a b. (a -> { state :: a, value :: b }) -> STRef r a -> ST r b ++modifyImpl a0 a1 = modifyImpl a0 a1 + + -- | Modify the value of a mutable reference by applying a function to the + -- | current value. The modified value is returned. +@@ -133,4 +143,5 @@ + modify f = modify' \s -> let s' = f s in { state: s', value: s' } + + -- | Set the value of a mutable reference. +-foreign import write :: forall a r. a -> STRef r a -> ST r a ++write :: forall a r. a -> STRef r a -> ST r a ++write a0 a1 = write a0 a1 +--- purescript-st@v6.2.0/src/Control/Monad/ST/Uncurried.purs ++++ stdlib/lib/Control/Monad/ST/Uncurried.purs +@@ -58,44 +58,44 @@ + + type role STFn10 representational representational representational representational representational representational representational representational representational representational nominal representational + +-foreign import mkSTFn1 :: forall a t r. +- (a -> ST t r) -> STFn1 a t r +-foreign import mkSTFn2 :: forall a b t r. +- (a -> b -> ST t r) -> STFn2 a b t r +-foreign import mkSTFn3 :: forall a b c t r. +- (a -> b -> c -> ST t r) -> STFn3 a b c t r +-foreign import mkSTFn4 :: forall a b c d t r. +- (a -> b -> c -> d -> ST t r) -> STFn4 a b c d t r +-foreign import mkSTFn5 :: forall a b c d e t r. +- (a -> b -> c -> d -> e -> ST t r) -> STFn5 a b c d e t r +-foreign import mkSTFn6 :: forall a b c d e f t r. +- (a -> b -> c -> d -> e -> f -> ST t r) -> STFn6 a b c d e f t r +-foreign import mkSTFn7 :: forall a b c d e f g t r. +- (a -> b -> c -> d -> e -> f -> g -> ST t r) -> STFn7 a b c d e f g t r +-foreign import mkSTFn8 :: forall a b c d e f g h t r. +- (a -> b -> c -> d -> e -> f -> g -> h -> ST t r) -> STFn8 a b c d e f g h t r +-foreign import mkSTFn9 :: forall a b c d e f g h i t r. +- (a -> b -> c -> d -> e -> f -> g -> h -> i -> ST t r) -> STFn9 a b c d e f g h i t r +-foreign import mkSTFn10 :: forall a b c d e f g h i j t r. +- (a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> ST t r) -> STFn10 a b c d e f g h i j t r ++mkSTFn1 :: forall a t r. (a -> ST t r) -> STFn1 a t r ++mkSTFn1 a0 = mkSTFn1 a0 ++mkSTFn2 :: forall a b t r. (a -> b -> ST t r) -> STFn2 a b t r ++mkSTFn2 a0 = mkSTFn2 a0 ++mkSTFn3 :: forall a b c t r. (a -> b -> c -> ST t r) -> STFn3 a b c t r ++mkSTFn3 a0 = mkSTFn3 a0 ++mkSTFn4 :: forall a b c d t r. (a -> b -> c -> d -> ST t r) -> STFn4 a b c d t r ++mkSTFn4 a0 = mkSTFn4 a0 ++mkSTFn5 :: forall a b c d e t r. (a -> b -> c -> d -> e -> ST t r) -> STFn5 a b c d e t r ++mkSTFn5 a0 = mkSTFn5 a0 ++mkSTFn6 :: forall a b c d e f t r. (a -> b -> c -> d -> e -> f -> ST t r) -> STFn6 a b c d e f t r ++mkSTFn6 a0 = mkSTFn6 a0 ++mkSTFn7 :: forall a b c d e f g t r. (a -> b -> c -> d -> e -> f -> g -> ST t r) -> STFn7 a b c d e f g t r ++mkSTFn7 a0 = mkSTFn7 a0 ++mkSTFn8 :: forall a b c d e f g h t r. (a -> b -> c -> d -> e -> f -> g -> h -> ST t r) -> STFn8 a b c d e f g h t r ++mkSTFn8 a0 = mkSTFn8 a0 ++mkSTFn9 :: forall a b c d e f g h i t r. (a -> b -> c -> d -> e -> f -> g -> h -> i -> ST t r) -> STFn9 a b c d e f g h i t r ++mkSTFn9 a0 = mkSTFn9 a0 ++mkSTFn10 :: forall a b c d e f g h i j t r. (a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> ST t r) -> STFn10 a b c d e f g h i j t r ++mkSTFn10 a0 = mkSTFn10 a0 + +-foreign import runSTFn1 :: forall a t r. +- STFn1 a t r -> a -> ST t r +-foreign import runSTFn2 :: forall a b t r. +- STFn2 a b t r -> a -> b -> ST t r +-foreign import runSTFn3 :: forall a b c t r. +- STFn3 a b c t r -> a -> b -> c -> ST t r +-foreign import runSTFn4 :: forall a b c d t r. +- STFn4 a b c d t r -> a -> b -> c -> d -> ST t r +-foreign import runSTFn5 :: forall a b c d e t r. +- STFn5 a b c d e t r -> a -> b -> c -> d -> e -> ST t r +-foreign import runSTFn6 :: forall a b c d e f t r. +- STFn6 a b c d e f t r -> a -> b -> c -> d -> e -> f -> ST t r +-foreign import runSTFn7 :: forall a b c d e f g t r. +- STFn7 a b c d e f g t r -> a -> b -> c -> d -> e -> f -> g -> ST t r +-foreign import runSTFn8 :: forall a b c d e f g h t r. +- STFn8 a b c d e f g h t r -> a -> b -> c -> d -> e -> f -> g -> h -> ST t r +-foreign import runSTFn9 :: forall a b c d e f g h i t r. +- STFn9 a b c d e f g h i t r -> a -> b -> c -> d -> e -> f -> g -> h -> i -> ST t r +-foreign import runSTFn10 :: forall a b c d e f g h i j t r. +- STFn10 a b c d e f g h i j t r -> a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> ST t r ++runSTFn1 :: forall a t r. STFn1 a t r -> a -> ST t r ++runSTFn1 a0 a1 = runSTFn1 a0 a1 ++runSTFn2 :: forall a b t r. STFn2 a b t r -> a -> b -> ST t r ++runSTFn2 a0 a1 a2 = runSTFn2 a0 a1 a2 ++runSTFn3 :: forall a b c t r. STFn3 a b c t r -> a -> b -> c -> ST t r ++runSTFn3 a0 a1 a2 a3 = runSTFn3 a0 a1 a2 a3 ++runSTFn4 :: forall a b c d t r. STFn4 a b c d t r -> a -> b -> c -> d -> ST t r ++runSTFn4 a0 a1 a2 a3 a4 = runSTFn4 a0 a1 a2 a3 a4 ++runSTFn5 :: forall a b c d e t r. STFn5 a b c d e t r -> a -> b -> c -> d -> e -> ST t r ++runSTFn5 a0 a1 a2 a3 a4 a5 = runSTFn5 a0 a1 a2 a3 a4 a5 ++runSTFn6 :: forall a b c d e f t r. STFn6 a b c d e f t r -> a -> b -> c -> d -> e -> f -> ST t r ++runSTFn6 a0 a1 a2 a3 a4 a5 a6 = runSTFn6 a0 a1 a2 a3 a4 a5 a6 ++runSTFn7 :: forall a b c d e f g t r. STFn7 a b c d e f g t r -> a -> b -> c -> d -> e -> f -> g -> ST t r ++runSTFn7 a0 a1 a2 a3 a4 a5 a6 a7 = runSTFn7 a0 a1 a2 a3 a4 a5 a6 a7 ++runSTFn8 :: forall a b c d e f g h t r. STFn8 a b c d e f g h t r -> a -> b -> c -> d -> e -> f -> g -> h -> ST t r ++runSTFn8 a0 a1 a2 a3 a4 a5 a6 a7 a8 = runSTFn8 a0 a1 a2 a3 a4 a5 a6 a7 a8 ++runSTFn9 :: forall a b c d e f g h i t r. STFn9 a b c d e f g h i t r -> a -> b -> c -> d -> e -> f -> g -> h -> i -> ST t r ++runSTFn9 a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 = runSTFn9 a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 ++runSTFn10 :: forall a b c d e f g h i j t r. STFn10 a b c d e f g h i j t r -> a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> ST t r ++runSTFn10 a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 = runSTFn10 a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 +--- purescript-arrays@v7.3.0/src/Data/Array/NonEmpty/Internal.purs ++++ stdlib/lib/Data/Array/NonEmpty/Internal.purs +@@ -72,13 +72,10 @@ + derive newtype instance altNonEmptyArray :: Alt NonEmptyArray + + -- we use FFI here to avoid the unncessary copy created by `tail` +-foreign import foldr1Impl :: forall a. Fn2 (a -> a -> a) (NonEmptyArray a) a +-foreign import foldl1Impl :: forall a. Fn2 (a -> a -> a) (NonEmptyArray a) a ++foldr1Impl :: forall a. Fn2 (a -> a -> a) (NonEmptyArray a) a ++foldr1Impl = foldr1Impl ++foldl1Impl :: forall a. Fn2 (a -> a -> a) (NonEmptyArray a) a ++foldl1Impl = foldl1Impl + +-foreign import traverse1Impl +- :: forall m a b +- . Fn3 +- (forall a' b'. (m (a' -> b') -> m a' -> m b')) +- (forall a' b'. (a' -> b') -> m a' -> m b') +- (a -> m b) +- (NonEmptyArray a -> m (NonEmptyArray b)) ++traverse1Impl :: forall m a b . Fn3 (forall a' b'. (m (a' -> b') -> m a' -> m b')) (forall a' b'. (a' -> b') -> m a' -> m b') (a -> m b) (NonEmptyArray a -> m (NonEmptyArray b)) ++traverse1Impl = traverse1Impl +--- purescript-arrays@v7.3.0/src/Data/Array/ST/Partial.purs ++++ stdlib/lib/Data/Array/ST/Partial.purs +@@ -21,7 +21,8 @@ + -> ST h a + peek = runSTFn2 peekImpl + +-foreign import peekImpl :: forall h a. STFn2 Int (STArray h a) h a ++peekImpl :: forall h a. STFn2 Int (STArray h a) h a ++peekImpl = peekImpl + + -- | Change the value at the specified index in a mutable array. + poke +@@ -33,4 +34,5 @@ + -> ST h Unit + poke = runSTFn3 pokeImpl + +-foreign import pokeImpl :: forall h a. STFn3 Int a (STArray h a) h Unit ++pokeImpl :: forall h a. STFn3 Int a (STArray h a) h Unit ++pokeImpl = pokeImpl +--- purescript-arrays@v7.3.0/src/Data/Array/ST.purs ++++ stdlib/lib/Data/Array/ST.purs +@@ -75,17 +75,20 @@ + unsafeFreeze :: forall h a. STArray h a -> ST h (Array a) + unsafeFreeze = runSTFn1 unsafeFreezeImpl + +-foreign import unsafeFreezeImpl :: forall h a. STFn1 (STArray h a) h (Array a) ++unsafeFreezeImpl :: forall h a. STFn1 (STArray h a) h (Array a) ++unsafeFreezeImpl = unsafeFreezeImpl + + -- | O(1) Convert an immutable array to a mutable array, without copying. The input + -- | array must not be used afterward. + unsafeThaw :: forall h a. Array a -> ST h (STArray h a) + unsafeThaw = runSTFn1 unsafeThawImpl + +-foreign import unsafeThawImpl :: forall h a. STFn1 (Array a) h (STArray h a) ++unsafeThawImpl :: forall h a. STFn1 (Array a) h (STArray h a) ++unsafeThawImpl = unsafeThawImpl + + -- | Create a new, empty mutable array. +-foreign import new :: forall h a. ST h (STArray h a) ++new :: forall h a. ST h (STArray h a) ++new = new + + thaw + :: forall h a +@@ -94,7 +97,8 @@ + thaw = runSTFn1 thawImpl + + -- | Create a mutable copy of an immutable array. +-foreign import thawImpl :: forall h a. STFn1 (Array a) h (STArray h a) ++thawImpl :: forall h a. STFn1 (Array a) h (STArray h a) ++thawImpl = thawImpl + + -- | Make a mutable copy of a mutable array. + clone +@@ -103,7 +107,8 @@ + -> ST h (STArray h a) + clone = runSTFn1 cloneImpl + +-foreign import cloneImpl :: forall h a. STFn1 (STArray h a) h (STArray h a) ++cloneImpl :: forall h a. STFn1 (STArray h a) h (STArray h a) ++cloneImpl = cloneImpl + + -- | Sort a mutable array in place. Sorting is stable: the order of equal + -- | elements is preserved. +@@ -114,9 +119,8 @@ + shift :: forall h a. STArray h a -> ST h (Maybe a) + shift = runSTFn3 shiftImpl Just Nothing + +-foreign import shiftImpl +- :: forall h a +- . STFn3 (forall b. b -> Maybe b) (forall b. Maybe b) (STArray h a) h (Maybe a) ++shiftImpl :: forall h a . STFn3 (forall b. b -> Maybe b) (forall b. Maybe b) (STArray h a) h (Maybe a) ++shiftImpl = shiftImpl + + -- | Sort a mutable array in place using a comparison function. Sorting is + -- | stable: the order of elements is preserved if they are equal according to +@@ -131,9 +135,8 @@ + EQ -> 0 + LT -> -1 + +-foreign import sortByImpl +- :: forall a h +- . STFn3 (a -> a -> Ordering) (Ordering -> Int) (STArray h a) h (STArray h a) ++sortByImpl :: forall a h . STFn3 (a -> a -> Ordering) (Ordering -> Int) (STArray h a) h (STArray h a) ++sortByImpl = sortByImpl + + -- | Sort a mutable array in place based on a projection. Sorting is stable: the + -- | order of elements is preserved if they are equal according to the projection. +@@ -152,7 +155,8 @@ + -> ST h (Array a) + freeze = runSTFn1 freezeImpl + +-foreign import freezeImpl :: forall h a. STFn1 (STArray h a) h (Array a) ++freezeImpl :: forall h a. STFn1 (STArray h a) h (Array a) ++freezeImpl = freezeImpl + + -- | Read the value at the specified index in a mutable array. + peek +@@ -162,7 +166,8 @@ + -> ST h (Maybe a) + peek = runSTFn4 peekImpl Just Nothing + +-foreign import peekImpl :: forall h a r. STFn4 (a -> r) r Int (STArray h a) h r ++peekImpl :: forall h a r. STFn4 (a -> r) r Int (STArray h a) h r ++peekImpl = peekImpl + + poke + :: forall h a +@@ -173,9 +178,11 @@ + poke = runSTFn3 pokeImpl + + -- | Change the value at the specified index in a mutable array. +-foreign import pokeImpl :: forall h a. STFn3 Int a (STArray h a) h Boolean +- +-foreign import lengthImpl :: forall h a. STFn1 (STArray h a) h Int ++pokeImpl :: forall h a. STFn3 Int a (STArray h a) h Boolean ++pokeImpl = pokeImpl ++ ++lengthImpl :: forall h a. STFn1 (STArray h a) h Int ++lengthImpl = lengthImpl + + -- | Get the number of elements in a mutable array. + length :: forall h a. STArray h a -> ST h Int +@@ -185,16 +192,16 @@ + pop :: forall h a. STArray h a -> ST h (Maybe a) + pop = runSTFn3 popImpl Just Nothing + +-foreign import popImpl +- :: forall h a +- . STFn3 (forall b. b -> Maybe b) (forall b. Maybe b) (STArray h a) h (Maybe a) ++popImpl :: forall h a . STFn3 (forall b. b -> Maybe b) (forall b. Maybe b) (STArray h a) h (Maybe a) ++popImpl = popImpl + + -- | Append an element to the end of a mutable array. Returns the new length of + -- | the array. + push :: forall h a. a -> (STArray h a) -> ST h Int + push = runSTFn2 pushImpl + +-foreign import pushImpl :: forall h a. STFn2 a (STArray h a) h Int ++pushImpl :: forall h a. STFn2 a (STArray h a) h Int ++pushImpl = pushImpl + + -- | Append the values in an immutable array to the end of a mutable array. + -- | Returns the new length of the mutable array. +@@ -205,9 +212,8 @@ + -> ST h Int + pushAll = runSTFn2 pushAllImpl + +-foreign import pushAllImpl +- :: forall h a +- . STFn2 (Array a) (STArray h a) h Int ++pushAllImpl :: forall h a . STFn2 (Array a) (STArray h a) h Int ++pushAllImpl = pushAllImpl + + -- | Append an element to the front of a mutable array. Returns the new length of + -- | the array. +@@ -223,9 +229,8 @@ + -> ST h Int + unshiftAll = runSTFn2 unshiftAllImpl + +-foreign import unshiftAllImpl +- :: forall h a +- . STFn2 (Array a) (STArray h a) h Int ++unshiftAllImpl :: forall h a . STFn2 (Array a) (STArray h a) h Int ++unshiftAllImpl = unshiftAllImpl + + -- | Mutate the element at the specified index using the supplied function. + modify :: forall h a. Int -> (a -> a) -> STArray h a -> ST h Boolean +@@ -245,9 +250,8 @@ + -> ST h (Array a) + splice = runSTFn4 spliceImpl + +-foreign import spliceImpl +- :: forall h a +- . STFn4 Int Int (Array a) (STArray h a) h (Array a) ++spliceImpl :: forall h a . STFn4 Int Int (Array a) (STArray h a) h (Array a) ++spliceImpl = spliceImpl + + -- | Create an immutable copy of a mutable array, where each element + -- | is labelled with its index in the original array. +@@ -257,6 +261,5 @@ + -> ST h (Array (Assoc a)) + toAssocArray = runSTFn1 toAssocArrayImpl + +-foreign import toAssocArrayImpl +- :: forall h a +- . STFn1 (STArray h a) h (Array (Assoc a)) ++toAssocArrayImpl :: forall h a . STFn1 (STArray h a) h (Array (Assoc a)) ++toAssocArrayImpl = toAssocArrayImpl +--- purescript-arrays@v7.3.0/src/Data/Array.purs ++++ stdlib/lib/Data/Array.purs +@@ -174,9 +174,8 @@ + fromFoldable :: forall f. Foldable f => f ~> Array + fromFoldable = runFn2 fromFoldableImpl F.foldr + +-foreign import fromFoldableImpl +- :: forall f a +- . Fn2 (forall b. (a -> b -> b) -> b -> f a -> b) (f a) (Array a) ++fromFoldableImpl :: forall f a . Fn2 (forall b. (a -> b -> b) -> b -> f a -> b) (f a) (Array a) ++fromFoldableImpl = fromFoldableImpl + + -- | Create an array of one element + -- | ```purescript +@@ -192,7 +191,8 @@ + range :: Int -> Int -> Array Int + range = runFn2 rangeImpl + +-foreign import rangeImpl :: Fn2 Int Int (Array Int) ++rangeImpl :: Fn2 Int Int (Array Int) ++rangeImpl = rangeImpl + + -- | Create an array containing a value repeated the specified number of times. + -- | ```purescript +@@ -201,7 +201,8 @@ + replicate :: forall a. Int -> a -> Array a + replicate = runFn2 replicateImpl + +-foreign import replicateImpl :: forall a. Fn2 Int a (Array a) ++replicateImpl :: forall a. Fn2 Int a (Array a) ++replicateImpl = replicateImpl + + -- | An infix synonym for `range`. + -- | ```purescript +@@ -240,7 +241,8 @@ + -- | ```purescript + -- | length ["Hello", "World"] = 2 + -- | ``` +-foreign import length :: forall a. Array a -> Int ++length :: forall a. Array a -> Int ++length a0 = length a0 + + -------------------------------------------------------------------------------- + -- Extending arrays ------------------------------------------------------------ +@@ -370,9 +372,8 @@ + uncons :: forall a. Array a -> Maybe { head :: a, tail :: Array a } + uncons = runFn3 unconsImpl (const Nothing) \x xs -> Just { head: x, tail: xs } + +-foreign import unconsImpl +- :: forall a b +- . Fn3 (Unit -> b) (a -> Array a -> b) (Array a) b ++unconsImpl :: forall a b . Fn3 (Unit -> b) (a -> Array a -> b) (Array a) b ++unconsImpl = unconsImpl + + -- | Break an array into its last element and all preceding elements. + -- | +@@ -402,9 +403,8 @@ + index :: forall a. Array a -> Int -> Maybe a + index = runFn4 indexImpl Just Nothing + +-foreign import indexImpl +- :: forall a +- . Fn4 (forall r. r -> Maybe r) (forall r. Maybe r) (Array a) Int (Maybe a) ++indexImpl :: forall a . Fn4 (forall r. r -> Maybe r) (forall r. Maybe r) (Array a) Int (Maybe a) ++indexImpl = indexImpl + + -- | An infix version of `index`. + -- | +@@ -459,14 +459,8 @@ + findMap :: forall a b. (a -> Maybe b) -> Array a -> Maybe b + findMap = runFn4 findMapImpl Nothing isJust + +-foreign import findMapImpl +- :: forall a b +- . Fn4 +- (forall c. Maybe c) +- (forall c. Maybe c -> Boolean) +- (a -> Maybe b) +- (Array a) +- (Maybe b) ++findMapImpl :: forall a b . Fn4 (forall c. Maybe c) (forall c. Maybe c -> Boolean) (a -> Maybe b) (Array a) (Maybe b) ++findMapImpl = findMapImpl + + -- | Find the first index for which a predicate holds. + -- | +@@ -478,14 +472,8 @@ + findIndex :: forall a. (a -> Boolean) -> Array a -> Maybe Int + findIndex = runFn4 findIndexImpl Just Nothing + +-foreign import findIndexImpl +- :: forall a +- . Fn4 +- (forall b. b -> Maybe b) +- (forall b. Maybe b) +- (a -> Boolean) +- (Array a) +- (Maybe Int) ++findIndexImpl :: forall a . Fn4 (forall b. b -> Maybe b) (forall b. Maybe b) (a -> Boolean) (Array a) (Maybe Int) ++findIndexImpl = findIndexImpl + + -- | Find the last index for which a predicate holds. + -- | +@@ -497,14 +485,8 @@ + findLastIndex :: forall a. (a -> Boolean) -> Array a -> Maybe Int + findLastIndex = runFn4 findLastIndexImpl Just Nothing + +-foreign import findLastIndexImpl +- :: forall a +- . Fn4 +- (forall b. b -> Maybe b) +- (forall b. Maybe b) +- (a -> Boolean) +- (Array a) +- (Maybe Int) ++findLastIndexImpl :: forall a . Fn4 (forall b. b -> Maybe b) (forall b. Maybe b) (a -> Boolean) (Array a) (Maybe Int) ++findLastIndexImpl = findLastIndexImpl + + -- | Insert an element at the specified index, creating a new array, or + -- | returning `Nothing` if the index is out of bounds. +@@ -517,15 +499,8 @@ + insertAt :: forall a. Int -> a -> Array a -> Maybe (Array a) + insertAt = runFn5 _insertAt Just Nothing + +-foreign import _insertAt +- :: forall a +- . Fn5 +- (forall b. b -> Maybe b) +- (forall b. Maybe b) +- Int +- a +- (Array a) +- (Maybe (Array a)) ++_insertAt :: forall a . Fn5 (forall b. b -> Maybe b) (forall b. Maybe b) Int a (Array a) (Maybe (Array a)) ++_insertAt = _insertAt + + -- | Delete the element at the specified index, creating a new array, or + -- | returning `Nothing` if the index is out of bounds. +@@ -538,14 +513,8 @@ + deleteAt :: forall a. Int -> Array a -> Maybe (Array a) + deleteAt = runFn4 _deleteAt Just Nothing + +-foreign import _deleteAt +- :: forall a +- . Fn4 +- (forall b. b -> Maybe b) +- (forall b. Maybe b) +- Int +- (Array a) +- (Maybe (Array a)) ++_deleteAt :: forall a . Fn4 (forall b. b -> Maybe b) (forall b. Maybe b) Int (Array a) (Maybe (Array a)) ++_deleteAt = _deleteAt + + -- | Change the element at the specified index, creating a new array, or + -- | returning `Nothing` if the index is out of bounds. +@@ -558,15 +527,8 @@ + updateAt :: forall a. Int -> a -> Array a -> Maybe (Array a) + updateAt = runFn5 _updateAt Just Nothing + +-foreign import _updateAt +- :: forall a +- . Fn5 +- (forall b. b -> Maybe b) +- (forall b. Maybe b) +- Int +- a +- (Array a) +- (Maybe (Array a)) ++_updateAt :: forall a . Fn5 (forall b. b -> Maybe b) (forall b. Maybe b) Int a (Array a) (Maybe (Array a)) ++_updateAt = _updateAt + + -- | Apply a function to the element at the specified index, creating a new + -- | array, or returning `Nothing` if the index is out of bounds. +@@ -639,7 +601,8 @@ + -- | reverse [1, 2, 3] = [3, 2, 1] + -- | ``` + -- | +-foreign import reverse :: forall a. Array a -> Array a ++reverse :: forall a. Array a -> Array a ++reverse a0 = reverse a0 + + -- | Flatten an array of arrays, creating a new array. + -- | +@@ -647,7 +610,8 @@ + -- | concat [[1, 2, 3], [], [4, 5, 6]] = [1, 2, 3, 4, 5, 6] + -- | ``` + -- | +-foreign import concat :: forall a. Array (Array a) -> Array a ++concat :: forall a. Array (Array a) -> Array a ++concat a0 = concat a0 + + -- | Apply a function to each element in an array, and flatten the results + -- | into a single, new array. +@@ -670,9 +634,8 @@ + filter :: forall a. (a -> Boolean) -> Array a -> Array a + filter = runFn2 filterImpl + +-foreign import filterImpl +- :: forall a +- . Fn2 (a -> Boolean) (Array a) (Array a) ++filterImpl :: forall a . Fn2 (a -> Boolean) (Array a) (Array a) ++filterImpl = filterImpl + + -- | Partition an array using a predicate function, creating a set of + -- | new arrays. One for the values satisfying the predicate function +@@ -689,9 +652,8 @@ + -> { yes :: Array a, no :: Array a } + partition = runFn2 partitionImpl + +-foreign import partitionImpl +- :: forall a +- . Fn2 (a -> Boolean) (Array a) { yes :: Array a, no :: Array a } ++partitionImpl :: forall a . Fn2 (a -> Boolean) (Array a) { yes :: Array a, no :: Array a } ++partitionImpl = partitionImpl + + -- | Splits an array into two subarrays, where `before` contains the elements + -- | up to (but not including) the given index, and `after` contains the rest +@@ -854,7 +816,8 @@ + scanl :: forall a b. (b -> a -> b) -> b -> Array a -> Array b + scanl = runFn3 scanlImpl + +-foreign import scanlImpl :: forall a b. Fn3 (b -> a -> b) b (Array a) (Array b) ++scanlImpl :: forall a b. Fn3 (b -> a -> b) b (Array a) (Array b) ++scanlImpl = scanlImpl + + -- | Fold a data structure from the right, keeping all intermediate results + -- | instead of only the final result. Note that the initial value does not +@@ -867,7 +830,8 @@ + scanr :: forall a b. (a -> b -> b) -> b -> Array a -> Array b + scanr = runFn3 scanrImpl + +-foreign import scanrImpl :: forall a b. Fn3 (a -> b -> b) b (Array a) (Array b) ++scanrImpl :: forall a b. Fn3 (a -> b -> b) b (Array a) (Array b) ++scanrImpl = scanrImpl + + -------------------------------------------------------------------------------- + -- Sorting --------------------------------------------------------------------- +@@ -911,7 +875,8 @@ + sortWith :: forall a b. Ord b => (a -> b) -> Array a -> Array a + sortWith f = sortBy (comparing f) + +-foreign import sortByImpl :: forall a. Fn3 (a -> a -> Ordering) (Ordering -> Int) (Array a) (Array a) ++sortByImpl :: forall a. Fn3 (a -> a -> Ordering) (Ordering -> Int) (Array a) (Array a) ++sortByImpl = sortByImpl + + -------------------------------------------------------------------------------- + -- Subarrays ------------------------------------------------------------------- +@@ -929,7 +894,8 @@ + slice :: forall a. Int -> Int -> Array a -> Array a + slice = runFn3 sliceImpl + +-foreign import sliceImpl :: forall a. Fn3 Int Int (Array a) (Array a) ++sliceImpl :: forall a. Fn3 Int Int (Array a) (Array a) ++sliceImpl = sliceImpl + + -- | Keep only a number of elements from the start of an array, creating a new + -- | array. +@@ -1250,13 +1216,8 @@ + -> Array c + zipWith = runFn3 zipWithImpl + +-foreign import zipWithImpl +- :: forall a b c +- . Fn3 +- (a -> b -> c) +- (Array a) +- (Array b) +- (Array c) ++zipWithImpl :: forall a b c . Fn3 (a -> b -> c) (Array a) (Array b) (Array c) ++zipWithImpl = zipWithImpl + + -- | A generalization of `zipWith` which accumulates results in some + -- | `Applicative` functor. +@@ -1319,7 +1280,8 @@ + any :: forall a. (a -> Boolean) -> Array a -> Boolean + any = runFn2 anyImpl + +-foreign import anyImpl :: forall a. Fn2 (a -> Boolean) (Array a) Boolean ++anyImpl :: forall a. Fn2 (a -> Boolean) (Array a) Boolean ++anyImpl = anyImpl + + -- | Returns true if all the array elements satisfy the given predicate. + -- | iterating the array only as necessary and stopping as soon as the predicate +@@ -1333,7 +1295,8 @@ + all :: forall a. (a -> Boolean) -> Array a -> Boolean + all = runFn2 allImpl + +-foreign import allImpl :: forall a. Fn2 (a -> Boolean) (Array a) Boolean ++allImpl :: forall a. Fn2 (a -> Boolean) (Array a) Boolean ++allImpl = allImpl + + -- | Perform a fold using a monadic step function. + -- | +@@ -1368,4 +1331,5 @@ + unsafeIndex :: forall a. Partial => Array a -> Int -> a + unsafeIndex = runFn2 unsafeIndexImpl + +-foreign import unsafeIndexImpl :: forall a. Fn2 (Array a) Int a ++unsafeIndexImpl :: forall a. Fn2 (Array a) Int a ++unsafeIndexImpl = unsafeIndexImpl +--- purescript-prelude@v6.0.1/src/Data/Bounded.purs ++++ stdlib/lib/Data/Bounded.purs +@@ -37,16 +37,20 @@ + top = topInt + bottom = bottomInt + +-foreign import topInt :: Int +-foreign import bottomInt :: Int ++topInt :: Int ++topInt = 2147483647 ++bottomInt :: Int ++bottomInt = intSub (intSub 0 2147483647) 1 + + -- | Characters fall within the Unicode range. + instance boundedChar :: Bounded Char where + top = topChar + bottom = bottomChar + +-foreign import topChar :: Char +-foreign import bottomChar :: Char ++topChar :: Char ++topChar = intToChar 65535 ++bottomChar :: Char ++bottomChar = intToChar 0 + + instance boundedOrdering :: Bounded Ordering where + top = GT +@@ -56,8 +60,10 @@ + top = unit + bottom = unit + +-foreign import topNumber :: Number +-foreign import bottomNumber :: Number ++topNumber :: Number ++topNumber = numberDiv 1.0 0.0 ++bottomNumber :: Number ++bottomNumber = numberNeg (numberDiv 1.0 0.0) + + instance boundedNumber :: Bounded Number where + top = topNumber +--- purescript-enums@v6.0.1/src/Data/Enum.purs ++++ stdlib/lib/Data/Enum.purs +@@ -317,5 +317,7 @@ + charToEnum n | n >= toCharCode bottom && n <= toCharCode top = Just (fromCharCode n) + charToEnum _ = Nothing + +-foreign import toCharCode :: Char -> Int +-foreign import fromCharCode :: Int -> Char ++toCharCode :: Char -> Int ++toCharCode a0 = toCharCode a0 ++fromCharCode :: Int -> Char ++fromCharCode a0 = fromCharCode a0 +--- purescript-prelude@v6.0.1/src/Data/Eq.purs ++++ stdlib/lib/Data/Eq.purs +@@ -45,19 +45,19 @@ + infix 4 notEq as /= + + instance eqBoolean :: Eq Boolean where +- eq = eqBooleanImpl ++ eq x y = eqBooleanImpl x y + + instance eqInt :: Eq Int where +- eq = eqIntImpl ++ eq x y = eqIntImpl x y + + instance eqNumber :: Eq Number where +- eq = eqNumberImpl ++ eq x y = eqNumberImpl x y + + instance eqChar :: Eq Char where +- eq = eqCharImpl ++ eq x y = eqCharImpl x y + + instance eqString :: Eq String where +- eq = eqStringImpl ++ eq x y = eqStringImpl x y + + instance eqUnit :: Eq Unit where + eq _ _ = true +@@ -66,7 +66,7 @@ + eq _ _ = true + + instance eqArray :: Eq a => Eq (Array a) where +- eq = eqArrayImpl eq ++ eq xs ys = eqArrayImpl eq xs ys + + instance eqRec :: (RL.RowToList row list, EqRecord list row) => Eq (Record row) where + eq = eqRecord (Proxy :: Proxy list) +@@ -74,13 +74,32 @@ + instance eqProxy :: Eq (Proxy a) where + eq _ _ = true + +-foreign import eqBooleanImpl :: Boolean -> Boolean -> Boolean +-foreign import eqIntImpl :: Int -> Int -> Boolean +-foreign import eqNumberImpl :: Number -> Number -> Boolean +-foreign import eqCharImpl :: Char -> Char -> Boolean +-foreign import eqStringImpl :: String -> String -> Boolean ++eqBooleanImpl :: Boolean -> Boolean -> Boolean ++eqBooleanImpl a0 a1 = booleanEq a0 a1 ++eqIntImpl :: Int -> Int -> Boolean ++eqIntImpl a0 a1 = intEq a0 a1 ++eqNumberImpl :: Number -> Number -> Boolean ++eqNumberImpl a0 a1 = numberEq a0 a1 ++eqCharImpl :: Char -> Char -> Boolean ++eqCharImpl a0 a1 = charEq a0 a1 ++eqStringImpl :: String -> String -> Boolean ++eqStringImpl a0 a1 = eqBytes (stringToBytes a0) (stringToBytes a1) 0 + +-foreign import eqArrayImpl :: forall a. (a -> a -> Boolean) -> Array a -> Array a -> Boolean ++eqBytes :: Array Int -> Array Int -> Int -> Boolean ++eqBytes xs ys index = ++ if intGe index (arrayLength xs) then intEq (arrayLength xs) (arrayLength ys) ++ else if intGe index (arrayLength ys) then false ++ else if intEq (arrayIndex xs index) (arrayIndex ys index) then eqBytes xs ys (intAdd index 1) ++ else false ++ ++eqArrayImpl :: forall a. (a -> a -> Boolean) -> Array a -> Array a -> Boolean ++eqArrayImpl a0 a1 a2 = eqArrayFrom a0 a1 a2 0 ++ ++eqArrayFrom :: forall a. (a -> a -> Boolean) -> Array a -> Array a -> Int -> Boolean ++eqArrayFrom eq xs ys index = ++ if intGe index (arrayLength xs) then intEq (arrayLength xs) (arrayLength ys) ++ else if eq (arrayIndex xs index) (arrayIndex ys index) then eqArrayFrom eq xs ys (intAdd index 1) ++ else false + + -- | The `Eq1` type class represents type constructors with decidable equality. + class Eq1 f where +--- purescript-prelude@v6.0.1/src/Data/EuclideanRing.purs ++++ stdlib/lib/Data/EuclideanRing.purs +@@ -72,20 +72,25 @@ + infixl 7 div as / + + instance euclideanRingInt :: EuclideanRing Int where +- degree = intDegree +- div = intDiv +- mod = intMod ++ degree x = intDegree x ++ div x y = intDiv x y ++ mod x y = intMod x y + + instance euclideanRingNumber :: EuclideanRing Number where + degree _ = 1 +- div = numDiv ++ div x y = numDiv x y + mod _ _ = 0.0 + +-foreign import intDegree :: Int -> Int +-foreign import intDiv :: Int -> Int -> Int +-foreign import intMod :: Int -> Int -> Int ++intDegree :: Int -> Int ++intDegree a0 = if intEq a0 minInt32 then 2147483647 else if intLt a0 0 then intNeg a0 else a0 + +-foreign import numDiv :: Number -> Number -> Number ++minInt32 :: Int ++minInt32 = intSub (intSub 0 2147483647) 1 ++ ++ ++ ++numDiv :: Number -> Number -> Number ++numDiv a0 a1 = numberDiv a0 a1 + + -- | The *greatest common divisor* of two values. + gcd :: forall a. Eq a => EuclideanRing a => a -> a -> a +--- purescript-foldable-traversable@v6.0.0/src/Data/Foldable.purs ++++ stdlib/lib/Data/Foldable.purs +@@ -132,8 +132,10 @@ + foldl = foldlArray + foldMap = foldMapDefaultR + +-foreign import foldrArray :: forall a b. (a -> b -> b) -> b -> Array a -> b +-foreign import foldlArray :: forall a b. (b -> a -> b) -> b -> Array a -> b ++foldrArray :: forall a b. (a -> b -> b) -> b -> Array a -> b ++foldrArray a0 a1 a2 = foldrArray a0 a1 a2 ++foldlArray :: forall a b. (b -> a -> b) -> b -> Array a -> b ++foldlArray a0 a1 a2 = foldlArray a0 a1 a2 + + instance foldableMaybe :: Foldable Maybe where + foldr _ z Nothing = z +@@ -223,7 +225,7 @@ + + -- | Fold a data structure, accumulating values in some `Monoid`. + fold :: forall f m. Foldable f => Monoid m => f m -> m +-fold = foldMap identity ++fold xs = foldMap identity xs + + -- | Similar to 'foldl', but the result is encapsulated in a monad. + -- | +--- purescript-functions@v6.0.0/src/Data/Function/Uncurried.purs ++++ stdlib/lib/Data/Function/Uncurried.purs +@@ -56,69 +56,89 @@ + type role Fn10 representational representational representational representational representational representational representational representational representational representational representational + + -- | Create a function of no arguments +-foreign import mkFn0 :: forall a. (Unit -> a) -> Fn0 a ++mkFn0 :: forall a. (Unit -> a) -> Fn0 a ++mkFn0 a0 = mkFn0 a0 + + -- | Create a function of one argument + mkFn1 :: forall a b. (a -> b) -> Fn1 a b + mkFn1 f = f + + -- | Create a function of two arguments from a curried function +-foreign import mkFn2 :: forall a b c. (a -> b -> c) -> Fn2 a b c ++mkFn2 :: forall a b c. (a -> b -> c) -> Fn2 a b c ++mkFn2 a0 = mkFn2 a0 + + -- | Create a function of three arguments from a curried function +-foreign import mkFn3 :: forall a b c d. (a -> b -> c -> d) -> Fn3 a b c d ++mkFn3 :: forall a b c d. (a -> b -> c -> d) -> Fn3 a b c d ++mkFn3 a0 = mkFn3 a0 + + -- | Create a function of four arguments from a curried function +-foreign import mkFn4 :: forall a b c d e. (a -> b -> c -> d -> e) -> Fn4 a b c d e ++mkFn4 :: forall a b c d e. (a -> b -> c -> d -> e) -> Fn4 a b c d e ++mkFn4 a0 = mkFn4 a0 + + -- | Create a function of five arguments from a curried function +-foreign import mkFn5 :: forall a b c d e f. (a -> b -> c -> d -> e -> f) -> Fn5 a b c d e f ++mkFn5 :: forall a b c d e f. (a -> b -> c -> d -> e -> f) -> Fn5 a b c d e f ++mkFn5 a0 = mkFn5 a0 + + -- | Create a function of six arguments from a curried function +-foreign import mkFn6 :: forall a b c d e f g. (a -> b -> c -> d -> e -> f -> g) -> Fn6 a b c d e f g ++mkFn6 :: forall a b c d e f g. (a -> b -> c -> d -> e -> f -> g) -> Fn6 a b c d e f g ++mkFn6 a0 = mkFn6 a0 + + -- | Create a function of seven arguments from a curried function +-foreign import mkFn7 :: forall a b c d e f g h. (a -> b -> c -> d -> e -> f -> g -> h) -> Fn7 a b c d e f g h ++mkFn7 :: forall a b c d e f g h. (a -> b -> c -> d -> e -> f -> g -> h) -> Fn7 a b c d e f g h ++mkFn7 a0 = mkFn7 a0 + + -- | Create a function of eight arguments from a curried function +-foreign import mkFn8 :: forall a b c d e f g h i. (a -> b -> c -> d -> e -> f -> g -> h -> i) -> Fn8 a b c d e f g h i ++mkFn8 :: forall a b c d e f g h i. (a -> b -> c -> d -> e -> f -> g -> h -> i) -> Fn8 a b c d e f g h i ++mkFn8 a0 = mkFn8 a0 + + -- | Create a function of nine arguments from a curried function +-foreign import mkFn9 :: forall a b c d e f g h i j. (a -> b -> c -> d -> e -> f -> g -> h -> i -> j) -> Fn9 a b c d e f g h i j ++mkFn9 :: forall a b c d e f g h i j. (a -> b -> c -> d -> e -> f -> g -> h -> i -> j) -> Fn9 a b c d e f g h i j ++mkFn9 a0 = mkFn9 a0 + + -- | Create a function of ten arguments from a curried function +-foreign import mkFn10 :: forall a b c d e f g h i j k. (a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> k) -> Fn10 a b c d e f g h i j k ++mkFn10 :: forall a b c d e f g h i j k. (a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> k) -> Fn10 a b c d e f g h i j k ++mkFn10 a0 = mkFn10 a0 + + -- | Apply a function of no arguments +-foreign import runFn0 :: forall a. Fn0 a -> a ++runFn0 :: forall a. Fn0 a -> a ++runFn0 a0 = runFn0 a0 + + -- | Apply a function of one argument + runFn1 :: forall a b. Fn1 a b -> a -> b + runFn1 f = f + + -- | Apply a function of two arguments +-foreign import runFn2 :: forall a b c. Fn2 a b c -> a -> b -> c ++runFn2 :: forall a b c. Fn2 a b c -> a -> b -> c ++runFn2 a0 a1 a2 = runFn2 a0 a1 a2 + + -- | Apply a function of three arguments +-foreign import runFn3 :: forall a b c d. Fn3 a b c d -> a -> b -> c -> d ++runFn3 :: forall a b c d. Fn3 a b c d -> a -> b -> c -> d ++runFn3 a0 a1 a2 a3 = runFn3 a0 a1 a2 a3 + + -- | Apply a function of four arguments +-foreign import runFn4 :: forall a b c d e. Fn4 a b c d e -> a -> b -> c -> d -> e ++runFn4 :: forall a b c d e. Fn4 a b c d e -> a -> b -> c -> d -> e ++runFn4 a0 a1 a2 a3 a4 = runFn4 a0 a1 a2 a3 a4 + + -- | Apply a function of five arguments +-foreign import runFn5 :: forall a b c d e f. Fn5 a b c d e f -> a -> b -> c -> d -> e -> f ++runFn5 :: forall a b c d e f. Fn5 a b c d e f -> a -> b -> c -> d -> e -> f ++runFn5 a0 a1 a2 a3 a4 a5 = runFn5 a0 a1 a2 a3 a4 a5 + + -- | Apply a function of six arguments +-foreign import runFn6 :: forall a b c d e f g. Fn6 a b c d e f g -> a -> b -> c -> d -> e -> f -> g ++runFn6 :: forall a b c d e f g. Fn6 a b c d e f g -> a -> b -> c -> d -> e -> f -> g ++runFn6 a0 a1 a2 a3 a4 a5 a6 = runFn6 a0 a1 a2 a3 a4 a5 a6 + + -- | Apply a function of seven arguments +-foreign import runFn7 :: forall a b c d e f g h. Fn7 a b c d e f g h -> a -> b -> c -> d -> e -> f -> g -> h ++runFn7 :: forall a b c d e f g h. Fn7 a b c d e f g h -> a -> b -> c -> d -> e -> f -> g -> h ++runFn7 a0 a1 a2 a3 a4 a5 a6 a7 = runFn7 a0 a1 a2 a3 a4 a5 a6 a7 + + -- | Apply a function of eight arguments +-foreign import runFn8 :: forall a b c d e f g h i. Fn8 a b c d e f g h i -> a -> b -> c -> d -> e -> f -> g -> h -> i ++runFn8 :: forall a b c d e f g h i. Fn8 a b c d e f g h i -> a -> b -> c -> d -> e -> f -> g -> h -> i ++runFn8 a0 a1 a2 a3 a4 a5 a6 a7 a8 = runFn8 a0 a1 a2 a3 a4 a5 a6 a7 a8 + + -- | Apply a function of nine arguments +-foreign import runFn9 :: forall a b c d e f g h i j. Fn9 a b c d e f g h i j -> a -> b -> c -> d -> e -> f -> g -> h -> i -> j ++runFn9 :: forall a b c d e f g h i j. Fn9 a b c d e f g h i j -> a -> b -> c -> d -> e -> f -> g -> h -> i -> j ++runFn9 a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 = runFn9 a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 + + -- | Apply a function of ten arguments +-foreign import 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 ++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 ++runFn10 a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 = runFn10 a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 +--- purescript-prelude@v6.0.1/src/Data/Functor.purs ++++ stdlib/lib/Data/Functor.purs +@@ -47,12 +47,20 @@ + map = compose + + instance functorArray :: Functor Array where +- map = arrayMap ++ map x y = arrayMap x y + + instance functorProxy :: Functor Proxy where + map _ _ = Proxy + +-foreign import arrayMap :: forall a b. (a -> b) -> Array a -> Array b ++arrayMap :: forall a b. (a -> b) -> Array a -> Array b ++arrayMap a0 a1 = mapArrayFrom a0 a1 0 ++ ++mapArrayFrom :: forall a b. (a -> b) -> Array a -> Int -> Array b ++mapArrayFrom f xs index = ++ if intLt index (arrayLength xs) then ++ arrayAppend [f (arrayIndex xs index)] (mapArrayFrom f xs (intAdd index 1)) ++ else ++ [] + + -- | The `void` function is used to ignore the type wrapped by a + -- | [`Functor`](#functor), replacing it with `Unit` and keeping only the type +@@ -67,18 +75,18 @@ + -- | print (n * n) + -- | ``` + void :: forall f a. Functor f => f a -> f Unit +-void = map (const unit) ++void fa = map (\_ -> unit) fa + + -- | Ignore the return value of a computation, using the specified return value + -- | instead. + voidRight :: forall f a b. Functor f => a -> f b -> f a +-voidRight x = map (const x) ++voidRight x fa = map (\_ -> x) fa + + infixl 4 voidRight as <$ + + -- | A version of `voidRight` with its arguments flipped. + voidLeft :: forall f a b. Functor f => f a -> b -> f b +-voidLeft f x = const x <$> f ++voidLeft fa x = (\_ -> x) <$> fa + + infixl 4 voidLeft as $> + +--- purescript-foldable-traversable@v6.0.0/src/Data/FunctorWithIndex.purs ++++ stdlib/lib/Data/FunctorWithIndex.purs +@@ -35,7 +35,8 @@ + class Functor f <= FunctorWithIndex i f | f -> i where + mapWithIndex :: forall a b. (i -> a -> b) -> f a -> f b + +-foreign import mapWithIndexArray :: forall a b. (Int -> a -> b) -> Array a -> Array b ++mapWithIndexArray :: forall a b. (Int -> a -> b) -> Array a -> Array b ++mapWithIndexArray a0 a1 = mapWithIndexArray a0 a1 + + instance functorWithIndexArray :: FunctorWithIndex Int Array where + mapWithIndex = mapWithIndexArray +--- purescript-prelude@v6.0.1/src/Data/HeytingAlgebra/Generic.purs ++++ stdlib/lib/Data/HeytingAlgebra/Generic.purs +@@ -67,4 +67,4 @@ + + -- | A `Generic` implementation of the `not` member from the `HeytingAlgebra` type class. + genericNot :: forall a rep. Generic a rep => GenericHeytingAlgebra rep => a -> a +-genericNot x = to $ genericNot' (from x) +\ No newline at end of file ++genericNot x = to $ genericNot' (from x) +--- purescript-prelude@v6.0.1/src/Data/HeytingAlgebra.purs ++++ stdlib/lib/Data/HeytingAlgebra.purs +@@ -64,9 +64,9 @@ + ff = false + tt = true + implies a b = not a || b +- conj = boolConj +- disj = boolDisj +- not = boolNot ++ conj x y = boolConj x y ++ disj x y = boolDisj x y ++ not x = boolNot x + + instance heytingAlgebraUnit :: HeytingAlgebra Unit where + ff = unit +@@ -100,9 +100,12 @@ + implies = impliesRecord (Proxy :: Proxy list) + not = notRecord (Proxy :: Proxy list) + +-foreign import boolConj :: Boolean -> Boolean -> Boolean +-foreign import boolDisj :: Boolean -> Boolean -> Boolean +-foreign import boolNot :: Boolean -> Boolean ++boolConj :: Boolean -> Boolean -> Boolean ++boolConj a0 a1 = booleanAnd a0 a1 ++boolDisj :: Boolean -> Boolean -> Boolean ++boolDisj a0 a1 = booleanOr a0 a1 ++boolNot :: Boolean -> Boolean ++boolNot a0 = booleanNot a0 + + -- | A class for records where all fields have `HeytingAlgebra` instances, used + -- | to implement the `HeytingAlgebra` instance for records. +--- purescript-integers@v6.0.0/src/Data/Int/Bits.purs ++++ stdlib/lib/Data/Int/Bits.purs +@@ -10,28 +10,35 @@ + ) where + + -- | Bitwise AND. +-foreign import and :: Int -> Int -> Int ++and :: Int -> Int -> Int ++and a0 a1 = intAnd a0 a1 + + infixl 10 and as .&. + + -- | Bitwise OR. +-foreign import or :: Int -> Int -> Int ++or :: Int -> Int -> Int ++or a0 a1 = intOr a0 a1 + + infixl 10 or as .|. + + -- | Bitwise XOR. +-foreign import xor :: Int -> Int -> Int ++xor :: Int -> Int -> Int ++xor a0 a1 = intXor a0 a1 + + infixl 10 xor as .^. + + -- | Bitwise shift left. +-foreign import shl :: Int -> Int -> Int ++shl :: Int -> Int -> Int ++shl a0 a1 = intShl a0 a1 + + -- | Bitwise shift right. +-foreign import shr :: Int -> Int -> Int ++shr :: Int -> Int -> Int ++shr a0 a1 = intShr a0 a1 + + -- | Bitwise zero-fill shift right. +-foreign import zshr :: Int -> Int -> Int ++zshr :: Int -> Int -> Int ++zshr a0 a1 = intZshr a0 a1 + + -- | Bitwise NOT. +-foreign import complement :: Int -> Int ++complement :: Int -> Int ++complement a0 = intComplement a0 +--- purescript-integers@v6.0.0/src/Data/Int.purs ++++ stdlib/lib/Data/Int.purs +@@ -37,11 +37,8 @@ + fromNumber :: Number -> Maybe Int + fromNumber = fromNumberImpl Just Nothing + +-foreign import fromNumberImpl +- :: (forall a. a -> Maybe a) +- -> (forall a. Maybe a) +- -> Number +- -> Maybe Int ++fromNumberImpl :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Number -> Maybe Int ++fromNumberImpl a0 a1 a2 = fromNumberImpl a0 a1 a2 + + -- | Convert a `Number` to an `Int`, by taking the closest integer equal to or + -- | less than the argument. Values outside the `Int` range are clamped, `NaN` +@@ -78,7 +75,8 @@ + + -- | Converts an `Int` value back into a `Number`. Any `Int` is a valid `Number` + -- | so there is no loss of precision with this function. +-foreign import toNumber :: Int -> Number ++toNumber :: Int -> Number ++toNumber a0 = toNumber a0 + + -- | Reads an `Int` from a `String` value. The number must parse as an integer + -- | and fall within the valid range of values for the `Int` type, otherwise +@@ -224,7 +222,8 @@ + -- | div 2 (-3) == 0 + -- | quot 2 (-3) == 0 + -- | ``` +-foreign import quot :: Int -> Int -> Int ++quot :: Int -> Int -> Int ++quot a0 a1 = quot a0 a1 + + -- | The `rem` function provides the remainder after _truncating_ integer + -- | division (see the documentation for the `EuclideanRing` class). It is +@@ -242,16 +241,15 @@ + -- | mod 2 (-3) == 2 + -- | rem 2 (-3) == 2 + -- | ``` +-foreign import rem :: Int -> Int -> Int ++rem :: Int -> Int -> Int ++rem a0 a1 = rem a0 a1 + + -- | Raise an Int to the power of another Int. +-foreign import pow :: Int -> Int -> Int +- +-foreign import fromStringAsImpl +- :: (forall a. a -> Maybe a) +- -> (forall a. Maybe a) +- -> Radix +- -> String +- -> Maybe Int +- +-foreign import toStringAs :: Radix -> Int -> String ++pow :: Int -> Int -> Int ++pow a0 a1 = pow a0 a1 ++ ++fromStringAsImpl :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Radix -> String -> Maybe Int ++fromStringAsImpl a0 a1 a2 a3 = fromStringAsImpl a0 a1 a2 a3 ++ ++toStringAs :: Radix -> Int -> String ++toStringAs a0 a1 = toStringAs a0 a1 +--- purescript-lazy@v6.0.0/src/Data/Lazy.purs ++++ stdlib/lib/Data/Lazy.purs +@@ -32,10 +32,12 @@ + type role Lazy representational + + -- | Defer a computation, creating a `Lazy` value. +-foreign import defer :: forall a. (Unit -> a) -> Lazy a ++defer :: forall a. (Unit -> a) -> Lazy a ++defer a0 = defer a0 + + -- | Force evaluation of a `Lazy` value. +-foreign import force :: forall a. Lazy a -> a ++force :: forall a. Lazy a -> a ++force a0 = force a0 + + instance semiringLazy :: Semiring a => Semiring (Lazy a) where + add a b = defer \_ -> force a + force b +--- purescript-numbers@v9.0.1/src/Data/Number/Format.purs ++++ stdlib/lib/Data/Number/Format.purs +@@ -30,9 +30,12 @@ + + import Prelude + +-foreign import toPrecisionNative :: Int -> Number -> String +-foreign import toFixedNative :: Int -> Number -> String +-foreign import toExponentialNative :: Int -> Number -> String ++toPrecisionNative :: Int -> Number -> String ++toPrecisionNative a0 a1 = toPrecisionNative a0 a1 ++toFixedNative :: Int -> Number -> String ++toFixedNative a0 a1 = toFixedNative a0 a1 ++toExponentialNative :: Int -> Number -> String ++toExponentialNative a0 a1 = toExponentialNative a0 a1 + + -- | The `Format` data type specifies how a number will be formatted. + data Format +@@ -73,4 +76,5 @@ + -- | > toString 1.2e-10 + -- | "1.2e-10" + -- | ``` +-foreign import toString :: Number -> String ++toString :: Number -> String ++toString a0 = toString a0 +--- purescript-numbers@v9.0.1/src/Data/Number.purs ++++ stdlib/lib/Data/Number.purs +@@ -44,7 +44,8 @@ + -- | > nan + -- | NaN + -- | ``` +-foreign import nan :: Number ++nan :: Number ++nan = nan + + -- | Test whether a number is NaN. + -- | ```purs +@@ -54,7 +55,8 @@ + -- | > isNaN nan + -- | true + -- | ``` +-foreign import isNaN :: Number -> Boolean ++isNaN :: Number -> Boolean ++isNaN a0 = isNaN a0 + + -- | Positive infinity. For negative infinity use `(-infinity)` + -- | ```purs +@@ -64,7 +66,8 @@ + -- | > (-infinity) + -- | - Infinity + -- | ``` +-foreign import infinity :: Number ++infinity :: Number ++infinity = infinity + + -- | Test whether a number is finite. + -- | ```purs +@@ -80,7 +83,8 @@ + -- | > isFinite nan + -- | false + -- | ``` +-foreign import isFinite :: Number -> Boolean ++isFinite :: Number -> Boolean ++isFinite a0 = isFinite a0 + + -- | Attempt to parse a `Number` using JavaScripts `parseFloat`. Returns + -- | `Nothing` if the parse fails or if the result is not a finite number. +@@ -112,7 +116,8 @@ + fromString :: String -> Maybe Number + fromString str = runFn4 fromStringImpl str isFinite Just Nothing + +-foreign import fromStringImpl :: Fn4 String (Number -> Boolean) (forall a. a -> Maybe a) (forall a. Maybe a) (Maybe Number) ++fromStringImpl :: Fn4 String (Number -> Boolean) (forall a. a -> Maybe a) (forall a. Maybe a) (Maybe Number) ++fromStringImpl = fromStringImpl + + -- | Returns the absolute value of the argument. + -- | ```purs +@@ -120,28 +125,32 @@ + -- | > sign x * abs x == x + -- | true + -- | ``` +-foreign import abs :: Number -> Number ++abs :: Number -> Number ++abs a0 = abs a0 + + -- | Returns the inverse cosine in radians of the argument. + -- | ```purs + -- | > acos 0.0 == pi / 2.0 + -- | true + -- | ``` +-foreign import acos :: Number -> Number ++acos :: Number -> Number ++acos a0 = acos a0 + + -- | Returns the inverse sine in radians of the argument. + -- | ```purs + -- | > asin 1.0 == pi / 2.0 + -- | true + -- | ``` +-foreign import asin :: Number -> Number ++asin :: Number -> Number ++asin a0 = asin a0 + + -- | Returns the inverse tangent in radians of the argument. + -- | ```purs + -- | > atan 1.0 == pi / 4.0 + -- | true + -- | ``` +-foreign import atan :: Number -> Number ++atan :: Number -> Number ++atan a0 = atan a0 + + -- | Four-quadrant tangent inverse. Given the arguments `y` and `x`, returns + -- | the inverse tangent of `y / x`, where the signs of both arguments are used +@@ -154,49 +163,57 @@ + -- | > atan2 1.0 0.0 == pi / 2.0 + -- | true + -- | ``` +-foreign import atan2 :: Number -> Number -> Number ++atan2 :: Number -> Number -> Number ++atan2 a0 a1 = atan2 a0 a1 + + -- | Returns the smallest integer not smaller than the argument. + -- | ```purs + -- | > ceil 1.5 + -- | 2.0 + -- | ``` +-foreign import ceil :: Number -> Number ++ceil :: Number -> Number ++ceil a0 = ceil a0 + + -- | Returns the cosine of the argument, where the argument is in radians. + -- | ```purs + -- | > cos (pi / 4.0) == sqrt2 / 2.0 + -- | true + -- | ``` +-foreign import cos :: Number -> Number ++cos :: Number -> Number ++cos a0 = cos a0 + + -- | Returns `e` exponentiated to the power of the argument. + -- | ```purs + -- | > exp 1.0 + -- | 2.718281828459045 + -- | ``` +-foreign import exp :: Number -> Number ++exp :: Number -> Number ++exp a0 = exp a0 + + -- | Returns the largest integer not larger than the argument. + -- | ```purs + -- | > floor 1.5 + -- | 1.0 + -- | ``` +-foreign import floor :: Number -> Number ++floor :: Number -> Number ++floor a0 = floor a0 + + -- | Returns the natural logarithm of a number. + -- | ```purs + -- | > log e + -- | 1.0 +-foreign import log :: Number -> Number ++log :: Number -> Number ++log a0 = log a0 + + -- | Returns the largest of two numbers. Unlike `max` in Data.Ord this version + -- | returns NaN if either argument is NaN. +-foreign import max :: Number -> Number -> Number ++max :: Number -> Number -> Number ++max a0 a1 = max a0 a1 + + -- | Returns the smallest of two numbers. Unlike `min` in Data.Ord this version + -- | returns NaN if either argument is NaN. +-foreign import min :: Number -> Number -> Number ++min :: Number -> Number -> Number ++min a0 a1 = min a0 a1 + + -- | Return the first argument exponentiated to the power of the second argument. + -- | ```purs +@@ -206,14 +223,16 @@ + -- | true + -- | ``` + +-foreign import pow :: Number -> Number -> Number ++pow :: Number -> Number -> Number ++pow a0 a1 = pow a0 a1 + + -- | Computes the remainder after division. This is the same as JavaScript's `%` operator. + -- ```purs + -- > 5.3 % 2.0 + -- 1.2999999999999998 + -- ``` +-foreign import remainder :: Number -> Number -> Number ++remainder :: Number -> Number -> Number ++remainder a0 a1 = remainder a0 a1 + + infixl 7 remainder as % + +@@ -222,7 +241,8 @@ + -- | > round 1.5 + -- | 2.0 + -- | ``` +-foreign import round :: Number -> Number ++round :: Number -> Number ++round a0 = round a0 + + -- | Returns either a positive or negative +/- 1, indicating the sign of the + -- | argument. If the argument is 0, it will return a +/- 0. If the argument is +@@ -232,28 +252,32 @@ + -- | > sign x * abs x == x + -- | true + -- | ``` +-foreign import sign :: Number -> Number ++sign :: Number -> Number ++sign a0 = sign a0 + + -- | Returns the sine of the argument, where the argument is in radians. + -- | ```purs + -- | > sin (pi / 2.0) + -- | 1.0 + -- | ``` +-foreign import sin :: Number -> Number ++sin :: Number -> Number ++sin a0 = sin a0 + + -- | Returns the square root of the argument. + -- | ```purs + -- | > sqrt 49.0 + -- | 7.0 + -- | ``` +-foreign import sqrt :: Number -> Number ++sqrt :: Number -> Number ++sqrt a0 = sqrt a0 + + -- | Returns the tangent of the argument, where the argument is in radians. + -- | ``` + -- | > tan (pi / 4.0) + -- | 0.9999999999999999 + -- | ``` +-foreign import tan :: Number -> Number ++tan :: Number -> Number ++tan a0 = tan a0 + + -- | Truncates the decimal portion of a number. Equivalent to `floor` if the + -- | number is positive, and `ceil` if the number is negative. +@@ -261,7 +285,8 @@ + -- | ceil 1.5 + -- | 2.0 + -- | ``` +-foreign import trunc :: Number -> Number ++trunc :: Number -> Number ++trunc a0 = trunc a0 + + -- | The base of the natural logarithm, also known as Euler's number or *e*. + -- | ```purs +--- purescript-prelude@v6.0.1/src/Data/Ord.purs ++++ stdlib/lib/Data/Ord.purs +@@ -49,19 +49,19 @@ + compare :: a -> a -> Ordering + + instance ordBoolean :: Ord Boolean where +- compare = ordBooleanImpl LT EQ GT ++ compare x y = ordBooleanImpl LT EQ GT x y + + instance ordInt :: Ord Int where +- compare = ordIntImpl LT EQ GT ++ compare x y = ordIntImpl LT EQ GT x y + + instance ordNumber :: Ord Number where +- compare = ordNumberImpl LT EQ GT ++ compare x y = ordNumberImpl LT EQ GT x y + + instance ordString :: Ord String where +- compare = ordStringImpl LT EQ GT ++ compare x y = ordStringImpl LT EQ GT x y + + instance ordChar :: Ord Char where +- compare = ordCharImpl LT EQ GT ++ compare x y = ordCharImpl LT EQ GT x y + + instance ordUnit :: Ord Unit where + compare _ _ = EQ +@@ -81,47 +81,46 @@ + LT -> 1 + GT -> -1 + +-foreign import ordBooleanImpl +- :: Ordering +- -> Ordering +- -> Ordering +- -> Boolean +- -> Boolean +- -> Ordering +- +-foreign import ordIntImpl +- :: Ordering +- -> Ordering +- -> Ordering +- -> Int +- -> Int +- -> Ordering +- +-foreign import ordNumberImpl +- :: Ordering +- -> Ordering +- -> Ordering +- -> Number +- -> Number +- -> Ordering +- +-foreign import ordStringImpl +- :: Ordering +- -> Ordering +- -> Ordering +- -> String +- -> String +- -> Ordering +- +-foreign import ordCharImpl +- :: Ordering +- -> Ordering +- -> Ordering +- -> Char +- -> Char +- -> Ordering +- +-foreign import ordArrayImpl :: forall a. (a -> a -> Int) -> Array a -> Array a -> Int ++ordBooleanImpl :: Ordering -> Ordering -> Ordering -> Boolean -> Boolean -> Ordering ++ordBooleanImpl a0 a1 a2 a3 a4 = if booleanEq a3 a4 then a1 else if a3 then a2 else a0 ++ ++ordIntImpl :: Ordering -> Ordering -> Ordering -> Int -> Int -> Ordering ++ordIntImpl a0 a1 a2 a3 a4 = if intLt a3 a4 then a0 else if intEq a3 a4 then a1 else a2 ++ ++ordNumberImpl :: Ordering -> Ordering -> Ordering -> Number -> Number -> Ordering ++ordNumberImpl a0 a1 a2 a3 a4 = if numberLt a3 a4 then a0 else if numberEq a3 a4 then a1 else a2 ++ ++ordStringImpl :: Ordering -> Ordering -> Ordering -> String -> String -> Ordering ++ordStringImpl a0 a1 a2 a3 a4 = ordBytes a0 a1 a2 (stringToBytes a3) (stringToBytes a4) 0 ++ ++ordBytes :: forall a. a -> a -> a -> Array Int -> Array Int -> Int -> a ++ordBytes lt eq gt xs ys index = ++ if intGe index (arrayLength xs) then ++ if intEq (arrayLength xs) (arrayLength ys) then eq ++ else if intGt (arrayLength xs) (arrayLength ys) then gt ++ else lt ++ else if intGe index (arrayLength ys) then gt ++ else if intLt (arrayIndex xs index) (arrayIndex ys index) then lt ++ else if intEq (arrayIndex xs index) (arrayIndex ys index) then ordBytes lt eq gt xs ys (intAdd index 1) ++ else gt ++ ++ordCharImpl :: Ordering -> Ordering -> Ordering -> Char -> Char -> Ordering ++ordCharImpl a0 a1 a2 a3 a4 = if charLt a3 a4 then a0 else if charEq a3 a4 then a1 else a2 ++ ++ordArrayImpl :: forall a. (a -> a -> Int) -> Array a -> Array a -> Int ++ordArrayImpl a0 a1 a2 = ordArrayFrom a0 a1 a2 0 ++ ++ordArrayFrom :: forall a. (a -> a -> Int) -> Array a -> Array a -> Int -> Int ++ordArrayFrom compareOne xs ys index = ++ if intLt index (arrayLength xs) then ++ if intLt index (arrayLength ys) then ++ let ++ order = compareOne (arrayIndex xs index) (arrayIndex ys index) ++ in ++ if intEq order 0 then ordArrayFrom compareOne xs ys (intAdd index 1) else order ++ else intNeg 1 ++ else if intEq (arrayLength xs) (arrayLength ys) then 0 ++ else 1 + + instance ordOrdering :: Ord Ordering where + compare LT LT = EQ +--- purescript-prelude@v6.0.1/src/Data/Reflectable.purs ++++ stdlib/lib/Data/Reflectable.purs +@@ -33,7 +33,8 @@ + instance Reifiable String + + -- local definition for use in `reifyType` +-foreign import unsafeCoerce :: forall a b. a -> b ++unsafeCoerce :: forall a b. a -> b ++unsafeCoerce a0 = unsafeCoerce a0 + + -- | Reify a value of type `t` such that it can be consumed by a + -- | function constrained by the `Reflectable` type class. For +--- purescript-prelude@v6.0.1/src/Data/Ring/Generic.purs ++++ stdlib/lib/Data/Ring/Generic.purs +@@ -21,4 +21,4 @@ + + -- | A `Generic` implementation of the `sub` member from the `Ring` type class. + genericSub :: forall a rep. Generic a rep => GenericRing rep => a -> a -> a +-genericSub x y = to $ from x `genericSub'` from y +\ No newline at end of file ++genericSub x y = to $ from x `genericSub'` from y +--- purescript-prelude@v6.0.1/src/Data/Ring.purs ++++ stdlib/lib/Data/Ring.purs +@@ -30,10 +30,10 @@ + infixl 6 sub as - + + instance ringInt :: Ring Int where +- sub = intSub ++ sub x y = intSub x y + + instance ringNumber :: Ring Number where +- sub = numSub ++ sub x y = numSub x y + + instance ringUnit :: Ring Unit where + sub _ _ = unit +@@ -51,8 +51,9 @@ + negate :: forall a. Ring a => a -> a + negate a = zero - a + +-foreign import intSub :: Int -> Int -> Int +-foreign import numSub :: Number -> Number -> Number ++ ++numSub :: Number -> Number -> Number ++numSub a0 a1 = numberSub a0 a1 + + -- | A class for records where all fields have `Ring` instances, used to + -- | implement the `Ring` instance for records. +--- purescript-prelude@v6.0.1/src/Data/Semigroup.purs ++++ stdlib/lib/Data/Semigroup.purs +@@ -37,7 +37,7 @@ + infixr 5 append as <> + + instance semigroupString :: Semigroup String where +- append = concatString ++ append x y = concatString x y + + instance semigroupUnit :: Semigroup Unit where + append _ _ = unit +@@ -49,7 +49,7 @@ + append f g x = f x <> g x + + instance semigroupArray :: Semigroup (Array a) where +- append = concatArray ++ append x y = concatArray x y + + instance semigroupProxy :: Semigroup (Proxy a) where + append _ _ = Proxy +@@ -57,8 +57,10 @@ + instance semigroupRecord :: (RL.RowToList row list, SemigroupRecord list row row) => Semigroup (Record row) where + append = appendRecord (Proxy :: Proxy list) + +-foreign import concatString :: String -> String -> String +-foreign import concatArray :: forall a. Array a -> Array a -> Array a ++concatString :: String -> String -> String ++concatString a0 a1 = bytesToString (arrayAppend (stringToBytes a0) (stringToBytes a1)) ++concatArray :: forall a. Array a -> Array a -> Array a ++concatArray a0 a1 = arrayAppend a0 a1 + + -- | A class for records where all fields have `Semigroup` instances, used to + -- | implement the `Semigroup` instance for records. +--- purescript-prelude@v6.0.1/src/Data/Semiring/Generic.purs ++++ stdlib/lib/Data/Semiring/Generic.purs +@@ -48,4 +48,4 @@ + + -- | A `Generic` implementation of the `mul` member from the `Semiring` type class. + genericMul :: forall a rep. Generic a rep => GenericSemiring rep => a -> a -> a +-genericMul x y = to $ from x `genericMul'` from y +\ No newline at end of file ++genericMul x y = to $ from x `genericMul'` from y +--- purescript-prelude@v6.0.1/src/Data/Semiring.purs ++++ stdlib/lib/Data/Semiring.purs +@@ -51,15 +51,15 @@ + infixl 7 mul as * + + instance semiringInt :: Semiring Int where +- add = intAdd ++ add x y = intAdd x y + zero = 0 +- mul = intMul ++ mul x y = intMul x y + one = 1 + + instance semiringNumber :: Semiring Number where +- add = numAdd ++ add x y = numAdd x y + zero = 0.0 +- mul = numMul ++ mul x y = numMul x y + one = 1.0 + + instance semiringFn :: Semiring b => Semiring (a -> b) where +@@ -86,10 +86,12 @@ + one = oneRecord (Proxy :: Proxy list) (Proxy :: Proxy row) + zero = zeroRecord (Proxy :: Proxy list) (Proxy :: Proxy row) + +-foreign import intAdd :: Int -> Int -> Int +-foreign import intMul :: Int -> Int -> Int +-foreign import numAdd :: Number -> Number -> Number +-foreign import numMul :: Number -> Number -> Number ++ ++ ++numAdd :: Number -> Number -> Number ++numAdd a0 a1 = numberAdd a0 a1 ++numMul :: Number -> Number -> Number ++numMul a0 a1 = numberMul a0 a1 + + -- | A class for records where all fields have `Semiring` instances, used to + -- | implement the `Semiring` instance for records. +--- purescript-prelude@v6.0.1/src/Data/Show/Generic.purs ++++ stdlib/lib/Data/Show/Generic.purs +@@ -54,4 +54,11 @@ + genericShow :: forall a rep. Generic a rep => GenericShow rep => a -> String + genericShow x = genericShow' (from x) + +-foreign import intercalate :: String -> Array String -> String ++intercalate :: String -> Array String -> String ++intercalate a0 a1 = intercalateFrom a0 a1 0 ++ ++intercalateFrom :: String -> Array String -> Int -> String ++intercalateFrom separator values index = ++ if intGe index (arrayLength values) then "" ++ else if intEq index 0 then arrayIndex values index <> intercalateFrom separator values (intAdd index 1) ++ else separator <> arrayIndex values index <> intercalateFrom separator values (intAdd index 1) +--- purescript-prelude@v6.0.1/src/Data/Show.purs ++++ stdlib/lib/Data/Show.purs +@@ -1,97 +1,280 @@ ++-- | The `Show` class. ++-- | ++-- | `show` renders a value as text. The `Int`, `Number`, `Boolean`, `Char`, ++-- | `String`, `Unit`, and `Array` instances are the ones the corpus actually ++-- | applies `show` to. `Show Unit` lives here; `Data.Unit` only re-exports the ++-- | builtin. A record instance is not here because `reflectSymbol` has no ++-- | runtime. ++-- | ++-- | `Boolean`, `Int`, `Char`, `String`, `Unit`, and `Array` match the official ++-- | spelling, including the `Char`/`String` escapes. `Number` uses the same ++-- | shape as the official instance — decimal digits, `.0` on an integer token, ++-- | scientific form outside `(1e-6, 1e21)` — but the digits come from the ++-- | numeric primitives, not from a correctly rounded ECMAScript conversion. ++-- | Integers whose absolute value is below `1e21`, powers of ten, and the ++-- | dyadic fractions the digit loop reaches exactly match; other fractions ++-- | print a deterministic expansion that can differ from `purs`. + module Data.Show + ( class Show + , show +- , class ShowRecordFields +- , showRecordFields + ) where + + import Data.Semigroup ((<>)) +-import Data.Symbol (class IsSymbol, reflectSymbol) +-import Data.Unit (Unit) +-import Data.Void (Void, absurd) +-import Prim.Row (class Nub) +-import Prim.RowList as RL +-import Record.Unsafe (unsafeGet) +-import Type.Proxy (Proxy(..)) +- +--- | The `Show` type class represents those types which can be converted into +--- | a human-readable `String` representation. +--- | +--- | While not required, it is recommended that for any expression `x`, the +--- | string `show x` be executable PureScript code which evaluates to the same +--- | value as the expression `x`. ++ ++-- | A type that can be rendered as text. + class Show a where + show :: a -> String + ++instance showBoolean :: Show Boolean where ++ show value = if value then "true" else "false" ++ ++instance showInt :: Show Int where ++ show value = ++ if intEq value minInt then "-2147483648" ++ else if intLt value 0 then "-" <> showPositiveInt (intNeg value) ++ else showPositiveInt value ++ ++instance showNumber :: Show Number where ++ show value = ++ if numberNe value value then "NaN" ++ else if numberEq value positiveInfinity then "Infinity" ++ else if numberEq value negativeInfinity then "-Infinity" ++ else if numberLt value 0.0 then "-" <> showPositiveNumber (numberNeg value) ++ else showPositiveNumber value ++ ++instance showChar :: Show Char where ++ show value = "'" <> escapeCode (charToInt value) false false <> "'" ++ ++instance showString :: Show String where ++ show value = "\"" <> escapeBytes (stringToBytes value) 0 <> "\"" ++ + instance showUnit :: Show Unit where + show _ = "unit" + +-instance showBoolean :: Show Boolean where +- show true = "true" +- show false = "false" +- +-instance showInt :: Show Int where +- show = showIntImpl +- +-instance showNumber :: Show Number where +- show = showNumberImpl +- +-instance showChar :: Show Char where +- show = showCharImpl +- +-instance showString :: Show String where +- show = showStringImpl +- + instance showArray :: Show a => Show (Array a) where +- show = showArrayImpl show +- +-instance showProxy :: Show (Proxy a) where +- show _ = "Proxy" +- +-instance showVoid :: Show Void where +- show = absurd +- +-instance showRecord :: +- ( Nub rs rs +- , RL.RowToList rs ls +- , ShowRecordFields ls rs +- ) => +- Show (Record rs) where +- show record = "{" <> showRecordFields (Proxy :: Proxy ls) record <> "}" +- +--- | A class for records where all fields have `Show` instances, used to +--- | implement the `Show` instance for records. +-class ShowRecordFields :: RL.RowList Type -> Row Type -> Constraint +-class ShowRecordFields rowlist row where +- showRecordFields :: Proxy rowlist -> Record row -> String +- +-instance showRecordFieldsNil :: ShowRecordFields RL.Nil row where +- showRecordFields _ _ = "" +-else +-instance showRecordFieldsConsNil :: +- ( IsSymbol key +- , Show focus +- ) => +- ShowRecordFields (RL.Cons key focus RL.Nil) row where +- showRecordFields _ record = " " <> key <> ": " <> show focus <> " " +- where +- key = reflectSymbol (Proxy :: Proxy key) +- focus = unsafeGet key record :: focus +-else +-instance showRecordFieldsCons :: +- ( IsSymbol key +- , ShowRecordFields rowlistTail row +- , Show focus +- ) => +- ShowRecordFields (RL.Cons key focus rowlistTail) row where +- showRecordFields _ record = " " <> key <> ": " <> show focus <> "," <> tail +- where +- key = reflectSymbol (Proxy :: Proxy key) +- focus = unsafeGet key record :: focus +- tail = showRecordFields (Proxy :: Proxy rowlistTail) record +- +-foreign import showIntImpl :: Int -> String +-foreign import showNumberImpl :: Number -> String +-foreign import showCharImpl :: Char -> String +-foreign import showStringImpl :: String -> String +-foreign import showArrayImpl :: forall a. (a -> String) -> Array a -> String ++ show value = "[" <> showElements value 0 <> "]" ++ ++minInt :: Int ++minInt = intSub (intSub 0 2147483647) 1 ++ ++positiveInfinity :: Number ++positiveInfinity = numberDiv 1.0 0.0 ++ ++negativeInfinity :: Number ++negativeInfinity = numberDiv (numberNeg 1.0) 0.0 ++ ++showPositiveInt :: Int -> String ++showPositiveInt value = ++ if intLt value 10 then digit value ++ else showPositiveInt (intDiv value 10) <> digit (intMod value 10) ++ ++digit :: Int -> String ++digit value = bytesToString [intAdd 48 value] ++ ++-- | A non-negative `Number`. Integers below `1e21` print as decimal digits ++-- | plus `.0`. Smaller magnitudes print as a fixed expansion. The rest use ++-- | scientific form, which is what the official `Show Number` does past ++-- | those thresholds. ++showPositiveNumber :: Number -> String ++showPositiveNumber value = ++ if numberEq value 0.0 then "0.0" ++ else if numberGe value 1.0e21 then showScientific value ++ else if numberLt value 1.0e-6 then showScientific value ++ else if numberEq value (floorNumber value) then showIntegerNumber value <> ".0" ++ else showMixed value ++ ++showMixed :: Number -> String ++showMixed value = ++ let ++ whole = floorNumber value ++ fraction = numberSub value whole ++ wholeText = if numberEq whole 0.0 then "0" else showIntegerNumber whole ++ fractionText = digitsToString (trimZeros (collectFraction fraction 17 [])) 0 ++ in ++ if intEq (arrayLength (stringToBytes fractionText)) 0 then wholeText <> ".0" ++ else wholeText <> "." <> fractionText ++ ++showScientific :: Number -> String ++showScientific value = ++ let ++ exponent = powerOfTen value 0 ++ mantissa = scaleToUnit value exponent ++ in ++ showMantissa mantissa <> showExponent exponent ++ ++showMantissa :: Number -> String ++showMantissa value = ++ let ++ whole = numberToInt (floorNumber value) ++ fraction = numberSub value (intToNumber whole) ++ fractionText = digitsToString (trimZeros (collectFraction fraction 17 [])) 0 ++ in ++ if intEq (arrayLength (stringToBytes fractionText)) 0 then showPositiveInt whole ++ else showPositiveInt whole <> "." <> fractionText ++ ++showExponent :: Int -> String ++showExponent value = ++ if intLt value 0 then "e-" <> showPositiveInt (intNeg value) ++ else "e+" <> showPositiveInt value ++ ++powerOfTen :: Number -> Int -> Int ++powerOfTen value exponent = ++ if numberEq value 0.0 then exponent ++ else if numberGe value 10.0 then powerOfTen (numberDiv value 10.0) (intAdd exponent 1) ++ else if numberLt value 1.0 then powerOfTen (numberMul value 10.0) (intSub exponent 1) ++ else exponent ++ ++scaleToUnit :: Number -> Int -> Number ++scaleToUnit value exponent = ++ if intEq exponent 0 then value ++ else if intGt exponent 0 then scaleToUnit (numberDiv value 10.0) (intSub exponent 1) ++ else scaleToUnit (numberMul value 10.0) (intAdd exponent 1) ++ ++-- | Floor of a non-negative number. Groups of nine digits stay inside `Int`, ++-- | so a value past `2^31` still floors without a wider primitive. ++floorNumber :: Number -> Number ++floorNumber value = ++ if numberLt value 1000000000.0 then intToNumber (numberToInt value) ++ else ++ let ++ billions = floorNumber (numberDiv value 1000000000.0) ++ base = numberMul billions 1000000000.0 ++ low = numberToInt (numberSub value base) ++ in ++ numberAdd base (intToNumber low) ++ ++showIntegerNumber :: Number -> String ++showIntegerNumber value = ++ if numberLt value 1000000000.0 then showPositiveInt (numberToInt value) ++ else ++ let ++ billions = floorNumber (numberDiv value 1000000000.0) ++ base = numberMul billions 1000000000.0 ++ low = numberToInt (numberSub value base) ++ in ++ showIntegerNumber billions <> pad9 low ++ ++pad9 :: Int -> String ++pad9 value = ++ if intLt value 10 then "00000000" <> showPositiveInt value ++ else if intLt value 100 then "0000000" <> showPositiveInt value ++ else if intLt value 1000 then "000000" <> showPositiveInt value ++ else if intLt value 10000 then "00000" <> showPositiveInt value ++ else if intLt value 100000 then "0000" <> showPositiveInt value ++ else if intLt value 1000000 then "000" <> showPositiveInt value ++ else if intLt value 10000000 then "00" <> showPositiveInt value ++ else if intLt value 100000000 then "0" <> showPositiveInt value ++ else showPositiveInt value ++ ++collectFraction :: Number -> Int -> Array Int -> Array Int ++collectFraction fraction remaining digits = ++ if intEq remaining 0 then digits ++ else if numberEq fraction 0.0 then digits ++ else ++ let ++ scaled = numberMul fraction 10.0 ++ digitValue = numberToInt scaled ++ rest = numberSub scaled (intToNumber digitValue) ++ in ++ collectFraction rest (intSub remaining 1) (arrayAppend digits [digitValue]) ++ ++trimZeros :: Array Int -> Array Int ++trimZeros digits = ++ let ++ length = arrayLength digits ++ in ++ if intEq length 0 then digits ++ else if intEq (arrayIndex digits (intSub length 1)) 0 then trimZeros (takePrefix digits (intSub length 1)) ++ else digits ++ ++takePrefix :: Array Int -> Int -> Array Int ++takePrefix digits count = copyPrefix digits count 0 ++ ++copyPrefix :: Array Int -> Int -> Int -> Array Int ++copyPrefix digits count index = ++ if intGe index count then [] ++ else arrayAppend [arrayIndex digits index] (copyPrefix digits count (intAdd index 1)) ++ ++digitsToString :: Array Int -> Int -> String ++digitsToString digits index = ++ if intGe index (arrayLength digits) then "" ++ else digit (arrayIndex digits index) <> digitsToString digits (intAdd index 1) ++ ++showElements :: forall a. Show a => Array a -> Int -> String ++showElements values index = ++ if intGe index (arrayLength values) then "" ++ else if intEq index 0 then show (arrayIndex values index) <> showElements values (intAdd index 1) ++ else "," <> show (arrayIndex values index) <> showElements values (intAdd index 1) ++ ++-- | The body of a `Char` or `String` escape, without the surrounding quotes. ++-- | `ampersand` is true when a numeric escape must not swallow a following ++-- | digit, which is the official `\&` rule. ++escapeCode :: Int -> Boolean -> Boolean -> String ++escapeCode code ampersand inString = ++ if intEq code 7 then "\\a" ++ else if intEq code 8 then "\\b" ++ else if intEq code 12 then "\\f" ++ else if intEq code 10 then "\\n" ++ else if intEq code 13 then "\\r" ++ else if intEq code 9 then "\\t" ++ else if intEq code 11 then "\\v" ++ else if intLt code 32 then "\\" <> showPositiveInt code <> emptyAmpersand ampersand ++ else if intEq code 127 then "\\127" <> emptyAmpersand ampersand ++ else if intEq code 92 then "\\\\" ++ else if intEq code 34 then if inString then "\\\"" else utf8 code ++ else if intEq code 39 then if inString then utf8 code else "\\'" ++ else utf8 code ++ ++emptyAmpersand :: Boolean -> String ++emptyAmpersand needed = if needed then "\\&" else "" ++ ++escapeBytes :: Array Int -> Int -> String ++escapeBytes bytes index = ++ if intGe index (arrayLength bytes) then "" ++ else ++ let ++ decoded = decode bytes index ++ code = arrayIndex decoded 0 ++ next = arrayIndex decoded 1 ++ in ++ escapeCode code (nextIsDigit bytes next) true <> escapeBytes bytes next ++ ++nextIsDigit :: Array Int -> Int -> Boolean ++nextIsDigit bytes index = ++ if intGe index (arrayLength bytes) then false ++ else ++ let ++ code = arrayIndex (decode bytes index) 0 ++ in ++ if intLt code 48 then false else intLe code 57 ++ ++-- | One Unicode scalar and the index of the byte after it. The bytes are ++-- | well-formed UTF-8 because they came from `stringToBytes`. ++decode :: Array Int -> Int -> Array Int ++decode bytes index = ++ let ++ first = arrayIndex bytes index ++ in ++ if intLt first 128 then [first, intAdd index 1] ++ else if intLt first 224 then ++ [ intAdd (intShl (intAnd first 31) 6) (continuation bytes (intAdd index 1)) ++ , intAdd index 2 ++ ] ++ else if intLt first 240 then ++ [ intAdd (intAdd (intShl (intAnd first 15) 12) (intShl (continuation bytes (intAdd index 1)) 6)) (continuation bytes (intAdd index 2)) ++ , intAdd index 3 ++ ] ++ else ++ [ intAdd (intAdd (intAdd (intShl (intAnd first 7) 18) (intShl (continuation bytes (intAdd index 1)) 12)) (intShl (continuation bytes (intAdd index 2)) 6)) (continuation bytes (intAdd index 3)) ++ , intAdd index 4 ++ ] ++ ++continuation :: Array Int -> Int -> Int ++continuation bytes index = intAnd (arrayIndex bytes index) 63 ++ ++utf8 :: Int -> String ++utf8 code = ++ if intLt code 128 then bytesToString [code] ++ else if intLt code 2048 then bytesToString [intOr 192 (intShr code 6), intOr 128 (intAnd code 63)] ++ else if intLt code 65536 then bytesToString [intOr 224 (intShr code 12), intOr 128 (intAnd (intShr code 6) 63), intOr 128 (intAnd code 63)] ++ else bytesToString [intOr 240 (intShr code 18), intOr 128 (intAnd (intShr code 12) 63), intOr 128 (intAnd (intShr code 6) 63), intOr 128 (intAnd code 63)] +--- purescript-strings@v6.0.1/src/Data/String/CodePoints.purs ++++ stdlib/lib/Data/String/CodePoints.purs +@@ -88,10 +88,8 @@ + singleton :: CodePoint -> String + singleton = _singleton singletonFallback + +-foreign import _singleton +- :: (CodePoint -> String) +- -> CodePoint +- -> String ++_singleton :: (CodePoint -> String) -> CodePoint -> String ++_singleton a0 a1 = _singleton a0 a1 + + singletonFallback :: CodePoint -> String + singletonFallback (CodePoint cp) | cp <= 0xFFFF = fromCharCode cp +@@ -114,10 +112,8 @@ + fromCodePointArray :: Array CodePoint -> String + fromCodePointArray = _fromCodePointArray singletonFallback + +-foreign import _fromCodePointArray +- :: (CodePoint -> String) +- -> Array CodePoint +- -> String ++_fromCodePointArray :: (CodePoint -> String) -> Array CodePoint -> String ++_fromCodePointArray a0 a1 = _fromCodePointArray a0 a1 + + -- | Creates an array of code points from a string. Operates in space and time + -- | linear to the length of the string. +@@ -133,11 +129,8 @@ + toCodePointArray :: String -> Array CodePoint + toCodePointArray = _toCodePointArray toCodePointArrayFallback unsafeCodePointAt0 + +-foreign import _toCodePointArray +- :: (String -> Array CodePoint) +- -> (String -> CodePoint) +- -> String +- -> Array CodePoint ++_toCodePointArray :: (String -> Array CodePoint) -> (String -> CodePoint) -> String -> Array CodePoint ++_toCodePointArray a0 a1 a2 = _toCodePointArray a0 a1 a2 + + toCodePointArrayFallback :: String -> Array CodePoint + toCodePointArrayFallback s = unfoldr unconsButWithTuple s +@@ -163,14 +156,8 @@ + codePointAt 0 s = Just (unsafeCodePointAt0 s) + codePointAt n s = _codePointAt codePointAtFallback Just Nothing unsafeCodePointAt0 n s + +-foreign import _codePointAt +- :: (Int -> String -> Maybe CodePoint) +- -> (forall a. a -> Maybe a) +- -> (forall a. Maybe a) +- -> (String -> CodePoint) +- -> Int +- -> String +- -> Maybe CodePoint ++_codePointAt :: (Int -> String -> Maybe CodePoint) -> (forall a. a -> Maybe a) -> (forall a. Maybe a) -> (String -> CodePoint) -> Int -> String -> Maybe CodePoint ++_codePointAt a0 a1 a2 a3 a4 a5 = _codePointAt a0 a1 a2 a3 a4 a5 + + codePointAtFallback :: Int -> String -> Maybe CodePoint + codePointAtFallback n s = case uncons s of +@@ -227,12 +214,8 @@ + countPrefix :: (CodePoint -> Boolean) -> String -> Int + countPrefix = _countPrefix countFallback unsafeCodePointAt0 + +-foreign import _countPrefix +- :: ((CodePoint -> Boolean) -> String -> Int) +- -> (String -> CodePoint) +- -> (CodePoint -> Boolean) +- -> String +- -> Int ++_countPrefix :: ((CodePoint -> Boolean) -> String -> Int) -> (String -> CodePoint) -> (CodePoint -> Boolean) -> String -> Int ++_countPrefix a0 a1 a2 a3 = _countPrefix a0 a1 a2 a3 + + countFallback :: (CodePoint -> Boolean) -> String -> Int + countFallback p s = countTail p s 0 +@@ -328,7 +311,8 @@ + take :: Int -> String -> String + take = _take takeFallback + +-foreign import _take :: (Int -> String -> String) -> Int -> String -> String ++_take :: (Int -> String -> String) -> Int -> String -> String ++_take a0 a1 a2 = _take a0 a1 a2 + + takeFallback :: Int -> String -> String + takeFallback n _ | n < 1 = "" +@@ -418,10 +402,8 @@ + unsafeCodePointAt0 :: String -> CodePoint + unsafeCodePointAt0 = _unsafeCodePointAt0 unsafeCodePointAt0Fallback + +-foreign import _unsafeCodePointAt0 +- :: (String -> CodePoint) +- -> String +- -> CodePoint ++_unsafeCodePointAt0 :: (String -> CodePoint) -> String -> CodePoint ++_unsafeCodePointAt0 a0 a1 = _unsafeCodePointAt0 a0 a1 + + unsafeCodePointAt0Fallback :: String -> CodePoint + unsafeCodePointAt0Fallback s = +--- purescript-strings@v6.0.1/src/Data/String/CodeUnits.purs ++++ stdlib/lib/Data/String/CodeUnits.purs +@@ -80,21 +80,24 @@ + -- | singleton 'l' == "l" + -- | ``` + -- | +-foreign import singleton :: Char -> String ++singleton :: Char -> String ++singleton a0 = singleton a0 + + -- | Converts an array of characters into a string. + -- | + -- | ```purescript + -- | fromCharArray ['H', 'e', 'l', 'l', 'o'] == "Hello" + -- | ``` +-foreign import fromCharArray :: Array Char -> String ++fromCharArray :: Array Char -> String ++fromCharArray a0 = fromCharArray a0 + + -- | Converts the string into an array of characters. + -- | + -- | ```purescript + -- | toCharArray "Hello☺\n" == ['H','e','l','l','o','☺','\n'] + -- | ``` +-foreign import toCharArray :: String -> Array Char ++toCharArray :: String -> Array Char ++toCharArray a0 = toCharArray a0 + + -- | Returns the character at the given index, if the index is within bounds. + -- | +@@ -106,12 +109,8 @@ + charAt :: Int -> String -> Maybe Char + charAt = _charAt Just Nothing + +-foreign import _charAt +- :: (forall a. a -> Maybe a) +- -> (forall a. Maybe a) +- -> Int +- -> String +- -> Maybe Char ++_charAt :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Int -> String -> Maybe Char ++_charAt a0 a1 a2 a3 = _charAt a0 a1 a2 a3 + + -- | Converts the string to a character, if the length of the string is + -- | exactly `1`. +@@ -123,11 +122,8 @@ + toChar :: String -> Maybe Char + toChar = _toChar Just Nothing + +-foreign import _toChar +- :: (forall a. a -> Maybe a) +- -> (forall a. Maybe a) +- -> String +- -> Maybe Char ++_toChar :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> String -> Maybe Char ++_toChar a0 a1 a2 = _toChar a0 a1 a2 + + -- | Returns the first character and the rest of the string, + -- | if the string is not empty. +@@ -147,7 +143,8 @@ + -- | length "Hello World" == 11 + -- | ``` + -- | +-foreign import length :: String -> Int ++length :: String -> Int ++length a0 = length a0 + + -- | Returns the number of contiguous characters at the beginning + -- | of the string for which the predicate holds. +@@ -156,7 +153,8 @@ + -- | countPrefix (_ /= ' ') "Hello World" == 5 -- since length "Hello" == 5 + -- | ``` + -- | +-foreign import countPrefix :: (Char -> Boolean) -> String -> Int ++countPrefix :: (Char -> Boolean) -> String -> Int ++countPrefix a0 a1 = countPrefix a0 a1 + + -- | Returns the index of the first occurrence of the pattern in the + -- | given string. Returns `Nothing` if there is no match. +@@ -169,12 +167,8 @@ + indexOf :: Pattern -> String -> Maybe Int + indexOf = _indexOf Just Nothing + +-foreign import _indexOf +- :: (forall a. a -> Maybe a) +- -> (forall a. Maybe a) +- -> Pattern +- -> String +- -> Maybe Int ++_indexOf :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Pattern -> String -> Maybe Int ++_indexOf a0 a1 a2 a3 = _indexOf a0 a1 a2 a3 + + -- | Returns the index of the first occurrence of the pattern in the + -- | given string, starting at the specified index. Returns `Nothing` if there is +@@ -188,13 +182,8 @@ + indexOf' :: Pattern -> Int -> String -> Maybe Int + indexOf' = _indexOfStartingAt Just Nothing + +-foreign import _indexOfStartingAt +- :: (forall a. a -> Maybe a) +- -> (forall a. Maybe a) +- -> Pattern +- -> Int +- -> String +- -> Maybe Int ++_indexOfStartingAt :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Pattern -> Int -> String -> Maybe Int ++_indexOfStartingAt a0 a1 a2 a3 a4 = _indexOfStartingAt a0 a1 a2 a3 a4 + + -- | Returns the index of the last occurrence of the pattern in the + -- | given string. Returns `Nothing` if there is no match. +@@ -207,12 +196,8 @@ + lastIndexOf :: Pattern -> String -> Maybe Int + lastIndexOf = _lastIndexOf Just Nothing + +-foreign import _lastIndexOf +- :: (forall a. a -> Maybe a) +- -> (forall a. Maybe a) +- -> Pattern +- -> String +- -> Maybe Int ++_lastIndexOf :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Pattern -> String -> Maybe Int ++_lastIndexOf a0 a1 a2 a3 = _lastIndexOf a0 a1 a2 a3 + + -- | Returns the index of the last occurrence of the pattern in the + -- | given string, starting at the specified index and searching +@@ -235,13 +220,8 @@ + lastIndexOf' :: Pattern -> Int -> String -> Maybe Int + lastIndexOf' = _lastIndexOfStartingAt Just Nothing + +-foreign import _lastIndexOfStartingAt +- :: (forall a. a -> Maybe a) +- -> (forall a. Maybe a) +- -> Pattern +- -> Int +- -> String +- -> Maybe Int ++_lastIndexOfStartingAt :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Pattern -> Int -> String -> Maybe Int ++_lastIndexOfStartingAt a0 a1 a2 a3 a4 = _lastIndexOfStartingAt a0 a1 a2 a3 a4 + + -- | Returns the first `n` characters of the string. + -- | +@@ -249,7 +229,8 @@ + -- | take 5 "Hello World" == "Hello" + -- | ``` + -- | +-foreign import take :: Int -> String -> String ++take :: Int -> String -> String ++take a0 a1 = take a0 a1 + + -- | Returns the last `n` characters of the string. + -- | +@@ -276,7 +257,8 @@ + -- | drop 6 "Hello World" == "World" + -- | ``` + -- | +-foreign import drop :: Int -> String -> String ++drop :: Int -> String -> String ++drop a0 a1 = drop a0 a1 + + -- | Returns the string without the last `n` characters. + -- | +@@ -308,7 +290,8 @@ + -- | slice (-4) (-1) "purescript" == "rip" + -- | slice (-4) 3 "purescript" == "" + -- | ``` +-foreign import slice :: Int -> Int -> String -> String ++slice :: Int -> Int -> String -> String ++slice a0 a1 a2 = slice a0 a1 a2 + + -- | Splits a string into two substrings, where `before` contains the + -- | characters up to (but not including) the given index, and `after` contains +@@ -329,4 +312,5 @@ + -- | (splitAt i s).before <> (splitAt i s).after == s + -- | splitAt i s == {before: take i s, after: drop i s} + -- | ``` +-foreign import splitAt :: Int -> String -> { before :: String, after :: String } ++splitAt :: Int -> String -> { before :: String, after :: String } ++splitAt a0 a1 = splitAt a0 a1 +--- purescript-strings@v6.0.1/src/Data/String/Common.purs ++++ stdlib/lib/Data/String/Common.purs +@@ -34,27 +34,24 @@ + localeCompare :: String -> String -> Ordering + localeCompare = _localeCompare LT EQ GT + +-foreign import _localeCompare +- :: Ordering +- -> Ordering +- -> Ordering +- -> String +- -> String +- -> Ordering ++_localeCompare :: Ordering -> Ordering -> Ordering -> String -> String -> Ordering ++_localeCompare a0 a1 a2 a3 a4 = _localeCompare a0 a1 a2 a3 a4 + + -- | Replaces the first occurence of the pattern with the replacement string. + -- | + -- | ```purescript + -- | replace (Pattern "<=") (Replacement "≤") "a <= b <= c" == "a ≤ b <= c" + -- | ``` +-foreign import replace :: Pattern -> Replacement -> String -> String ++replace :: Pattern -> Replacement -> String -> String ++replace a0 a1 a2 = replace a0 a1 a2 + + -- | Replaces all occurences of the pattern with the replacement string. + -- | + -- | ```purescript + -- | replaceAll (Pattern "<=") (Replacement "≤") "a <= b <= c" == "a ≤ b ≤ c" + -- | ``` +-foreign import replaceAll :: Pattern -> Replacement -> String -> String ++replaceAll :: Pattern -> Replacement -> String -> String ++replaceAll a0 a1 a2 = replaceAll a0 a1 a2 + + -- | Returns the substrings of the second string separated along occurences + -- | of the first string. +@@ -62,21 +59,24 @@ + -- | ```purescript + -- | split (Pattern " ") "hello world" == ["hello", "world"] + -- | ``` +-foreign import split :: Pattern -> String -> Array String ++split :: Pattern -> String -> Array String ++split a0 a1 = split a0 a1 + + -- | Returns the argument converted to lowercase. + -- | + -- | ```purescript + -- | toLower "hElLo" == "hello" + -- | ``` +-foreign import toLower :: String -> String ++toLower :: String -> String ++toLower a0 = toLower a0 + + -- | Returns the argument converted to uppercase. + -- | + -- | ```purescript + -- | toUpper "Hello" == "HELLO" + -- | ``` +-foreign import toUpper :: String -> String ++toUpper :: String -> String ++toUpper a0 = toUpper a0 + + -- | Removes whitespace from the beginning and end of a string, including + -- | [whitespace characters](http://www.ecma-international.org/ecma-262/5.1/#sec-7.2) +@@ -85,7 +85,8 @@ + -- | ```purescript + -- | trim " Hello \n World\n\t " == "Hello \n World" + -- | ``` +-foreign import trim :: String -> String ++trim :: String -> String ++trim a0 = trim a0 + + -- | Joins the strings in the array together, inserting the first argument + -- | as separator between them. +@@ -93,4 +94,5 @@ + -- | ```purescript + -- | joinWith ", " ["apple", "banana", "orange"] == "apple, banana, orange" + -- | ``` +-foreign import joinWith :: String -> Array String -> String ++joinWith :: String -> Array String -> String ++joinWith a0 a1 = joinWith a0 a1 +--- purescript-strings@v6.0.1/src/Data/String/Regex.purs ++++ stdlib/lib/Data/String/Regex.purs +@@ -28,17 +28,14 @@ + -- | Wraps Javascript `RegExp` objects. + foreign import data Regex :: Type + +-foreign import showRegexImpl :: Regex -> String ++showRegexImpl :: Regex -> String ++showRegexImpl a0 = showRegexImpl a0 + + instance showRegex :: Show Regex where + show = showRegexImpl + +-foreign import regexImpl +- :: (String -> Either String Regex) +- -> (Regex -> Either String Regex) +- -> String +- -> String +- -> Either String Regex ++regexImpl :: (String -> Either String Regex) -> (Regex -> Either String Regex) -> String -> String -> Either String Regex ++regexImpl a0 a1 a2 a3 = regexImpl a0 a1 a2 a3 + + -- | Constructs a `Regex` from a pattern string and flags. Fails with + -- | `Left error` if the pattern contains a syntax error. +@@ -46,14 +43,16 @@ + regex s f = regexImpl Left Right s $ renderFlags f + + -- | Returns the pattern string used to construct the given `Regex`. +-foreign import source :: Regex -> String ++source :: Regex -> String ++source a0 = source a0 + + -- | Returns the `RegexFlags` used to construct the given `Regex`. + flags :: Regex -> RegexFlags + flags = RegexFlags <<< flagsImpl + + -- | Returns the `RegexFlags` inner record used to construct the given `Regex`. +-foreign import flagsImpl :: Regex -> RegexFlagsRec ++flagsImpl :: Regex -> RegexFlagsRec ++flagsImpl a0 = flagsImpl a0 + + -- | Returns the string representation of the given `RegexFlags`. + renderFlags :: RegexFlags -> String +@@ -79,14 +78,11 @@ + -- | Returns `true` if the `Regex` matches the string. In contrast to + -- | `RegExp.prototype.test()` in JavaScript, `test` does not affect + -- | the `lastIndex` property of the Regex. +-foreign import test :: Regex -> String -> Boolean ++test :: Regex -> String -> Boolean ++test a0 a1 = test a0 a1 + +-foreign import _match +- :: (forall r. r -> Maybe r) +- -> (forall r. Maybe r) +- -> Regex +- -> String +- -> Maybe (NonEmptyArray (Maybe String)) ++_match :: (forall r. r -> Maybe r) -> (forall r. Maybe r) -> Regex -> String -> Maybe (NonEmptyArray (Maybe String)) ++_match a0 a1 a2 a3 = _match a0 a1 a2 a3 + + -- | Matches the string against the `Regex` and returns an array of matches + -- | if there were any. Each match has type `Maybe String`, where `Nothing` +@@ -98,15 +94,11 @@ + -- | Replaces occurrences of the `Regex` with the first string. The replacement + -- | string can include special replacement patterns escaped with `"$"`. + -- | See [reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace). +-foreign import replace :: Regex -> String -> String -> String ++replace :: Regex -> String -> String -> String ++replace a0 a1 a2 = replace a0 a1 a2 + +-foreign import _replaceBy +- :: (forall r. r -> Maybe r) +- -> (forall r. Maybe r) +- -> Regex +- -> (String -> Array (Maybe String) -> String) +- -> String +- -> String ++_replaceBy :: (forall r. r -> Maybe r) -> (forall r. Maybe r) -> Regex -> (String -> Array (Maybe String) -> String) -> String -> String ++_replaceBy a0 a1 a2 a3 a4 = _replaceBy a0 a1 a2 a3 a4 + + -- | Transforms occurrences of the `Regex` using a function of the matched + -- | substring and a list of captured substrings of type `Maybe String`, +@@ -115,12 +107,8 @@ + replace' :: Regex -> (String -> Array (Maybe String) -> String) -> String -> String + replace' = _replaceBy Just Nothing + +-foreign import _search +- :: (forall r. r -> Maybe r) +- -> (forall r. Maybe r) +- -> Regex +- -> String +- -> Maybe Int ++_search :: (forall r. r -> Maybe r) -> (forall r. Maybe r) -> Regex -> String -> Maybe Int ++_search a0 a1 a2 a3 = _search a0 a1 a2 a3 + + -- | Returns `Just` the index of the first match of the `Regex` in the string, + -- | or `Nothing` if there is no match. +@@ -128,4 +116,5 @@ + search = _search Just Nothing + + -- | Split the string into an array of substrings along occurrences of the `Regex`. +-foreign import split :: Regex -> String -> Array String ++split :: Regex -> String -> Array String ++split a0 a1 = split a0 a1 +--- purescript-strings@v6.0.1/src/Data/String/Unsafe.purs ++++ stdlib/lib/Data/String/Unsafe.purs +@@ -7,9 +7,11 @@ + -- | Returns the character at the given index. + -- | + -- | **Unsafe:** throws runtime exception if the index is out of bounds. +-foreign import charAt :: Int -> String -> Char ++charAt :: Int -> String -> Char ++charAt a0 a1 = charAt a0 a1 + + -- | Converts a string of length `1` to a character. + -- | + -- | **Unsafe:** throws runtime exception if length is not `1`. +-foreign import char :: String -> Char ++char :: String -> Char ++char a0 = char a0 +--- purescript-prelude@v6.0.1/src/Data/Symbol.purs ++++ stdlib/lib/Data/Symbol.purs +@@ -11,7 +11,8 @@ + reflectSymbol :: Proxy sym -> String + + -- local definition for use in `reifySymbol` +-foreign import unsafeCoerce :: forall a b. a -> b ++unsafeCoerce :: forall a b. a -> b ++unsafeCoerce a0 = unsafeCoerce a0 + + reifySymbol :: forall r. String -> (forall sym. IsSymbol sym => Proxy sym -> r) -> r + reifySymbol s f = coerce f { reflectSymbol: \_ -> s } Proxy +--- purescript-foldable-traversable@v6.0.0/src/Data/Traversable.purs ++++ stdlib/lib/Data/Traversable.purs +@@ -103,14 +103,8 @@ + traverse = traverseArrayImpl apply map pure + sequence = sequenceDefault + +-foreign import traverseArrayImpl +- :: forall m a b +- . (forall x y. m (x -> y) -> m x -> m y) +- -> (forall x y. (x -> y) -> m x -> m y) +- -> (forall x. x -> m x) +- -> (a -> m b) +- -> Array a +- -> m (Array b) ++traverseArrayImpl :: forall m a b . (forall x y. m (x -> y) -> m x -> m y) -> (forall x y. (x -> y) -> m x -> m y) -> (forall x. x -> m x) -> (a -> m b) -> Array a -> m (Array b) ++traverseArrayImpl a0 a1 a2 a3 a4 = traverseArrayImpl a0 a1 a2 a3 a4 + + instance traversableMaybe :: Traversable Maybe where + traverse _ Nothing = pure Nothing +--- purescript-tuples@v7.0.0/src/Data/Tuple.purs ++++ stdlib/lib/Data/Tuple.purs +@@ -1,135 +1,47 @@ +--- | A data type and functions for working with ordered pairs. +-module Data.Tuple where ++-- | A strict product of two values. Native tuple syntax remains a closed ++-- | record, while this library type provides the constructor used by the core ++-- | libraries. WIT tuples continue to map to closed records as specified by ++-- | DEC-13. ++module Data.Tuple ++ ( Tuple(..) ++ , fst ++ , snd ++ , curry ++ , uncurry ++ , swap ++ ) where + +-import Prelude ++import Data.Eq (class Eq) ++import Data.Functor (class Functor) ++import Data.Ord (class Ord) ++import Data.Show (class Show, show) ++import Data.Semigroup ((<>)) + +-import Control.Comonad (class Comonad) +-import Control.Extend (class Extend) +-import Control.Lazy (class Lazy, defer) +-import Data.Eq (class Eq1) +-import Data.Functor.Invariant (class Invariant, imapF) +-import Data.Generic.Rep (class Generic) +-import Data.HeytingAlgebra (implies, ff, tt) +-import Data.Ord (class Ord1) +- +--- | A simple product type for wrapping a pair of component values. + data Tuple a b = Tuple a b + +--- | Allows `Tuple`s to be rendered as a string with `show` whenever there are +--- | `Show` instances for both component types. +-instance showTuple :: (Show a, Show b) => Show (Tuple a b) where +- show (Tuple a b) = "(Tuple " <> show a <> " " <> show b <> ")" +- +--- | Allows `Tuple`s to be checked for equality with `==` and `/=` whenever +--- | there are `Eq` instances for both component types. + derive instance eqTuple :: (Eq a, Eq b) => Eq (Tuple a b) +- +-derive instance eq1Tuple :: Eq a => Eq1 (Tuple a) +- +--- | Allows `Tuple`s to be compared with `compare`, `>`, `>=`, `<` and `<=` +--- | whenever there are `Ord` instances for both component types. To obtain +--- | the result, the `fst`s are `compare`d, and if they are `EQ`ual, the +--- | `snd`s are `compare`d. + derive instance ordTuple :: (Ord a, Ord b) => Ord (Tuple a b) +- +-derive instance ord1Tuple :: Ord a => Ord1 (Tuple a) +- +-instance boundedTuple :: (Bounded a, Bounded b) => Bounded (Tuple a b) where +- top = Tuple top top +- bottom = Tuple bottom bottom +- +-instance semigroupoidTuple :: Semigroupoid Tuple where +- compose (Tuple _ c) (Tuple a _) = Tuple a c +- +--- | The `Semigroup` instance enables use of the associative operator `<>` on +--- | `Tuple`s whenever there are `Semigroup` instances for the component +--- | types. The `<>` operator is applied pairwise, so: +--- | ```purescript +--- | (Tuple a1 b1) <> (Tuple a2 b2) = Tuple (a1 <> a2) (b1 <> b2) +--- | ``` +-instance semigroupTuple :: (Semigroup a, Semigroup b) => Semigroup (Tuple a b) where +- append (Tuple a1 b1) (Tuple a2 b2) = Tuple (a1 <> a2) (b1 <> b2) +- +-instance monoidTuple :: (Monoid a, Monoid b) => Monoid (Tuple a b) where +- mempty = Tuple mempty mempty +- +-instance semiringTuple :: (Semiring a, Semiring b) => Semiring (Tuple a b) where +- add (Tuple x1 y1) (Tuple x2 y2) = Tuple (add x1 x2) (add y1 y2) +- one = Tuple one one +- mul (Tuple x1 y1) (Tuple x2 y2) = Tuple (mul x1 x2) (mul y1 y2) +- zero = Tuple zero zero +- +-instance ringTuple :: (Ring a, Ring b) => Ring (Tuple a b) where +- sub (Tuple x1 y1) (Tuple x2 y2) = Tuple (sub x1 x2) (sub y1 y2) +- +-instance commutativeRingTuple :: (CommutativeRing a, CommutativeRing b) => CommutativeRing (Tuple a b) +- +-instance heytingAlgebraTuple :: (HeytingAlgebra a, HeytingAlgebra b) => HeytingAlgebra (Tuple a b) where +- tt = Tuple tt tt +- ff = Tuple ff ff +- implies (Tuple x1 y1) (Tuple x2 y2) = Tuple (x1 `implies` x2) (y1 `implies` y2) +- conj (Tuple x1 y1) (Tuple x2 y2) = Tuple (conj x1 x2) (conj y1 y2) +- disj (Tuple x1 y1) (Tuple x2 y2) = Tuple (disj x1 x2) (disj y1 y2) +- not (Tuple x y) = Tuple (not x) (not y) +- +-instance booleanAlgebraTuple :: (BooleanAlgebra a, BooleanAlgebra b) => BooleanAlgebra (Tuple a b) +- +--- | The `Functor` instance allows functions to transform the contents of a +--- | `Tuple` with the `<$>` operator, applying the function to the second +--- | component, so: +--- | ```purescript +--- | f <$> (Tuple x y) = Tuple x (f y) +--- | ```` + derive instance functorTuple :: Functor (Tuple a) + +-derive instance genericTuple :: Generic (Tuple a b) _ ++instance showTuple :: (Show a, Show b) => Show (Tuple a b) where ++ show (Tuple first second) = "(Tuple " <> show first <> " " <> show second <> ")" + +-instance invariantTuple :: Invariant (Tuple a) where +- imap = imapF ++-- | The first component. `fst (Tuple x y)` is `x`. ++fst :: forall a b. Tuple a b -> a ++fst (Tuple first _) = first + +--- | The `Apply` instance allows functions to transform the contents of a +--- | `Tuple` with the `<*>` operator whenever there is a `Semigroup` instance +--- | for the `fst` component, so: +--- | ```purescript +--- | (Tuple a1 f) <*> (Tuple a2 x) == Tuple (a1 <> a2) (f x) +--- | ``` +-instance applyTuple :: (Semigroup a) => Apply (Tuple a) where +- apply (Tuple a1 f) (Tuple a2 x) = Tuple (a1 <> a2) (f x) ++-- | The second component. `snd (Tuple x y)` is `y`. ++snd :: forall a b. Tuple a b -> b ++snd (Tuple _ second) = second + +-instance applicativeTuple :: (Monoid a) => Applicative (Tuple a) where +- pure = Tuple mempty ++-- | Turns a function of a pair into a function of two arguments. ++curry :: forall a b c. (Tuple a b -> c) -> a -> b -> c ++curry f x y = f (Tuple x y) + +-instance bindTuple :: (Semigroup a) => Bind (Tuple a) where +- bind (Tuple a1 b) f = case f b of +- Tuple a2 c -> Tuple (a1 <> a2) c ++-- | Turns a function of two arguments into a function of a pair. ++uncurry :: forall a b c. (a -> b -> c) -> Tuple a b -> c ++uncurry f (Tuple first second) = f first second + +-instance monadTuple :: (Monoid a) => Monad (Tuple a) +- +-instance extendTuple :: Extend (Tuple a) where +- extend f t@(Tuple a _) = Tuple a (f t) +- +-instance comonadTuple :: Comonad (Tuple a) where +- extract = snd +- +-instance lazyTuple :: (Lazy a, Lazy b) => Lazy (Tuple a b) where +- defer f = Tuple (defer $ \_ -> fst (f unit)) (defer $ \_ -> snd (f unit)) +- +--- | Returns the first component of a tuple. +-fst :: forall a b. Tuple a b -> a +-fst (Tuple a _) = a +- +--- | Returns the second component of a tuple. +-snd :: forall a b. Tuple a b -> b +-snd (Tuple _ b) = b +- +--- | Turn a function that expects a tuple into a function of two arguments. +-curry :: forall a b c. (Tuple a b -> c) -> a -> b -> c +-curry f a b = f (Tuple a b) +- +--- | Turn a function of two arguments into a function that expects a tuple. +-uncurry :: forall a b c. (a -> b -> c) -> Tuple a b -> c +-uncurry f (Tuple a b) = f a b +- +--- | Exchange the first and second components of a tuple. ++-- | Exchanges the two components. + swap :: forall a b. Tuple a b -> Tuple b a +-swap (Tuple a b) = Tuple b a ++swap (Tuple first second) = Tuple second first +--- purescript-unfoldable@v6.0.0/src/Data/Unfoldable.purs ++++ stdlib/lib/Data/Unfoldable.purs +@@ -44,15 +44,8 @@ + instance unfoldableMaybe :: Unfoldable Maybe where + unfoldr f b = fst <$> f b + +-foreign import unfoldrArrayImpl +- :: forall a b +- . (forall x. Maybe x -> Boolean) +- -> (forall x. Maybe x -> x) +- -> (forall x y. Tuple x y -> x) +- -> (forall x y. Tuple x y -> y) +- -> (b -> Maybe (Tuple a b)) +- -> b +- -> Array a ++unfoldrArrayImpl :: forall a b . (forall x. Maybe x -> Boolean) -> (forall x. Maybe x -> x) -> (forall x y. Tuple x y -> x) -> (forall x y. Tuple x y -> y) -> (b -> Maybe (Tuple a b)) -> b -> Array a ++unfoldrArrayImpl a0 a1 a2 a3 a4 a5 = unfoldrArrayImpl a0 a1 a2 a3 a4 a5 + + -- | Replicate a value some natural number of times. + -- | For example: +--- purescript-unfoldable@v6.0.0/src/Data/Unfoldable1.purs ++++ stdlib/lib/Data/Unfoldable1.purs +@@ -45,15 +45,8 @@ + instance unfoldable1Maybe :: Unfoldable1 Maybe where + unfoldr1 f b = Just (fst (f b)) + +-foreign import unfoldr1ArrayImpl +- :: forall a b +- . (forall x. Maybe x -> Boolean) +- -> (forall x. Maybe x -> x) +- -> (forall x y. Tuple x y -> x) +- -> (forall x y. Tuple x y -> y) +- -> (b -> Tuple a (Maybe b)) +- -> b +- -> Array a ++unfoldr1ArrayImpl :: forall a b . (forall x. Maybe x -> Boolean) -> (forall x. Maybe x -> x) -> (forall x y. Tuple x y -> x) -> (forall x y. Tuple x y -> y) -> (b -> Tuple a (Maybe b)) -> b -> Array a ++unfoldr1ArrayImpl a0 a1 a2 a3 a4 a5 = unfoldr1ArrayImpl a0 a1 a2 a3 a4 a5 + + -- | Replicate a value `n` times. At least one value will be produced, so values + -- | `n` less than 1 will be treated as 1. +--- purescript-prelude@v6.0.1/src/Data/Unit.purs ++++ stdlib/lib/Data/Unit.purs +@@ -1,14 +1,5 @@ +-module Data.Unit where +- +--- | The `Unit` type has a single inhabitant, called `unit`. It represents +--- | values with no computational content. +--- | +--- | `Unit` is often used, wrapped in a monadic type constructor, as the +--- | return type of a computation where only the _effects_ are important. +--- | +--- | When returning a value of type `Unit` from an FFI function, it is +--- | recommended to use `undefined`, or not return a value at all. +-foreign import data Unit :: Type +- +--- | `unit` is the sole inhabitant of the `Unit` type. +-foreign import unit :: Unit ++-- | `Unit` is a compiler builtin, the same type as an unqualified ++-- | `Unit`. This module re-exports that builtin and the `unit` ++-- | primitive so `import Data.Unit` matches the official library ++-- | without declaring a second unit type. ++module Data.Unit (Unit, unit) where +--- purescript-console@v6.0.0/src/Effect/Console.purs ++++ stdlib/lib/Effect/Console.purs +@@ -1,68 +1,23 @@ +-module Effect.Console where ++-- | The corpus's console surface, a thin binding over the platform layer ++-- | `WASI.Console` ++-- | ([DEC-11](../../../decision/DEC-11-primitive-ffi-stdlib-wrappers.md)). ++-- | ++-- | Nothing here chooses a stream or performs a write: `log` and `error` are the ++-- | platform functions under their corpus names, and `warn` is the platform's. ++-- | That keeps one place that decides where output goes, rather than a ++-- | wrapper that could drift from it. ++-- | ++-- | `logShow` adds no I/O of its own: it is `log` of `Data.Show.show`, so the ++-- | rendering is the library's `Show` and the destination is still the one ++-- | `WASI.Console.log` decides. It is a wrapper like `log`, not a second ++-- | stringifier. ++module Effect.Console (log, warn, error, logShow) where + +-import Effect (Effect) ++import Prelude ++import WASI.Console (error, log, warn) ++import Data.Show (class Show, show) + +-import Data.Show (class Show, show) +-import Data.Unit (Unit) +- +--- | Write a message to the console. +-foreign import log +- :: String +- -> Effect Unit +- +--- | Write a value to the console, using its `Show` instance to produce a +--- | `String`. ++-- | Writes the `Show` rendering of a value. `log` already writes the newline, ++-- | so this is `log` composed with the library's `show`. + logShow :: forall a. Show a => a -> Effect Unit +-logShow a = log (show a) +- +--- | Write an warning to the console. +-foreign import warn +- :: String +- -> Effect Unit +- +--- | Write an warning value to the console, using its `Show` instance to produce +--- | a `String`. +-warnShow :: forall a. Show a => a -> Effect Unit +-warnShow a = warn (show a) +- +--- | Write an error to the console. +-foreign import error +- :: String +- -> Effect Unit +- +--- | Write an error value to the console, using its `Show` instance to produce a +--- | `String`. +-errorShow :: forall a. Show a => a -> Effect Unit +-errorShow a = error (show a) +- +--- | Write an info message to the console. +-foreign import info +- :: String +- -> Effect Unit +- +--- | Write an info value to the console, using its `Show` instance to produce a +--- | `String`. +-infoShow :: forall a. Show a => a -> Effect Unit +-infoShow a = info (show a) +- +--- | Write an debug message to the console. +-foreign import debug +- :: String +- -> Effect Unit +- +--- | Write an debug value to the console, using its `Show` instance to produce a +--- | `String`. +-debugShow :: forall a. Show a => a -> Effect Unit +-debugShow a = debug (show a) +- +--- | Start a named timer. +-foreign import time :: String -> Effect Unit +- +--- | Print the time since a named timer started in milliseconds. +-foreign import timeLog :: String -> Effect Unit +- +--- | Stop a named timer and print time since it started in milliseconds. +-foreign import timeEnd :: String -> Effect Unit +- +--- | Clears the console +-foreign import clear :: Effect Unit ++logShow value = log (show value) +--- purescript-refs@v6.0.0/src/Effect/Ref.purs ++++ stdlib/lib/Effect/Ref.purs +@@ -41,24 +41,28 @@ + type role Ref representational + + -- | Create a new mutable reference containing the specified value. +-foreign import _new :: forall s. s -> Effect (Ref s) ++_new :: forall s. s -> Effect (Ref s) ++_new a0 = _new a0 + + new :: forall s. s -> Effect (Ref s) + new = _new + + -- | Create a new mutable reference containing a value that can refer to the + -- | `Ref` being created. +-foreign import newWithSelf :: forall s. (Ref s -> s) -> Effect (Ref s) ++newWithSelf :: forall s. (Ref s -> s) -> Effect (Ref s) ++newWithSelf a0 = newWithSelf a0 + + -- | Read the current value of a mutable reference. +-foreign import read :: forall s. Ref s -> Effect s ++read :: forall s. Ref s -> Effect s ++read a0 = read a0 + + -- | Update the value of a mutable reference by applying a function + -- | to the current value. + modify' :: forall s b. (s -> { state :: s, value :: b }) -> Ref s -> Effect b + modify' = modifyImpl + +-foreign import modifyImpl :: forall s b. (s -> { state :: s, value :: b }) -> Ref s -> Effect b ++modifyImpl :: forall s b. (s -> { state :: s, value :: b }) -> Ref s -> Effect b ++modifyImpl a0 a1 = modifyImpl a0 a1 + + -- | Update the value of a mutable reference by applying a function + -- | to the current value. The updated value is returned. +@@ -70,4 +74,5 @@ + modify_ f s = void $ modify f s + + -- | Update the value of a mutable reference to the specified value. +-foreign import write :: forall s. s -> Ref s -> Effect Unit ++write :: forall s. s -> Ref s -> Effect Unit ++write a0 a1 = write a0 a1 +--- purescript-effect@v4.0.0/src/Effect.purs ++++ stdlib/lib/Effect.purs +@@ -1,72 +1,29 @@ +--- | This module provides the `Effect` type, which is used to represent +--- | _native_ effects. The `Effect` type provides a typed API for effectful +--- | computations, while at the same time generating efficient JavaScript. ++-- | The `Effect` monad: the corpus-facing name for the abstract effect ++-- | interface this compiler already has. ++-- | ++-- | The operations themselves are anchored in `Prelude` rather than here, and ++-- | that is deliberate rather than an oversight. Two pieces of the compiler key ++-- | on that module by name: `check_run_effect_scope` resolves the trusted ++-- | `Prelude.runEffect` value, and `psrs_core::effect::operations` synthesizes ++-- | `pure`, `bind`, and `run` from the externals declared there. Moving the ++-- | foreign imports into this module would move the entry point with them. ++-- | ++-- | So this module is the public surface the corpus imports, and `Prelude` ++-- | remains the owner of the primitive interface. The dependency runs one way: ++-- | `Effect` imports `Prelude`, never the reverse. + module Effect + ( Effect +- , untilE, whileE, forE, foreachE ++ , pure ++ , bind ++ , discard ++ , map ++ , apply ++ , untilE + ) where + +-import Prelude ++import Prelude (Effect, apply, bind, discard, map, pure) + +-import Control.Apply (lift2) +- +--- | A native effect. The type parameter denotes the return type of running the +--- | effect, that is, an `Effect Int` is a possibly-effectful computation which +--- | eventually produces a value of the type `Int` when it finishes. +-foreign import data Effect :: Type -> Type +- +-type role Effect representational +- +-instance functorEffect :: Functor Effect where +- map = liftA1 +- +-instance applyEffect :: Apply Effect where +- apply = ap +- +-instance applicativeEffect :: Applicative Effect where +- pure = pureE +- +-foreign import pureE :: forall a. a -> Effect a +- +-instance bindEffect :: Bind Effect where +- bind = bindE +- +-foreign import bindE :: forall a b. Effect a -> (a -> Effect b) -> Effect b +- +-instance monadEffect :: Monad Effect +- +--- | The `Semigroup` instance for effects allows you to run two effects, one +--- | after the other, and then combine their results using the result type's +--- | `Semigroup` instance. +-instance semigroupEffect :: Semigroup a => Semigroup (Effect a) where +- append = lift2 append +- +--- | If you have a `Monoid a` instance, then `mempty :: Effect a` is defined as +--- | `pure mempty`. +-instance monoidEffect :: Monoid a => Monoid (Effect a) where +- mempty = pureE mempty +- +--- | Loop until a condition becomes `true`. +--- | +--- | `untilE b` is an effectful computation which repeatedly runs the effectful +--- | computation `b`, until its return value is `true`. +-foreign import untilE :: Effect Boolean -> Effect Unit +- +--- | Loop while a condition is `true`. +--- | +--- | `whileE b m` is effectful computation which runs the effectful computation +--- | `b`. If its result is `true`, it runs the effectful computation `m` and +--- | loops. If not, the computation ends. +-foreign import whileE :: forall a. Effect Boolean -> Effect a -> Effect Unit +- +--- | Loop over a consecutive collection of numbers. +--- | +--- | `forE lo hi f` runs the computation returned by the function `f` for each +--- | of the inputs between `lo` (inclusive) and `hi` (exclusive). +-foreign import forE :: Int -> Int -> (Int -> Effect Unit) -> Effect Unit +- +--- | Loop over an array of values. +--- | +--- | `foreachE xs f` runs the computation returned by the function `f` for each +--- | of the inputs `xs`. +-foreign import foreachE :: forall a. Array a -> (a -> Effect Unit) -> Effect Unit ++-- | Repeats an effect until it returns `true`. ++untilE :: Effect Boolean -> Effect Unit ++untilE action = bind action \done -> ++ if done then pure unit else untilE action +--- purescript-partial@v4.0.0/src/Partial/Unsafe.purs ++++ stdlib/lib/Partial/Unsafe.purs +@@ -13,7 +13,8 @@ + -- either a dependency or reimplementing it here. + -- Rather than doing that, we'll use a type signature + -- of `a -> b` instead. +-foreign import _unsafePartial :: forall a b. a -> b ++_unsafePartial :: forall a b. a -> b ++_unsafePartial a0 = _unsafePartial a0 + + -- | Discharge a partiality constraint, unsafely. + unsafePartial :: forall a. (Partial => a) -> a +--- purescript-partial@v4.0.0/src/Partial.purs ++++ stdlib/lib/Partial.purs +@@ -12,4 +12,5 @@ + crashWith :: forall a. Partial => String -> a + crashWith = _crashWith + +-foreign import _crashWith :: forall a. String -> a ++_crashWith :: forall a. String -> a ++_crashWith a0 = _crashWith a0 +--- purescript-prelude@v6.0.1/src/Prelude.purs ++++ stdlib/lib/Prelude.purs +@@ -1,17 +1,20 @@ +--- | `Prelude` is a module that re-exports many other foundational modules from the `purescript-prelude` library +--- | (e.g. the Monad type class hierarchy, the Monoid type classes, Eq, Ord, etc.). ++-- | The primitive surface the rest of the library and the corpus build on. + -- | +--- | Typically, this module will be imported in most other libraries and projects as an open import. ++-- | The class hierarchy is the official `purescript-prelude` v6.0.1 re-export ++-- | list. `Effect` stays in this module: `check_run_effect_scope` resolves ++-- | `Prelude.runEffect`, and `psrs_core::effect::operations` synthesizes ++-- | `effectPure`, `effectBind`, `runEffect`, and `trap` from the `psrs:effect` ++-- | bindings declared here. The class methods `pure` and `bind` are the ++-- | official `Applicative` and `Bind` methods; the `Effect` instances call ++-- | those bindings, so creating an action still does not run it. + -- | +--- | ``` +--- | module MyModule where +--- | +--- | import Prelude -- open import +--- | +--- | import Data.Maybe (Maybe(..)) -- closed import +--- | ``` ++-- | `unit` is not declared here. `Unit` is a builtin, re-exported through ++-- | `Data.Unit`, and the one `Unit` value is `Intrinsic::Unit`. + module Prelude +- ( module Control.Applicative ++ ( Effect ++ , runEffect ++ , trap ++ , module Control.Applicative + , module Control.Apply + , module Control.Bind + , module Control.Category +@@ -68,3 +71,34 @@ + import Data.Show (class Show, show) + import Data.Unit (Unit, unit) + import Data.Void (Void, absurd) ++ ++foreign import data Effect :: Type -> Type ++ ++-- | Builds an `Effect` that returns `value`. Lowering replaces this binding; ++-- | the `Applicative` instance is what user code calls `pure`. ++foreign import "psrs:effect#pure" effectPure :: forall a. a -> Effect a ++ ++-- | Sequences two effects. The `Bind` instance is what user code calls `bind`. ++foreign import "psrs:effect#bind" effectBind :: forall a b. Effect a -> (a -> Effect b) -> Effect b ++ ++foreign import "psrs:effect#run" runEffect :: forall a. Effect a -> a ++ ++-- | The effect that escapes instead of returning. An uncaught failure on this ++-- | target is a guest trap, so this is the one operation whose result never ++-- | exists; it is how a library reports an assertion that did not hold. ++foreign import "psrs:effect#trap" trap :: Effect Unit ++ ++instance functorEffect :: Functor Effect where ++ map f action = bind action (\value -> pure (f value)) ++ ++instance applyEffect :: Apply Effect where ++ apply wrapped action = ++ bind wrapped (\function -> bind action (\value -> pure (function value))) ++ ++instance applicativeEffect :: Applicative Effect where ++ pure value = effectPure value ++ ++instance bindEffect :: Bind Effect where ++ bind action next = effectBind action next ++ ++instance monadEffect :: Monad Effect +--- purescript-prelude@v6.0.1/src/Record/Unsafe.purs ++++ stdlib/lib/Record/Unsafe.purs +@@ -7,21 +7,25 @@ + module Record.Unsafe where + + -- | Checks if a record has a key, using a string for the key. +-foreign import unsafeHas :: forall r1. String -> Record r1 -> Boolean ++unsafeHas :: forall r1. String -> Record r1 -> Boolean ++unsafeHas a0 a1 = unsafeHas a0 a1 + + -- | Unsafely gets a value from a record, using a string for the key. + -- | + -- | If the key does not exist this will cause a runtime error elsewhere. +-foreign import unsafeGet :: forall r a. String -> Record r -> a ++unsafeGet :: forall r a. String -> Record r -> a ++unsafeGet a0 a1 = unsafeGet a0 a1 + + -- | Unsafely sets a value on a record, using a string for the key. + -- | + -- | The output record's row is unspecified so can be coerced to any row. If the + -- | output type is incorrect it will cause a runtime error elsewhere. +-foreign import unsafeSet :: forall r1 r2 a. String -> a -> Record r1 -> Record r2 ++unsafeSet :: forall r1 r2 a. String -> a -> Record r1 -> Record r2 ++unsafeSet a0 a1 a2 = unsafeSet a0 a1 a2 + + -- | Unsafely removes a value on a record, using a string for the key. + -- | + -- | The output record's row is unspecified so can be coerced to any row. If the + -- | output type is incorrect it will cause a runtime error elsewhere. +-foreign import unsafeDelete :: forall r1 r2. String -> Record r1 -> Record r2 ++unsafeDelete :: forall r1 r2. String -> Record r1 -> Record r2 ++unsafeDelete a0 a1 = unsafeDelete a0 a1 +--- purescript-assert@v6.0.0/src/Test/Assert.purs ++++ stdlib/lib/Test/Assert.purs +@@ -1,134 +1,70 @@ +-module Test.Assert +- ( assert +- , assert' +- , assertEqual +- , assertEqual' +- , assertFalse +- , assertFalse' +- , assertThrows +- , assertThrows' +- , assertTrue +- , assertTrue' +- ) where ++-- | The corpus's assertion surface: the `Test.Assert` module the `passing` ++-- | suite imports. ++-- | ++-- | A failed assertion must be visible to whatever runs the program, and this ++-- | target's only such signal is a guest trap — a non-zero exit code is a ++-- | recorded result, not a failure. So the failure path writes the message to ++-- | standard error and then escapes through `Prelude.trap` ++-- | ([DEC-11](../../../decision/DEC-11-primitive-ffi-stdlib-wrappers.md): the ++-- | wrapper owns the corpus name, the primitive stays in `Prelude`). ++-- | ++-- | **Deliberately absent**, with the reason recorded rather than approximated: ++-- | ++-- | - `assertEqual` and `assertEqual'` compare with `Eq` and print with `Show`. ++-- | Both classes are declared (`Data.Eq`, `Data.Show`), so the surface itself ++-- | is writable — the official signature is what this compiler cannot yet ++-- | elaborate. The corpus calls the record form ++-- | (`assertEqual' "label" { expected: e, actual: a }`), whose official type is ++-- | `forall a. Eq a => Show a => String -> { actual :: a, expected :: a } -> ++-- | Effect Unit`. A constraint whose quantified variable appears inside a ++-- | record type is elaborated with the *record* as the constraint's argument, ++-- | so `Eq a` is wanted for `{ actual :: a, expected :: a }` and the ++-- | declaration is rejected with `NoInstanceFound`. The same signature with a ++-- | type synonym for the record fails identically, so it is constraint ++-- | elaboration rather than the record syntax. Approximating the signature ++-- | would change the official API the corpus calls, so the functions stay out ++-- | until that is fixed. #137 carries the minimal reproduction and the probes ++-- | that separate this defect from record syntax. ++-- | - `assertThrows` and `assertThrows'` need to observe that evaluating an ++-- | argument failed. A trap is not observable from inside the guest without ++-- | the Wasm exceptions proposal, which is outside the target profile ++-- | ([DEC-05](../../../decision/DEC-05-wasmtime-feature-set.md)), so there is ++-- | no honest implementation to write yet. ++module Test.Assert (assert, assert', assertTrue, assertFalse) where + + import Prelude +- + import Effect (Effect) + import Effect.Console (error) + +--- | Throws a runtime exception with message "Assertion failed" when the boolean +--- | value is false. ++-- | Escapes when the boolean is false. The message is written to standard ++-- | error first, so the trap carries the diagnostic with it. ++assert' :: String -> Boolean -> Effect Unit ++assert' message condition = ++ if condition ++ then pure unit ++ else abortWith message ++ ++-- | Escapes with the default message when the boolean is false. + assert :: Boolean -> Effect Unit + assert = assert' "Assertion failed" + +--- | Throws a runtime exception with the specified message when the boolean +--- | value is false. +-assert' :: String -> Boolean -> Effect Unit +-assert' = assertImpl ++-- | Escapes unless the value is `true`, naming both values. ++assertTrue :: Boolean -> Effect Unit ++assertTrue actual = ++ if actual ++ then pure unit ++ else abortWith "Assertion failed: Expected: true\nActual: false" + +-foreign import assertImpl +- :: String +- -> Boolean +- -> Effect Unit ++-- | Escapes unless the value is `false`, naming both values. ++assertFalse :: Boolean -> Effect Unit ++assertFalse actual = ++ if actual ++ then abortWith "Assertion failed: Expected: false\nActual: true" ++ else pure unit + +--- | Throws a runtime exception with message "Assertion failed: An error should +--- | have been thrown", unless the argument throws an exception when evaluated. +--- | +--- | This function is specifically for testing unsafe pure code; for example, +--- | to make sure that an exception is thrown if a precondition is not +--- | satisfied. Functions which use `Effect a` can be +--- | tested with `catchException` instead. +-assertThrows :: forall a. (Unit -> a) -> Effect Unit +-assertThrows = +- assertThrows' "Assertion failed: An error should have been thrown" +- +--- | Throws a runtime exception with the specified message, unless the argument +--- | throws an exception when evaluated. +--- | +--- | This function is specifically for testing unsafe pure code; for example, +--- | to make sure that an exception is thrown if a precondition is not +--- | satisfied. Functions which use `Effect a` can be +--- | tested with `catchException` instead. +-assertThrows' +- :: forall a +- . String +- -> (Unit -> a) +- -> Effect Unit +-assertThrows' msg fn = assert' msg =<< checkThrows fn +- +-foreign import checkThrows +- :: forall a +- . (Unit -> a) +- -> Effect Boolean +- +--- | Compares the `expected` and `actual` values for equality and +--- | throws a runtime exception when the values are not equal. +--- | +--- | The message indicates the expected value and the actual value. +-assertEqual +- :: forall a +- . Eq a +- => Show a +- => { actual :: a, expected :: a } +- -> Effect Unit +-assertEqual = assertEqual' "" +- +--- | Compares the `expected` and `actual` values for equality and throws a +--- | runtime exception with the specified message when the values are not equal. +--- | +--- | The message also indicates the expected value and the actual value. +-assertEqual' +- :: forall a +- . Eq a +- => Show a +- => String +- -> { actual :: a, expected :: a } +- -> Effect Unit +-assertEqual' userMessage {actual, expected} = do +- unless result $ error message +- assert' message result +- where +- message = (if userMessage == "" then "" else userMessage <> "\n") +- <> "Expected: " <> show expected +- <> "\nActual: " <> show actual +- result = actual == expected +- +--- | Throws a runtime exception when the value is `false`. +--- | +--- | The message indicates the expected value (`true`) +--- | and the actual value (`false`). +-assertTrue +- :: Boolean +- -> Effect Unit +-assertTrue actual = assertEqual { actual, expected: true } +- +--- | Throws a runtime exception with the specified message when the value is +--- | `false`. +--- | +--- | The message also indicates the expected value (`true`) +--- | and the actual value (`false`). +-assertTrue' +- :: String +- -> Boolean +- -> Effect Unit +-assertTrue' message actual = assertEqual' message { actual, expected: true } +- +--- | Throws a runtime exception when the value is `true`. +--- | +--- | The message indicates the expected value (`false`) +--- | and the actual value (`true`). +-assertFalse +- :: Boolean +- -> Effect Unit +-assertFalse actual = assertEqual { actual, expected: false } +- +--- | Throws a runtime exception with the specified message when the value is +--- | `true`. +--- | +--- | The message also indicates the expected value (`false`) +--- | and the actual value (`true`). +-assertFalse' +- :: String +- -> Boolean +- -> Effect Unit +-assertFalse' message actual = assertEqual' message { actual, expected: false } ++-- | The escape itself. Kept private: a caller reports a failure by choosing the ++-- | message, not by reaching for the trap. ++abortWith :: String -> Effect Unit ++abortWith message = do ++ _ <- error message ++ trap +--- purescript-unsafe-coerce@v6.0.0/src/Unsafe/Coerce.purs ++++ stdlib/lib/Unsafe/Coerce.purs +@@ -24,4 +24,5 @@ + -- | `unsafeCoerce` can now be accomplished via `coerce` from + -- | `purescript-safe-coerce`. See that library's documentation for more + -- | context. +-foreign import unsafeCoerce :: forall a b. a -> b ++unsafeCoerce :: forall a b. a -> b ++unsafeCoerce a0 = unsafeCoerce a0 +--- purescript-effect@v4.0.0/src/Effect/Class.purs ++++ /dev/null +@@ -1,19 +0,0 @@ +-module Effect.Class where +- +-import Control.Category (identity) +-import Control.Monad (class Monad) +-import Effect (Effect) +- +--- | The `MonadEffect` class captures those monads which support native effects. +--- | +--- | Instances are provided for `Effect` itself, and the standard monad +--- | transformers. +--- | +--- | `liftEffect` can be used in any appropriate monad transformer stack to lift an +--- | action of type `Effect a` into the monad. +--- | +-class Monad m <= MonadEffect m where +- liftEffect :: forall a. Effect a -> m a +- +-instance monadEffectEffect :: MonadEffect Effect where +- liftEffect = identity +--- purescript-console@v6.0.0/src/Effect/Class/Console.purs ++++ /dev/null +@@ -1,49 +0,0 @@ +-module Effect.Class.Console where +- +-import Data.Function ((<<<)) +-import Data.Show (class Show) +-import Data.Unit (Unit) +-import Effect.Class (class MonadEffect, liftEffect) +-import Effect.Console as EffConsole +- +-log :: forall m. MonadEffect m => String -> m Unit +-log = liftEffect <<< EffConsole.log +- +-logShow :: forall m a. MonadEffect m => Show a => a -> m Unit +-logShow = liftEffect <<< EffConsole.logShow +- +-warn :: forall m. MonadEffect m => String -> m Unit +-warn = liftEffect <<< EffConsole.warn +- +-warnShow :: forall m a. MonadEffect m => Show a => a -> m Unit +-warnShow = liftEffect <<< EffConsole.warnShow +- +-error :: forall m. MonadEffect m => String -> m Unit +-error = liftEffect <<< EffConsole.error +- +-errorShow :: forall m a. MonadEffect m => Show a => a -> m Unit +-errorShow = liftEffect <<< EffConsole.errorShow +- +-info :: forall m. MonadEffect m => String -> m Unit +-info = liftEffect <<< EffConsole.info +- +-infoShow :: forall m a. MonadEffect m => Show a => a -> m Unit +-infoShow = liftEffect <<< EffConsole.infoShow +- +-debug :: forall m. MonadEffect m => String -> m Unit +-debug = liftEffect <<< EffConsole.debug +- +-debugShow :: forall m a. MonadEffect m => Show a => a -> m Unit +-debugShow = liftEffect <<< EffConsole.debugShow +- +-time :: forall m. MonadEffect m => String -> m Unit +-time = liftEffect <<< EffConsole.time +- +-timeLog :: forall m. MonadEffect m => String -> m Unit +-timeLog = liftEffect <<< EffConsole.timeLog +- +-timeEnd :: forall m. MonadEffect m => String -> m Unit +-timeEnd = liftEffect <<< EffConsole.timeEnd +- +-clear :: forall m. MonadEffect m => m Unit +-clear = liftEffect EffConsole.clear +--- purescript-effect@v4.0.0/src/Effect/Uncurried.purs ++++ /dev/null +@@ -1,286 +0,0 @@ +--- | This module defines types for effectful uncurried functions, as well as +--- | functions for converting back and forth between them. +--- | +--- | This makes it possible to give a PureScript type to JavaScript functions +--- | such as this one: +--- | +--- | ```javascript +--- | function logMessage(level, message) { +--- | console.log(level + ": " + message); +--- | } +--- | ``` +--- | +--- | In particular, note that `logMessage` performs effects immediately after +--- | receiving all of its parameters, so giving it the type `Data.Function.Fn2 +--- | String String Unit`, while convenient, would effectively be a lie. +--- | +--- | One way to handle this would be to convert the function into the normal +--- | PureScript form (namely, a curried function returning an Effect action), +--- | and performing the marshalling in JavaScript, in the FFI module, like this: +--- | +--- | ```purescript +--- | -- In the PureScript file: +--- | foreign import logMessage :: String -> String -> Effect Unit +--- | ``` +--- | +--- | ```javascript +--- | // In the FFI file: +--- | exports.logMessage = function(level) { +--- | return function(message) { +--- | return function() { +--- | logMessage(level, message); +--- | }; +--- | }; +--- | }; +--- | ``` +--- | +--- | This method, unfortunately, turns out to be both tiresome and error-prone. +--- | This module offers an alternative solution. By providing you with: +--- | +--- | * the ability to give the real `logMessage` function a PureScript type, +--- | and +--- | * functions for converting between this form and the normal PureScript +--- | form, +--- | +--- | the FFI boilerplate is no longer needed. The previous example becomes: +--- | +--- | ```purescript +--- | -- In the PureScript file: +--- | foreign import logMessageImpl :: EffectFn2 String String Unit +--- | ``` +--- | +--- | ```javascript +--- | // In the FFI file: +--- | exports.logMessageImpl = logMessage +--- | ``` +--- | +--- | You can then use `runEffectFn2` to provide a nicer version: +--- | +--- | ```purescript +--- | logMessage :: String -> String -> Effect Unit +--- | logMessage = runEffectFn2 logMessageImpl +--- | ``` +--- | +--- | (note that this has the same type as the original `logMessage`). +--- | +--- | Effectively, we have reduced the risk of errors by moving as much code into +--- | PureScript as possible, so that we can leverage the type system. Hopefully, +--- | this is a little less tiresome too. +--- | +--- | Here's a slightly more advanced example. Here, because we are using +--- | callbacks, we need to use `mkEffectFn{N}` as well. +--- | +--- | Suppose our `logMessage` changes so that it sometimes sends details of the +--- | message to some external server, and in those cases, we want the resulting +--- | `HttpResponse` (for whatever reason). +--- | +--- | ```javascript +--- | function logMessage(level, message, callback) { +--- | console.log(level + ": " + message); +--- | if (level > LogLevel.WARN) { +--- | LogAggregatorService.post("/logs", { +--- | level: level, +--- | message: message +--- | }, callback); +--- | } else { +--- | callback(null); +--- | } +--- | } +--- | ``` +--- | +--- | The import then looks like this: +--- | ```purescript +--- | foreign import logMessageImpl +--- | EffectFn3 +--- | String +--- | String +--- | (EffectFn1 (Nullable HttpResponse) Unit) +--- | Unit +--- | ``` +--- | +--- | And, as before, the FFI file is extremely simple: +--- | +--- | ```javascript +--- | exports.logMessageImpl = logMessage +--- | ``` +--- | +--- | Finally, we use `runEffectFn{N}` and `mkEffectFn{N}` for a more comfortable +--- | PureScript version: +--- | +--- | ```purescript +--- | logMessage :: +--- | String -> +--- | String -> +--- | (Nullable HttpResponse -> Effect Unit) -> +--- | Effect Unit +--- | logMessage level message callback = +--- | runEffectFn3 logMessageImpl level message (mkEffectFn1 callback) +--- | ``` +--- | +--- | The general naming scheme for functions and types in this module is as +--- | follows: +--- | +--- | * `EffectFn{N}` means, an uncurried function which accepts N arguments and +--- | performs some effects. The first N arguments are the actual function's +--- | argument. The last type argument is the return type. +--- | * `runEffectFn{N}` takes an `EffectFn` of N arguments, and converts it into +--- | the normal PureScript form: a curried function which returns an Effect +--- | action. +--- | * `mkEffectFn{N}` is the inverse of `runEffectFn{N}`. It can be useful for +--- | callbacks. +--- | +- +-module Effect.Uncurried where +- +-import Data.Monoid (class Monoid, class Semigroup, mempty, (<>)) +-import Effect (Effect) +- +-foreign import data EffectFn1 :: Type -> Type -> Type +- +-type role EffectFn1 representational representational +- +-foreign import data EffectFn2 :: Type -> Type -> Type -> Type +- +-type role EffectFn2 representational representational representational +- +-foreign import data EffectFn3 :: Type -> Type -> Type -> Type -> Type +- +-type role EffectFn3 representational representational representational representational +- +-foreign import data EffectFn4 :: Type -> Type -> Type -> Type -> Type -> Type +- +-type role EffectFn4 representational representational representational representational representational +- +-foreign import data EffectFn5 :: Type -> Type -> Type -> Type -> Type -> Type -> Type +- +-type role EffectFn5 representational representational representational representational representational representational +- +-foreign import data EffectFn6 :: Type -> Type -> Type -> Type -> Type -> Type -> Type -> Type +- +-type role EffectFn6 representational representational representational representational representational representational representational +- +-foreign import data EffectFn7 :: Type -> Type -> Type -> Type -> Type -> Type -> Type -> Type -> Type +- +-type role EffectFn7 representational representational representational representational representational representational representational representational +- +-foreign import data EffectFn8 :: Type -> Type -> Type -> Type -> Type -> Type -> Type -> Type -> Type -> Type +- +-type role EffectFn8 representational representational representational representational representational representational representational representational representational +- +-foreign import data EffectFn9 :: Type -> Type -> Type -> Type -> Type -> Type -> Type -> Type -> Type -> Type -> Type +- +-type role EffectFn9 representational representational representational representational representational representational representational representational representational representational +- +-foreign import data EffectFn10 :: Type -> Type -> Type -> Type -> Type -> Type -> Type -> Type -> Type -> Type -> Type -> Type +- +-type role EffectFn10 representational representational representational representational representational representational representational representational representational representational representational +- +-foreign import mkEffectFn1 :: forall a r. +- (a -> Effect r) -> EffectFn1 a r +-foreign import mkEffectFn2 :: forall a b r. +- (a -> b -> Effect r) -> EffectFn2 a b r +-foreign import mkEffectFn3 :: forall a b c r. +- (a -> b -> c -> Effect r) -> EffectFn3 a b c r +-foreign import mkEffectFn4 :: forall a b c d r. +- (a -> b -> c -> d -> Effect r) -> EffectFn4 a b c d r +-foreign import mkEffectFn5 :: forall a b c d e r. +- (a -> b -> c -> d -> e -> Effect r) -> EffectFn5 a b c d e r +-foreign import mkEffectFn6 :: forall a b c d e f r. +- (a -> b -> c -> d -> e -> f -> Effect r) -> EffectFn6 a b c d e f r +-foreign import mkEffectFn7 :: forall a b c d e f g r. +- (a -> b -> c -> d -> e -> f -> g -> Effect r) -> EffectFn7 a b c d e f g r +-foreign import mkEffectFn8 :: forall a b c d e f g h r. +- (a -> b -> c -> d -> e -> f -> g -> h -> Effect r) -> EffectFn8 a b c d e f g h r +-foreign import mkEffectFn9 :: forall a b c d e f g h i r. +- (a -> b -> c -> d -> e -> f -> g -> h -> i -> Effect r) -> EffectFn9 a b c d e f g h i r +-foreign import mkEffectFn10 :: forall a b c d e f g h i j r. +- (a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> Effect r) -> EffectFn10 a b c d e f g h i j r +- +-foreign import runEffectFn1 :: forall a r. +- EffectFn1 a r -> a -> Effect r +-foreign import runEffectFn2 :: forall a b r. +- EffectFn2 a b r -> a -> b -> Effect r +-foreign import runEffectFn3 :: forall a b c r. +- EffectFn3 a b c r -> a -> b -> c -> Effect r +-foreign import runEffectFn4 :: forall a b c d r. +- EffectFn4 a b c d r -> a -> b -> c -> d -> Effect r +-foreign import runEffectFn5 :: forall a b c d e r. +- EffectFn5 a b c d e r -> a -> b -> c -> d -> e -> Effect r +-foreign import runEffectFn6 :: forall a b c d e f r. +- EffectFn6 a b c d e f r -> a -> b -> c -> d -> e -> f -> Effect r +-foreign import runEffectFn7 :: forall a b c d e f g r. +- EffectFn7 a b c d e f g r -> a -> b -> c -> d -> e -> f -> g -> Effect r +-foreign import runEffectFn8 :: forall a b c d e f g h r. +- EffectFn8 a b c d e f g h r -> a -> b -> c -> d -> e -> f -> g -> h -> Effect r +-foreign import runEffectFn9 :: forall a b c d e f g h i r. +- EffectFn9 a b c d e f g h i r -> a -> b -> c -> d -> e -> f -> g -> h -> i -> Effect r +-foreign import runEffectFn10 :: forall a b c d e f g h i j r. +- EffectFn10 a b c d e f g h i j r -> a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> Effect r +- +--- The reason these are written eta-expanded instead of as: +--- ``` +--- append f1 f2 = mkEffectFnN $ runEffectFnN f1 <> runEffectFnN f2 +--- ``` +--- is to help the compiler recognize that it can emit uncurried +--- JS functions (which are more efficient), when an appended +--- EffectFn is applied to all its arguments +- +-instance semigroupEffectFn1 :: Semigroup r => Semigroup (EffectFn1 a r) where +- append f1 f2 = mkEffectFn1 \a -> runEffectFn1 f1 a <> runEffectFn1 f2 a +- +-instance semigroupEffectFn2 :: Semigroup r => Semigroup (EffectFn2 a b r) where +- append f1 f2 = mkEffectFn2 \a b -> runEffectFn2 f1 a b <> runEffectFn2 f2 a b +- +-instance semigroupEffectFn3 :: Semigroup r => Semigroup (EffectFn3 a b c r) where +- append f1 f2 = mkEffectFn3 \a b c -> runEffectFn3 f1 a b c <> runEffectFn3 f2 a b c +- +-instance semigroupEffectFn4 :: Semigroup r => Semigroup (EffectFn4 a b c d r) where +- append f1 f2 = mkEffectFn4 \a b c d -> runEffectFn4 f1 a b c d <> runEffectFn4 f2 a b c d +- +-instance semigroupEffectFn5 :: Semigroup r => Semigroup (EffectFn5 a b c d e r) where +- append f1 f2 = mkEffectFn5 \a b c d e -> runEffectFn5 f1 a b c d e <> runEffectFn5 f2 a b c d e +- +-instance semigroupEffectFn6 :: Semigroup r => Semigroup (EffectFn6 a b c d e f r) where +- append f1 f2 = mkEffectFn6 \a b c d e f -> runEffectFn6 f1 a b c d e f <> runEffectFn6 f2 a b c d e f +- +-instance semigroupEffectFn7 :: Semigroup r => Semigroup (EffectFn7 a b c d e f g r) where +- append f1 f2 = mkEffectFn7 \a b c d e f g -> runEffectFn7 f1 a b c d e f g <> runEffectFn7 f2 a b c d e f g +- +-instance semigroupEffectFn8 :: Semigroup r => Semigroup (EffectFn8 a b c d e f g h r) where +- append f1 f2 = mkEffectFn8 \a b c d e f g h -> runEffectFn8 f1 a b c d e f g h <> runEffectFn8 f2 a b c d e f g h +- +-instance semigroupEffectFn9 :: Semigroup r => Semigroup (EffectFn9 a b c d e f g h i r) where +- append f1 f2 = mkEffectFn9 \a b c d e f g h i -> runEffectFn9 f1 a b c d e f g h i <> runEffectFn9 f2 a b c d e f g h i +- +-instance semigroupEffectFn10 :: Semigroup r => Semigroup (EffectFn10 a b c d e f g h i j r) where +- append f1 f2 = mkEffectFn10 \a b c d e f g h i j -> runEffectFn10 f1 a b c d e f g h i j <> runEffectFn10 f2 a b c d e f g h i j +- +-instance monoidEffectFn1 :: Monoid r => Monoid (EffectFn1 a r) where +- mempty = mkEffectFn1 \_ -> mempty +- +-instance monoidEffectFn2 :: Monoid r => Monoid (EffectFn2 a b r) where +- mempty = mkEffectFn2 \_ _ -> mempty +- +-instance monoidEffectFn3 :: Monoid r => Monoid (EffectFn3 a b c r) where +- mempty = mkEffectFn3 \_ _ _ -> mempty +- +-instance monoidEffectFn4 :: Monoid r => Monoid (EffectFn4 a b c d r) where +- mempty = mkEffectFn4 \_ _ _ _ -> mempty +- +-instance monoidEffectFn5 :: Monoid r => Monoid (EffectFn5 a b c d e r) where +- mempty = mkEffectFn5 \_ _ _ _ _ -> mempty +- +-instance monoidEffectFn6 :: Monoid r => Monoid (EffectFn6 a b c d e f r) where +- mempty = mkEffectFn6 \_ _ _ _ _ _ -> mempty +- +-instance monoidEffectFn7 :: Monoid r => Monoid (EffectFn7 a b c d e f g r) where +- mempty = mkEffectFn7 \_ _ _ _ _ _ _ -> mempty +- +-instance monoidEffectFn8 :: Monoid r => Monoid (EffectFn8 a b c d e f g h r) where +- mempty = mkEffectFn8 \_ _ _ _ _ _ _ _ -> mempty +- +-instance monoidEffectFn9 :: Monoid r => Monoid (EffectFn9 a b c d e f g h i r) where +- mempty = mkEffectFn9 \_ _ _ _ _ _ _ _ _ -> mempty +- +-instance monoidEffectFn10 :: Monoid r => Monoid (EffectFn10 a b c d e f g h i j r) where +- mempty = mkEffectFn10 \_ _ _ _ _ _ _ _ _ _ -> mempty +--- purescript-effect@v4.0.0/src/Effect/Unsafe.purs ++++ /dev/null +@@ -1,8 +0,0 @@ +-module Effect.Unsafe where +- +-import Effect (Effect) +- +--- | Run an effectful computation. +--- | +--- | *Note*: use of this function can result in arbitrary side-effects. +-foreign import unsafePerformEffect :: forall a. Effect a -> a diff --git a/docs/implementation/stdlib/vendor-audit-2026-10-06/report.md b/docs/implementation/stdlib/vendor-audit-2026-10-06/report.md new file mode 100644 index 00000000..b7447fa5 --- /dev/null +++ b/docs/implementation/stdlib/vendor-audit-2026-10-06/report.md @@ -0,0 +1,291 @@ +# Vendored standard-library source audit + +## Result and policy + +The current vendored library does **not** satisfy the policy that differences +from official PureScript libraries must be justified by the Wasm/WASI target. +There are 200 exact self-recursive placeholder definitions in 30 modules, +removed official APIs and instances, and changes to ordinary PureScript +functions made to accommodate compiler limitations. These are defects, not +accepted target adaptations. + +This audit inspects revision `67369ba` on `stdlib/vendor-core-libraries` with a +clean starting worktree. Library sources were not changed. The audit adds only +this report, its evidence, and a repeatable inventory tool. No scoreboard was +run or updated. + +The successful `/tmp/psrs-stdlib-all.purs` diagnosis establishes acceptance of +these modified sources, with `main = 0`. It does not establish acceptance of +unchanged official sources or execution correctness of the exported APIs. +Unused declarations can disappear before backend layout and execution. + +## Baselines and coverage + +The original vendoring script `/tmp/vendor_stdlib.py` names `/tmp/ps-pkgs` and +`/tmp/purescript-prelude` as inputs. Its `render_foreign` operation deliberately +replaces an unsupported value foreign import with a self-recursive equation. +Its `EXACT` list also rewrites `void`, `voidRight`, `voidLeft`, and `fold`. +Commit `0118695ffffd71200ece889cf863086955c03a64` explicitly records that value +foreign imports become intrinsics or diverging stubs. The script was inspected +without running its source-writing entry point. + +All 41 upstream checkouts are clean, at exact version tags, and have +`https://github.com/purescript/...` origin URLs. Their tags, commits, paths, +and source SHA-256 hashes are captured in [inventory.json](inventory.json). +The compiler support manifest at +`/Users/biu/Projects/purescript/tests/support/bower.json` uses dependency ranges, +not a lockfile. These tags identify the available vendoring baselines; they do +not prove a uniquely pinned dependency set for compiler version 0.15.16. +The original 38 checkouts are supplemented by official effect v4.0.0, +console v6.0.0, and assert v6.0.0, downloaded after explicit user approval. +These versions satisfy the compiler support manifest's dependency ranges; +they are explicit comparison baselines, not a recovered original lockfile. +Four upstream modules from the supplemental packages are absent from the +vendored directory and are listed below. + +| Comparison of current `.purs` modules | Count | +| --- | ---: | +| Byte-identical to the available official baseline | 149 | +| Different only in final line termination | 3 | +| Source changes requiring review | 50 | +| Wasm/WASI platform additions without an official counterpart | 9 | +| Official baseline unavailable | 0 | +| Total current vendored modules | 211 | + +The 202 compared modules come from prelude v6.0.1 and 40 other tagged packages. +The complete per-module table is [modules.md](modules.md); the unfiltered patch +is [official-vs-vendored.diff](official-vs-vendored.diff). Newline-only changes +are in Data.HeytingAlgebra.Generic, Data.Ring.Generic, and Data.Semiring.Generic. + +The initially missing baselines are now verified against both clean tagged git +checkouts and independently downloaded tagged archives. The official commits +are effect `a192ddb923027d426d6ea3d8deb030c9aa7c7dda`, console +`3b83d7b792d03872afeea5e62b4f686ab0f09842`, and assert +`27c0edb57d2ee497eb5fab664f5601c35b613eda`. All current non-WASI modules have an +official comparison baseline; no baseline remains unavailable. + +## Confirmed violations + +### Foreign declarations replaced by nontermination + +Across the compared present modules, 267 upstream value foreign declarations +have changed: 200 have exact self-recursive replacements, 40 have nonrecursive +replacement declarations, and 27 declarations have no local replacement +signature. The last category includes values now re-exported from another +module or supplied as compiler primitives; it does not mean that all 27 public +values are absent. A +nonrecursive replacement is only a syntactic classification, not a correctness +judgment. All 200 detected placeholders correspond to original foreign values. +Their names and vendored line numbers are in the inventory. The count excludes +other possible incorrect implementations or forms of nontermination. + +For example, Data.Int replaces the official `foreign import toNumber` with +`toNumber a0 = toNumber a0`. Data.Array replaces a foreign `rangeImpl` with +`rangeImpl = rangeImpl`. Similar replacements affect ST operations, reference +mutation, uncurried functions, array construction and traversal, numeric +functions, regular expressions, string operations, partiality, and reflection. +A declaration being unreachable or supplied through a compiler interface does +not make its on-disk source replacement faithful. + +| Module containing placeholders | Definitions | +| --- | ---: | +| Control.Extend | 1 | +| Control.Monad.ST.Internal | 11 | +| Control.Monad.ST.Uncurried | 20 | +| Data.Array.NonEmpty.Internal | 3 | +| Data.Array.ST.Partial | 2 | +| Data.Array.ST | 17 | +| Data.Array | 24 | +| Data.Enum | 2 | +| Data.Foldable | 2 | +| Data.Function.Uncurried | 20 | +| Data.FunctorWithIndex | 1 | +| Data.Int | 7 | +| Data.Lazy | 2 | +| Data.Number.Format | 4 | +| Data.Number | 25 | +| Data.Reflectable | 1 | +| Data.String.CodePoints | 7 | +| Data.String.CodeUnits | 15 | +| Data.String.Common | 8 | +| Data.String.Regex | 10 | +| Data.String.Unsafe | 2 | +| Data.Symbol | 1 | +| Data.Traversable | 1 | +| Data.Unfoldable | 1 | +| Data.Unfoldable1 | 1 | +| Effect.Ref | 5 | +| Partial.Unsafe | 1 | +| Partial | 1 | +| Record.Unsafe | 4 | +| Unsafe.Coerce | 1 | + +The loader excludes compiler-provided Safe.Coerce and Unsafe.Coerce source +modules, so the Unsafe.Coerce placeholder is not evidence that every ordinary +use of the compiler primitive diverges. Data.Symbol retains source ownership; +its local `unsafeCoerce` placeholder is a separate declaration from the +canonical Unsafe.Coerce compiler interface. Static IsSymbol dictionary +synthesis does not establish runtime reification through this local body. + +### Removed pure APIs and instances + +**Data.Tuple:** upstream v7.0.0 declares 24 instances; the vendored module keeps +four. It removes Eq1, Ord1, Bounded, Semigroupoid, Semigroup, Monoid, Semiring, +Ring, CommutativeRing, HeytingAlgebra, BooleanAlgebra, Generic, Invariant, Apply, +Applicative, Bind, Monad, Extend, Comonad, and Lazy instances. The ADT is already +`data Tuple a b = Tuple a b` in both sources. Distinguishing that ADT from a WIT +record tuple provides no justification for removing its ordinary instances. + +**Data.Show:** the vendored module removes the exported ShowRecordFields class +and showRecordFields method, the Show instances for Proxy, Void, and records, +and all three ShowRecordFields instances. These are pure API removals, not +UTF-8 storage adaptations. Its header explicitly acknowledges that Number +rendering can differ from official correctly rounded formatting. A Wasm +implementation of numeric/string formatting needs behavior evidence; target +storage does not justify silently narrowing the class surface or approximating +unrelated numeric behavior. + +**Test.Assert:** the exact v6.0.0 diff confirms removal of six public functions: +assertEqual, assertEqual', assertTrue', assertFalse', assertThrows, and +assertThrows'. The first four are ordinary PureScript functions with no target +implementation requirement of their own. The current source documents omission +of the equality functions because of a compiler constraint-elaboration defect; +that rationale violates the policy. Replacing JavaScript assertion exceptions +with a Wasm trap, and limitations on observing a Wasm trap inside the guest, +are target-related concerns requiring a declared contract. They do not justify +removing the comparison and message-taking Boolean assertions. + +**Effect:** the exact v4.0.0 diff confirms removal of whileE, forE, and foreachE, +plus the pure semigroupEffect and monoidEffect instances. The ordinary Effect +Functor/Apply/Applicative/Bind/Monad instances and type ownership were moved to +Prelude, and the source role declaration was removed. The target state-token +protocol can justify implementation and ownership adaptations, but not deletion +of the two pure instances or omission of the standard loop APIs. The vendor +also exports pure, bind, discard, map, and apply, which upstream Effect does not +export. These additions belong in the reviewed target-interface inventory. + +**Effect.Console:** the exact v6.0.0 diff confirms removal of ten public values: +warnShow, errorShow, info, infoShow, debug, debugShow, time, timeLog, timeEnd, and +clear. The Show wrappers are pure composition over existing console operations; +warnShow and errorShow need no new host capability. Direct writes routed through +WASI.Console are target adaptations. JavaScript console timers/clear and +severity routing need explicit target decisions; they do not justify silently +removing the pure wrappers or presenting this reduced module as the full API. + +### Whole official modules omitted + +The supplemental packages also contain these four modules, all absent from the +current vendor. The inventory records their upstream hashes and links, and the +full diff records them as removals to `/dev/null`. + +| Official module | Source contract and disposition | +| --- | --- | +| Effect.Class | Pure MonadEffect class, liftEffect method, and Effect instance. No Wasm-specific reason for omission. | +| Effect.Class.Console | Pure MonadEffect lifting wrappers for all console operations. No Wasm-specific reason for wholesale omission. | +| Effect.Uncurried | EffectFn types and mk/run conversions. Requires a target calling-convention implementation or explicit unsupported bindings. | +| Effect.Unsafe | unsafePerformEffect foreign binding. Requires the checked target Effect execution protocol or explicit unsupported status. | + +### Ordinary source functions changed for the compiler + +The original `void = map (const unit)`, `voidRight x = map (const x)`, +`voidLeft f x = const x <$> f`, and `fold = foldMap identity` have been rewritten. +These are valid ordinary PureScript definitions and have no Wasm/WASI-specific +semantics. Restore the official forms and repair compiler handling at its +owning stage if necessary. + +Instance methods were also eta-expanded in Data.Eq, Data.EuclideanRing, +Data.Functor, Data.HeytingAlgebra, Data.Ord, Data.Ring, Data.Semigroup, and +Data.Semiring. The vendoring script explains this through its restriction that +bare intrinsics are not first-class values. A target binding can have a wrapper, +but that compiler limitation does not by itself authorize rewriting the +upstream instance definitions. Preserve the foreign value's source contract +and put implementation adaptation at the defined primitive boundary. + +### An incorrect FFI replacement + +Official `Data.Eq.js` checks the two array lengths before reading any element. +The vendored `eqArrayFrom` stops only when its index reaches the left array's +length. It can read beyond the right array's bounds when the left array is +longer. For `[1] == []`, the official operation returns false without reading +an element; the replacement attempts to index the empty right array. This is +an observable algorithm difference found by source inspection; this audit did +not execute a Wasm reproduction or claim a measured trap. + +## Target-related changes requiring separate evidence + +The other changed files must not be automatically approved merely because +an upstream foreign declaration was replaced. The following table accounts for +all 20 modified modules without an exact self-recursive placeholder. The other +30 modified modules are covered by the placeholder inventory above. + +| Module | Difference and disposition | +| --- | --- | +| Control.Apply | Replaces arrayApply FFI with recursive PureScript helpers. Target implementation candidate; equivalence and stack behavior unverified. | +| Control.Bind | Replaces arrayBind FFI with recursive PureScript helpers. Target implementation candidate; equivalence and stack behavior unverified. | +| Data.Bounded | Replaces numeric/character constants. Primitive representation candidate; validate bounds against the target Char/Number contracts. | +| Data.Eq | FFI replacements plus method rewrites; confirmed array bounds difference and unapproved pure rewrites. | +| Data.EuclideanRing | Removes intDiv/intMod foreign declarations and uses intrinsics, implements intDegree/numDiv, rewrites methods. Verify negative and zero divisors; pure method rewrites lack target necessity. | +| Data.Functor | Implements arrayMap with recursion, eta-expands map, rewrites three pure combinators. Pure combinator changes violate policy. | +| Data.HeytingAlgebra | Boolean primitive wrappers plus instance-method rewrites. Primitive implementation candidate; pure rewrites require restoration. | +| Data.Int.Bits | Seven FFI values call Wasm-oriented integer intrinsics. Target implementation candidate; validate bit and shift semantics. | +| Data.Ord | Numeric, character, array, and UTF-8 byte comparators plus method rewrites. UTF-8 ordering must follow DEC-16; method rewrites are compiler accommodations. | +| Data.Ring | Removes intSub declaration and implements numSub; rewrites both methods. Primitive bindings need an explicit target boundary, not source-method workarounds. | +| Data.Semigroup | Implements string concatenation through UTF-8 bytes and array concatenation through intrinsics, rewrites append methods. Storage adaptations are candidates; method rewrites are not established as necessary. | +| Data.Semiring | Removes intAdd/intMul declarations, adds numeric primitive wrappers, rewrites methods. Primitive adaptation candidate; method rewrites are compiler accommodations. | +| Data.Show.Generic | Replaces intercalate FFI with a PureScript helper. Target implementation candidate; output/order/boundary behavior unverified. | +| Data.Show | Target rendering implementation mixed with API removals and acknowledged Number behavior differences. Fails policy as a whole. | +| Data.Tuple | Pure implementation replaced and 20 instances removed. Fails policy. | +| Data.Unit | Replaces foreign type/value declarations with re-exports of compiler builtins. Requires an explicit primitive identity/representation justification; not blanket-approved. | +| Prelude | Adds Effect, runEffect, trap and their primitive bindings/instances. Documented target interface, but relocation and extra exports require API/dependency review rather than an assumption of upstream equality. | +| Effect | Target type and instances relocated to Prelude, standard loops and two pure instances omitted. Fails policy as a whole. | +| Effect.Console | WASI routing mixed with ten omitted values, including pure Show wrappers. Fails policy as a whole. | +| Test.Assert | Trap-based failure mixed with removed equality and message-taking Boolean assertions. Fails policy as a whole. | + +The nine WASI modules are platform additions: WASI, WASI.Resource, WASI.IO, +WASI.Clock, WASI.Random, WASI.Console, WASI.Process, WASI.FileSystem, and +WASI.Network. Their scope is target-related. This audit does not assert their +runtime correctness. Effect, Effect.Console, and Test.Assert are handwritten +platform wrappers whose exact differences are now included in the audit. + +DEC-16 permits target-specific Unicode scalar and UTF-8 behavior; it does not +permit dropping pure instances or using nontermination to fake an FFI value. +A reviewed adaptation must identify the target requirement and preserve the +public source contract wherever that requirement does not explicitly change it. +Unsupported bindings must remain explicit compile/link failures rather than +apparently implemented recursive functions. + +## Reproduction and next obligations + +Run from the repository root with the same clean upstream checkouts: + +```sh +python3 docs/workflow/tools/audit-stdlib-vendor.py \ + --vendor stdlib/lib \ + --upstream /tmp/ps-pkgs \ + --upstream /tmp/purescript-prelude \ + --upstream /tmp/psrs-stdlib-audit-20261006/upstream \ + --out /tmp/psrs-stdlib-audit-repeat +``` + +The tool checks clean tagged upstream checkouts, rejects duplicate module paths, +records byte hashes, emits an unfiltered unified diff, reports exact top-level +self-recursions, and flags changes to original code outside value FFI declaration +spans. Those flags are review evidence, not semantic approval. Matching source +paths and package tags do not certify behavior of generated primitive bindings. + +Required continuation: + +1. Keep all 41 comparison baselines pinned and preserve their source provenance. +2. Restore removed pure APIs/instances, omitted pure modules, and the ordinary + upstream function forms. +3. Restore unsupported foreign declarations instead of recursive placeholders; + connect supported values through the explicit Wasm/WASI primitive contract. +4. Keep target adaptations reviewable with their rationale and behavior tests, + including UTF-8 differences under DEC-16. +5. Rerun source compile acceptance after restoration; expect honest compiler or + linking blockers until the required target implementations exist. +6. Run focused value-sensitive runtime comparisons for each implemented binding. + A complete runtime scoreboard remains a separate measurement. + +No library repair, compiler change, push, PR, or issue operation was performed +as part of this source audit. diff --git a/docs/implementation/stdlib/vendor-restoration-2026-10-06/foreign-bindings.json b/docs/implementation/stdlib/vendor-restoration-2026-10-06/foreign-bindings.json new file mode 100644 index 00000000..2942cd0e --- /dev/null +++ b/docs/implementation/stdlib/vendor-restoration-2026-10-06/foreign-bindings.json @@ -0,0 +1,2206 @@ +{ + "schema_version": 1, + "scope": "Restored ordinary foreign declarations; runtime support is unimplemented.", + "bindings": [ + { + "value": "Control.Apply.arrayApply", + "module_path": "Control/Apply.purs", + "declaration": "foreign import arrayApply :: forall a b. Array (a -> b) -> Array a -> Array b", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Control/Apply.purs#L63", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Bind.arrayBind", + "module_path": "Control/Bind.purs", + "declaration": "foreign import arrayBind :: forall a b. Array a -> (a -> Array b) -> Array b", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Control/Bind.purs#L97", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Extend.arrayExtend", + "module_path": "Control/Extend.purs", + "declaration": "foreign import arrayExtend :: forall a b. (Array a -> b) -> Array a -> Array b", + "upstream_url": "https://github.com/purescript/purescript-control/blob/a6033808790879a17b2729e73747a9ed3fb2264e/src/Control/Extend.purs#L30", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Internal.map_", + "module_path": "Control/Monad/ST/Internal.purs", + "declaration": "foreign import map_ :: forall r a b. (a -> b) -> ST r a -> ST r b", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Internal.purs#L38", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Internal.pure_", + "module_path": "Control/Monad/ST/Internal.purs", + "declaration": "foreign import pure_ :: forall r a. a -> ST r a", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Internal.purs#L40", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Internal.bind_", + "module_path": "Control/Monad/ST/Internal.purs", + "declaration": "foreign import bind_ :: forall r a b. ST r a -> (a -> ST r b) -> ST r b", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Internal.purs#L42", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Internal.run", + "module_path": "Control/Monad/ST/Internal.purs", + "declaration": "foreign import run :: forall a. (forall r. ST r a) -> a", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Internal.purs#L89", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Internal.while", + "module_path": "Control/Monad/ST/Internal.purs", + "declaration": "foreign import while :: forall r a. ST r Boolean -> ST r a -> ST r Unit", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Internal.purs#L96", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Internal.for", + "module_path": "Control/Monad/ST/Internal.purs", + "declaration": "foreign import for :: forall r a. Int -> Int -> (Int -> ST r a) -> ST r Unit", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Internal.purs#L102", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Internal.foreach", + "module_path": "Control/Monad/ST/Internal.purs", + "declaration": "foreign import foreach :: forall r a. Array a -> (a -> ST r Unit) -> ST r Unit", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Internal.purs#L108", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Internal.new", + "module_path": "Control/Monad/ST/Internal.purs", + "declaration": "foreign import new :: forall a r. a -> ST r (STRef r a)", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Internal.purs#L117", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Internal.read", + "module_path": "Control/Monad/ST/Internal.purs", + "declaration": "foreign import read :: forall a r. STRef r a -> ST r a", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Internal.purs#L120", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Internal.modifyImpl", + "module_path": "Control/Monad/ST/Internal.purs", + "declaration": "foreign import modifyImpl :: forall r a b. (a -> { state :: a, value :: b }) -> STRef r a -> ST r b", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Internal.purs#L128", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Internal.write", + "module_path": "Control/Monad/ST/Internal.purs", + "declaration": "foreign import write :: forall a r. a -> STRef r a -> ST r a", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Internal.purs#L136", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Uncurried.mkSTFn1", + "module_path": "Control/Monad/ST/Uncurried.purs", + "declaration": "foreign import mkSTFn1 :: forall a t r. (a -> ST t r) -> STFn1 a t r", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Uncurried.purs#L61", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Uncurried.mkSTFn2", + "module_path": "Control/Monad/ST/Uncurried.purs", + "declaration": "foreign import mkSTFn2 :: forall a b t r. (a -> b -> ST t r) -> STFn2 a b t r", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Uncurried.purs#L63", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Uncurried.mkSTFn3", + "module_path": "Control/Monad/ST/Uncurried.purs", + "declaration": "foreign import mkSTFn3 :: forall a b c t r. (a -> b -> c -> ST t r) -> STFn3 a b c t r", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Uncurried.purs#L65", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Uncurried.mkSTFn4", + "module_path": "Control/Monad/ST/Uncurried.purs", + "declaration": "foreign import mkSTFn4 :: forall a b c d t r. (a -> b -> c -> d -> ST t r) -> STFn4 a b c d t r", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Uncurried.purs#L67", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Uncurried.mkSTFn5", + "module_path": "Control/Monad/ST/Uncurried.purs", + "declaration": "foreign import mkSTFn5 :: forall a b c d e t r. (a -> b -> c -> d -> e -> ST t r) -> STFn5 a b c d e t r", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Uncurried.purs#L69", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Uncurried.mkSTFn6", + "module_path": "Control/Monad/ST/Uncurried.purs", + "declaration": "foreign import mkSTFn6 :: forall a b c d e f t r. (a -> b -> c -> d -> e -> f -> ST t r) -> STFn6 a b c d e f t r", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Uncurried.purs#L71", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Uncurried.mkSTFn7", + "module_path": "Control/Monad/ST/Uncurried.purs", + "declaration": "foreign import mkSTFn7 :: forall a b c d e f g t r. (a -> b -> c -> d -> e -> f -> g -> ST t r) -> STFn7 a b c d e f g t r", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Uncurried.purs#L73", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Uncurried.mkSTFn8", + "module_path": "Control/Monad/ST/Uncurried.purs", + "declaration": "foreign import mkSTFn8 :: forall a b c d e f g h t r. (a -> b -> c -> d -> e -> f -> g -> h -> ST t r) -> STFn8 a b c d e f g h t r", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Uncurried.purs#L75", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Uncurried.mkSTFn9", + "module_path": "Control/Monad/ST/Uncurried.purs", + "declaration": "foreign import mkSTFn9 :: forall a b c d e f g h i t r. (a -> b -> c -> d -> e -> f -> g -> h -> i -> ST t r) -> STFn9 a b c d e f g h i t r", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Uncurried.purs#L77", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Uncurried.mkSTFn10", + "module_path": "Control/Monad/ST/Uncurried.purs", + "declaration": "foreign import mkSTFn10 :: forall a b c d e f g h i j t r. (a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> ST t r) -> STFn10 a b c d e f g h i j t r", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Uncurried.purs#L79", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Uncurried.runSTFn1", + "module_path": "Control/Monad/ST/Uncurried.purs", + "declaration": "foreign import runSTFn1 :: forall a t r. STFn1 a t r -> a -> ST t r", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Uncurried.purs#L82", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Uncurried.runSTFn2", + "module_path": "Control/Monad/ST/Uncurried.purs", + "declaration": "foreign import runSTFn2 :: forall a b t r. STFn2 a b t r -> a -> b -> ST t r", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Uncurried.purs#L84", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Uncurried.runSTFn3", + "module_path": "Control/Monad/ST/Uncurried.purs", + "declaration": "foreign import runSTFn3 :: forall a b c t r. STFn3 a b c t r -> a -> b -> c -> ST t r", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Uncurried.purs#L86", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Uncurried.runSTFn4", + "module_path": "Control/Monad/ST/Uncurried.purs", + "declaration": "foreign import runSTFn4 :: forall a b c d t r. STFn4 a b c d t r -> a -> b -> c -> d -> ST t r", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Uncurried.purs#L88", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Uncurried.runSTFn5", + "module_path": "Control/Monad/ST/Uncurried.purs", + "declaration": "foreign import runSTFn5 :: forall a b c d e t r. STFn5 a b c d e t r -> a -> b -> c -> d -> e -> ST t r", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Uncurried.purs#L90", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Uncurried.runSTFn6", + "module_path": "Control/Monad/ST/Uncurried.purs", + "declaration": "foreign import runSTFn6 :: forall a b c d e f t r. STFn6 a b c d e f t r -> a -> b -> c -> d -> e -> f -> ST t r", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Uncurried.purs#L92", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Uncurried.runSTFn7", + "module_path": "Control/Monad/ST/Uncurried.purs", + "declaration": "foreign import runSTFn7 :: forall a b c d e f g t r. STFn7 a b c d e f g t r -> a -> b -> c -> d -> e -> f -> g -> ST t r", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Uncurried.purs#L94", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Uncurried.runSTFn8", + "module_path": "Control/Monad/ST/Uncurried.purs", + "declaration": "foreign import runSTFn8 :: forall a b c d e f g h t r. STFn8 a b c d e f g h t r -> a -> b -> c -> d -> e -> f -> g -> h -> ST t r", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Uncurried.purs#L96", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Uncurried.runSTFn9", + "module_path": "Control/Monad/ST/Uncurried.purs", + "declaration": "foreign import runSTFn9 :: forall a b c d e f g h i t r. STFn9 a b c d e f g h i t r -> a -> b -> c -> d -> e -> f -> g -> h -> i -> ST t r", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Uncurried.purs#L98", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Control.Monad.ST.Uncurried.runSTFn10", + "module_path": "Control/Monad/ST/Uncurried.purs", + "declaration": "foreign import runSTFn10 :: forall a b c d e f g h i j t r. STFn10 a b c d e f g h i j t r -> a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> ST t r", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Uncurried.purs#L100", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.NonEmpty.Internal.foldr1Impl", + "module_path": "Data/Array/NonEmpty/Internal.purs", + "declaration": "foreign import foldr1Impl :: forall a. Fn2 (a -> a -> a) (NonEmptyArray a) a", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/NonEmpty/Internal.purs#L75", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.NonEmpty.Internal.foldl1Impl", + "module_path": "Data/Array/NonEmpty/Internal.purs", + "declaration": "foreign import foldl1Impl :: forall a. Fn2 (a -> a -> a) (NonEmptyArray a) a", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/NonEmpty/Internal.purs#L76", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.NonEmpty.Internal.traverse1Impl", + "module_path": "Data/Array/NonEmpty/Internal.purs", + "declaration": "foreign import traverse1Impl :: forall m a b . Fn3 (forall a' b'. (m (a' -> b') -> m a' -> m b')) (forall a' b'. (a' -> b') -> m a' -> m b') (a -> m b) (NonEmptyArray a -> m (NonEmptyArray b))", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/NonEmpty/Internal.purs#L78", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.ST.Partial.peekImpl", + "module_path": "Data/Array/ST/Partial.purs", + "declaration": "foreign import peekImpl :: forall h a. STFn2 Int (STArray h a) h a", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST/Partial.purs#L24", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.ST.Partial.pokeImpl", + "module_path": "Data/Array/ST/Partial.purs", + "declaration": "foreign import pokeImpl :: forall h a. STFn3 Int a (STArray h a) h Unit", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST/Partial.purs#L36", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.ST.unsafeFreezeImpl", + "module_path": "Data/Array/ST.purs", + "declaration": "foreign import unsafeFreezeImpl :: forall h a. STFn1 (STArray h a) h (Array a)", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST.purs#L78", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.ST.unsafeThawImpl", + "module_path": "Data/Array/ST.purs", + "declaration": "foreign import unsafeThawImpl :: forall h a. STFn1 (Array a) h (STArray h a)", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST.purs#L85", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.ST.new", + "module_path": "Data/Array/ST.purs", + "declaration": "foreign import new :: forall h a. ST h (STArray h a)", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST.purs#L88", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.ST.thawImpl", + "module_path": "Data/Array/ST.purs", + "declaration": "foreign import thawImpl :: forall h a. STFn1 (Array a) h (STArray h a)", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST.purs#L97", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.ST.cloneImpl", + "module_path": "Data/Array/ST.purs", + "declaration": "foreign import cloneImpl :: forall h a. STFn1 (STArray h a) h (STArray h a)", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST.purs#L106", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.ST.shiftImpl", + "module_path": "Data/Array/ST.purs", + "declaration": "foreign import shiftImpl :: forall h a . STFn3 (forall b. b -> Maybe b) (forall b. Maybe b) (STArray h a) h (Maybe a)", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST.purs#L117", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.ST.sortByImpl", + "module_path": "Data/Array/ST.purs", + "declaration": "foreign import sortByImpl :: forall a h . STFn3 (a -> a -> Ordering) (Ordering -> Int) (STArray h a) h (STArray h a)", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST.purs#L134", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.ST.freezeImpl", + "module_path": "Data/Array/ST.purs", + "declaration": "foreign import freezeImpl :: forall h a. STFn1 (STArray h a) h (Array a)", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST.purs#L155", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.ST.peekImpl", + "module_path": "Data/Array/ST.purs", + "declaration": "foreign import peekImpl :: forall h a r. STFn4 (a -> r) r Int (STArray h a) h r", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST.purs#L165", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.ST.pokeImpl", + "module_path": "Data/Array/ST.purs", + "declaration": "foreign import pokeImpl :: forall h a. STFn3 Int a (STArray h a) h Boolean", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST.purs#L176", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.ST.lengthImpl", + "module_path": "Data/Array/ST.purs", + "declaration": "foreign import lengthImpl :: forall h a. STFn1 (STArray h a) h Int", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST.purs#L178", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.ST.popImpl", + "module_path": "Data/Array/ST.purs", + "declaration": "foreign import popImpl :: forall h a . STFn3 (forall b. b -> Maybe b) (forall b. Maybe b) (STArray h a) h (Maybe a)", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST.purs#L188", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.ST.pushImpl", + "module_path": "Data/Array/ST.purs", + "declaration": "foreign import pushImpl :: forall h a. STFn2 a (STArray h a) h Int", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST.purs#L197", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.ST.pushAllImpl", + "module_path": "Data/Array/ST.purs", + "declaration": "foreign import pushAllImpl :: forall h a . STFn2 (Array a) (STArray h a) h Int", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST.purs#L208", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.ST.unshiftAllImpl", + "module_path": "Data/Array/ST.purs", + "declaration": "foreign import unshiftAllImpl :: forall h a . STFn2 (Array a) (STArray h a) h Int", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST.purs#L226", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.ST.spliceImpl", + "module_path": "Data/Array/ST.purs", + "declaration": "foreign import spliceImpl :: forall h a . STFn4 Int Int (Array a) (STArray h a) h (Array a)", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST.purs#L248", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.ST.toAssocArrayImpl", + "module_path": "Data/Array/ST.purs", + "declaration": "foreign import toAssocArrayImpl :: forall h a . STFn1 (STArray h a) h (Array (Assoc a))", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST.purs#L260", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.fromFoldableImpl", + "module_path": "Data/Array.purs", + "declaration": "foreign import fromFoldableImpl :: forall f a . Fn2 (forall b. (a -> b -> b) -> b -> f a -> b) (f a) (Array a)", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs#L177", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.rangeImpl", + "module_path": "Data/Array.purs", + "declaration": "foreign import rangeImpl :: Fn2 Int Int (Array Int)", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs#L195", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.replicateImpl", + "module_path": "Data/Array.purs", + "declaration": "foreign import replicateImpl :: forall a. Fn2 Int a (Array a)", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs#L204", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.length", + "module_path": "Data/Array.purs", + "declaration": "foreign import length :: forall a. Array a -> Int", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs#L243", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.unconsImpl", + "module_path": "Data/Array.purs", + "declaration": "foreign import unconsImpl :: forall a b . Fn3 (Unit -> b) (a -> Array a -> b) (Array a) b", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs#L373", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.indexImpl", + "module_path": "Data/Array.purs", + "declaration": "foreign import indexImpl :: forall a . Fn4 (forall r. r -> Maybe r) (forall r. Maybe r) (Array a) Int (Maybe a)", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs#L405", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.findMapImpl", + "module_path": "Data/Array.purs", + "declaration": "foreign import findMapImpl :: forall a b . Fn4 (forall c. Maybe c) (forall c. Maybe c -> Boolean) (a -> Maybe b) (Array a) (Maybe b)", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs#L462", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.findIndexImpl", + "module_path": "Data/Array.purs", + "declaration": "foreign import findIndexImpl :: forall a . Fn4 (forall b. b -> Maybe b) (forall b. Maybe b) (a -> Boolean) (Array a) (Maybe Int)", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs#L481", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.findLastIndexImpl", + "module_path": "Data/Array.purs", + "declaration": "foreign import findLastIndexImpl :: forall a . Fn4 (forall b. b -> Maybe b) (forall b. Maybe b) (a -> Boolean) (Array a) (Maybe Int)", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs#L500", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array._insertAt", + "module_path": "Data/Array.purs", + "declaration": "foreign import _insertAt :: forall a . Fn5 (forall b. b -> Maybe b) (forall b. Maybe b) Int a (Array a) (Maybe (Array a))", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs#L520", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array._deleteAt", + "module_path": "Data/Array.purs", + "declaration": "foreign import _deleteAt :: forall a . Fn4 (forall b. b -> Maybe b) (forall b. Maybe b) Int (Array a) (Maybe (Array a))", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs#L541", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array._updateAt", + "module_path": "Data/Array.purs", + "declaration": "foreign import _updateAt :: forall a . Fn5 (forall b. b -> Maybe b) (forall b. Maybe b) Int a (Array a) (Maybe (Array a))", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs#L561", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.reverse", + "module_path": "Data/Array.purs", + "declaration": "foreign import reverse :: forall a. Array a -> Array a", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs#L642", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.concat", + "module_path": "Data/Array.purs", + "declaration": "foreign import concat :: forall a. Array (Array a) -> Array a", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs#L650", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.filterImpl", + "module_path": "Data/Array.purs", + "declaration": "foreign import filterImpl :: forall a . Fn2 (a -> Boolean) (Array a) (Array a)", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs#L673", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.partitionImpl", + "module_path": "Data/Array.purs", + "declaration": "foreign import partitionImpl :: forall a . Fn2 (a -> Boolean) (Array a) { yes :: Array a, no :: Array a }", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs#L692", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.scanlImpl", + "module_path": "Data/Array.purs", + "declaration": "foreign import scanlImpl :: forall a b. Fn3 (b -> a -> b) b (Array a) (Array b)", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs#L857", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.scanrImpl", + "module_path": "Data/Array.purs", + "declaration": "foreign import scanrImpl :: forall a b. Fn3 (a -> b -> b) b (Array a) (Array b)", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs#L870", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.sortByImpl", + "module_path": "Data/Array.purs", + "declaration": "foreign import sortByImpl :: forall a. Fn3 (a -> a -> Ordering) (Ordering -> Int) (Array a) (Array a)", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs#L914", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.sliceImpl", + "module_path": "Data/Array.purs", + "declaration": "foreign import sliceImpl :: forall a. Fn3 Int Int (Array a) (Array a)", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs#L932", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.zipWithImpl", + "module_path": "Data/Array.purs", + "declaration": "foreign import zipWithImpl :: forall a b c . Fn3 (a -> b -> c) (Array a) (Array b) (Array c)", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs#L1253", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.anyImpl", + "module_path": "Data/Array.purs", + "declaration": "foreign import anyImpl :: forall a. Fn2 (a -> Boolean) (Array a) Boolean", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs#L1322", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.allImpl", + "module_path": "Data/Array.purs", + "declaration": "foreign import allImpl :: forall a. Fn2 (a -> Boolean) (Array a) Boolean", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs#L1336", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Array.unsafeIndexImpl", + "module_path": "Data/Array.purs", + "declaration": "foreign import unsafeIndexImpl :: forall a. Fn2 (Array a) Int a", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs#L1371", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Bounded.topInt", + "module_path": "Data/Bounded.purs", + "declaration": "foreign import topInt :: Int", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Bounded.purs#L40", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Bounded.bottomInt", + "module_path": "Data/Bounded.purs", + "declaration": "foreign import bottomInt :: Int", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Bounded.purs#L41", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Bounded.topChar", + "module_path": "Data/Bounded.purs", + "declaration": "foreign import topChar :: Char", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Bounded.purs#L48", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Bounded.bottomChar", + "module_path": "Data/Bounded.purs", + "declaration": "foreign import bottomChar :: Char", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Bounded.purs#L49", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Bounded.topNumber", + "module_path": "Data/Bounded.purs", + "declaration": "foreign import topNumber :: Number", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Bounded.purs#L59", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Bounded.bottomNumber", + "module_path": "Data/Bounded.purs", + "declaration": "foreign import bottomNumber :: Number", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Bounded.purs#L60", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Enum.toCharCode", + "module_path": "Data/Enum.purs", + "declaration": "foreign import toCharCode :: Char -> Int", + "upstream_url": "https://github.com/purescript/purescript-enums/blob/cd373c580b69fdc00e412bddbc299adabe242cc5/src/Data/Enum.purs#L320", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Enum.fromCharCode", + "module_path": "Data/Enum.purs", + "declaration": "foreign import fromCharCode :: Int -> Char", + "upstream_url": "https://github.com/purescript/purescript-enums/blob/cd373c580b69fdc00e412bddbc299adabe242cc5/src/Data/Enum.purs#L321", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Eq.eqBooleanImpl", + "module_path": "Data/Eq.purs", + "declaration": "foreign import eqBooleanImpl :: Boolean -> Boolean -> Boolean", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Eq.purs#L77", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Eq.eqIntImpl", + "module_path": "Data/Eq.purs", + "declaration": "foreign import eqIntImpl :: Int -> Int -> Boolean", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Eq.purs#L78", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Eq.eqNumberImpl", + "module_path": "Data/Eq.purs", + "declaration": "foreign import eqNumberImpl :: Number -> Number -> Boolean", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Eq.purs#L79", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Eq.eqCharImpl", + "module_path": "Data/Eq.purs", + "declaration": "foreign import eqCharImpl :: Char -> Char -> Boolean", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Eq.purs#L80", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Eq.eqStringImpl", + "module_path": "Data/Eq.purs", + "declaration": "foreign import eqStringImpl :: String -> String -> Boolean", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Eq.purs#L81", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Eq.eqArrayImpl", + "module_path": "Data/Eq.purs", + "declaration": "foreign import eqArrayImpl :: forall a. (a -> a -> Boolean) -> Array a -> Array a -> Boolean", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Eq.purs#L83", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.EuclideanRing.intDegree", + "module_path": "Data/EuclideanRing.purs", + "declaration": "foreign import intDegree :: Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/EuclideanRing.purs#L84", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.EuclideanRing.intDiv", + "module_path": "Data/EuclideanRing.purs", + "declaration": "foreign import intDiv :: Int -> Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/EuclideanRing.purs#L85", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.EuclideanRing.intMod", + "module_path": "Data/EuclideanRing.purs", + "declaration": "foreign import intMod :: Int -> Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/EuclideanRing.purs#L86", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.EuclideanRing.numDiv", + "module_path": "Data/EuclideanRing.purs", + "declaration": "foreign import numDiv :: Number -> Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/EuclideanRing.purs#L88", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Foldable.foldrArray", + "module_path": "Data/Foldable.purs", + "declaration": "foreign import foldrArray :: forall a b. (a -> b -> b) -> b -> Array a -> b", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/Foldable.purs#L135", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Foldable.foldlArray", + "module_path": "Data/Foldable.purs", + "declaration": "foreign import foldlArray :: forall a b. (b -> a -> b) -> b -> Array a -> b", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/Foldable.purs#L136", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Function.Uncurried.mkFn0", + "module_path": "Data/Function/Uncurried.purs", + "declaration": "foreign import mkFn0 :: forall a. (Unit -> a) -> Fn0 a", + "upstream_url": "https://github.com/purescript/purescript-functions/blob/f626f20580483977c5b27a01aac6471e28aff367/src/Data/Function/Uncurried.purs#L59", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Function.Uncurried.mkFn2", + "module_path": "Data/Function/Uncurried.purs", + "declaration": "foreign import mkFn2 :: forall a b c. (a -> b -> c) -> Fn2 a b c", + "upstream_url": "https://github.com/purescript/purescript-functions/blob/f626f20580483977c5b27a01aac6471e28aff367/src/Data/Function/Uncurried.purs#L66", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Function.Uncurried.mkFn3", + "module_path": "Data/Function/Uncurried.purs", + "declaration": "foreign import mkFn3 :: forall a b c d. (a -> b -> c -> d) -> Fn3 a b c d", + "upstream_url": "https://github.com/purescript/purescript-functions/blob/f626f20580483977c5b27a01aac6471e28aff367/src/Data/Function/Uncurried.purs#L69", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Function.Uncurried.mkFn4", + "module_path": "Data/Function/Uncurried.purs", + "declaration": "foreign import mkFn4 :: forall a b c d e. (a -> b -> c -> d -> e) -> Fn4 a b c d e", + "upstream_url": "https://github.com/purescript/purescript-functions/blob/f626f20580483977c5b27a01aac6471e28aff367/src/Data/Function/Uncurried.purs#L72", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Function.Uncurried.mkFn5", + "module_path": "Data/Function/Uncurried.purs", + "declaration": "foreign import mkFn5 :: forall a b c d e f. (a -> b -> c -> d -> e -> f) -> Fn5 a b c d e f", + "upstream_url": "https://github.com/purescript/purescript-functions/blob/f626f20580483977c5b27a01aac6471e28aff367/src/Data/Function/Uncurried.purs#L75", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Function.Uncurried.mkFn6", + "module_path": "Data/Function/Uncurried.purs", + "declaration": "foreign import mkFn6 :: forall a b c d e f g. (a -> b -> c -> d -> e -> f -> g) -> Fn6 a b c d e f g", + "upstream_url": "https://github.com/purescript/purescript-functions/blob/f626f20580483977c5b27a01aac6471e28aff367/src/Data/Function/Uncurried.purs#L78", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Function.Uncurried.mkFn7", + "module_path": "Data/Function/Uncurried.purs", + "declaration": "foreign import mkFn7 :: forall a b c d e f g h. (a -> b -> c -> d -> e -> f -> g -> h) -> Fn7 a b c d e f g h", + "upstream_url": "https://github.com/purescript/purescript-functions/blob/f626f20580483977c5b27a01aac6471e28aff367/src/Data/Function/Uncurried.purs#L81", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Function.Uncurried.mkFn8", + "module_path": "Data/Function/Uncurried.purs", + "declaration": "foreign import mkFn8 :: forall a b c d e f g h i. (a -> b -> c -> d -> e -> f -> g -> h -> i) -> Fn8 a b c d e f g h i", + "upstream_url": "https://github.com/purescript/purescript-functions/blob/f626f20580483977c5b27a01aac6471e28aff367/src/Data/Function/Uncurried.purs#L84", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Function.Uncurried.mkFn9", + "module_path": "Data/Function/Uncurried.purs", + "declaration": "foreign import mkFn9 :: forall a b c d e f g h i j. (a -> b -> c -> d -> e -> f -> g -> h -> i -> j) -> Fn9 a b c d e f g h i j", + "upstream_url": "https://github.com/purescript/purescript-functions/blob/f626f20580483977c5b27a01aac6471e28aff367/src/Data/Function/Uncurried.purs#L87", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Function.Uncurried.mkFn10", + "module_path": "Data/Function/Uncurried.purs", + "declaration": "foreign import mkFn10 :: forall a b c d e f g h i j k. (a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> k) -> Fn10 a b c d e f g h i j k", + "upstream_url": "https://github.com/purescript/purescript-functions/blob/f626f20580483977c5b27a01aac6471e28aff367/src/Data/Function/Uncurried.purs#L90", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Function.Uncurried.runFn0", + "module_path": "Data/Function/Uncurried.purs", + "declaration": "foreign import runFn0 :: forall a. Fn0 a -> a", + "upstream_url": "https://github.com/purescript/purescript-functions/blob/f626f20580483977c5b27a01aac6471e28aff367/src/Data/Function/Uncurried.purs#L93", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Function.Uncurried.runFn2", + "module_path": "Data/Function/Uncurried.purs", + "declaration": "foreign import runFn2 :: forall a b c. Fn2 a b c -> a -> b -> c", + "upstream_url": "https://github.com/purescript/purescript-functions/blob/f626f20580483977c5b27a01aac6471e28aff367/src/Data/Function/Uncurried.purs#L100", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Function.Uncurried.runFn3", + "module_path": "Data/Function/Uncurried.purs", + "declaration": "foreign import runFn3 :: forall a b c d. Fn3 a b c d -> a -> b -> c -> d", + "upstream_url": "https://github.com/purescript/purescript-functions/blob/f626f20580483977c5b27a01aac6471e28aff367/src/Data/Function/Uncurried.purs#L103", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Function.Uncurried.runFn4", + "module_path": "Data/Function/Uncurried.purs", + "declaration": "foreign import runFn4 :: forall a b c d e. Fn4 a b c d e -> a -> b -> c -> d -> e", + "upstream_url": "https://github.com/purescript/purescript-functions/blob/f626f20580483977c5b27a01aac6471e28aff367/src/Data/Function/Uncurried.purs#L106", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Function.Uncurried.runFn5", + "module_path": "Data/Function/Uncurried.purs", + "declaration": "foreign import runFn5 :: forall a b c d e f. Fn5 a b c d e f -> a -> b -> c -> d -> e -> f", + "upstream_url": "https://github.com/purescript/purescript-functions/blob/f626f20580483977c5b27a01aac6471e28aff367/src/Data/Function/Uncurried.purs#L109", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Function.Uncurried.runFn6", + "module_path": "Data/Function/Uncurried.purs", + "declaration": "foreign import runFn6 :: forall a b c d e f g. Fn6 a b c d e f g -> a -> b -> c -> d -> e -> f -> g", + "upstream_url": "https://github.com/purescript/purescript-functions/blob/f626f20580483977c5b27a01aac6471e28aff367/src/Data/Function/Uncurried.purs#L112", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Function.Uncurried.runFn7", + "module_path": "Data/Function/Uncurried.purs", + "declaration": "foreign import runFn7 :: forall a b c d e f g h. Fn7 a b c d e f g h -> a -> b -> c -> d -> e -> f -> g -> h", + "upstream_url": "https://github.com/purescript/purescript-functions/blob/f626f20580483977c5b27a01aac6471e28aff367/src/Data/Function/Uncurried.purs#L115", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Function.Uncurried.runFn8", + "module_path": "Data/Function/Uncurried.purs", + "declaration": "foreign import runFn8 :: forall a b c d e f g h i. Fn8 a b c d e f g h i -> a -> b -> c -> d -> e -> f -> g -> h -> i", + "upstream_url": "https://github.com/purescript/purescript-functions/blob/f626f20580483977c5b27a01aac6471e28aff367/src/Data/Function/Uncurried.purs#L118", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Function.Uncurried.runFn9", + "module_path": "Data/Function/Uncurried.purs", + "declaration": "foreign import runFn9 :: forall a b c d e f g h i j. Fn9 a b c d e f g h i j -> a -> b -> c -> d -> e -> f -> g -> h -> i -> j", + "upstream_url": "https://github.com/purescript/purescript-functions/blob/f626f20580483977c5b27a01aac6471e28aff367/src/Data/Function/Uncurried.purs#L121", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Function.Uncurried.runFn10", + "module_path": "Data/Function/Uncurried.purs", + "declaration": "foreign import 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", + "upstream_url": "https://github.com/purescript/purescript-functions/blob/f626f20580483977c5b27a01aac6471e28aff367/src/Data/Function/Uncurried.purs#L124", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Functor.arrayMap", + "module_path": "Data/Functor.purs", + "declaration": "foreign import arrayMap :: forall a b. (a -> b) -> Array a -> Array b", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Functor.purs#L55", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.FunctorWithIndex.mapWithIndexArray", + "module_path": "Data/FunctorWithIndex.purs", + "declaration": "foreign import mapWithIndexArray :: forall a b. (Int -> a -> b) -> Array a -> Array b", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/FunctorWithIndex.purs#L38", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.HeytingAlgebra.boolConj", + "module_path": "Data/HeytingAlgebra.purs", + "declaration": "foreign import boolConj :: Boolean -> Boolean -> Boolean", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/HeytingAlgebra.purs#L103", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.HeytingAlgebra.boolDisj", + "module_path": "Data/HeytingAlgebra.purs", + "declaration": "foreign import boolDisj :: Boolean -> Boolean -> Boolean", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/HeytingAlgebra.purs#L104", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.HeytingAlgebra.boolNot", + "module_path": "Data/HeytingAlgebra.purs", + "declaration": "foreign import boolNot :: Boolean -> Boolean", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/HeytingAlgebra.purs#L105", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Int.Bits.and", + "module_path": "Data/Int/Bits.purs", + "declaration": "foreign import and :: Int -> Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int/Bits.purs#L13", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Int.Bits.or", + "module_path": "Data/Int/Bits.purs", + "declaration": "foreign import or :: Int -> Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int/Bits.purs#L18", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Int.Bits.xor", + "module_path": "Data/Int/Bits.purs", + "declaration": "foreign import xor :: Int -> Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int/Bits.purs#L23", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Int.Bits.shl", + "module_path": "Data/Int/Bits.purs", + "declaration": "foreign import shl :: Int -> Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int/Bits.purs#L28", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Int.Bits.shr", + "module_path": "Data/Int/Bits.purs", + "declaration": "foreign import shr :: Int -> Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int/Bits.purs#L31", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Int.Bits.zshr", + "module_path": "Data/Int/Bits.purs", + "declaration": "foreign import zshr :: Int -> Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int/Bits.purs#L34", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Int.Bits.complement", + "module_path": "Data/Int/Bits.purs", + "declaration": "foreign import complement :: Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int/Bits.purs#L37", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Int.fromNumberImpl", + "module_path": "Data/Int.purs", + "declaration": "foreign import fromNumberImpl :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Number -> Maybe Int", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int.purs#L40", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Int.toNumber", + "module_path": "Data/Int.purs", + "declaration": "foreign import toNumber :: Int -> Number", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int.purs#L81", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Int.quot", + "module_path": "Data/Int.purs", + "declaration": "foreign import quot :: Int -> Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int.purs#L227", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Int.rem", + "module_path": "Data/Int.purs", + "declaration": "foreign import rem :: Int -> Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int.purs#L245", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Int.pow", + "module_path": "Data/Int.purs", + "declaration": "foreign import pow :: Int -> Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int.purs#L248", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Int.fromStringAsImpl", + "module_path": "Data/Int.purs", + "declaration": "foreign import fromStringAsImpl :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Radix -> String -> Maybe Int", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int.purs#L250", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Int.toStringAs", + "module_path": "Data/Int.purs", + "declaration": "foreign import toStringAs :: Radix -> Int -> String", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int.purs#L257", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Lazy.defer", + "module_path": "Data/Lazy.purs", + "declaration": "foreign import defer :: forall a. (Unit -> a) -> Lazy a", + "upstream_url": "https://github.com/purescript/purescript-lazy/blob/48347841226b27af5205a1a8ec71e27a93ce86fd/src/Data/Lazy.purs#L35", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Lazy.force", + "module_path": "Data/Lazy.purs", + "declaration": "foreign import force :: forall a. Lazy a -> a", + "upstream_url": "https://github.com/purescript/purescript-lazy/blob/48347841226b27af5205a1a8ec71e27a93ce86fd/src/Data/Lazy.purs#L38", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.Format.toPrecisionNative", + "module_path": "Data/Number/Format.purs", + "declaration": "foreign import toPrecisionNative :: Int -> Number -> String", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number/Format.purs#L33", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.Format.toFixedNative", + "module_path": "Data/Number/Format.purs", + "declaration": "foreign import toFixedNative :: Int -> Number -> String", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number/Format.purs#L34", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.Format.toExponentialNative", + "module_path": "Data/Number/Format.purs", + "declaration": "foreign import toExponentialNative :: Int -> Number -> String", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number/Format.purs#L35", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.Format.toString", + "module_path": "Data/Number/Format.purs", + "declaration": "foreign import toString :: Number -> String", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number/Format.purs#L76", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.nan", + "module_path": "Data/Number.purs", + "declaration": "foreign import nan :: Number", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L47", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.isNaN", + "module_path": "Data/Number.purs", + "declaration": "foreign import isNaN :: Number -> Boolean", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L57", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.infinity", + "module_path": "Data/Number.purs", + "declaration": "foreign import infinity :: Number", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L67", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.isFinite", + "module_path": "Data/Number.purs", + "declaration": "foreign import isFinite :: Number -> Boolean", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L83", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.fromStringImpl", + "module_path": "Data/Number.purs", + "declaration": "foreign import fromStringImpl :: Fn4 String (Number -> Boolean) (forall a. a -> Maybe a) (forall a. Maybe a) (Maybe Number)", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L115", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.abs", + "module_path": "Data/Number.purs", + "declaration": "foreign import abs :: Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L123", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.acos", + "module_path": "Data/Number.purs", + "declaration": "foreign import acos :: Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L130", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.asin", + "module_path": "Data/Number.purs", + "declaration": "foreign import asin :: Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L137", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.atan", + "module_path": "Data/Number.purs", + "declaration": "foreign import atan :: Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L144", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.atan2", + "module_path": "Data/Number.purs", + "declaration": "foreign import atan2 :: Number -> Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L157", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.ceil", + "module_path": "Data/Number.purs", + "declaration": "foreign import ceil :: Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L164", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.cos", + "module_path": "Data/Number.purs", + "declaration": "foreign import cos :: Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L171", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.exp", + "module_path": "Data/Number.purs", + "declaration": "foreign import exp :: Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L178", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.floor", + "module_path": "Data/Number.purs", + "declaration": "foreign import floor :: Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L185", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.log", + "module_path": "Data/Number.purs", + "declaration": "foreign import log :: Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L191", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.max", + "module_path": "Data/Number.purs", + "declaration": "foreign import max :: Number -> Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L195", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.min", + "module_path": "Data/Number.purs", + "declaration": "foreign import min :: Number -> Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L199", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.pow", + "module_path": "Data/Number.purs", + "declaration": "foreign import pow :: Number -> Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L209", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.remainder", + "module_path": "Data/Number.purs", + "declaration": "foreign import remainder :: Number -> Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L216", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.round", + "module_path": "Data/Number.purs", + "declaration": "foreign import round :: Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L225", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.sign", + "module_path": "Data/Number.purs", + "declaration": "foreign import sign :: Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L235", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.sin", + "module_path": "Data/Number.purs", + "declaration": "foreign import sin :: Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L242", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.sqrt", + "module_path": "Data/Number.purs", + "declaration": "foreign import sqrt :: Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L249", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.tan", + "module_path": "Data/Number.purs", + "declaration": "foreign import tan :: Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L256", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Number.trunc", + "module_path": "Data/Number.purs", + "declaration": "foreign import trunc :: Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs#L264", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Ord.ordBooleanImpl", + "module_path": "Data/Ord.purs", + "declaration": "foreign import ordBooleanImpl :: Ordering -> Ordering -> Ordering -> Boolean -> Boolean -> Ordering", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Ord.purs#L84", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Ord.ordIntImpl", + "module_path": "Data/Ord.purs", + "declaration": "foreign import ordIntImpl :: Ordering -> Ordering -> Ordering -> Int -> Int -> Ordering", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Ord.purs#L92", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Ord.ordNumberImpl", + "module_path": "Data/Ord.purs", + "declaration": "foreign import ordNumberImpl :: Ordering -> Ordering -> Ordering -> Number -> Number -> Ordering", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Ord.purs#L100", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Ord.ordStringImpl", + "module_path": "Data/Ord.purs", + "declaration": "foreign import ordStringImpl :: Ordering -> Ordering -> Ordering -> String -> String -> Ordering", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Ord.purs#L108", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Ord.ordCharImpl", + "module_path": "Data/Ord.purs", + "declaration": "foreign import ordCharImpl :: Ordering -> Ordering -> Ordering -> Char -> Char -> Ordering", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Ord.purs#L116", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Ord.ordArrayImpl", + "module_path": "Data/Ord.purs", + "declaration": "foreign import ordArrayImpl :: forall a. (a -> a -> Int) -> Array a -> Array a -> Int", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Ord.purs#L124", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Reflectable.unsafeCoerce", + "module_path": "Data/Reflectable.purs", + "declaration": "foreign import unsafeCoerce :: forall a b. a -> b", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Reflectable.purs#L36", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Ring.intSub", + "module_path": "Data/Ring.purs", + "declaration": "foreign import intSub :: Int -> Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Ring.purs#L54", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Ring.numSub", + "module_path": "Data/Ring.purs", + "declaration": "foreign import numSub :: Number -> Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Ring.purs#L55", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Semigroup.concatString", + "module_path": "Data/Semigroup.purs", + "declaration": "foreign import concatString :: String -> String -> String", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Semigroup.purs#L60", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Semigroup.concatArray", + "module_path": "Data/Semigroup.purs", + "declaration": "foreign import concatArray :: forall a. Array a -> Array a -> Array a", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Semigroup.purs#L61", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Semiring.intAdd", + "module_path": "Data/Semiring.purs", + "declaration": "foreign import intAdd :: Int -> Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Semiring.purs#L89", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Semiring.intMul", + "module_path": "Data/Semiring.purs", + "declaration": "foreign import intMul :: Int -> Int -> Int", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Semiring.purs#L90", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Semiring.numAdd", + "module_path": "Data/Semiring.purs", + "declaration": "foreign import numAdd :: Number -> Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Semiring.purs#L91", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Semiring.numMul", + "module_path": "Data/Semiring.purs", + "declaration": "foreign import numMul :: Number -> Number -> Number", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Semiring.purs#L92", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Show.Generic.intercalate", + "module_path": "Data/Show/Generic.purs", + "declaration": "foreign import intercalate :: String -> Array String -> String", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Show/Generic.purs#L57", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Show.showIntImpl", + "module_path": "Data/Show.purs", + "declaration": "foreign import showIntImpl :: Int -> String", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Show.purs#L93", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Show.showNumberImpl", + "module_path": "Data/Show.purs", + "declaration": "foreign import showNumberImpl :: Number -> String", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Show.purs#L94", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Show.showCharImpl", + "module_path": "Data/Show.purs", + "declaration": "foreign import showCharImpl :: Char -> String", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Show.purs#L95", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Show.showStringImpl", + "module_path": "Data/Show.purs", + "declaration": "foreign import showStringImpl :: String -> String", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Show.purs#L96", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Show.showArrayImpl", + "module_path": "Data/Show.purs", + "declaration": "foreign import showArrayImpl :: forall a. (a -> String) -> Array a -> String", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Show.purs#L97", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.CodePoints._singleton", + "module_path": "Data/String/CodePoints.purs", + "declaration": "foreign import _singleton :: (CodePoint -> String) -> CodePoint -> String", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodePoints.purs#L91", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.CodePoints._fromCodePointArray", + "module_path": "Data/String/CodePoints.purs", + "declaration": "foreign import _fromCodePointArray :: (CodePoint -> String) -> Array CodePoint -> String", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodePoints.purs#L117", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.CodePoints._toCodePointArray", + "module_path": "Data/String/CodePoints.purs", + "declaration": "foreign import _toCodePointArray :: (String -> Array CodePoint) -> (String -> CodePoint) -> String -> Array CodePoint", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodePoints.purs#L136", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.CodePoints._codePointAt", + "module_path": "Data/String/CodePoints.purs", + "declaration": "foreign import _codePointAt :: (Int -> String -> Maybe CodePoint) -> (forall a. a -> Maybe a) -> (forall a. Maybe a) -> (String -> CodePoint) -> Int -> String -> Maybe CodePoint", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodePoints.purs#L166", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.CodePoints._countPrefix", + "module_path": "Data/String/CodePoints.purs", + "declaration": "foreign import _countPrefix :: ((CodePoint -> Boolean) -> String -> Int) -> (String -> CodePoint) -> (CodePoint -> Boolean) -> String -> Int", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodePoints.purs#L230", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.CodePoints._take", + "module_path": "Data/String/CodePoints.purs", + "declaration": "foreign import _take :: (Int -> String -> String) -> Int -> String -> String", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodePoints.purs#L331", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.CodePoints._unsafeCodePointAt0", + "module_path": "Data/String/CodePoints.purs", + "declaration": "foreign import _unsafeCodePointAt0 :: (String -> CodePoint) -> String -> CodePoint", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodePoints.purs#L421", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.CodeUnits.singleton", + "module_path": "Data/String/CodeUnits.purs", + "declaration": "foreign import singleton :: Char -> String", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodeUnits.purs#L83", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.CodeUnits.fromCharArray", + "module_path": "Data/String/CodeUnits.purs", + "declaration": "foreign import fromCharArray :: Array Char -> String", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodeUnits.purs#L90", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.CodeUnits.toCharArray", + "module_path": "Data/String/CodeUnits.purs", + "declaration": "foreign import toCharArray :: String -> Array Char", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodeUnits.purs#L97", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.CodeUnits._charAt", + "module_path": "Data/String/CodeUnits.purs", + "declaration": "foreign import _charAt :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Int -> String -> Maybe Char", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodeUnits.purs#L109", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.CodeUnits._toChar", + "module_path": "Data/String/CodeUnits.purs", + "declaration": "foreign import _toChar :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> String -> Maybe Char", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodeUnits.purs#L126", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.CodeUnits.length", + "module_path": "Data/String/CodeUnits.purs", + "declaration": "foreign import length :: String -> Int", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodeUnits.purs#L150", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.CodeUnits.countPrefix", + "module_path": "Data/String/CodeUnits.purs", + "declaration": "foreign import countPrefix :: (Char -> Boolean) -> String -> Int", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodeUnits.purs#L159", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.CodeUnits._indexOf", + "module_path": "Data/String/CodeUnits.purs", + "declaration": "foreign import _indexOf :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Pattern -> String -> Maybe Int", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodeUnits.purs#L172", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.CodeUnits._indexOfStartingAt", + "module_path": "Data/String/CodeUnits.purs", + "declaration": "foreign import _indexOfStartingAt :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Pattern -> Int -> String -> Maybe Int", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodeUnits.purs#L191", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.CodeUnits._lastIndexOf", + "module_path": "Data/String/CodeUnits.purs", + "declaration": "foreign import _lastIndexOf :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Pattern -> String -> Maybe Int", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodeUnits.purs#L210", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.CodeUnits._lastIndexOfStartingAt", + "module_path": "Data/String/CodeUnits.purs", + "declaration": "foreign import _lastIndexOfStartingAt :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Pattern -> Int -> String -> Maybe Int", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodeUnits.purs#L238", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.CodeUnits.take", + "module_path": "Data/String/CodeUnits.purs", + "declaration": "foreign import take :: Int -> String -> String", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodeUnits.purs#L252", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.CodeUnits.drop", + "module_path": "Data/String/CodeUnits.purs", + "declaration": "foreign import drop :: Int -> String -> String", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodeUnits.purs#L279", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.CodeUnits.slice", + "module_path": "Data/String/CodeUnits.purs", + "declaration": "foreign import slice :: Int -> Int -> String -> String", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodeUnits.purs#L311", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.CodeUnits.splitAt", + "module_path": "Data/String/CodeUnits.purs", + "declaration": "foreign import splitAt :: Int -> String -> { before :: String, after :: String }", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodeUnits.purs#L332", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.Common._localeCompare", + "module_path": "Data/String/Common.purs", + "declaration": "foreign import _localeCompare :: Ordering -> Ordering -> Ordering -> String -> String -> Ordering", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Common.purs#L37", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.Common.replace", + "module_path": "Data/String/Common.purs", + "declaration": "foreign import replace :: Pattern -> Replacement -> String -> String", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Common.purs#L50", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.Common.replaceAll", + "module_path": "Data/String/Common.purs", + "declaration": "foreign import replaceAll :: Pattern -> Replacement -> String -> String", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Common.purs#L57", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.Common.split", + "module_path": "Data/String/Common.purs", + "declaration": "foreign import split :: Pattern -> String -> Array String", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Common.purs#L65", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.Common.toLower", + "module_path": "Data/String/Common.purs", + "declaration": "foreign import toLower :: String -> String", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Common.purs#L72", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.Common.toUpper", + "module_path": "Data/String/Common.purs", + "declaration": "foreign import toUpper :: String -> String", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Common.purs#L79", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.Common.trim", + "module_path": "Data/String/Common.purs", + "declaration": "foreign import trim :: String -> String", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Common.purs#L88", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.Common.joinWith", + "module_path": "Data/String/Common.purs", + "declaration": "foreign import joinWith :: String -> Array String -> String", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Common.purs#L96", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.Regex.showRegexImpl", + "module_path": "Data/String/Regex.purs", + "declaration": "foreign import showRegexImpl :: Regex -> String", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Regex.purs#L31", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.Regex.regexImpl", + "module_path": "Data/String/Regex.purs", + "declaration": "foreign import regexImpl :: (String -> Either String Regex) -> (Regex -> Either String Regex) -> String -> String -> Either String Regex", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Regex.purs#L36", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.Regex.source", + "module_path": "Data/String/Regex.purs", + "declaration": "foreign import source :: Regex -> String", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Regex.purs#L49", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.Regex.flagsImpl", + "module_path": "Data/String/Regex.purs", + "declaration": "foreign import flagsImpl :: Regex -> RegexFlagsRec", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Regex.purs#L56", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.Regex.test", + "module_path": "Data/String/Regex.purs", + "declaration": "foreign import test :: Regex -> String -> Boolean", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Regex.purs#L82", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.Regex._match", + "module_path": "Data/String/Regex.purs", + "declaration": "foreign import _match :: (forall r. r -> Maybe r) -> (forall r. Maybe r) -> Regex -> String -> Maybe (NonEmptyArray (Maybe String))", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Regex.purs#L84", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.Regex.replace", + "module_path": "Data/String/Regex.purs", + "declaration": "foreign import replace :: Regex -> String -> String -> String", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Regex.purs#L101", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.Regex._replaceBy", + "module_path": "Data/String/Regex.purs", + "declaration": "foreign import _replaceBy :: (forall r. r -> Maybe r) -> (forall r. Maybe r) -> Regex -> (String -> Array (Maybe String) -> String) -> String -> String", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Regex.purs#L103", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.Regex._search", + "module_path": "Data/String/Regex.purs", + "declaration": "foreign import _search :: (forall r. r -> Maybe r) -> (forall r. Maybe r) -> Regex -> String -> Maybe Int", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Regex.purs#L118", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.Regex.split", + "module_path": "Data/String/Regex.purs", + "declaration": "foreign import split :: Regex -> String -> Array String", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Regex.purs#L131", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.Unsafe.charAt", + "module_path": "Data/String/Unsafe.purs", + "declaration": "foreign import charAt :: Int -> String -> Char", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Unsafe.purs#L10", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.String.Unsafe.char", + "module_path": "Data/String/Unsafe.purs", + "declaration": "foreign import char :: String -> Char", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Unsafe.purs#L15", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Symbol.unsafeCoerce", + "module_path": "Data/Symbol.purs", + "declaration": "foreign import unsafeCoerce :: forall a b. a -> b", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Symbol.purs#L14", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Traversable.traverseArrayImpl", + "module_path": "Data/Traversable.purs", + "declaration": "foreign import traverseArrayImpl :: forall m a b . (forall x y. m (x -> y) -> m x -> m y) -> (forall x y. (x -> y) -> m x -> m y) -> (forall x. x -> m x) -> (a -> m b) -> Array a -> m (Array b)", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/Traversable.purs#L106", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Unfoldable.unfoldrArrayImpl", + "module_path": "Data/Unfoldable.purs", + "declaration": "foreign import unfoldrArrayImpl :: forall a b . (forall x. Maybe x -> Boolean) -> (forall x. Maybe x -> x) -> (forall x y. Tuple x y -> x) -> (forall x y. Tuple x y -> y) -> (b -> Maybe (Tuple a b)) -> b -> Array a", + "upstream_url": "https://github.com/purescript/purescript-unfoldable/blob/493dfe04ed590e20d8f69079df2f58486882748d/src/Data/Unfoldable.purs#L47", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Data.Unfoldable1.unfoldr1ArrayImpl", + "module_path": "Data/Unfoldable1.purs", + "declaration": "foreign import unfoldr1ArrayImpl :: forall a b . (forall x. Maybe x -> Boolean) -> (forall x. Maybe x -> x) -> (forall x y. Tuple x y -> x) -> (forall x y. Tuple x y -> y) -> (b -> Tuple a (Maybe b)) -> b -> Array a", + "upstream_url": "https://github.com/purescript/purescript-unfoldable/blob/493dfe04ed590e20d8f69079df2f58486882748d/src/Data/Unfoldable1.purs#L48", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Effect.Console.time", + "module_path": "Effect/Console.purs", + "declaration": "foreign import time :: String -> Effect Unit", + "upstream_url": "https://github.com/purescript/purescript-console/blob/3b83d7b792d03872afeea5e62b4f686ab0f09842/src/Effect/Console.purs#L59", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Effect.Console.timeLog", + "module_path": "Effect/Console.purs", + "declaration": "foreign import timeLog :: String -> Effect Unit", + "upstream_url": "https://github.com/purescript/purescript-console/blob/3b83d7b792d03872afeea5e62b4f686ab0f09842/src/Effect/Console.purs#L62", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Effect.Console.timeEnd", + "module_path": "Effect/Console.purs", + "declaration": "foreign import timeEnd :: String -> Effect Unit", + "upstream_url": "https://github.com/purescript/purescript-console/blob/3b83d7b792d03872afeea5e62b4f686ab0f09842/src/Effect/Console.purs#L65", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Effect.Console.clear", + "module_path": "Effect/Console.purs", + "declaration": "foreign import clear :: Effect Unit", + "upstream_url": "https://github.com/purescript/purescript-console/blob/3b83d7b792d03872afeea5e62b4f686ab0f09842/src/Effect/Console.purs#L68", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Effect.Ref._new", + "module_path": "Effect/Ref.purs", + "declaration": "foreign import _new :: forall s. s -> Effect (Ref s)", + "upstream_url": "https://github.com/purescript/purescript-refs/blob/f8e6216da4cb9309fde1f20cd6f69ac3a3b7f9e8/src/Effect/Ref.purs#L44", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Effect.Ref.newWithSelf", + "module_path": "Effect/Ref.purs", + "declaration": "foreign import newWithSelf :: forall s. (Ref s -> s) -> Effect (Ref s)", + "upstream_url": "https://github.com/purescript/purescript-refs/blob/f8e6216da4cb9309fde1f20cd6f69ac3a3b7f9e8/src/Effect/Ref.purs#L51", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Effect.Ref.read", + "module_path": "Effect/Ref.purs", + "declaration": "foreign import read :: forall s. Ref s -> Effect s", + "upstream_url": "https://github.com/purescript/purescript-refs/blob/f8e6216da4cb9309fde1f20cd6f69ac3a3b7f9e8/src/Effect/Ref.purs#L54", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Effect.Ref.modifyImpl", + "module_path": "Effect/Ref.purs", + "declaration": "foreign import modifyImpl :: forall s b. (s -> { state :: s, value :: b }) -> Ref s -> Effect b", + "upstream_url": "https://github.com/purescript/purescript-refs/blob/f8e6216da4cb9309fde1f20cd6f69ac3a3b7f9e8/src/Effect/Ref.purs#L61", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Effect.Ref.write", + "module_path": "Effect/Ref.purs", + "declaration": "foreign import write :: forall s. s -> Ref s -> Effect Unit", + "upstream_url": "https://github.com/purescript/purescript-refs/blob/f8e6216da4cb9309fde1f20cd6f69ac3a3b7f9e8/src/Effect/Ref.purs#L73", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Effect.Uncurried.mkEffectFn1", + "module_path": "Effect/Uncurried.purs", + "declaration": "foreign import mkEffectFn1 :: forall a r. (a -> Effect r) -> EffectFn1 a r", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Uncurried.purs#L178", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": false + }, + { + "value": "Effect.Uncurried.mkEffectFn2", + "module_path": "Effect/Uncurried.purs", + "declaration": "foreign import mkEffectFn2 :: forall a b r. (a -> b -> Effect r) -> EffectFn2 a b r", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Uncurried.purs#L180", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": false + }, + { + "value": "Effect.Uncurried.mkEffectFn3", + "module_path": "Effect/Uncurried.purs", + "declaration": "foreign import mkEffectFn3 :: forall a b c r. (a -> b -> c -> Effect r) -> EffectFn3 a b c r", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Uncurried.purs#L182", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": false + }, + { + "value": "Effect.Uncurried.mkEffectFn4", + "module_path": "Effect/Uncurried.purs", + "declaration": "foreign import mkEffectFn4 :: forall a b c d r. (a -> b -> c -> d -> Effect r) -> EffectFn4 a b c d r", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Uncurried.purs#L184", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": false + }, + { + "value": "Effect.Uncurried.mkEffectFn5", + "module_path": "Effect/Uncurried.purs", + "declaration": "foreign import mkEffectFn5 :: forall a b c d e r. (a -> b -> c -> d -> e -> Effect r) -> EffectFn5 a b c d e r", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Uncurried.purs#L186", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": false + }, + { + "value": "Effect.Uncurried.mkEffectFn6", + "module_path": "Effect/Uncurried.purs", + "declaration": "foreign import mkEffectFn6 :: forall a b c d e f r. (a -> b -> c -> d -> e -> f -> Effect r) -> EffectFn6 a b c d e f r", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Uncurried.purs#L188", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": false + }, + { + "value": "Effect.Uncurried.mkEffectFn7", + "module_path": "Effect/Uncurried.purs", + "declaration": "foreign import mkEffectFn7 :: forall a b c d e f g r. (a -> b -> c -> d -> e -> f -> g -> Effect r) -> EffectFn7 a b c d e f g r", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Uncurried.purs#L190", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": false + }, + { + "value": "Effect.Uncurried.mkEffectFn8", + "module_path": "Effect/Uncurried.purs", + "declaration": "foreign import mkEffectFn8 :: forall a b c d e f g h r. (a -> b -> c -> d -> e -> f -> g -> h -> Effect r) -> EffectFn8 a b c d e f g h r", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Uncurried.purs#L192", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": false + }, + { + "value": "Effect.Uncurried.mkEffectFn9", + "module_path": "Effect/Uncurried.purs", + "declaration": "foreign import mkEffectFn9 :: forall a b c d e f g h i r. (a -> b -> c -> d -> e -> f -> g -> h -> i -> Effect r) -> EffectFn9 a b c d e f g h i r", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Uncurried.purs#L194", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": false + }, + { + "value": "Effect.Uncurried.mkEffectFn10", + "module_path": "Effect/Uncurried.purs", + "declaration": "foreign import mkEffectFn10 :: forall a b c d e f g h i j r. (a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> Effect r) -> EffectFn10 a b c d e f g h i j r", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Uncurried.purs#L196", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": false + }, + { + "value": "Effect.Uncurried.runEffectFn1", + "module_path": "Effect/Uncurried.purs", + "declaration": "foreign import runEffectFn1 :: forall a r. EffectFn1 a r -> a -> Effect r", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Uncurried.purs#L199", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": false + }, + { + "value": "Effect.Uncurried.runEffectFn2", + "module_path": "Effect/Uncurried.purs", + "declaration": "foreign import runEffectFn2 :: forall a b r. EffectFn2 a b r -> a -> b -> Effect r", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Uncurried.purs#L201", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": false + }, + { + "value": "Effect.Uncurried.runEffectFn3", + "module_path": "Effect/Uncurried.purs", + "declaration": "foreign import runEffectFn3 :: forall a b c r. EffectFn3 a b c r -> a -> b -> c -> Effect r", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Uncurried.purs#L203", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": false + }, + { + "value": "Effect.Uncurried.runEffectFn4", + "module_path": "Effect/Uncurried.purs", + "declaration": "foreign import runEffectFn4 :: forall a b c d r. EffectFn4 a b c d r -> a -> b -> c -> d -> Effect r", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Uncurried.purs#L205", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": false + }, + { + "value": "Effect.Uncurried.runEffectFn5", + "module_path": "Effect/Uncurried.purs", + "declaration": "foreign import runEffectFn5 :: forall a b c d e r. EffectFn5 a b c d e r -> a -> b -> c -> d -> e -> Effect r", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Uncurried.purs#L207", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": false + }, + { + "value": "Effect.Uncurried.runEffectFn6", + "module_path": "Effect/Uncurried.purs", + "declaration": "foreign import runEffectFn6 :: forall a b c d e f r. EffectFn6 a b c d e f r -> a -> b -> c -> d -> e -> f -> Effect r", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Uncurried.purs#L209", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": false + }, + { + "value": "Effect.Uncurried.runEffectFn7", + "module_path": "Effect/Uncurried.purs", + "declaration": "foreign import runEffectFn7 :: forall a b c d e f g r. EffectFn7 a b c d e f g r -> a -> b -> c -> d -> e -> f -> g -> Effect r", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Uncurried.purs#L211", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": false + }, + { + "value": "Effect.Uncurried.runEffectFn8", + "module_path": "Effect/Uncurried.purs", + "declaration": "foreign import runEffectFn8 :: forall a b c d e f g h r. EffectFn8 a b c d e f g h r -> a -> b -> c -> d -> e -> f -> g -> h -> Effect r", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Uncurried.purs#L213", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": false + }, + { + "value": "Effect.Uncurried.runEffectFn9", + "module_path": "Effect/Uncurried.purs", + "declaration": "foreign import runEffectFn9 :: forall a b c d e f g h i r. EffectFn9 a b c d e f g h i r -> a -> b -> c -> d -> e -> f -> g -> h -> i -> Effect r", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Uncurried.purs#L215", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": false + }, + { + "value": "Effect.Uncurried.runEffectFn10", + "module_path": "Effect/Uncurried.purs", + "declaration": "foreign import runEffectFn10 :: forall a b c d e f g h i j r. EffectFn10 a b c d e f g h i j r -> a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> Effect r", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Uncurried.purs#L217", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": false + }, + { + "value": "Effect.Unsafe.unsafePerformEffect", + "module_path": "Effect/Unsafe.purs", + "declaration": "foreign import unsafePerformEffect :: forall a. Effect a -> a", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Unsafe.purs#L8", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": false + }, + { + "value": "Partial.Unsafe._unsafePartial", + "module_path": "Partial/Unsafe.purs", + "declaration": "foreign import _unsafePartial :: forall a b. a -> b", + "upstream_url": "https://github.com/purescript/purescript-partial/blob/0fa0646f5ea1ec5f0c46dcbd770c705a6c9ad3ec/src/Partial/Unsafe.purs#L16", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Partial._crashWith", + "module_path": "Partial.purs", + "declaration": "foreign import _crashWith :: forall a. String -> a", + "upstream_url": "https://github.com/purescript/purescript-partial/blob/0fa0646f5ea1ec5f0c46dcbd770c705a6c9ad3ec/src/Partial.purs#L15", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Record.Unsafe.unsafeHas", + "module_path": "Record/Unsafe.purs", + "declaration": "foreign import unsafeHas :: forall r1. String -> Record r1 -> Boolean", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Record/Unsafe.purs#L10", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Record.Unsafe.unsafeGet", + "module_path": "Record/Unsafe.purs", + "declaration": "foreign import unsafeGet :: forall r a. String -> Record r -> a", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Record/Unsafe.purs#L15", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Record.Unsafe.unsafeSet", + "module_path": "Record/Unsafe.purs", + "declaration": "foreign import unsafeSet :: forall r1 r2 a. String -> a -> Record r1 -> Record r2", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Record/Unsafe.purs#L21", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Record.Unsafe.unsafeDelete", + "module_path": "Record/Unsafe.purs", + "declaration": "foreign import unsafeDelete :: forall r1 r2. String -> Record r1 -> Record r2", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Record/Unsafe.purs#L27", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Test.Assert.checkThrows", + "module_path": "Test/Assert.purs", + "declaration": "foreign import checkThrows :: forall a . (Unit -> a) -> Effect Boolean", + "upstream_url": "https://github.com/purescript/purescript-assert/blob/27c0edb57d2ee497eb5fab664f5601c35b613eda/src/Test/Assert.purs#L59", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": true + }, + { + "value": "Unsafe.Coerce.unsafeCoerce", + "module_path": "Unsafe/Coerce.purs", + "declaration": "foreign import unsafeCoerce :: forall a b. a -> b", + "upstream_url": "https://github.com/purescript/purescript-unsafe-coerce/blob/ab956f82e66e633f647fb3098e8ddd3ec58d689f/src/Unsafe/Coerce.purs#L27", + "target_implementation": "missing", + "all_stdlib_reproducer_reports_missing": false + } + ] +} diff --git a/docs/implementation/stdlib/vendor-restoration-2026-10-06/foreign-bindings.md b/docs/implementation/stdlib/vendor-restoration-2026-10-06/foreign-bindings.md new file mode 100644 index 00000000..2e3f0777 --- /dev/null +++ b/docs/implementation/stdlib/vendor-restoration-2026-10-06/foreign-bindings.md @@ -0,0 +1,59 @@ +# Ordinary foreign bindings awaiting target implementation + +Generated from the pinned source inventory. Every entry still lacks a target +implementation and runtime acceptance evidence. The full reproducer imports only +a subset of the inventory; absence from its errors does not establish support. + +| Official module | Missing bindings | Reported by full reproducer | +| --- | ---: | ---: | +| `Control/Apply.purs` | 1 | 1 | +| `Control/Bind.purs` | 1 | 1 | +| `Control/Extend.purs` | 1 | 1 | +| `Control/Monad/ST/Internal.purs` | 11 | 11 | +| `Control/Monad/ST/Uncurried.purs` | 20 | 20 | +| `Data/Array.purs` | 24 | 24 | +| `Data/Array/NonEmpty/Internal.purs` | 3 | 3 | +| `Data/Array/ST.purs` | 17 | 17 | +| `Data/Array/ST/Partial.purs` | 2 | 2 | +| `Data/Bounded.purs` | 6 | 6 | +| `Data/Enum.purs` | 2 | 2 | +| `Data/Eq.purs` | 6 | 6 | +| `Data/EuclideanRing.purs` | 4 | 4 | +| `Data/Foldable.purs` | 2 | 2 | +| `Data/Function/Uncurried.purs` | 20 | 20 | +| `Data/Functor.purs` | 1 | 1 | +| `Data/FunctorWithIndex.purs` | 1 | 1 | +| `Data/HeytingAlgebra.purs` | 3 | 3 | +| `Data/Int.purs` | 7 | 7 | +| `Data/Int/Bits.purs` | 7 | 7 | +| `Data/Lazy.purs` | 2 | 2 | +| `Data/Number.purs` | 25 | 25 | +| `Data/Number/Format.purs` | 4 | 4 | +| `Data/Ord.purs` | 6 | 6 | +| `Data/Reflectable.purs` | 1 | 1 | +| `Data/Ring.purs` | 2 | 2 | +| `Data/Semigroup.purs` | 2 | 2 | +| `Data/Semiring.purs` | 4 | 4 | +| `Data/Show.purs` | 5 | 5 | +| `Data/Show/Generic.purs` | 1 | 1 | +| `Data/String/CodePoints.purs` | 7 | 7 | +| `Data/String/CodeUnits.purs` | 15 | 15 | +| `Data/String/Common.purs` | 8 | 8 | +| `Data/String/Regex.purs` | 10 | 10 | +| `Data/String/Unsafe.purs` | 2 | 2 | +| `Data/Symbol.purs` | 1 | 1 | +| `Data/Traversable.purs` | 1 | 1 | +| `Data/Unfoldable.purs` | 1 | 1 | +| `Data/Unfoldable1.purs` | 1 | 1 | +| `Effect/Console.purs` | 4 | 4 | +| `Effect/Ref.purs` | 5 | 5 | +| `Effect/Uncurried.purs` | 20 | 0 | +| `Effect/Unsafe.purs` | 1 | 0 | +| `Partial.purs` | 1 | 1 | +| `Partial/Unsafe.purs` | 1 | 1 | +| `Record/Unsafe.purs` | 4 | 4 | +| `Test/Assert.purs` | 1 | 1 | +| `Unsafe/Coerce.purs` | 1 | 0 | + +See [foreign-bindings.json](foreign-bindings.json) for each original declaration +and pinned upstream source link. diff --git a/docs/implementation/stdlib/vendor-restoration-2026-10-06/inventory.json b/docs/implementation/stdlib/vendor-restoration-2026-10-06/inventory.json new file mode 100644 index 00000000..767b80cb --- /dev/null +++ b/docs/implementation/stdlib/vendor-restoration-2026-10-06/inventory.json @@ -0,0 +1,5101 @@ +{ + "schema_version": 1, + "compiler_revision": "67369ba012755b1586d1250fac6a7254e096bb03", + "counts": { + "identical": 201, + "modified": 5, + "platform_addition": 9, + "vendored_modules": 215, + "packages": 41, + "direct_self_recursions": 0, + "modules_with_direct_self_recursions": 0, + "upstream_modules_absent_from_vendor": [], + "upstream_value_foreign_declarations": { + "foreign_declaration_retained": 275, + "declaration_removed": 1, + "nonrecursive_replacement": 12 + } + }, + "packages": [ + { + "name": "purescript-arrays", + "checkout": "/private/tmp/ps-pkgs/purescript-arrays", + "commit": "6554b3d9c1ebb871477ffa88c2f3850d714b42b0", + "tag": "v7.3.0", + "remote": "https://github.com/purescript/purescript-arrays.git" + }, + { + "name": "purescript-bifunctors", + "checkout": "/private/tmp/ps-pkgs/purescript-bifunctors", + "commit": "d35e0f1e5a33d59a226859b6bc53075fb58ced87", + "tag": "v6.1.0", + "remote": "https://github.com/purescript/purescript-bifunctors.git" + }, + { + "name": "purescript-const", + "checkout": "/private/tmp/ps-pkgs/purescript-const", + "commit": "ab9570cf2b6e67f7e441178211db1231cfd75c37", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-const.git" + }, + { + "name": "purescript-contravariant", + "checkout": "/private/tmp/ps-pkgs/purescript-contravariant", + "commit": "9ad3e105b8855bcc25f4e0893c784789d05a58de", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-contravariant.git" + }, + { + "name": "purescript-control", + "checkout": "/private/tmp/ps-pkgs/purescript-control", + "commit": "a6033808790879a17b2729e73747a9ed3fb2264e", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-control.git" + }, + { + "name": "purescript-distributive", + "checkout": "/private/tmp/ps-pkgs/purescript-distributive", + "commit": "6005e513642e855ebf6f884d24a35c2803ca252a", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-distributive.git" + }, + { + "name": "purescript-either", + "checkout": "/private/tmp/ps-pkgs/purescript-either", + "commit": "af655a04ed2fd694b6688af39ee20d7907ad0763", + "tag": "v6.1.0", + "remote": "https://github.com/purescript/purescript-either.git" + }, + { + "name": "purescript-enums", + "checkout": "/private/tmp/ps-pkgs/purescript-enums", + "commit": "cd373c580b69fdc00e412bddbc299adabe242cc5", + "tag": "v6.0.1", + "remote": "https://github.com/purescript/purescript-enums.git" + }, + { + "name": "purescript-exists", + "checkout": "/private/tmp/ps-pkgs/purescript-exists", + "commit": "f765b4ace7869c27b9c05949e18c843881f9173b", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-exists.git" + }, + { + "name": "purescript-filterable", + "checkout": "/private/tmp/ps-pkgs/purescript-filterable", + "commit": "7c5b8c72779997f2b17d12ce478ff81e7ddda285", + "tag": "v5.0.0", + "remote": "https://github.com/purescript/purescript-filterable.git" + }, + { + "name": "purescript-foldable-traversable", + "checkout": "/private/tmp/ps-pkgs/purescript-foldable-traversable", + "commit": "b3926f870532d287ea59e2d5cd3873b81ef2a93a", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-foldable-traversable.git" + }, + { + "name": "purescript-functions", + "checkout": "/private/tmp/ps-pkgs/purescript-functions", + "commit": "f626f20580483977c5b27a01aac6471e28aff367", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-functions.git" + }, + { + "name": "purescript-functors", + "checkout": "/private/tmp/ps-pkgs/purescript-functors", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "tag": "v5.0.0", + "remote": "https://github.com/purescript/purescript-functors.git" + }, + { + "name": "purescript-gen", + "checkout": "/private/tmp/ps-pkgs/purescript-gen", + "commit": "9fbcc2a1261c32e30d79c5418edef4d96fe76931", + "tag": "v4.0.0", + "remote": "https://github.com/purescript/purescript-gen.git" + }, + { + "name": "purescript-identity", + "checkout": "/private/tmp/ps-pkgs/purescript-identity", + "commit": "ef6768f8a52ab0bc943a85f5761ba07c257f639f", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-identity.git" + }, + { + "name": "purescript-integers", + "checkout": "/private/tmp/ps-pkgs/purescript-integers", + "commit": "54d712b25c594833083d15dc9ff2418eb9c52822", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-integers.git" + }, + { + "name": "purescript-invariant", + "checkout": "/private/tmp/ps-pkgs/purescript-invariant", + "commit": "1d2a196d51e90623adb88496c2cfd759c6736894", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-invariant.git" + }, + { + "name": "purescript-lazy", + "checkout": "/private/tmp/ps-pkgs/purescript-lazy", + "commit": "48347841226b27af5205a1a8ec71e27a93ce86fd", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-lazy.git" + }, + { + "name": "purescript-lists", + "checkout": "/private/tmp/ps-pkgs/purescript-lists", + "commit": "b113451e5b41cad87d669a3165f955c71cd863e2", + "tag": "v7.0.0", + "remote": "https://github.com/purescript/purescript-lists.git" + }, + { + "name": "purescript-maybe", + "checkout": "/private/tmp/ps-pkgs/purescript-maybe", + "commit": "c6f98ac1088766287106c5d9c8e30e7648d36786", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-maybe.git" + }, + { + "name": "purescript-newtype", + "checkout": "/private/tmp/ps-pkgs/purescript-newtype", + "commit": "29d8e6dd77aec2c975c948364ec3faf26e14ee7b", + "tag": "v5.0.0", + "remote": "https://github.com/purescript/purescript-newtype.git" + }, + { + "name": "purescript-nonempty", + "checkout": "/private/tmp/ps-pkgs/purescript-nonempty", + "commit": "28150ecc7419238b187abd609a92a645273348bb", + "tag": "v7.0.0", + "remote": "https://github.com/purescript/purescript-nonempty.git" + }, + { + "name": "purescript-numbers", + "checkout": "/private/tmp/ps-pkgs/purescript-numbers", + "commit": "27d54effdd2c0e7a86fe356b1cd813dca5981c2d", + "tag": "v9.0.1", + "remote": "https://github.com/purescript/purescript-numbers.git" + }, + { + "name": "purescript-ordered-collections", + "checkout": "/private/tmp/ps-pkgs/purescript-ordered-collections", + "commit": "b9c8ec72b53ee56d6cb6dc962d5ad7ea4708b438", + "tag": "v3.2.0", + "remote": "https://github.com/purescript/purescript-ordered-collections.git" + }, + { + "name": "purescript-orders", + "checkout": "/private/tmp/ps-pkgs/purescript-orders", + "commit": "f86db621ec5eef1274145f8b1fd8ebbfe0ed4a2c", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-orders.git" + }, + { + "name": "purescript-partial", + "checkout": "/private/tmp/ps-pkgs/purescript-partial", + "commit": "0fa0646f5ea1ec5f0c46dcbd770c705a6c9ad3ec", + "tag": "v4.0.0", + "remote": "https://github.com/purescript/purescript-partial.git" + }, + { + "name": "purescript-profunctor", + "checkout": "/private/tmp/ps-pkgs/purescript-profunctor", + "commit": "17f927ce69710085f2bbefc9a37506828d1ebc5a", + "tag": "v6.0.1", + "remote": "https://github.com/purescript/purescript-profunctor.git" + }, + { + "name": "purescript-refs", + "checkout": "/private/tmp/ps-pkgs/purescript-refs", + "commit": "f8e6216da4cb9309fde1f20cd6f69ac3a3b7f9e8", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-refs.git" + }, + { + "name": "purescript-safe-coerce", + "checkout": "/private/tmp/ps-pkgs/purescript-safe-coerce", + "commit": "7fa799ae80a38b8d948efcb52608e58e198b3da7", + "tag": "v2.0.0", + "remote": "https://github.com/purescript/purescript-safe-coerce.git" + }, + { + "name": "purescript-st", + "checkout": "/private/tmp/ps-pkgs/purescript-st", + "commit": "fc2fe2972bb12e6a2bd3b295baf01577240c23ac", + "tag": "v6.2.0", + "remote": "https://github.com/purescript/purescript-st.git" + }, + { + "name": "purescript-strings", + "checkout": "/private/tmp/ps-pkgs/purescript-strings", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "tag": "v6.0.1", + "remote": "https://github.com/purescript/purescript-strings.git" + }, + { + "name": "purescript-tailrec", + "checkout": "/private/tmp/ps-pkgs/purescript-tailrec", + "commit": "5661a10afbd4849bd2e45139ea567beb40b20f9f", + "tag": "v6.1.0", + "remote": "https://github.com/purescript/purescript-tailrec.git" + }, + { + "name": "purescript-tuples", + "checkout": "/private/tmp/ps-pkgs/purescript-tuples", + "commit": "4f52da2729b448c8564369378f1232d8d2dc1d8b", + "tag": "v7.0.0", + "remote": "https://github.com/purescript/purescript-tuples.git" + }, + { + "name": "purescript-type-equality", + "checkout": "/private/tmp/ps-pkgs/purescript-type-equality", + "commit": "0525b7d39e0fbd81b4209518139fb8ab02695774", + "tag": "v4.0.1", + "remote": "https://github.com/purescript/purescript-type-equality.git" + }, + { + "name": "purescript-typelevel-prelude", + "checkout": "/private/tmp/ps-pkgs/purescript-typelevel-prelude", + "commit": "dca2fe3c8cfd5527d4fe70c4bedfda30148405bf", + "tag": "v7.0.0", + "remote": "https://github.com/purescript/purescript-typelevel-prelude.git" + }, + { + "name": "purescript-unfoldable", + "checkout": "/private/tmp/ps-pkgs/purescript-unfoldable", + "commit": "493dfe04ed590e20d8f69079df2f58486882748d", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-unfoldable.git" + }, + { + "name": "purescript-unsafe-coerce", + "checkout": "/private/tmp/ps-pkgs/purescript-unsafe-coerce", + "commit": "ab956f82e66e633f647fb3098e8ddd3ec58d689f", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-unsafe-coerce.git" + }, + { + "name": "purescript-prelude", + "checkout": "/private/tmp/purescript-prelude", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "tag": "v6.0.1", + "remote": "https://github.com/purescript/purescript-prelude.git" + }, + { + "name": "purescript-assert", + "checkout": "/private/tmp/psrs-stdlib-audit-20261006/upstream/purescript-assert", + "commit": "27c0edb57d2ee497eb5fab664f5601c35b613eda", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-assert.git" + }, + { + "name": "purescript-console", + "checkout": "/private/tmp/psrs-stdlib-audit-20261006/upstream/purescript-console", + "commit": "3b83d7b792d03872afeea5e62b4f686ab0f09842", + "tag": "v6.0.0", + "remote": "https://github.com/purescript/purescript-console.git" + }, + { + "name": "purescript-effect", + "checkout": "/private/tmp/psrs-stdlib-audit-20261006/upstream/purescript-effect", + "commit": "a192ddb923027d426d6ea3d8deb030c9aa7c7dda", + "tag": "v4.0.0", + "remote": "https://github.com/purescript/purescript-effect.git" + } + ], + "modules": [ + { + "path": "Control/Alt.purs", + "vendored_sha256": "f02098c849efebd9eaeff8340215637a6152db97b10743a86dec6fae149061a1", + "vendored_lines": 42, + "self_recursions": [], + "package": "purescript-control", + "tag": "v6.0.0", + "commit": "a6033808790879a17b2729e73747a9ed3fb2264e", + "upstream_sha256": "f02098c849efebd9eaeff8340215637a6152db97b10743a86dec6fae149061a1", + "upstream_url": "https://github.com/purescript/purescript-control/blob/a6033808790879a17b2729e73747a9ed3fb2264e/src/Control/Alt.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Alternative.purs", + "vendored_sha256": "86e1c20be4b0d570377b607f53fdbce0c642065973b79b9211883dc2b9d9b329", + "vendored_lines": 50, + "self_recursions": [], + "package": "purescript-control", + "tag": "v6.0.0", + "commit": "a6033808790879a17b2729e73747a9ed3fb2264e", + "upstream_sha256": "86e1c20be4b0d570377b607f53fdbce0c642065973b79b9211883dc2b9d9b329", + "upstream_url": "https://github.com/purescript/purescript-control/blob/a6033808790879a17b2729e73747a9ed3fb2264e/src/Control/Alternative.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Applicative.purs", + "vendored_sha256": "4cf9abb5b98e7569c66375a725ce3815c57f020f1dc0513fc4bd853cd70b567b", + "vendored_lines": 70, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "4cf9abb5b98e7569c66375a725ce3815c57f020f1dc0513fc4bd853cd70b567b", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Control/Applicative.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Apply.purs", + "vendored_sha256": "02712ac645e851672d659d9b21171514b6d727efa7de08461399d3e7e581859b", + "vendored_lines": 104, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "02712ac645e851672d659d9b21171514b6d727efa7de08461399d3e7e581859b", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Control/Apply.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "arrayApply", + "line": 63, + "declaration": "foreign import arrayApply :: forall a b. Array (a -> b) -> Array a -> Array b", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Biapplicative.purs", + "vendored_sha256": "5c3baecdf32b1e0eb8c871de70eac69aa558b2dca8710b7b3d84badb20ef00bb", + "vendored_lines": 12, + "self_recursions": [], + "package": "purescript-bifunctors", + "tag": "v6.1.0", + "commit": "d35e0f1e5a33d59a226859b6bc53075fb58ced87", + "upstream_sha256": "5c3baecdf32b1e0eb8c871de70eac69aa558b2dca8710b7b3d84badb20ef00bb", + "upstream_url": "https://github.com/purescript/purescript-bifunctors/blob/d35e0f1e5a33d59a226859b6bc53075fb58ced87/src/Control/Biapplicative.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Biapply.purs", + "vendored_sha256": "e2adb06728bb1a789579e3876c0a3894f1cd3ee7e5acef3268a13023a1733de0", + "vendored_lines": 59, + "self_recursions": [], + "package": "purescript-bifunctors", + "tag": "v6.1.0", + "commit": "d35e0f1e5a33d59a226859b6bc53075fb58ced87", + "upstream_sha256": "e2adb06728bb1a789579e3876c0a3894f1cd3ee7e5acef3268a13023a1733de0", + "upstream_url": "https://github.com/purescript/purescript-bifunctors/blob/d35e0f1e5a33d59a226859b6bc53075fb58ced87/src/Control/Biapply.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Bind.purs", + "vendored_sha256": "ea03e5ffa8c55c101018ceb9bf19fca9af792b49113a73be4a85867ef2c022f7", + "vendored_lines": 150, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "ea03e5ffa8c55c101018ceb9bf19fca9af792b49113a73be4a85867ef2c022f7", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Control/Bind.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "arrayBind", + "line": 97, + "declaration": "foreign import arrayBind :: forall a b. Array a -> (a -> Array b) -> Array b", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Category.purs", + "vendored_sha256": "9cd6a331c6e9a7e17f35adb8fd11ba9fbe4fd2034eed34979d9cfbef68fe4230", + "vendored_lines": 22, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "9cd6a331c6e9a7e17f35adb8fd11ba9fbe4fd2034eed34979d9cfbef68fe4230", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Control/Category.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Comonad.purs", + "vendored_sha256": "8ddf03de6841c4e570432c7186445ef912d9a63d1eb272e668657ab73d981521", + "vendored_lines": 21, + "self_recursions": [], + "package": "purescript-control", + "tag": "v6.0.0", + "commit": "a6033808790879a17b2729e73747a9ed3fb2264e", + "upstream_sha256": "8ddf03de6841c4e570432c7186445ef912d9a63d1eb272e668657ab73d981521", + "upstream_url": "https://github.com/purescript/purescript-control/blob/a6033808790879a17b2729e73747a9ed3fb2264e/src/Control/Comonad.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Extend.purs", + "vendored_sha256": "20d5e8394e1e755187e594cda0a3f63147fa69f703e18d8b99244d59c55d2900", + "vendored_lines": 59, + "self_recursions": [], + "package": "purescript-control", + "tag": "v6.0.0", + "commit": "a6033808790879a17b2729e73747a9ed3fb2264e", + "upstream_sha256": "20d5e8394e1e755187e594cda0a3f63147fa69f703e18d8b99244d59c55d2900", + "upstream_url": "https://github.com/purescript/purescript-control/blob/a6033808790879a17b2729e73747a9ed3fb2264e/src/Control/Extend.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "arrayExtend", + "line": 30, + "declaration": "foreign import arrayExtend :: forall a b. (Array a -> b) -> Array a -> Array b", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Lazy.purs", + "vendored_sha256": "05fce3e86c1c1b409f863368edb7991894c2c0d83ce99cdf7a3f84cdc30e9d0a", + "vendored_lines": 25, + "self_recursions": [], + "package": "purescript-control", + "tag": "v6.0.0", + "commit": "a6033808790879a17b2729e73747a9ed3fb2264e", + "upstream_sha256": "05fce3e86c1c1b409f863368edb7991894c2c0d83ce99cdf7a3f84cdc30e9d0a", + "upstream_url": "https://github.com/purescript/purescript-control/blob/a6033808790879a17b2729e73747a9ed3fb2264e/src/Control/Lazy.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Monad/Gen/Class.purs", + "vendored_sha256": "88fc916494c75e3309471a0852d0c740691570d68dd9dbb92ed4547e9e20f17a", + "vendored_lines": 29, + "self_recursions": [], + "package": "purescript-gen", + "tag": "v4.0.0", + "commit": "9fbcc2a1261c32e30d79c5418edef4d96fe76931", + "upstream_sha256": "88fc916494c75e3309471a0852d0c740691570d68dd9dbb92ed4547e9e20f17a", + "upstream_url": "https://github.com/purescript/purescript-gen/blob/9fbcc2a1261c32e30d79c5418edef4d96fe76931/src/Control/Monad/Gen/Class.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Monad/Gen/Common.purs", + "vendored_sha256": "d31ab1b6246acefd0e92f320483d413861e5727de6429d1b4ca391f8120e89a4", + "vendored_lines": 67, + "self_recursions": [], + "package": "purescript-gen", + "tag": "v4.0.0", + "commit": "9fbcc2a1261c32e30d79c5418edef4d96fe76931", + "upstream_sha256": "d31ab1b6246acefd0e92f320483d413861e5727de6429d1b4ca391f8120e89a4", + "upstream_url": "https://github.com/purescript/purescript-gen/blob/9fbcc2a1261c32e30d79c5418edef4d96fe76931/src/Control/Monad/Gen/Common.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Monad/Gen.purs", + "vendored_sha256": "1af7bc60432e7fb6f3e6e75da3b78be19bc2d1ad7e9cfcdcfcba4446ec317f1b", + "vendored_lines": 132, + "self_recursions": [], + "package": "purescript-gen", + "tag": "v4.0.0", + "commit": "9fbcc2a1261c32e30d79c5418edef4d96fe76931", + "upstream_sha256": "1af7bc60432e7fb6f3e6e75da3b78be19bc2d1ad7e9cfcdcfcba4446ec317f1b", + "upstream_url": "https://github.com/purescript/purescript-gen/blob/9fbcc2a1261c32e30d79c5418edef4d96fe76931/src/Control/Monad/Gen.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Monad/Rec/Class.purs", + "vendored_sha256": "fd2ea9d0000c0194b78949bcaf9c931dfc12c92de322f0cadf73d566c994ea2f", + "vendored_lines": 191, + "self_recursions": [], + "package": "purescript-tailrec", + "tag": "v6.1.0", + "commit": "5661a10afbd4849bd2e45139ea567beb40b20f9f", + "upstream_sha256": "fd2ea9d0000c0194b78949bcaf9c931dfc12c92de322f0cadf73d566c994ea2f", + "upstream_url": "https://github.com/purescript/purescript-tailrec/blob/5661a10afbd4849bd2e45139ea567beb40b20f9f/src/Control/Monad/Rec/Class.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Monad/ST/Class.purs", + "vendored_sha256": "227bf813cc68b69b167ade39f2531d0ba45dc259252001bb97758e08bc93247d", + "vendored_lines": 17, + "self_recursions": [], + "package": "purescript-st", + "tag": "v6.2.0", + "commit": "fc2fe2972bb12e6a2bd3b295baf01577240c23ac", + "upstream_sha256": "227bf813cc68b69b167ade39f2531d0ba45dc259252001bb97758e08bc93247d", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Class.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Monad/ST/Global.purs", + "vendored_sha256": "4cb7ab62ecd6c6dfacfce32920bf5d9a3dc0f7ce7d9a25a16b453d0a4fc0f9e3", + "vendored_lines": 18, + "self_recursions": [], + "package": "purescript-st", + "tag": "v6.2.0", + "commit": "fc2fe2972bb12e6a2bd3b295baf01577240c23ac", + "upstream_sha256": "4cb7ab62ecd6c6dfacfce32920bf5d9a3dc0f7ce7d9a25a16b453d0a4fc0f9e3", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Global.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Monad/ST/Internal.purs", + "vendored_sha256": "00159fcd8f3fa41d7ba5b50c596043bc46859f95f8d877b9ff18dbc37c7fad99", + "vendored_lines": 136, + "self_recursions": [], + "package": "purescript-st", + "tag": "v6.2.0", + "commit": "fc2fe2972bb12e6a2bd3b295baf01577240c23ac", + "upstream_sha256": "00159fcd8f3fa41d7ba5b50c596043bc46859f95f8d877b9ff18dbc37c7fad99", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Internal.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "map_", + "line": 38, + "declaration": "foreign import map_ :: forall r a b. (a -> b) -> ST r a -> ST r b", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "pure_", + "line": 40, + "declaration": "foreign import pure_ :: forall r a. a -> ST r a", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "bind_", + "line": 42, + "declaration": "foreign import bind_ :: forall r a b. ST r a -> (a -> ST r b) -> ST r b", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "run", + "line": 89, + "declaration": "foreign import run :: forall a. (forall r. ST r a) -> a", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "while", + "line": 96, + "declaration": "foreign import while :: forall r a. ST r Boolean -> ST r a -> ST r Unit", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "for", + "line": 102, + "declaration": "foreign import for :: forall r a. Int -> Int -> (Int -> ST r a) -> ST r Unit", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "foreach", + "line": 108, + "declaration": "foreign import foreach :: forall r a. Array a -> (a -> ST r Unit) -> ST r Unit", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "new", + "line": 117, + "declaration": "foreign import new :: forall a r. a -> ST r (STRef r a)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "read", + "line": 120, + "declaration": "foreign import read :: forall a r. STRef r a -> ST r a", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "modifyImpl", + "line": 128, + "declaration": "foreign import modifyImpl :: forall r a b. (a -> { state :: a, value :: b }) -> STRef r a -> ST r b", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "write", + "line": 136, + "declaration": "foreign import write :: forall a r. a -> STRef r a -> ST r a", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Monad/ST/Ref.purs", + "vendored_sha256": "cd155ad39db037f2b26d422461da697bc3b7b5fe9bfc93702df55cba38158d3a", + "vendored_lines": 3, + "self_recursions": [], + "package": "purescript-st", + "tag": "v6.2.0", + "commit": "fc2fe2972bb12e6a2bd3b295baf01577240c23ac", + "upstream_sha256": "cd155ad39db037f2b26d422461da697bc3b7b5fe9bfc93702df55cba38158d3a", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Ref.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Monad/ST/Uncurried.purs", + "vendored_sha256": "bd2411e5e23d8b446ef2f6b924422071eb99c6c629395df21eb9821a341988a6", + "vendored_lines": 101, + "self_recursions": [], + "package": "purescript-st", + "tag": "v6.2.0", + "commit": "fc2fe2972bb12e6a2bd3b295baf01577240c23ac", + "upstream_sha256": "bd2411e5e23d8b446ef2f6b924422071eb99c6c629395df21eb9821a341988a6", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST/Uncurried.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "mkSTFn1", + "line": 61, + "declaration": "foreign import mkSTFn1 :: forall a t r. (a -> ST t r) -> STFn1 a t r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkSTFn2", + "line": 63, + "declaration": "foreign import mkSTFn2 :: forall a b t r. (a -> b -> ST t r) -> STFn2 a b t r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkSTFn3", + "line": 65, + "declaration": "foreign import mkSTFn3 :: forall a b c t r. (a -> b -> c -> ST t r) -> STFn3 a b c t r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkSTFn4", + "line": 67, + "declaration": "foreign import mkSTFn4 :: forall a b c d t r. (a -> b -> c -> d -> ST t r) -> STFn4 a b c d t r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkSTFn5", + "line": 69, + "declaration": "foreign import mkSTFn5 :: forall a b c d e t r. (a -> b -> c -> d -> e -> ST t r) -> STFn5 a b c d e t r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkSTFn6", + "line": 71, + "declaration": "foreign import mkSTFn6 :: forall a b c d e f t r. (a -> b -> c -> d -> e -> f -> ST t r) -> STFn6 a b c d e f t r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkSTFn7", + "line": 73, + "declaration": "foreign import mkSTFn7 :: forall a b c d e f g t r. (a -> b -> c -> d -> e -> f -> g -> ST t r) -> STFn7 a b c d e f g t r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkSTFn8", + "line": 75, + "declaration": "foreign import mkSTFn8 :: forall a b c d e f g h t r. (a -> b -> c -> d -> e -> f -> g -> h -> ST t r) -> STFn8 a b c d e f g h t r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkSTFn9", + "line": 77, + "declaration": "foreign import mkSTFn9 :: forall a b c d e f g h i t r. (a -> b -> c -> d -> e -> f -> g -> h -> i -> ST t r) -> STFn9 a b c d e f g h i t r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkSTFn10", + "line": 79, + "declaration": "foreign import mkSTFn10 :: forall a b c d e f g h i j t r. (a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> ST t r) -> STFn10 a b c d e f g h i j t r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runSTFn1", + "line": 82, + "declaration": "foreign import runSTFn1 :: forall a t r. STFn1 a t r -> a -> ST t r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runSTFn2", + "line": 84, + "declaration": "foreign import runSTFn2 :: forall a b t r. STFn2 a b t r -> a -> b -> ST t r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runSTFn3", + "line": 86, + "declaration": "foreign import runSTFn3 :: forall a b c t r. STFn3 a b c t r -> a -> b -> c -> ST t r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runSTFn4", + "line": 88, + "declaration": "foreign import runSTFn4 :: forall a b c d t r. STFn4 a b c d t r -> a -> b -> c -> d -> ST t r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runSTFn5", + "line": 90, + "declaration": "foreign import runSTFn5 :: forall a b c d e t r. STFn5 a b c d e t r -> a -> b -> c -> d -> e -> ST t r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runSTFn6", + "line": 92, + "declaration": "foreign import runSTFn6 :: forall a b c d e f t r. STFn6 a b c d e f t r -> a -> b -> c -> d -> e -> f -> ST t r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runSTFn7", + "line": 94, + "declaration": "foreign import runSTFn7 :: forall a b c d e f g t r. STFn7 a b c d e f g t r -> a -> b -> c -> d -> e -> f -> g -> ST t r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runSTFn8", + "line": 96, + "declaration": "foreign import runSTFn8 :: forall a b c d e f g h t r. STFn8 a b c d e f g h t r -> a -> b -> c -> d -> e -> f -> g -> h -> ST t r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runSTFn9", + "line": 98, + "declaration": "foreign import runSTFn9 :: forall a b c d e f g h i t r. STFn9 a b c d e f g h i t r -> a -> b -> c -> d -> e -> f -> g -> h -> i -> ST t r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runSTFn10", + "line": 100, + "declaration": "foreign import runSTFn10 :: forall a b c d e f g h i j t r. STFn10 a b c d e f g h i j t r -> a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> ST t r", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Monad/ST.purs", + "vendored_sha256": "316ee75613cd7960cdaefdf6c26613c14badcc8a06ee2405617ed089072598a0", + "vendored_lines": 3, + "self_recursions": [], + "package": "purescript-st", + "tag": "v6.2.0", + "commit": "fc2fe2972bb12e6a2bd3b295baf01577240c23ac", + "upstream_sha256": "316ee75613cd7960cdaefdf6c26613c14badcc8a06ee2405617ed089072598a0", + "upstream_url": "https://github.com/purescript/purescript-st/blob/fc2fe2972bb12e6a2bd3b295baf01577240c23ac/src/Control/Monad/ST.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Monad.purs", + "vendored_sha256": "0bad91df889b370157f300b5e5e0241272d9149380a6c5322a81af58a6fdc67c", + "vendored_lines": 86, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "0bad91df889b370157f300b5e5e0241272d9149380a6c5322a81af58a6fdc67c", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Control/Monad.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/MonadPlus.purs", + "vendored_sha256": "4a5f2eeb965af319ffb1427c1571235ca3de44881e19aa83f2cb12e8404df68f", + "vendored_lines": 32, + "self_recursions": [], + "package": "purescript-control", + "tag": "v6.0.0", + "commit": "a6033808790879a17b2729e73747a9ed3fb2264e", + "upstream_sha256": "4a5f2eeb965af319ffb1427c1571235ca3de44881e19aa83f2cb12e8404df68f", + "upstream_url": "https://github.com/purescript/purescript-control/blob/a6033808790879a17b2729e73747a9ed3fb2264e/src/Control/MonadPlus.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Plus.purs", + "vendored_sha256": "308ba7c0a3a65e9b81cf2cc6f0a7d8e4f77043c1c2c481c04b5ab22ccf991f19", + "vendored_lines": 27, + "self_recursions": [], + "package": "purescript-control", + "tag": "v6.0.0", + "commit": "a6033808790879a17b2729e73747a9ed3fb2264e", + "upstream_sha256": "308ba7c0a3a65e9b81cf2cc6f0a7d8e4f77043c1c2c481c04b5ab22ccf991f19", + "upstream_url": "https://github.com/purescript/purescript-control/blob/a6033808790879a17b2729e73747a9ed3fb2264e/src/Control/Plus.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Control/Semigroupoid.purs", + "vendored_sha256": "aefe8332e035b2f990e25981ac56abb63a71efed5d5e986657483dd51c04834a", + "vendored_lines": 25, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "aefe8332e035b2f990e25981ac56abb63a71efed5d5e986657483dd51c04834a", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Control/Semigroupoid.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Array/NonEmpty/Internal.purs", + "vendored_sha256": "59ce6b9e860ccdd2a089fb055008b629cd9362bb3a7f703e6bd1fb2f7a642ae6", + "vendored_lines": 84, + "self_recursions": [], + "package": "purescript-arrays", + "tag": "v7.3.0", + "commit": "6554b3d9c1ebb871477ffa88c2f3850d714b42b0", + "upstream_sha256": "59ce6b9e860ccdd2a089fb055008b629cd9362bb3a7f703e6bd1fb2f7a642ae6", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/NonEmpty/Internal.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "foldr1Impl", + "line": 75, + "declaration": "foreign import foldr1Impl :: forall a. Fn2 (a -> a -> a) (NonEmptyArray a) a", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "foldl1Impl", + "line": 76, + "declaration": "foreign import foldl1Impl :: forall a. Fn2 (a -> a -> a) (NonEmptyArray a) a", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "traverse1Impl", + "line": 78, + "declaration": "foreign import traverse1Impl :: forall m a b . Fn3 (forall a' b'. (m (a' -> b') -> m a' -> m b')) (forall a' b'. (a' -> b') -> m a' -> m b') (a -> m b) (NonEmptyArray a -> m (NonEmptyArray b))", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Array/NonEmpty.purs", + "vendored_sha256": "4e42eb0655fef616939cdf91f8e87b8c5e3581f34a538a2066cd9a644302b8d4", + "vendored_lines": 598, + "self_recursions": [], + "package": "purescript-arrays", + "tag": "v7.3.0", + "commit": "6554b3d9c1ebb871477ffa88c2f3850d714b42b0", + "upstream_sha256": "4e42eb0655fef616939cdf91f8e87b8c5e3581f34a538a2066cd9a644302b8d4", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/NonEmpty.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Array/Partial.purs", + "vendored_sha256": "21c2d565d728a650e1f0e3521570c518434a97aaad087893237d20c18fda8184", + "vendored_lines": 35, + "self_recursions": [], + "package": "purescript-arrays", + "tag": "v7.3.0", + "commit": "6554b3d9c1ebb871477ffa88c2f3850d714b42b0", + "upstream_sha256": "21c2d565d728a650e1f0e3521570c518434a97aaad087893237d20c18fda8184", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/Partial.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Array/ST/Iterator.purs", + "vendored_sha256": "96dd803b0c983a56714d1dbbf6d86193c245660e6acb4faffa2cec6f6e1d2583", + "vendored_lines": 80, + "self_recursions": [], + "package": "purescript-arrays", + "tag": "v7.3.0", + "commit": "6554b3d9c1ebb871477ffa88c2f3850d714b42b0", + "upstream_sha256": "96dd803b0c983a56714d1dbbf6d86193c245660e6acb4faffa2cec6f6e1d2583", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST/Iterator.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Array/ST/Partial.purs", + "vendored_sha256": "dd9dfaae9bd0942eb341324035ed96880dcf2e167033ef8164d700270af5957a", + "vendored_lines": 36, + "self_recursions": [], + "package": "purescript-arrays", + "tag": "v7.3.0", + "commit": "6554b3d9c1ebb871477ffa88c2f3850d714b42b0", + "upstream_sha256": "dd9dfaae9bd0942eb341324035ed96880dcf2e167033ef8164d700270af5957a", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST/Partial.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "peekImpl", + "line": 24, + "declaration": "foreign import peekImpl :: forall h a. STFn2 Int (STArray h a) h a", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "pokeImpl", + "line": 36, + "declaration": "foreign import pokeImpl :: forall h a. STFn3 Int a (STArray h a) h Unit", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Array/ST.purs", + "vendored_sha256": "4af1b59e242a89e3e5aa427c4486fce587173b3008f369d9d00de398a84ea4dc", + "vendored_lines": 262, + "self_recursions": [], + "package": "purescript-arrays", + "tag": "v7.3.0", + "commit": "6554b3d9c1ebb871477ffa88c2f3850d714b42b0", + "upstream_sha256": "4af1b59e242a89e3e5aa427c4486fce587173b3008f369d9d00de398a84ea4dc", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array/ST.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "unsafeFreezeImpl", + "line": 78, + "declaration": "foreign import unsafeFreezeImpl :: forall h a. STFn1 (STArray h a) h (Array a)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "unsafeThawImpl", + "line": 85, + "declaration": "foreign import unsafeThawImpl :: forall h a. STFn1 (Array a) h (STArray h a)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "new", + "line": 88, + "declaration": "foreign import new :: forall h a. ST h (STArray h a)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "thawImpl", + "line": 97, + "declaration": "foreign import thawImpl :: forall h a. STFn1 (Array a) h (STArray h a)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "cloneImpl", + "line": 106, + "declaration": "foreign import cloneImpl :: forall h a. STFn1 (STArray h a) h (STArray h a)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "shiftImpl", + "line": 117, + "declaration": "foreign import shiftImpl :: forall h a . STFn3 (forall b. b -> Maybe b) (forall b. Maybe b) (STArray h a) h (Maybe a)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "sortByImpl", + "line": 134, + "declaration": "foreign import sortByImpl :: forall a h . STFn3 (a -> a -> Ordering) (Ordering -> Int) (STArray h a) h (STArray h a)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "freezeImpl", + "line": 155, + "declaration": "foreign import freezeImpl :: forall h a. STFn1 (STArray h a) h (Array a)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "peekImpl", + "line": 165, + "declaration": "foreign import peekImpl :: forall h a r. STFn4 (a -> r) r Int (STArray h a) h r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "pokeImpl", + "line": 176, + "declaration": "foreign import pokeImpl :: forall h a. STFn3 Int a (STArray h a) h Boolean", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "lengthImpl", + "line": 178, + "declaration": "foreign import lengthImpl :: forall h a. STFn1 (STArray h a) h Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "popImpl", + "line": 188, + "declaration": "foreign import popImpl :: forall h a . STFn3 (forall b. b -> Maybe b) (forall b. Maybe b) (STArray h a) h (Maybe a)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "pushImpl", + "line": 197, + "declaration": "foreign import pushImpl :: forall h a. STFn2 a (STArray h a) h Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "pushAllImpl", + "line": 208, + "declaration": "foreign import pushAllImpl :: forall h a . STFn2 (Array a) (STArray h a) h Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "unshiftAllImpl", + "line": 226, + "declaration": "foreign import unshiftAllImpl :: forall h a . STFn2 (Array a) (STArray h a) h Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "spliceImpl", + "line": 248, + "declaration": "foreign import spliceImpl :: forall h a . STFn4 Int Int (Array a) (STArray h a) h (Array a)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "toAssocArrayImpl", + "line": 260, + "declaration": "foreign import toAssocArrayImpl :: forall h a . STFn1 (STArray h a) h (Array (Assoc a))", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Array.purs", + "vendored_sha256": "d336825bf660ee79f03c559eaa1a22f41ec5a194b8cce79c5b24877160c32455", + "vendored_lines": 1371, + "self_recursions": [], + "package": "purescript-arrays", + "tag": "v7.3.0", + "commit": "6554b3d9c1ebb871477ffa88c2f3850d714b42b0", + "upstream_sha256": "d336825bf660ee79f03c559eaa1a22f41ec5a194b8cce79c5b24877160c32455", + "upstream_url": "https://github.com/purescript/purescript-arrays/blob/6554b3d9c1ebb871477ffa88c2f3850d714b42b0/src/Data/Array.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "fromFoldableImpl", + "line": 177, + "declaration": "foreign import fromFoldableImpl :: forall f a . Fn2 (forall b. (a -> b -> b) -> b -> f a -> b) (f a) (Array a)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "rangeImpl", + "line": 195, + "declaration": "foreign import rangeImpl :: Fn2 Int Int (Array Int)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "replicateImpl", + "line": 204, + "declaration": "foreign import replicateImpl :: forall a. Fn2 Int a (Array a)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "length", + "line": 243, + "declaration": "foreign import length :: forall a. Array a -> Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "unconsImpl", + "line": 373, + "declaration": "foreign import unconsImpl :: forall a b . Fn3 (Unit -> b) (a -> Array a -> b) (Array a) b", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "indexImpl", + "line": 405, + "declaration": "foreign import indexImpl :: forall a . Fn4 (forall r. r -> Maybe r) (forall r. Maybe r) (Array a) Int (Maybe a)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "findMapImpl", + "line": 462, + "declaration": "foreign import findMapImpl :: forall a b . Fn4 (forall c. Maybe c) (forall c. Maybe c -> Boolean) (a -> Maybe b) (Array a) (Maybe b)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "findIndexImpl", + "line": 481, + "declaration": "foreign import findIndexImpl :: forall a . Fn4 (forall b. b -> Maybe b) (forall b. Maybe b) (a -> Boolean) (Array a) (Maybe Int)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "findLastIndexImpl", + "line": 500, + "declaration": "foreign import findLastIndexImpl :: forall a . Fn4 (forall b. b -> Maybe b) (forall b. Maybe b) (a -> Boolean) (Array a) (Maybe Int)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "_insertAt", + "line": 520, + "declaration": "foreign import _insertAt :: forall a . Fn5 (forall b. b -> Maybe b) (forall b. Maybe b) Int a (Array a) (Maybe (Array a))", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "_deleteAt", + "line": 541, + "declaration": "foreign import _deleteAt :: forall a . Fn4 (forall b. b -> Maybe b) (forall b. Maybe b) Int (Array a) (Maybe (Array a))", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "_updateAt", + "line": 561, + "declaration": "foreign import _updateAt :: forall a . Fn5 (forall b. b -> Maybe b) (forall b. Maybe b) Int a (Array a) (Maybe (Array a))", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "reverse", + "line": 642, + "declaration": "foreign import reverse :: forall a. Array a -> Array a", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "concat", + "line": 650, + "declaration": "foreign import concat :: forall a. Array (Array a) -> Array a", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "filterImpl", + "line": 673, + "declaration": "foreign import filterImpl :: forall a . Fn2 (a -> Boolean) (Array a) (Array a)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "partitionImpl", + "line": 692, + "declaration": "foreign import partitionImpl :: forall a . Fn2 (a -> Boolean) (Array a) { yes :: Array a, no :: Array a }", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "scanlImpl", + "line": 857, + "declaration": "foreign import scanlImpl :: forall a b. Fn3 (b -> a -> b) b (Array a) (Array b)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "scanrImpl", + "line": 870, + "declaration": "foreign import scanrImpl :: forall a b. Fn3 (a -> b -> b) b (Array a) (Array b)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "sortByImpl", + "line": 914, + "declaration": "foreign import sortByImpl :: forall a. Fn3 (a -> a -> Ordering) (Ordering -> Int) (Array a) (Array a)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "sliceImpl", + "line": 932, + "declaration": "foreign import sliceImpl :: forall a. Fn3 Int Int (Array a) (Array a)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "zipWithImpl", + "line": 1253, + "declaration": "foreign import zipWithImpl :: forall a b c . Fn3 (a -> b -> c) (Array a) (Array b) (Array c)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "anyImpl", + "line": 1322, + "declaration": "foreign import anyImpl :: forall a. Fn2 (a -> Boolean) (Array a) Boolean", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "allImpl", + "line": 1336, + "declaration": "foreign import allImpl :: forall a. Fn2 (a -> Boolean) (Array a) Boolean", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "unsafeIndexImpl", + "line": 1371, + "declaration": "foreign import unsafeIndexImpl :: forall a. Fn2 (Array a) Int a", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Bifoldable.purs", + "vendored_sha256": "ba0f8f91040f1e49a0ef10ad012eb9be475fa820332eb3d6bca9345c1a3c4446", + "vendored_lines": 198, + "self_recursions": [], + "package": "purescript-foldable-traversable", + "tag": "v6.0.0", + "commit": "b3926f870532d287ea59e2d5cd3873b81ef2a93a", + "upstream_sha256": "ba0f8f91040f1e49a0ef10ad012eb9be475fa820332eb3d6bca9345c1a3c4446", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/Bifoldable.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Bifunctor/Join.purs", + "vendored_sha256": "5db9d1533e678809804465de95637fa980d264314428d25f18e10b673bba1308", + "vendored_lines": 31, + "self_recursions": [], + "package": "purescript-bifunctors", + "tag": "v6.1.0", + "commit": "d35e0f1e5a33d59a226859b6bc53075fb58ced87", + "upstream_sha256": "5db9d1533e678809804465de95637fa980d264314428d25f18e10b673bba1308", + "upstream_url": "https://github.com/purescript/purescript-bifunctors/blob/d35e0f1e5a33d59a226859b6bc53075fb58ced87/src/Data/Bifunctor/Join.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Bifunctor.purs", + "vendored_sha256": "00c91e7e32934e59b1196a793247218670ed5ad6c69acb8ef4eec27db63a8e1a", + "vendored_lines": 46, + "self_recursions": [], + "package": "purescript-bifunctors", + "tag": "v6.1.0", + "commit": "d35e0f1e5a33d59a226859b6bc53075fb58ced87", + "upstream_sha256": "00c91e7e32934e59b1196a793247218670ed5ad6c69acb8ef4eec27db63a8e1a", + "upstream_url": "https://github.com/purescript/purescript-bifunctors/blob/d35e0f1e5a33d59a226859b6bc53075fb58ced87/src/Data/Bifunctor.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Bitraversable.purs", + "vendored_sha256": "1f6eae4a9fb4457664d7fa4c822b451d15a1d320a28b12f81010622885e2cb84", + "vendored_lines": 136, + "self_recursions": [], + "package": "purescript-foldable-traversable", + "tag": "v6.0.0", + "commit": "b3926f870532d287ea59e2d5cd3873b81ef2a93a", + "upstream_sha256": "1f6eae4a9fb4457664d7fa4c822b451d15a1d320a28b12f81010622885e2cb84", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/Bitraversable.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Boolean.purs", + "vendored_sha256": "cbffb99db81cebc7626ab43b43f785f62ffe8c59b3781174907b05d8fb171eb2", + "vendored_lines": 10, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "cbffb99db81cebc7626ab43b43f785f62ffe8c59b3781174907b05d8fb171eb2", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Boolean.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/BooleanAlgebra.purs", + "vendored_sha256": "cc5bcc84af3a96675d3b8ee03b7754a818dd0dd49d20248cf0e17c4793f5ba96", + "vendored_lines": 43, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "cc5bcc84af3a96675d3b8ee03b7754a818dd0dd49d20248cf0e17c4793f5ba96", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/BooleanAlgebra.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Bounded/Generic.purs", + "vendored_sha256": "110cac55f2eadb6a6614397a31b5c167e20983d3ccdce89647ee975e24058f3f", + "vendored_lines": 56, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "110cac55f2eadb6a6614397a31b5c167e20983d3ccdce89647ee975e24058f3f", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Bounded/Generic.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Bounded.purs", + "vendored_sha256": "f67cb72dce353b0eecdd53214a76f879e52eba6335f1305c2ea744063c913d0a", + "vendored_lines": 105, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "f67cb72dce353b0eecdd53214a76f879e52eba6335f1305c2ea744063c913d0a", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Bounded.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "topInt", + "line": 40, + "declaration": "foreign import topInt :: Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "bottomInt", + "line": 41, + "declaration": "foreign import bottomInt :: Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "topChar", + "line": 48, + "declaration": "foreign import topChar :: Char", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "bottomChar", + "line": 49, + "declaration": "foreign import bottomChar :: Char", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "topNumber", + "line": 59, + "declaration": "foreign import topNumber :: Number", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "bottomNumber", + "line": 60, + "declaration": "foreign import bottomNumber :: Number", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Char/Gen.purs", + "vendored_sha256": "3ddf70117671935799e26aade08e1f932221200ab98a1b50c90ad50c090a6ec2", + "vendored_lines": 35, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "3ddf70117671935799e26aade08e1f932221200ab98a1b50c90ad50c090a6ec2", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/Char/Gen.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Char.purs", + "vendored_sha256": "3923ba1cf2abddfc3d4bebb1a2f84f198aa9b23b1fae8e68a281f2188cc79ee7", + "vendored_lines": 16, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "3923ba1cf2abddfc3d4bebb1a2f84f198aa9b23b1fae8e68a281f2188cc79ee7", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/Char.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/CommutativeRing.purs", + "vendored_sha256": "bcb4f94840a1544d53287ef6d44e51f8af66119f12910d3678f101a78a7fea05", + "vendored_lines": 44, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "bcb4f94840a1544d53287ef6d44e51f8af66119f12910d3678f101a78a7fea05", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/CommutativeRing.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Compactable.purs", + "vendored_sha256": "431fbe83796ed62eecb1e3311a7f4a7e5601aee50c611fcb11eff5f29bfe143a", + "vendored_lines": 164, + "self_recursions": [], + "package": "purescript-filterable", + "tag": "v5.0.0", + "commit": "7c5b8c72779997f2b17d12ce478ff81e7ddda285", + "upstream_sha256": "431fbe83796ed62eecb1e3311a7f4a7e5601aee50c611fcb11eff5f29bfe143a", + "upstream_url": "https://github.com/purescript/purescript-filterable/blob/7c5b8c72779997f2b17d12ce478ff81e7ddda285/src/Data/Compactable.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Comparison.purs", + "vendored_sha256": "b94e588dbd3b667ef023394842ed4d0e2afb10b8b315173f5e1095fc3094fc02", + "vendored_lines": 25, + "self_recursions": [], + "package": "purescript-contravariant", + "tag": "v6.0.0", + "commit": "9ad3e105b8855bcc25f4e0893c784789d05a58de", + "upstream_sha256": "b94e588dbd3b667ef023394842ed4d0e2afb10b8b315173f5e1095fc3094fc02", + "upstream_url": "https://github.com/purescript/purescript-contravariant/blob/9ad3e105b8855bcc25f4e0893c784789d05a58de/src/Data/Comparison.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Const.purs", + "vendored_sha256": "a6e5d9429e810f5b834720a35e3103c4f271ca6401b19e7f7f8250e96cb04828", + "vendored_lines": 63, + "self_recursions": [], + "package": "purescript-const", + "tag": "v6.0.0", + "commit": "ab9570cf2b6e67f7e441178211db1231cfd75c37", + "upstream_sha256": "a6e5d9429e810f5b834720a35e3103c4f271ca6401b19e7f7f8250e96cb04828", + "upstream_url": "https://github.com/purescript/purescript-const/blob/ab9570cf2b6e67f7e441178211db1231cfd75c37/src/Data/Const.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Decidable.purs", + "vendored_sha256": "d3260262715019947e3db7351810c3af103bd86cc4e94dcca3a28c22a3d3072a", + "vendored_lines": 29, + "self_recursions": [], + "package": "purescript-contravariant", + "tag": "v6.0.0", + "commit": "9ad3e105b8855bcc25f4e0893c784789d05a58de", + "upstream_sha256": "d3260262715019947e3db7351810c3af103bd86cc4e94dcca3a28c22a3d3072a", + "upstream_url": "https://github.com/purescript/purescript-contravariant/blob/9ad3e105b8855bcc25f4e0893c784789d05a58de/src/Data/Decidable.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Decide.purs", + "vendored_sha256": "0301661ba4052b57c193b57cac94dd78ebdb40ae91eb1d182637e6a68d8c9718", + "vendored_lines": 42, + "self_recursions": [], + "package": "purescript-contravariant", + "tag": "v6.0.0", + "commit": "9ad3e105b8855bcc25f4e0893c784789d05a58de", + "upstream_sha256": "0301661ba4052b57c193b57cac94dd78ebdb40ae91eb1d182637e6a68d8c9718", + "upstream_url": "https://github.com/purescript/purescript-contravariant/blob/9ad3e105b8855bcc25f4e0893c784789d05a58de/src/Data/Decide.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Distributive.purs", + "vendored_sha256": "47bf3b62d71dbeef667007924b75ea18cc4179ef1f111a359d19463e1fa49279", + "vendored_lines": 67, + "self_recursions": [], + "package": "purescript-distributive", + "tag": "v6.0.0", + "commit": "6005e513642e855ebf6f884d24a35c2803ca252a", + "upstream_sha256": "47bf3b62d71dbeef667007924b75ea18cc4179ef1f111a359d19463e1fa49279", + "upstream_url": "https://github.com/purescript/purescript-distributive/blob/6005e513642e855ebf6f884d24a35c2803ca252a/src/Data/Distributive.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Divide.purs", + "vendored_sha256": "cc9f771f056da1b686569a299b46fd0d09be4544bda5e95a01551ce4e0f2d021", + "vendored_lines": 46, + "self_recursions": [], + "package": "purescript-contravariant", + "tag": "v6.0.0", + "commit": "9ad3e105b8855bcc25f4e0893c784789d05a58de", + "upstream_sha256": "cc9f771f056da1b686569a299b46fd0d09be4544bda5e95a01551ce4e0f2d021", + "upstream_url": "https://github.com/purescript/purescript-contravariant/blob/9ad3e105b8855bcc25f4e0893c784789d05a58de/src/Data/Divide.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Divisible.purs", + "vendored_sha256": "fe939175ea5070c0233b11fbf6dd035bae54a6e590dc47253977bbfba7beae82", + "vendored_lines": 25, + "self_recursions": [], + "package": "purescript-contravariant", + "tag": "v6.0.0", + "commit": "9ad3e105b8855bcc25f4e0893c784789d05a58de", + "upstream_sha256": "fe939175ea5070c0233b11fbf6dd035bae54a6e590dc47253977bbfba7beae82", + "upstream_url": "https://github.com/purescript/purescript-contravariant/blob/9ad3e105b8855bcc25f4e0893c784789d05a58de/src/Data/Divisible.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/DivisionRing.purs", + "vendored_sha256": "be3721f83db50d4d1bcc6f2cba1cb30e2be39b376440e215c79ab0e0d7b688f5", + "vendored_lines": 55, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "be3721f83db50d4d1bcc6f2cba1cb30e2be39b376440e215c79ab0e0d7b688f5", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/DivisionRing.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Either/Inject.purs", + "vendored_sha256": "95f071e80bf9658d34b08093bc03ee3fbbb9ef6af3457ad4f203c396ea507e41", + "vendored_lines": 23, + "self_recursions": [], + "package": "purescript-either", + "tag": "v6.1.0", + "commit": "af655a04ed2fd694b6688af39ee20d7907ad0763", + "upstream_sha256": "95f071e80bf9658d34b08093bc03ee3fbbb9ef6af3457ad4f203c396ea507e41", + "upstream_url": "https://github.com/purescript/purescript-either/blob/af655a04ed2fd694b6688af39ee20d7907ad0763/src/Data/Either/Inject.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Either/Nested.purs", + "vendored_sha256": "fa22a911e6c9f62dd7f1bfabfb98962824f75a7f5291e6fafc628dba658cb274", + "vendored_lines": 278, + "self_recursions": [], + "package": "purescript-either", + "tag": "v6.1.0", + "commit": "af655a04ed2fd694b6688af39ee20d7907ad0763", + "upstream_sha256": "fa22a911e6c9f62dd7f1bfabfb98962824f75a7f5291e6fafc628dba658cb274", + "upstream_url": "https://github.com/purescript/purescript-either/blob/af655a04ed2fd694b6688af39ee20d7907ad0763/src/Data/Either/Nested.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Either.purs", + "vendored_sha256": "047a8ae72c32cd064fddb773a739dc130c0cce615270d403491898fbf6c6f5d9", + "vendored_lines": 294, + "self_recursions": [], + "package": "purescript-either", + "tag": "v6.1.0", + "commit": "af655a04ed2fd694b6688af39ee20d7907ad0763", + "upstream_sha256": "047a8ae72c32cd064fddb773a739dc130c0cce615270d403491898fbf6c6f5d9", + "upstream_url": "https://github.com/purescript/purescript-either/blob/af655a04ed2fd694b6688af39ee20d7907ad0763/src/Data/Either.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Enum/Gen.purs", + "vendored_sha256": "3d7234b9dd417d2a31510cf22c691da3cfd0bb4a7bc2265c68cbae019dbcdc13", + "vendored_lines": 18, + "self_recursions": [], + "package": "purescript-enums", + "tag": "v6.0.1", + "commit": "cd373c580b69fdc00e412bddbc299adabe242cc5", + "upstream_sha256": "3d7234b9dd417d2a31510cf22c691da3cfd0bb4a7bc2265c68cbae019dbcdc13", + "upstream_url": "https://github.com/purescript/purescript-enums/blob/cd373c580b69fdc00e412bddbc299adabe242cc5/src/Data/Enum/Gen.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Enum/Generic.purs", + "vendored_sha256": "1c9eea073e25c24c7cca9455487ebec47f0072b1ecba7e2076e7da56fa473846", + "vendored_lines": 118, + "self_recursions": [], + "package": "purescript-enums", + "tag": "v6.0.1", + "commit": "cd373c580b69fdc00e412bddbc299adabe242cc5", + "upstream_sha256": "1c9eea073e25c24c7cca9455487ebec47f0072b1ecba7e2076e7da56fa473846", + "upstream_url": "https://github.com/purescript/purescript-enums/blob/cd373c580b69fdc00e412bddbc299adabe242cc5/src/Data/Enum/Generic.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Enum.purs", + "vendored_sha256": "a7ffaa7a8cc15c362852409f1aba80a5b7792d89353635d54317d43e864e5e9c", + "vendored_lines": 321, + "self_recursions": [], + "package": "purescript-enums", + "tag": "v6.0.1", + "commit": "cd373c580b69fdc00e412bddbc299adabe242cc5", + "upstream_sha256": "a7ffaa7a8cc15c362852409f1aba80a5b7792d89353635d54317d43e864e5e9c", + "upstream_url": "https://github.com/purescript/purescript-enums/blob/cd373c580b69fdc00e412bddbc299adabe242cc5/src/Data/Enum.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "toCharCode", + "line": 320, + "declaration": "foreign import toCharCode :: Char -> Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "fromCharCode", + "line": 321, + "declaration": "foreign import fromCharCode :: Int -> Char", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Eq/Generic.purs", + "vendored_sha256": "9cd6184fb35329e232a2b593e1e3404e89692b36b99104f800bfe23894627373", + "vendored_lines": 35, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "9cd6184fb35329e232a2b593e1e3404e89692b36b99104f800bfe23894627373", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Eq/Generic.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Eq.purs", + "vendored_sha256": "53036679ea014e3c452fce0ee8ce26f58de222cd88eed2586d6dd7dcea3fd872", + "vendored_lines": 115, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "53036679ea014e3c452fce0ee8ce26f58de222cd88eed2586d6dd7dcea3fd872", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Eq.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "eqBooleanImpl", + "line": 77, + "declaration": "foreign import eqBooleanImpl :: Boolean -> Boolean -> Boolean", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "eqIntImpl", + "line": 78, + "declaration": "foreign import eqIntImpl :: Int -> Int -> Boolean", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "eqNumberImpl", + "line": 79, + "declaration": "foreign import eqNumberImpl :: Number -> Number -> Boolean", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "eqCharImpl", + "line": 80, + "declaration": "foreign import eqCharImpl :: Char -> Char -> Boolean", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "eqStringImpl", + "line": 81, + "declaration": "foreign import eqStringImpl :: String -> String -> Boolean", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "eqArrayImpl", + "line": 83, + "declaration": "foreign import eqArrayImpl :: forall a. (a -> a -> Boolean) -> Array a -> Array a -> Boolean", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Equivalence.purs", + "vendored_sha256": "82d547469f77f4be0ba5c8f151d623a12a2a3edaa3d7b65c7f01110ed696932f", + "vendored_lines": 31, + "self_recursions": [], + "package": "purescript-contravariant", + "tag": "v6.0.0", + "commit": "9ad3e105b8855bcc25f4e0893c784789d05a58de", + "upstream_sha256": "82d547469f77f4be0ba5c8f151d623a12a2a3edaa3d7b65c7f01110ed696932f", + "upstream_url": "https://github.com/purescript/purescript-contravariant/blob/9ad3e105b8855bcc25f4e0893c784789d05a58de/src/Data/Equivalence.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/EuclideanRing.purs", + "vendored_sha256": "15d83e118a173ae91fff5b12152afdacc549c12da0a541602d0cf5d702fae863", + "vendored_lines": 100, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "15d83e118a173ae91fff5b12152afdacc549c12da0a541602d0cf5d702fae863", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/EuclideanRing.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "intDegree", + "line": 84, + "declaration": "foreign import intDegree :: Int -> Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "intDiv", + "line": 85, + "declaration": "foreign import intDiv :: Int -> Int -> Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "intMod", + "line": 86, + "declaration": "foreign import intMod :: Int -> Int -> Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "numDiv", + "line": 88, + "declaration": "foreign import numDiv :: Number -> Number -> Number", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Exists.purs", + "vendored_sha256": "74aec7e155ecd0bac49cacb3f87506223193a5f0ff4ae1dd0a8e134775d7794d", + "vendored_lines": 57, + "self_recursions": [], + "package": "purescript-exists", + "tag": "v6.0.0", + "commit": "f765b4ace7869c27b9c05949e18c843881f9173b", + "upstream_sha256": "74aec7e155ecd0bac49cacb3f87506223193a5f0ff4ae1dd0a8e134775d7794d", + "upstream_url": "https://github.com/purescript/purescript-exists/blob/f765b4ace7869c27b9c05949e18c843881f9173b/src/Data/Exists.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Field.purs", + "vendored_sha256": "a27a8bb18ff139cd8eb5c7512c9aa999050faad79f02727e285360b2cda5f06d", + "vendored_lines": 41, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "a27a8bb18ff139cd8eb5c7512c9aa999050faad79f02727e285360b2cda5f06d", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Field.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Filterable.purs", + "vendored_sha256": "459ef72163c124e3ea0f24a4682773ba8562dc9a1e8709a994eb3bd0aa3895bf", + "vendored_lines": 229, + "self_recursions": [], + "package": "purescript-filterable", + "tag": "v5.0.0", + "commit": "7c5b8c72779997f2b17d12ce478ff81e7ddda285", + "upstream_sha256": "459ef72163c124e3ea0f24a4682773ba8562dc9a1e8709a994eb3bd0aa3895bf", + "upstream_url": "https://github.com/purescript/purescript-filterable/blob/7c5b8c72779997f2b17d12ce478ff81e7ddda285/src/Data/Filterable.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Foldable.purs", + "vendored_sha256": "5557319b5a97cf265d53cd68266d721b31f28de3240b10e7728adae8a2070052", + "vendored_lines": 471, + "self_recursions": [], + "package": "purescript-foldable-traversable", + "tag": "v6.0.0", + "commit": "b3926f870532d287ea59e2d5cd3873b81ef2a93a", + "upstream_sha256": "5557319b5a97cf265d53cd68266d721b31f28de3240b10e7728adae8a2070052", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/Foldable.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "foldrArray", + "line": 135, + "declaration": "foreign import foldrArray :: forall a b. (a -> b -> b) -> b -> Array a -> b", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "foldlArray", + "line": 136, + "declaration": "foreign import foldlArray :: forall a b. (b -> a -> b) -> b -> Array a -> b", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/FoldableWithIndex.purs", + "vendored_sha256": "2e10b31b454a84f9597ddb15e43fcfa5bfa84cae8cc5e8b6bbbc5a2018755944", + "vendored_lines": 370, + "self_recursions": [], + "package": "purescript-foldable-traversable", + "tag": "v6.0.0", + "commit": "b3926f870532d287ea59e2d5cd3873b81ef2a93a", + "upstream_sha256": "2e10b31b454a84f9597ddb15e43fcfa5bfa84cae8cc5e8b6bbbc5a2018755944", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/FoldableWithIndex.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Function/Uncurried.purs", + "vendored_sha256": "4e210a49f64e1862cc8530bcbe661c350c964568bbfa2f79bbb1fbe394152d02", + "vendored_lines": 124, + "self_recursions": [], + "package": "purescript-functions", + "tag": "v6.0.0", + "commit": "f626f20580483977c5b27a01aac6471e28aff367", + "upstream_sha256": "4e210a49f64e1862cc8530bcbe661c350c964568bbfa2f79bbb1fbe394152d02", + "upstream_url": "https://github.com/purescript/purescript-functions/blob/f626f20580483977c5b27a01aac6471e28aff367/src/Data/Function/Uncurried.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "mkFn0", + "line": 59, + "declaration": "foreign import mkFn0 :: forall a. (Unit -> a) -> Fn0 a", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkFn2", + "line": 66, + "declaration": "foreign import mkFn2 :: forall a b c. (a -> b -> c) -> Fn2 a b c", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkFn3", + "line": 69, + "declaration": "foreign import mkFn3 :: forall a b c d. (a -> b -> c -> d) -> Fn3 a b c d", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkFn4", + "line": 72, + "declaration": "foreign import mkFn4 :: forall a b c d e. (a -> b -> c -> d -> e) -> Fn4 a b c d e", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkFn5", + "line": 75, + "declaration": "foreign import mkFn5 :: forall a b c d e f. (a -> b -> c -> d -> e -> f) -> Fn5 a b c d e f", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkFn6", + "line": 78, + "declaration": "foreign import mkFn6 :: forall a b c d e f g. (a -> b -> c -> d -> e -> f -> g) -> Fn6 a b c d e f g", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkFn7", + "line": 81, + "declaration": "foreign import mkFn7 :: forall a b c d e f g h. (a -> b -> c -> d -> e -> f -> g -> h) -> Fn7 a b c d e f g h", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkFn8", + "line": 84, + "declaration": "foreign import mkFn8 :: forall a b c d e f g h i. (a -> b -> c -> d -> e -> f -> g -> h -> i) -> Fn8 a b c d e f g h i", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkFn9", + "line": 87, + "declaration": "foreign import mkFn9 :: forall a b c d e f g h i j. (a -> b -> c -> d -> e -> f -> g -> h -> i -> j) -> Fn9 a b c d e f g h i j", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkFn10", + "line": 90, + "declaration": "foreign import mkFn10 :: forall a b c d e f g h i j k. (a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> k) -> Fn10 a b c d e f g h i j k", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runFn0", + "line": 93, + "declaration": "foreign import runFn0 :: forall a. Fn0 a -> a", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runFn2", + "line": 100, + "declaration": "foreign import runFn2 :: forall a b c. Fn2 a b c -> a -> b -> c", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runFn3", + "line": 103, + "declaration": "foreign import runFn3 :: forall a b c d. Fn3 a b c d -> a -> b -> c -> d", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runFn4", + "line": 106, + "declaration": "foreign import runFn4 :: forall a b c d e. Fn4 a b c d e -> a -> b -> c -> d -> e", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runFn5", + "line": 109, + "declaration": "foreign import runFn5 :: forall a b c d e f. Fn5 a b c d e f -> a -> b -> c -> d -> e -> f", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runFn6", + "line": 112, + "declaration": "foreign import runFn6 :: forall a b c d e f g. Fn6 a b c d e f g -> a -> b -> c -> d -> e -> f -> g", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runFn7", + "line": 115, + "declaration": "foreign import runFn7 :: forall a b c d e f g h. Fn7 a b c d e f g h -> a -> b -> c -> d -> e -> f -> g -> h", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runFn8", + "line": 118, + "declaration": "foreign import runFn8 :: forall a b c d e f g h i. Fn8 a b c d e f g h i -> a -> b -> c -> d -> e -> f -> g -> h -> i", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runFn9", + "line": 121, + "declaration": "foreign import runFn9 :: forall a b c d e f g h i j. Fn9 a b c d e f g h i j -> a -> b -> c -> d -> e -> f -> g -> h -> i -> j", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runFn10", + "line": 124, + "declaration": "foreign import 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", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Function.purs", + "vendored_sha256": "2142b200f60953ac4fe648b765da7cf6cfb046e8600ffef3484a7f3e4f8f03b7", + "vendored_lines": 120, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "2142b200f60953ac4fe648b765da7cf6cfb046e8600ffef3484a7f3e4f8f03b7", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Function.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/App.purs", + "vendored_sha256": "2c90077463cc2deb82dc888dd7c6fc10fa465d444c7b48fa042b4a2d4e834711", + "vendored_lines": 56, + "self_recursions": [], + "package": "purescript-functors", + "tag": "v5.0.0", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "upstream_sha256": "2c90077463cc2deb82dc888dd7c6fc10fa465d444c7b48fa042b4a2d4e834711", + "upstream_url": "https://github.com/purescript/purescript-functors/blob/022ffd7a2a7ec12080314f3d217b400674a247b4/src/Data/Functor/App.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Clown.purs", + "vendored_sha256": "9e0ee4050d69d24a0965026576288b9742f1e77941b819d510e1317bbe1ca1c2", + "vendored_lines": 44, + "self_recursions": [], + "package": "purescript-functors", + "tag": "v5.0.0", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "upstream_sha256": "9e0ee4050d69d24a0965026576288b9742f1e77941b819d510e1317bbe1ca1c2", + "upstream_url": "https://github.com/purescript/purescript-functors/blob/022ffd7a2a7ec12080314f3d217b400674a247b4/src/Data/Functor/Clown.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Compose.purs", + "vendored_sha256": "1ea6725c558c1d8424b8e8bf03331879bcc589ec95ef0208dea7dcb90861d202", + "vendored_lines": 58, + "self_recursions": [], + "package": "purescript-functors", + "tag": "v5.0.0", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "upstream_sha256": "1ea6725c558c1d8424b8e8bf03331879bcc589ec95ef0208dea7dcb90861d202", + "upstream_url": "https://github.com/purescript/purescript-functors/blob/022ffd7a2a7ec12080314f3d217b400674a247b4/src/Data/Functor/Compose.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Contravariant.purs", + "vendored_sha256": "ecb01e255618d9b446bc72d042d10c44a84ce7b10df14f838e50fa49a8ddd4ab", + "vendored_lines": 35, + "self_recursions": [], + "package": "purescript-contravariant", + "tag": "v6.0.0", + "commit": "9ad3e105b8855bcc25f4e0893c784789d05a58de", + "upstream_sha256": "ecb01e255618d9b446bc72d042d10c44a84ce7b10df14f838e50fa49a8ddd4ab", + "upstream_url": "https://github.com/purescript/purescript-contravariant/blob/9ad3e105b8855bcc25f4e0893c784789d05a58de/src/Data/Functor/Contravariant.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Coproduct/Inject.purs", + "vendored_sha256": "dfccf4433db792b3a1246b72b7aab69d39a593750c242a741a4adc92a012b743", + "vendored_lines": 24, + "self_recursions": [], + "package": "purescript-functors", + "tag": "v5.0.0", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "upstream_sha256": "dfccf4433db792b3a1246b72b7aab69d39a593750c242a741a4adc92a012b743", + "upstream_url": "https://github.com/purescript/purescript-functors/blob/022ffd7a2a7ec12080314f3d217b400674a247b4/src/Data/Functor/Coproduct/Inject.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Coproduct/Nested.purs", + "vendored_sha256": "efd0e04c36996fd958425c21f3c6748cf3c08abf4ff014b09eca3687937e193c", + "vendored_lines": 273, + "self_recursions": [], + "package": "purescript-functors", + "tag": "v5.0.0", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "upstream_sha256": "efd0e04c36996fd958425c21f3c6748cf3c08abf4ff014b09eca3687937e193c", + "upstream_url": "https://github.com/purescript/purescript-functors/blob/022ffd7a2a7ec12080314f3d217b400674a247b4/src/Data/Functor/Coproduct/Nested.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Coproduct.purs", + "vendored_sha256": "bdef394de4a506431d3397ed765e861daf47989882f1f8d97419edb312a62637", + "vendored_lines": 76, + "self_recursions": [], + "package": "purescript-functors", + "tag": "v5.0.0", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "upstream_sha256": "bdef394de4a506431d3397ed765e861daf47989882f1f8d97419edb312a62637", + "upstream_url": "https://github.com/purescript/purescript-functors/blob/022ffd7a2a7ec12080314f3d217b400674a247b4/src/Data/Functor/Coproduct.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Costar.purs", + "vendored_sha256": "3bde423fa6f47a635e62c4a55fa4eb1ebbd32dcb5776b748bb4bc59b892c77b5", + "vendored_lines": 66, + "self_recursions": [], + "package": "purescript-functors", + "tag": "v5.0.0", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "upstream_sha256": "3bde423fa6f47a635e62c4a55fa4eb1ebbd32dcb5776b748bb4bc59b892c77b5", + "upstream_url": "https://github.com/purescript/purescript-functors/blob/022ffd7a2a7ec12080314f3d217b400674a247b4/src/Data/Functor/Costar.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Flip.purs", + "vendored_sha256": "b3d31bfa23a05a6ed6c4185775bb45ff2e0c562cd9c95c711b1cf636459c153b", + "vendored_lines": 44, + "self_recursions": [], + "package": "purescript-functors", + "tag": "v5.0.0", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "upstream_sha256": "b3d31bfa23a05a6ed6c4185775bb45ff2e0c562cd9c95c711b1cf636459c153b", + "upstream_url": "https://github.com/purescript/purescript-functors/blob/022ffd7a2a7ec12080314f3d217b400674a247b4/src/Data/Functor/Flip.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Invariant.purs", + "vendored_sha256": "070d432e594b5423ebd865d101dbddaf1bd64690917a6ebeabd559393c152f26", + "vendored_lines": 57, + "self_recursions": [], + "package": "purescript-invariant", + "tag": "v6.0.0", + "commit": "1d2a196d51e90623adb88496c2cfd759c6736894", + "upstream_sha256": "070d432e594b5423ebd865d101dbddaf1bd64690917a6ebeabd559393c152f26", + "upstream_url": "https://github.com/purescript/purescript-invariant/blob/1d2a196d51e90623adb88496c2cfd759c6736894/src/Data/Functor/Invariant.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Joker.purs", + "vendored_sha256": "3c32235123a30b6e38273bbfcf6cf50cef94d9a11e7da16e7ee3d7e0ed98cac4", + "vendored_lines": 60, + "self_recursions": [], + "package": "purescript-functors", + "tag": "v5.0.0", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "upstream_sha256": "3c32235123a30b6e38273bbfcf6cf50cef94d9a11e7da16e7ee3d7e0ed98cac4", + "upstream_url": "https://github.com/purescript/purescript-functors/blob/022ffd7a2a7ec12080314f3d217b400674a247b4/src/Data/Functor/Joker.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Product/Nested.purs", + "vendored_sha256": "eabec66003663a5e64b64e6abf317a27e8660d5c09475bbd38d9219db0834580", + "vendored_lines": 112, + "self_recursions": [], + "package": "purescript-functors", + "tag": "v5.0.0", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "upstream_sha256": "eabec66003663a5e64b64e6abf317a27e8660d5c09475bbd38d9219db0834580", + "upstream_url": "https://github.com/purescript/purescript-functors/blob/022ffd7a2a7ec12080314f3d217b400674a247b4/src/Data/Functor/Product/Nested.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Product.purs", + "vendored_sha256": "c43e912ffe484cfd0e01cafa143474e140f06a85c7177c9f2cc6b59b51d18a37", + "vendored_lines": 60, + "self_recursions": [], + "package": "purescript-functors", + "tag": "v5.0.0", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "upstream_sha256": "c43e912ffe484cfd0e01cafa143474e140f06a85c7177c9f2cc6b59b51d18a37", + "upstream_url": "https://github.com/purescript/purescript-functors/blob/022ffd7a2a7ec12080314f3d217b400674a247b4/src/Data/Functor/Product.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor/Product2.purs", + "vendored_sha256": "80763924484595b34488d8cca7bd8e5d399aebc7a79442e1625d7ddbebec9c7c", + "vendored_lines": 40, + "self_recursions": [], + "package": "purescript-functors", + "tag": "v5.0.0", + "commit": "022ffd7a2a7ec12080314f3d217b400674a247b4", + "upstream_sha256": "80763924484595b34488d8cca7bd8e5d399aebc7a79442e1625d7ddbebec9c7c", + "upstream_url": "https://github.com/purescript/purescript-functors/blob/022ffd7a2a7ec12080314f3d217b400674a247b4/src/Data/Functor/Product2.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Functor.purs", + "vendored_sha256": "fc5b0cc22a6c4d96a5779eb629cb304cebaa910f03a77192628c131a867bbeb8", + "vendored_lines": 106, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "fc5b0cc22a6c4d96a5779eb629cb304cebaa910f03a77192628c131a867bbeb8", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Functor.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "arrayMap", + "line": 55, + "declaration": "foreign import arrayMap :: forall a b. (a -> b) -> Array a -> Array b", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/FunctorWithIndex.purs", + "vendored_sha256": "cdffaeb5485f7a5a12796b5db2a9a81b7212d50dacd6405a87bc3f57fbc15f6c", + "vendored_lines": 93, + "self_recursions": [], + "package": "purescript-foldable-traversable", + "tag": "v6.0.0", + "commit": "b3926f870532d287ea59e2d5cd3873b81ef2a93a", + "upstream_sha256": "cdffaeb5485f7a5a12796b5db2a9a81b7212d50dacd6405a87bc3f57fbc15f6c", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/FunctorWithIndex.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "mapWithIndexArray", + "line": 38, + "declaration": "foreign import mapWithIndexArray :: forall a b. (Int -> a -> b) -> Array a -> Array b", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Generic/Rep.purs", + "vendored_sha256": "fc1e17e76bf53bf8dc86593ca2d27a77f921e8aa67bffcb6265fdc473736862d", + "vendored_lines": 62, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "fc1e17e76bf53bf8dc86593ca2d27a77f921e8aa67bffcb6265fdc473736862d", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Generic/Rep.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/HeytingAlgebra/Generic.purs", + "vendored_sha256": "c5022ab44881b51e4b62d06994d92f04b037ce97ff2c50054e4b90d91a463bbe", + "vendored_lines": 70, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "c5022ab44881b51e4b62d06994d92f04b037ce97ff2c50054e4b90d91a463bbe", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/HeytingAlgebra/Generic.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/HeytingAlgebra.purs", + "vendored_sha256": "797b9e8358e10691e93c8b1552723507fd12647441f90db11b01e35850bce75b", + "vendored_lines": 171, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "797b9e8358e10691e93c8b1552723507fd12647441f90db11b01e35850bce75b", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/HeytingAlgebra.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "boolConj", + "line": 103, + "declaration": "foreign import boolConj :: Boolean -> Boolean -> Boolean", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "boolDisj", + "line": 104, + "declaration": "foreign import boolDisj :: Boolean -> Boolean -> Boolean", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "boolNot", + "line": 105, + "declaration": "foreign import boolNot :: Boolean -> Boolean", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Identity.purs", + "vendored_sha256": "65f7f8ca763a00e78b46453c480a22e41e43dcda5a582cfea9eecd4b4448f87d", + "vendored_lines": 72, + "self_recursions": [], + "package": "purescript-identity", + "tag": "v6.0.0", + "commit": "ef6768f8a52ab0bc943a85f5761ba07c257f639f", + "upstream_sha256": "65f7f8ca763a00e78b46453c480a22e41e43dcda5a582cfea9eecd4b4448f87d", + "upstream_url": "https://github.com/purescript/purescript-identity/blob/ef6768f8a52ab0bc943a85f5761ba07c257f639f/src/Data/Identity.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Int/Bits.purs", + "vendored_sha256": "6377bce107895b0021264a30a35c8e50867b084349407009061728bb42c436de", + "vendored_lines": 37, + "self_recursions": [], + "package": "purescript-integers", + "tag": "v6.0.0", + "commit": "54d712b25c594833083d15dc9ff2418eb9c52822", + "upstream_sha256": "6377bce107895b0021264a30a35c8e50867b084349407009061728bb42c436de", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int/Bits.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "and", + "line": 13, + "declaration": "foreign import and :: Int -> Int -> Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "or", + "line": 18, + "declaration": "foreign import or :: Int -> Int -> Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "xor", + "line": 23, + "declaration": "foreign import xor :: Int -> Int -> Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "shl", + "line": 28, + "declaration": "foreign import shl :: Int -> Int -> Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "shr", + "line": 31, + "declaration": "foreign import shr :: Int -> Int -> Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "zshr", + "line": 34, + "declaration": "foreign import zshr :: Int -> Int -> Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "complement", + "line": 37, + "declaration": "foreign import complement :: Int -> Int", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Int.purs", + "vendored_sha256": "53a7c199f384719a5f615faf6141e9b0ffa2fa9f66b9806d30a0f196be074eec", + "vendored_lines": 257, + "self_recursions": [], + "package": "purescript-integers", + "tag": "v6.0.0", + "commit": "54d712b25c594833083d15dc9ff2418eb9c52822", + "upstream_sha256": "53a7c199f384719a5f615faf6141e9b0ffa2fa9f66b9806d30a0f196be074eec", + "upstream_url": "https://github.com/purescript/purescript-integers/blob/54d712b25c594833083d15dc9ff2418eb9c52822/src/Data/Int.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "fromNumberImpl", + "line": 40, + "declaration": "foreign import fromNumberImpl :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Number -> Maybe Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "toNumber", + "line": 81, + "declaration": "foreign import toNumber :: Int -> Number", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "quot", + "line": 227, + "declaration": "foreign import quot :: Int -> Int -> Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "rem", + "line": 245, + "declaration": "foreign import rem :: Int -> Int -> Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "pow", + "line": 248, + "declaration": "foreign import pow :: Int -> Int -> Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "fromStringAsImpl", + "line": 250, + "declaration": "foreign import fromStringAsImpl :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Radix -> String -> Maybe Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "toStringAs", + "line": 257, + "declaration": "foreign import toStringAs :: Radix -> Int -> String", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Lazy.purs", + "vendored_sha256": "9da5e2562a1a05764552d20dc3dc7119142550350a1f290d1919f502ebe54202", + "vendored_lines": 142, + "self_recursions": [], + "package": "purescript-lazy", + "tag": "v6.0.0", + "commit": "48347841226b27af5205a1a8ec71e27a93ce86fd", + "upstream_sha256": "9da5e2562a1a05764552d20dc3dc7119142550350a1f290d1919f502ebe54202", + "upstream_url": "https://github.com/purescript/purescript-lazy/blob/48347841226b27af5205a1a8ec71e27a93ce86fd/src/Data/Lazy.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "defer", + "line": 35, + "declaration": "foreign import defer :: forall a. (Unit -> a) -> Lazy a", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "force", + "line": 38, + "declaration": "foreign import force :: forall a. Lazy a -> a", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/List/Internal.purs", + "vendored_sha256": "9b542dbc4823476b26b6912cd0dbd47c3af97bc35f66f11b421aa59f28d6fc42", + "vendored_lines": 63, + "self_recursions": [], + "package": "purescript-lists", + "tag": "v7.0.0", + "commit": "b113451e5b41cad87d669a3165f955c71cd863e2", + "upstream_sha256": "9b542dbc4823476b26b6912cd0dbd47c3af97bc35f66f11b421aa59f28d6fc42", + "upstream_url": "https://github.com/purescript/purescript-lists/blob/b113451e5b41cad87d669a3165f955c71cd863e2/src/Data/List/Internal.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/List/Lazy/NonEmpty.purs", + "vendored_sha256": "5b06d7bdbb485fa4766134635b43d22f1e148f0e4bbc61f7c4e78710686b2586", + "vendored_lines": 88, + "self_recursions": [], + "package": "purescript-lists", + "tag": "v7.0.0", + "commit": "b113451e5b41cad87d669a3165f955c71cd863e2", + "upstream_sha256": "5b06d7bdbb485fa4766134635b43d22f1e148f0e4bbc61f7c4e78710686b2586", + "upstream_url": "https://github.com/purescript/purescript-lists/blob/b113451e5b41cad87d669a3165f955c71cd863e2/src/Data/List/Lazy/NonEmpty.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/List/Lazy/Types.purs", + "vendored_sha256": "68fa44b813560d340230fd122e965568c2920600c8153fd22219e27ab7307d64", + "vendored_lines": 295, + "self_recursions": [], + "package": "purescript-lists", + "tag": "v7.0.0", + "commit": "b113451e5b41cad87d669a3165f955c71cd863e2", + "upstream_sha256": "68fa44b813560d340230fd122e965568c2920600c8153fd22219e27ab7307d64", + "upstream_url": "https://github.com/purescript/purescript-lists/blob/b113451e5b41cad87d669a3165f955c71cd863e2/src/Data/List/Lazy/Types.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/List/Lazy.purs", + "vendored_sha256": "69f373e427611ef8124b9a49f4c02638aca287bc48bf095371da8afe090fe9b8", + "vendored_lines": 780, + "self_recursions": [], + "package": "purescript-lists", + "tag": "v7.0.0", + "commit": "b113451e5b41cad87d669a3165f955c71cd863e2", + "upstream_sha256": "69f373e427611ef8124b9a49f4c02638aca287bc48bf095371da8afe090fe9b8", + "upstream_url": "https://github.com/purescript/purescript-lists/blob/b113451e5b41cad87d669a3165f955c71cd863e2/src/Data/List/Lazy.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/List/NonEmpty.purs", + "vendored_sha256": "2b0911320ac74ddc1a80957592c965b370ee81de740f9c24f048d4f7d5c4b0d7", + "vendored_lines": 307, + "self_recursions": [], + "package": "purescript-lists", + "tag": "v7.0.0", + "commit": "b113451e5b41cad87d669a3165f955c71cd863e2", + "upstream_sha256": "2b0911320ac74ddc1a80957592c965b370ee81de740f9c24f048d4f7d5c4b0d7", + "upstream_url": "https://github.com/purescript/purescript-lists/blob/b113451e5b41cad87d669a3165f955c71cd863e2/src/Data/List/NonEmpty.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/List/Partial.purs", + "vendored_sha256": "95e40bb55ef84403901b6a19ac9367bae5cce79330703a8d4db60d57bf6fab4b", + "vendored_lines": 30, + "self_recursions": [], + "package": "purescript-lists", + "tag": "v7.0.0", + "commit": "b113451e5b41cad87d669a3165f955c71cd863e2", + "upstream_sha256": "95e40bb55ef84403901b6a19ac9367bae5cce79330703a8d4db60d57bf6fab4b", + "upstream_url": "https://github.com/purescript/purescript-lists/blob/b113451e5b41cad87d669a3165f955c71cd863e2/src/Data/List/Partial.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/List/Types.purs", + "vendored_sha256": "8a7d87397d8edc746e7fa1459d67e90ef9e2dd09bf7bd55baa9a4b549e316dd3", + "vendored_lines": 264, + "self_recursions": [], + "package": "purescript-lists", + "tag": "v7.0.0", + "commit": "b113451e5b41cad87d669a3165f955c71cd863e2", + "upstream_sha256": "8a7d87397d8edc746e7fa1459d67e90ef9e2dd09bf7bd55baa9a4b549e316dd3", + "upstream_url": "https://github.com/purescript/purescript-lists/blob/b113451e5b41cad87d669a3165f955c71cd863e2/src/Data/List/Types.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/List/ZipList.purs", + "vendored_sha256": "09acb25d60dd823ee7ce94d33e5ac9bb46aee527638dd69ccf82f1532da13719", + "vendored_lines": 66, + "self_recursions": [], + "package": "purescript-lists", + "tag": "v7.0.0", + "commit": "b113451e5b41cad87d669a3165f955c71cd863e2", + "upstream_sha256": "09acb25d60dd823ee7ce94d33e5ac9bb46aee527638dd69ccf82f1532da13719", + "upstream_url": "https://github.com/purescript/purescript-lists/blob/b113451e5b41cad87d669a3165f955c71cd863e2/src/Data/List/ZipList.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/List.purs", + "vendored_sha256": "fde72dc9da24f67efb446f57806890e95b3b40a4f0389ee313072de4cc2d56aa", + "vendored_lines": 826, + "self_recursions": [], + "package": "purescript-lists", + "tag": "v7.0.0", + "commit": "b113451e5b41cad87d669a3165f955c71cd863e2", + "upstream_sha256": "fde72dc9da24f67efb446f57806890e95b3b40a4f0389ee313072de4cc2d56aa", + "upstream_url": "https://github.com/purescript/purescript-lists/blob/b113451e5b41cad87d669a3165f955c71cd863e2/src/Data/List.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Map/Gen.purs", + "vendored_sha256": "9f2671c0a0a1efbad92a52b4e02517c06e08df9f741f59a9d8ae290d002a19e9", + "vendored_lines": 24, + "self_recursions": [], + "package": "purescript-ordered-collections", + "tag": "v3.2.0", + "commit": "b9c8ec72b53ee56d6cb6dc962d5ad7ea4708b438", + "upstream_sha256": "9f2671c0a0a1efbad92a52b4e02517c06e08df9f741f59a9d8ae290d002a19e9", + "upstream_url": "https://github.com/purescript/purescript-ordered-collections/blob/b9c8ec72b53ee56d6cb6dc962d5ad7ea4708b438/src/Data/Map/Gen.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Map/Internal.purs", + "vendored_sha256": "03608bed5db4804aa898eff034bb5b2feab0676948b19aefa606dc927eeb5cc9", + "vendored_lines": 988, + "self_recursions": [], + "package": "purescript-ordered-collections", + "tag": "v3.2.0", + "commit": "b9c8ec72b53ee56d6cb6dc962d5ad7ea4708b438", + "upstream_sha256": "03608bed5db4804aa898eff034bb5b2feab0676948b19aefa606dc927eeb5cc9", + "upstream_url": "https://github.com/purescript/purescript-ordered-collections/blob/b9c8ec72b53ee56d6cb6dc962d5ad7ea4708b438/src/Data/Map/Internal.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Map.purs", + "vendored_sha256": "8a999f683cf7f64950707e6cdc8e9a735974725e98b72c7a6d891f5f801e6242", + "vendored_lines": 65, + "self_recursions": [], + "package": "purescript-ordered-collections", + "tag": "v3.2.0", + "commit": "b9c8ec72b53ee56d6cb6dc962d5ad7ea4708b438", + "upstream_sha256": "8a999f683cf7f64950707e6cdc8e9a735974725e98b72c7a6d891f5f801e6242", + "upstream_url": "https://github.com/purescript/purescript-ordered-collections/blob/b9c8ec72b53ee56d6cb6dc962d5ad7ea4708b438/src/Data/Map.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Maybe/First.purs", + "vendored_sha256": "7cfcf5744a424bc512fd7e7e04a71484c609a52cf94ef5552bcf7afa8f967abf", + "vendored_lines": 68, + "self_recursions": [], + "package": "purescript-maybe", + "tag": "v6.0.0", + "commit": "c6f98ac1088766287106c5d9c8e30e7648d36786", + "upstream_sha256": "7cfcf5744a424bc512fd7e7e04a71484c609a52cf94ef5552bcf7afa8f967abf", + "upstream_url": "https://github.com/purescript/purescript-maybe/blob/c6f98ac1088766287106c5d9c8e30e7648d36786/src/Data/Maybe/First.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Maybe/Last.purs", + "vendored_sha256": "cc86da8e91615e04b7973bdc5f7467db4adc8df722eb509bc2cf0e735cbb93ab", + "vendored_lines": 67, + "self_recursions": [], + "package": "purescript-maybe", + "tag": "v6.0.0", + "commit": "c6f98ac1088766287106c5d9c8e30e7648d36786", + "upstream_sha256": "cc86da8e91615e04b7973bdc5f7467db4adc8df722eb509bc2cf0e735cbb93ab", + "upstream_url": "https://github.com/purescript/purescript-maybe/blob/c6f98ac1088766287106c5d9c8e30e7648d36786/src/Data/Maybe/Last.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Maybe.purs", + "vendored_sha256": "dd49326b74a5ba9c5d0e0aaca7c05226aadb9b9bf851d9a3fdbc34df23319fa8", + "vendored_lines": 312, + "self_recursions": [], + "package": "purescript-maybe", + "tag": "v6.0.0", + "commit": "c6f98ac1088766287106c5d9c8e30e7648d36786", + "upstream_sha256": "dd49326b74a5ba9c5d0e0aaca7c05226aadb9b9bf851d9a3fdbc34df23319fa8", + "upstream_url": "https://github.com/purescript/purescript-maybe/blob/c6f98ac1088766287106c5d9c8e30e7648d36786/src/Data/Maybe.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Monoid/Additive.purs", + "vendored_sha256": "9b44eba5a733dbd6f7e908ea1c4eb44b61694f5a3d1d1e157c7c81300eb7417a", + "vendored_lines": 44, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "9b44eba5a733dbd6f7e908ea1c4eb44b61694f5a3d1d1e157c7c81300eb7417a", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Monoid/Additive.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Monoid/Alternate.purs", + "vendored_sha256": "fb6b0d43a0c82f6028d64d957fad49d9ce48eb8a213ea8d2f0498d4be8f2de8d", + "vendored_lines": 60, + "self_recursions": [], + "package": "purescript-control", + "tag": "v6.0.0", + "commit": "a6033808790879a17b2729e73747a9ed3fb2264e", + "upstream_sha256": "fb6b0d43a0c82f6028d64d957fad49d9ce48eb8a213ea8d2f0498d4be8f2de8d", + "upstream_url": "https://github.com/purescript/purescript-control/blob/a6033808790879a17b2729e73747a9ed3fb2264e/src/Data/Monoid/Alternate.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Monoid/Conj.purs", + "vendored_sha256": "0e3bedf55635d1c04225ff1a756bc55b94afafa8fb8d327f118e4c1f58f40858", + "vendored_lines": 51, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "0e3bedf55635d1c04225ff1a756bc55b94afafa8fb8d327f118e4c1f58f40858", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Monoid/Conj.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Monoid/Disj.purs", + "vendored_sha256": "e1eb141917201b0b866c433e19e68a6b0d80705d8fb6ea3d61d103d4a01c735f", + "vendored_lines": 51, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "e1eb141917201b0b866c433e19e68a6b0d80705d8fb6ea3d61d103d4a01c735f", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Monoid/Disj.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Monoid/Dual.purs", + "vendored_sha256": "83c40da0d7125aa11225efe267713892ecaa044d66da05d048a1194ac0c48f6c", + "vendored_lines": 44, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "83c40da0d7125aa11225efe267713892ecaa044d66da05d048a1194ac0c48f6c", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Monoid/Dual.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Monoid/Endo.purs", + "vendored_sha256": "aa5b2bc60841651e0bf54a683e6d6d99f4aca932bf583691b3fdd71a32aa5594", + "vendored_lines": 30, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "aa5b2bc60841651e0bf54a683e6d6d99f4aca932bf583691b3fdd71a32aa5594", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Monoid/Endo.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Monoid/Generic.purs", + "vendored_sha256": "a3df423b6bc60580e9e80612ae7c1682445853c55086363ceed129e43c72bf70", + "vendored_lines": 27, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "a3df423b6bc60580e9e80612ae7c1682445853c55086363ceed129e43c72bf70", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Monoid/Generic.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Monoid/Multiplicative.purs", + "vendored_sha256": "0d01fac2de65e733c640b045e00f60422e1b776010ee69e7714dd659bb8c7796", + "vendored_lines": 44, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "0d01fac2de65e733c640b045e00f60422e1b776010ee69e7714dd659bb8c7796", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Monoid/Multiplicative.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Monoid.purs", + "vendored_sha256": "a1bf093434dc1d8f162a19369f3376516f3f899e17875711be3d7164fb20e605", + "vendored_lines": 120, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "a1bf093434dc1d8f162a19369f3376516f3f899e17875711be3d7164fb20e605", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Monoid.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/NaturalTransformation.purs", + "vendored_sha256": "3196072783cdc7135047788ad6e58073e1cfeb50d284f038b79b65db2df9eb7e", + "vendored_lines": 20, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "3196072783cdc7135047788ad6e58073e1cfeb50d284f038b79b65db2df9eb7e", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/NaturalTransformation.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Newtype.purs", + "vendored_sha256": "572911ec613a337d30004c92120afea496af97d34b63617d884d4a96f03d8304", + "vendored_lines": 308, + "self_recursions": [], + "package": "purescript-newtype", + "tag": "v5.0.0", + "commit": "29d8e6dd77aec2c975c948364ec3faf26e14ee7b", + "upstream_sha256": "572911ec613a337d30004c92120afea496af97d34b63617d884d4a96f03d8304", + "upstream_url": "https://github.com/purescript/purescript-newtype/blob/29d8e6dd77aec2c975c948364ec3faf26e14ee7b/src/Data/Newtype.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/NonEmpty.purs", + "vendored_sha256": "c48366ad44cc7b4311d5032ef27ae8d2ea39f603d7bb33f03d51536758976d7e", + "vendored_lines": 174, + "self_recursions": [], + "package": "purescript-nonempty", + "tag": "v7.0.0", + "commit": "28150ecc7419238b187abd609a92a645273348bb", + "upstream_sha256": "c48366ad44cc7b4311d5032ef27ae8d2ea39f603d7bb33f03d51536758976d7e", + "upstream_url": "https://github.com/purescript/purescript-nonempty/blob/28150ecc7419238b187abd609a92a645273348bb/src/Data/NonEmpty.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Number/Approximate.purs", + "vendored_sha256": "fbd544b15b42845be1a3fa11f1ec3c61d290baa300d41b7e41e3aacf707eacff", + "vendored_lines": 95, + "self_recursions": [], + "package": "purescript-numbers", + "tag": "v9.0.1", + "commit": "27d54effdd2c0e7a86fe356b1cd813dca5981c2d", + "upstream_sha256": "fbd544b15b42845be1a3fa11f1ec3c61d290baa300d41b7e41e3aacf707eacff", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number/Approximate.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Number/Format.purs", + "vendored_sha256": "0922126e6d31ecc361a00b265ecf302fb1f6449ca11d2cc2331a0ae174d9f83f", + "vendored_lines": 76, + "self_recursions": [], + "package": "purescript-numbers", + "tag": "v9.0.1", + "commit": "27d54effdd2c0e7a86fe356b1cd813dca5981c2d", + "upstream_sha256": "0922126e6d31ecc361a00b265ecf302fb1f6449ca11d2cc2331a0ae174d9f83f", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number/Format.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "toPrecisionNative", + "line": 33, + "declaration": "foreign import toPrecisionNative :: Int -> Number -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "toFixedNative", + "line": 34, + "declaration": "foreign import toFixedNative :: Int -> Number -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "toExponentialNative", + "line": 35, + "declaration": "foreign import toExponentialNative :: Int -> Number -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "toString", + "line": 76, + "declaration": "foreign import toString :: Number -> String", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Number.purs", + "vendored_sha256": "13c04cf0d293c54825fc95b5b3d543379a8d913e110d9e5056ca5388a31afc87", + "vendored_lines": 363, + "self_recursions": [], + "package": "purescript-numbers", + "tag": "v9.0.1", + "commit": "27d54effdd2c0e7a86fe356b1cd813dca5981c2d", + "upstream_sha256": "13c04cf0d293c54825fc95b5b3d543379a8d913e110d9e5056ca5388a31afc87", + "upstream_url": "https://github.com/purescript/purescript-numbers/blob/27d54effdd2c0e7a86fe356b1cd813dca5981c2d/src/Data/Number.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "nan", + "line": 47, + "declaration": "foreign import nan :: Number", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "isNaN", + "line": 57, + "declaration": "foreign import isNaN :: Number -> Boolean", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "infinity", + "line": 67, + "declaration": "foreign import infinity :: Number", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "isFinite", + "line": 83, + "declaration": "foreign import isFinite :: Number -> Boolean", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "fromStringImpl", + "line": 115, + "declaration": "foreign import fromStringImpl :: Fn4 String (Number -> Boolean) (forall a. a -> Maybe a) (forall a. Maybe a) (Maybe Number)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "abs", + "line": 123, + "declaration": "foreign import abs :: Number -> Number", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "acos", + "line": 130, + "declaration": "foreign import acos :: Number -> Number", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "asin", + "line": 137, + "declaration": "foreign import asin :: Number -> Number", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "atan", + "line": 144, + "declaration": "foreign import atan :: Number -> Number", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "atan2", + "line": 157, + "declaration": "foreign import atan2 :: Number -> Number -> Number", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "ceil", + "line": 164, + "declaration": "foreign import ceil :: Number -> Number", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "cos", + "line": 171, + "declaration": "foreign import cos :: Number -> Number", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "exp", + "line": 178, + "declaration": "foreign import exp :: Number -> Number", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "floor", + "line": 185, + "declaration": "foreign import floor :: Number -> Number", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "log", + "line": 191, + "declaration": "foreign import log :: Number -> Number", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "max", + "line": 195, + "declaration": "foreign import max :: Number -> Number -> Number", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "min", + "line": 199, + "declaration": "foreign import min :: Number -> Number -> Number", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "pow", + "line": 209, + "declaration": "foreign import pow :: Number -> Number -> Number", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "remainder", + "line": 216, + "declaration": "foreign import remainder :: Number -> Number -> Number", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "round", + "line": 225, + "declaration": "foreign import round :: Number -> Number", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "sign", + "line": 235, + "declaration": "foreign import sign :: Number -> Number", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "sin", + "line": 242, + "declaration": "foreign import sin :: Number -> Number", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "sqrt", + "line": 249, + "declaration": "foreign import sqrt :: Number -> Number", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "tan", + "line": 256, + "declaration": "foreign import tan :: Number -> Number", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "trunc", + "line": 264, + "declaration": "foreign import trunc :: Number -> Number", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Op.purs", + "vendored_sha256": "400182c9f9c76ef9eaab32377405195986d70d1f4a4a04cc4833fba0e035cec6", + "vendored_lines": 22, + "self_recursions": [], + "package": "purescript-contravariant", + "tag": "v6.0.0", + "commit": "9ad3e105b8855bcc25f4e0893c784789d05a58de", + "upstream_sha256": "400182c9f9c76ef9eaab32377405195986d70d1f4a4a04cc4833fba0e035cec6", + "upstream_url": "https://github.com/purescript/purescript-contravariant/blob/9ad3e105b8855bcc25f4e0893c784789d05a58de/src/Data/Op.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Ord/Down.purs", + "vendored_sha256": "1c1ac8c739112884f86076eaae7ebe1a24c533892c044940813ceee796d840bc", + "vendored_lines": 26, + "self_recursions": [], + "package": "purescript-orders", + "tag": "v6.0.0", + "commit": "f86db621ec5eef1274145f8b1fd8ebbfe0ed4a2c", + "upstream_sha256": "1c1ac8c739112884f86076eaae7ebe1a24c533892c044940813ceee796d840bc", + "upstream_url": "https://github.com/purescript/purescript-orders/blob/f86db621ec5eef1274145f8b1fd8ebbfe0ed4a2c/src/Data/Ord/Down.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Ord/Generic.purs", + "vendored_sha256": "f1a8a7f58a29e0d22087de7acb3e7eb4a7e159fa5cb589d127274efcb91246ca", + "vendored_lines": 39, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "f1a8a7f58a29e0d22087de7acb3e7eb4a7e159fa5cb589d127274efcb91246ca", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Ord/Generic.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Ord/Max.purs", + "vendored_sha256": "0ba5b99631c3d19685c2ff3141d8e7701933e53828ca4277c0060cdb010b6255", + "vendored_lines": 29, + "self_recursions": [], + "package": "purescript-orders", + "tag": "v6.0.0", + "commit": "f86db621ec5eef1274145f8b1fd8ebbfe0ed4a2c", + "upstream_sha256": "0ba5b99631c3d19685c2ff3141d8e7701933e53828ca4277c0060cdb010b6255", + "upstream_url": "https://github.com/purescript/purescript-orders/blob/f86db621ec5eef1274145f8b1fd8ebbfe0ed4a2c/src/Data/Ord/Max.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Ord/Min.purs", + "vendored_sha256": "2ff841d2cdab3ddacb446d4562a49b7e17a76bfa23441188f02a77a65db4a6f1", + "vendored_lines": 29, + "self_recursions": [], + "package": "purescript-orders", + "tag": "v6.0.0", + "commit": "f86db621ec5eef1274145f8b1fd8ebbfe0ed4a2c", + "upstream_sha256": "2ff841d2cdab3ddacb446d4562a49b7e17a76bfa23441188f02a77a65db4a6f1", + "upstream_url": "https://github.com/purescript/purescript-orders/blob/f86db621ec5eef1274145f8b1fd8ebbfe0ed4a2c/src/Data/Ord/Min.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Ord.purs", + "vendored_sha256": "ba132ffbd2784283842f3e5c0b8a47be006968229bcd70abce0764a21b8a47ae", + "vendored_lines": 264, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "ba132ffbd2784283842f3e5c0b8a47be006968229bcd70abce0764a21b8a47ae", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Ord.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "ordBooleanImpl", + "line": 84, + "declaration": "foreign import ordBooleanImpl :: Ordering -> Ordering -> Ordering -> Boolean -> Boolean -> Ordering", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "ordIntImpl", + "line": 92, + "declaration": "foreign import ordIntImpl :: Ordering -> Ordering -> Ordering -> Int -> Int -> Ordering", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "ordNumberImpl", + "line": 100, + "declaration": "foreign import ordNumberImpl :: Ordering -> Ordering -> Ordering -> Number -> Number -> Ordering", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "ordStringImpl", + "line": 108, + "declaration": "foreign import ordStringImpl :: Ordering -> Ordering -> Ordering -> String -> String -> Ordering", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "ordCharImpl", + "line": 116, + "declaration": "foreign import ordCharImpl :: Ordering -> Ordering -> Ordering -> Char -> Char -> Ordering", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "ordArrayImpl", + "line": 124, + "declaration": "foreign import ordArrayImpl :: forall a. (a -> a -> Int) -> Array a -> Array a -> Int", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Ordering.purs", + "vendored_sha256": "9a53fe2e7bd7afb11b8db093b1511bc210fc88c1661a78fe4d89800b20cae0a1", + "vendored_lines": 36, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "9a53fe2e7bd7afb11b8db093b1511bc210fc88c1661a78fe4d89800b20cae0a1", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Ordering.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Predicate.purs", + "vendored_sha256": "75e1d24c5f0eecc0d11f7664bd38edd641f5fcb039577d11213eac1a819a2d1c", + "vendored_lines": 18, + "self_recursions": [], + "package": "purescript-contravariant", + "tag": "v6.0.0", + "commit": "9ad3e105b8855bcc25f4e0893c784789d05a58de", + "upstream_sha256": "75e1d24c5f0eecc0d11f7664bd38edd641f5fcb039577d11213eac1a819a2d1c", + "upstream_url": "https://github.com/purescript/purescript-contravariant/blob/9ad3e105b8855bcc25f4e0893c784789d05a58de/src/Data/Predicate.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Profunctor/Choice.purs", + "vendored_sha256": "687391e4819de27fda58acb18be86fdd99bf52166b1ca736525fa4c3310d752a", + "vendored_lines": 83, + "self_recursions": [], + "package": "purescript-profunctor", + "tag": "v6.0.1", + "commit": "17f927ce69710085f2bbefc9a37506828d1ebc5a", + "upstream_sha256": "687391e4819de27fda58acb18be86fdd99bf52166b1ca736525fa4c3310d752a", + "upstream_url": "https://github.com/purescript/purescript-profunctor/blob/17f927ce69710085f2bbefc9a37506828d1ebc5a/src/Data/Profunctor/Choice.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Profunctor/Closed.purs", + "vendored_sha256": "2ebbae569682f0bb828e69417c6b991eb6ac54ae0c439e1da9a44064a613d01d", + "vendored_lines": 12, + "self_recursions": [], + "package": "purescript-profunctor", + "tag": "v6.0.1", + "commit": "17f927ce69710085f2bbefc9a37506828d1ebc5a", + "upstream_sha256": "2ebbae569682f0bb828e69417c6b991eb6ac54ae0c439e1da9a44064a613d01d", + "upstream_url": "https://github.com/purescript/purescript-profunctor/blob/17f927ce69710085f2bbefc9a37506828d1ebc5a/src/Data/Profunctor/Closed.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Profunctor/Cochoice.purs", + "vendored_sha256": "d04b01c617f374356eba5358777bf945f6e41c274b7c8c58cd4327d2d6cfe117", + "vendored_lines": 9, + "self_recursions": [], + "package": "purescript-profunctor", + "tag": "v6.0.1", + "commit": "17f927ce69710085f2bbefc9a37506828d1ebc5a", + "upstream_sha256": "d04b01c617f374356eba5358777bf945f6e41c274b7c8c58cd4327d2d6cfe117", + "upstream_url": "https://github.com/purescript/purescript-profunctor/blob/17f927ce69710085f2bbefc9a37506828d1ebc5a/src/Data/Profunctor/Cochoice.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Profunctor/Costrong.purs", + "vendored_sha256": "7d7eb115e194c2d8518b070a3b39809b1b9bc0bdcfe3e48a29b40e9bd6748513", + "vendored_lines": 9, + "self_recursions": [], + "package": "purescript-profunctor", + "tag": "v6.0.1", + "commit": "17f927ce69710085f2bbefc9a37506828d1ebc5a", + "upstream_sha256": "7d7eb115e194c2d8518b070a3b39809b1b9bc0bdcfe3e48a29b40e9bd6748513", + "upstream_url": "https://github.com/purescript/purescript-profunctor/blob/17f927ce69710085f2bbefc9a37506828d1ebc5a/src/Data/Profunctor/Costrong.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Profunctor/Join.purs", + "vendored_sha256": "acbfbd68ca96516e42a4416e1c587b8db4cfae572f7814fa9784cbfeaf286c9f", + "vendored_lines": 28, + "self_recursions": [], + "package": "purescript-profunctor", + "tag": "v6.0.1", + "commit": "17f927ce69710085f2bbefc9a37506828d1ebc5a", + "upstream_sha256": "acbfbd68ca96516e42a4416e1c587b8db4cfae572f7814fa9784cbfeaf286c9f", + "upstream_url": "https://github.com/purescript/purescript-profunctor/blob/17f927ce69710085f2bbefc9a37506828d1ebc5a/src/Data/Profunctor/Join.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Profunctor/Split.purs", + "vendored_sha256": "32b0f17f7d2140c6f7ed3e536d0ec5220b504017deedcb798df586e4c61ca909", + "vendored_lines": 39, + "self_recursions": [], + "package": "purescript-profunctor", + "tag": "v6.0.1", + "commit": "17f927ce69710085f2bbefc9a37506828d1ebc5a", + "upstream_sha256": "32b0f17f7d2140c6f7ed3e536d0ec5220b504017deedcb798df586e4c61ca909", + "upstream_url": "https://github.com/purescript/purescript-profunctor/blob/17f927ce69710085f2bbefc9a37506828d1ebc5a/src/Data/Profunctor/Split.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Profunctor/Star.purs", + "vendored_sha256": "52953a97b7701dbe8619f61bf2bc0b13d2606efbf85d201527dbda5348c05b40", + "vendored_lines": 80, + "self_recursions": [], + "package": "purescript-profunctor", + "tag": "v6.0.1", + "commit": "17f927ce69710085f2bbefc9a37506828d1ebc5a", + "upstream_sha256": "52953a97b7701dbe8619f61bf2bc0b13d2606efbf85d201527dbda5348c05b40", + "upstream_url": "https://github.com/purescript/purescript-profunctor/blob/17f927ce69710085f2bbefc9a37506828d1ebc5a/src/Data/Profunctor/Star.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Profunctor/Strong.purs", + "vendored_sha256": "7ce15c8be67cd563c7d611891e73be2018fae763e26b9b10d790cab4110cc1cb", + "vendored_lines": 80, + "self_recursions": [], + "package": "purescript-profunctor", + "tag": "v6.0.1", + "commit": "17f927ce69710085f2bbefc9a37506828d1ebc5a", + "upstream_sha256": "7ce15c8be67cd563c7d611891e73be2018fae763e26b9b10d790cab4110cc1cb", + "upstream_url": "https://github.com/purescript/purescript-profunctor/blob/17f927ce69710085f2bbefc9a37506828d1ebc5a/src/Data/Profunctor/Strong.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Profunctor.purs", + "vendored_sha256": "f0e9030ebb6252131618dcbc5e181d98dab01020126bbcedd0344d78cea8c9e1", + "vendored_lines": 44, + "self_recursions": [], + "package": "purescript-profunctor", + "tag": "v6.0.1", + "commit": "17f927ce69710085f2bbefc9a37506828d1ebc5a", + "upstream_sha256": "f0e9030ebb6252131618dcbc5e181d98dab01020126bbcedd0344d78cea8c9e1", + "upstream_url": "https://github.com/purescript/purescript-profunctor/blob/17f927ce69710085f2bbefc9a37506828d1ebc5a/src/Data/Profunctor.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Reflectable.purs", + "vendored_sha256": "3335b7b50707e01e8fe217afa89c1ccc3d9b1c9323329bd153094079a6890bfc", + "vendored_lines": 57, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "3335b7b50707e01e8fe217afa89c1ccc3d9b1c9323329bd153094079a6890bfc", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Reflectable.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "unsafeCoerce", + "line": 36, + "declaration": "foreign import unsafeCoerce :: forall a b. a -> b", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Ring/Generic.purs", + "vendored_sha256": "4aec6adf51e6cb9e63f3004968d2bda3801d31cbc60e8ffff183307393eabee4", + "vendored_lines": 24, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "4aec6adf51e6cb9e63f3004968d2bda3801d31cbc60e8ffff183307393eabee4", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Ring/Generic.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Ring.purs", + "vendored_sha256": "1abcaa302136c9731ec2d89ee1fe29f3acbec0dd32da45d30a8d46cc6fb87b7d", + "vendored_lines": 78, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "1abcaa302136c9731ec2d89ee1fe29f3acbec0dd32da45d30a8d46cc6fb87b7d", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Ring.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "intSub", + "line": 54, + "declaration": "foreign import intSub :: Int -> Int -> Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "numSub", + "line": 55, + "declaration": "foreign import numSub :: Number -> Number -> Number", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Semigroup/First.purs", + "vendored_sha256": "9df1dca916c4a4d344ddf580f2a3d3a1cb270dc8df91044147d590e8bc315347", + "vendored_lines": 40, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "9df1dca916c4a4d344ddf580f2a3d3a1cb270dc8df91044147d590e8bc315347", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Semigroup/First.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Semigroup/Foldable.purs", + "vendored_sha256": "7bbb43d0a9354e63d409e6b01f4ee634f0e900c6516ff17734eee0e0cbb03629", + "vendored_lines": 178, + "self_recursions": [], + "package": "purescript-foldable-traversable", + "tag": "v6.0.0", + "commit": "b3926f870532d287ea59e2d5cd3873b81ef2a93a", + "upstream_sha256": "7bbb43d0a9354e63d409e6b01f4ee634f0e900c6516ff17734eee0e0cbb03629", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/Semigroup/Foldable.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Semigroup/Generic.purs", + "vendored_sha256": "c33f0a8dea5d46a2c7ac0b7805840a443b2ed3d82d226319c243ff0b29ded7bd", + "vendored_lines": 31, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "c33f0a8dea5d46a2c7ac0b7805840a443b2ed3d82d226319c243ff0b29ded7bd", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Semigroup/Generic.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Semigroup/Last.purs", + "vendored_sha256": "f3aadefc2e6687234ad3d4690dd61e436bfa3aca9b57e0a1a4da7328738f7c11", + "vendored_lines": 40, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "f3aadefc2e6687234ad3d4690dd61e436bfa3aca9b57e0a1a4da7328738f7c11", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Semigroup/Last.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Semigroup/Traversable.purs", + "vendored_sha256": "361841e1f1960110c9bdaebdbdd8637742f626a26942e61fe7a8ee91fcf8b447", + "vendored_lines": 72, + "self_recursions": [], + "package": "purescript-foldable-traversable", + "tag": "v6.0.0", + "commit": "b3926f870532d287ea59e2d5cd3873b81ef2a93a", + "upstream_sha256": "361841e1f1960110c9bdaebdbdd8637742f626a26942e61fe7a8ee91fcf8b447", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/Semigroup/Traversable.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Semigroup.purs", + "vendored_sha256": "e14ab5200a3ba533334c3d06cbeac9bd9fdb3428c64a9d59926f69284ce4f9a8", + "vendored_lines": 84, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "e14ab5200a3ba533334c3d06cbeac9bd9fdb3428c64a9d59926f69284ce4f9a8", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Semigroup.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "concatString", + "line": 60, + "declaration": "foreign import concatString :: String -> String -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "concatArray", + "line": 61, + "declaration": "foreign import concatArray :: forall a. Array a -> Array a -> Array a", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Semiring/Generic.purs", + "vendored_sha256": "13e146664e64d99ebe5d942ef3bd897ed78c85e2ab737374d1976b37c36b8601", + "vendored_lines": 51, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "13e146664e64d99ebe5d942ef3bd897ed78c85e2ab737374d1976b37c36b8601", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Semiring/Generic.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Semiring.purs", + "vendored_sha256": "6e588bb9736b230b5bd6410ce86f9e9e4d4326ab250a66fa239a7c5f822e7072", + "vendored_lines": 140, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "6e588bb9736b230b5bd6410ce86f9e9e4d4326ab250a66fa239a7c5f822e7072", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Semiring.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "intAdd", + "line": 89, + "declaration": "foreign import intAdd :: Int -> Int -> Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "intMul", + "line": 90, + "declaration": "foreign import intMul :: Int -> Int -> Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "numAdd", + "line": 91, + "declaration": "foreign import numAdd :: Number -> Number -> Number", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "numMul", + "line": 92, + "declaration": "foreign import numMul :: Number -> Number -> Number", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Set/NonEmpty.purs", + "vendored_sha256": "45a49dc49087f55b9c3c315a1fa68e62313a55bf6aab9f8df2bba52ccf7a4dd5", + "vendored_lines": 163, + "self_recursions": [], + "package": "purescript-ordered-collections", + "tag": "v3.2.0", + "commit": "b9c8ec72b53ee56d6cb6dc962d5ad7ea4708b438", + "upstream_sha256": "45a49dc49087f55b9c3c315a1fa68e62313a55bf6aab9f8df2bba52ccf7a4dd5", + "upstream_url": "https://github.com/purescript/purescript-ordered-collections/blob/b9c8ec72b53ee56d6cb6dc962d5ad7ea4708b438/src/Data/Set/NonEmpty.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Set.purs", + "vendored_sha256": "8281a67c595c3948a64b00cb3cb21d26c8b5f7a80c686e77320c85fdea46aee7", + "vendored_lines": 188, + "self_recursions": [], + "package": "purescript-ordered-collections", + "tag": "v3.2.0", + "commit": "b9c8ec72b53ee56d6cb6dc962d5ad7ea4708b438", + "upstream_sha256": "8281a67c595c3948a64b00cb3cb21d26c8b5f7a80c686e77320c85fdea46aee7", + "upstream_url": "https://github.com/purescript/purescript-ordered-collections/blob/b9c8ec72b53ee56d6cb6dc962d5ad7ea4708b438/src/Data/Set.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Show/Generic.purs", + "vendored_sha256": "bd4a82c3cb1e734f775ff6a40fae47b053d336313faf47efb8b4b5d78895376f", + "vendored_lines": 57, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "bd4a82c3cb1e734f775ff6a40fae47b053d336313faf47efb8b4b5d78895376f", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Show/Generic.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "intercalate", + "line": 57, + "declaration": "foreign import intercalate :: String -> Array String -> String", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Show.purs", + "vendored_sha256": "4422b4bbe3d67f1f85575937fb7b9127af4443fe951926a95f6ecf0822032e6c", + "vendored_lines": 97, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "4422b4bbe3d67f1f85575937fb7b9127af4443fe951926a95f6ecf0822032e6c", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Show.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "showIntImpl", + "line": 93, + "declaration": "foreign import showIntImpl :: Int -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "showNumberImpl", + "line": 94, + "declaration": "foreign import showNumberImpl :: Number -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "showCharImpl", + "line": 95, + "declaration": "foreign import showCharImpl :: Char -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "showStringImpl", + "line": 96, + "declaration": "foreign import showStringImpl :: String -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "showArrayImpl", + "line": 97, + "declaration": "foreign import showArrayImpl :: forall a. (a -> String) -> Array a -> String", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/CaseInsensitive.purs", + "vendored_sha256": "1d889da73589b5bed249153cc55a01654a912756bc062ae1f12df3357c2ca9ed", + "vendored_lines": 22, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "1d889da73589b5bed249153cc55a01654a912756bc062ae1f12df3357c2ca9ed", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CaseInsensitive.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/CodePoints.purs", + "vendored_sha256": "a8415ac45b14a0d78334af6d0fb533c7bfc3cfb3ed90e28113c453c9b15f170b", + "vendored_lines": 436, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "a8415ac45b14a0d78334af6d0fb533c7bfc3cfb3ed90e28113c453c9b15f170b", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodePoints.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "_singleton", + "line": 91, + "declaration": "foreign import _singleton :: (CodePoint -> String) -> CodePoint -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "_fromCodePointArray", + "line": 117, + "declaration": "foreign import _fromCodePointArray :: (CodePoint -> String) -> Array CodePoint -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "_toCodePointArray", + "line": 136, + "declaration": "foreign import _toCodePointArray :: (String -> Array CodePoint) -> (String -> CodePoint) -> String -> Array CodePoint", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "_codePointAt", + "line": 166, + "declaration": "foreign import _codePointAt :: (Int -> String -> Maybe CodePoint) -> (forall a. a -> Maybe a) -> (forall a. Maybe a) -> (String -> CodePoint) -> Int -> String -> Maybe CodePoint", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "_countPrefix", + "line": 230, + "declaration": "foreign import _countPrefix :: ((CodePoint -> Boolean) -> String -> Int) -> (String -> CodePoint) -> (CodePoint -> Boolean) -> String -> Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "_take", + "line": 331, + "declaration": "foreign import _take :: (Int -> String -> String) -> Int -> String -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "_unsafeCodePointAt0", + "line": 421, + "declaration": "foreign import _unsafeCodePointAt0 :: (String -> CodePoint) -> String -> CodePoint", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/CodeUnits.purs", + "vendored_sha256": "801f13b0d180a49a454d6456c1c5a7a235a2a7d1827ab31537506e9b279a0648", + "vendored_lines": 332, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "801f13b0d180a49a454d6456c1c5a7a235a2a7d1827ab31537506e9b279a0648", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/CodeUnits.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "singleton", + "line": 83, + "declaration": "foreign import singleton :: Char -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "fromCharArray", + "line": 90, + "declaration": "foreign import fromCharArray :: Array Char -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "toCharArray", + "line": 97, + "declaration": "foreign import toCharArray :: String -> Array Char", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "_charAt", + "line": 109, + "declaration": "foreign import _charAt :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Int -> String -> Maybe Char", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "_toChar", + "line": 126, + "declaration": "foreign import _toChar :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> String -> Maybe Char", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "length", + "line": 150, + "declaration": "foreign import length :: String -> Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "countPrefix", + "line": 159, + "declaration": "foreign import countPrefix :: (Char -> Boolean) -> String -> Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "_indexOf", + "line": 172, + "declaration": "foreign import _indexOf :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Pattern -> String -> Maybe Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "_indexOfStartingAt", + "line": 191, + "declaration": "foreign import _indexOfStartingAt :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Pattern -> Int -> String -> Maybe Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "_lastIndexOf", + "line": 210, + "declaration": "foreign import _lastIndexOf :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Pattern -> String -> Maybe Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "_lastIndexOfStartingAt", + "line": 238, + "declaration": "foreign import _lastIndexOfStartingAt :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> Pattern -> Int -> String -> Maybe Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "take", + "line": 252, + "declaration": "foreign import take :: Int -> String -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "drop", + "line": 279, + "declaration": "foreign import drop :: Int -> String -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "slice", + "line": 311, + "declaration": "foreign import slice :: Int -> Int -> String -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "splitAt", + "line": 332, + "declaration": "foreign import splitAt :: Int -> String -> { before :: String, after :: String }", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/Common.purs", + "vendored_sha256": "286d269bba5d0010924bc62a45188f664d93f7486707f9a3d5038ad7699a2f84", + "vendored_lines": 96, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "286d269bba5d0010924bc62a45188f664d93f7486707f9a3d5038ad7699a2f84", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Common.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "_localeCompare", + "line": 37, + "declaration": "foreign import _localeCompare :: Ordering -> Ordering -> Ordering -> String -> String -> Ordering", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "replace", + "line": 50, + "declaration": "foreign import replace :: Pattern -> Replacement -> String -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "replaceAll", + "line": 57, + "declaration": "foreign import replaceAll :: Pattern -> Replacement -> String -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "split", + "line": 65, + "declaration": "foreign import split :: Pattern -> String -> Array String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "toLower", + "line": 72, + "declaration": "foreign import toLower :: String -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "toUpper", + "line": 79, + "declaration": "foreign import toUpper :: String -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "trim", + "line": 88, + "declaration": "foreign import trim :: String -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "joinWith", + "line": 96, + "declaration": "foreign import joinWith :: String -> Array String -> String", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/Gen.purs", + "vendored_sha256": "fdc6f113d91213f5571aaef42726fd6966eba12c56cb30c40736ff9398a6585b", + "vendored_lines": 43, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "fdc6f113d91213f5571aaef42726fd6966eba12c56cb30c40736ff9398a6585b", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Gen.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/NonEmpty/CaseInsensitive.purs", + "vendored_sha256": "73160accd99f00200e60a5431d70f197aded453aac143f15d842d8fcb984d803", + "vendored_lines": 22, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "73160accd99f00200e60a5431d70f197aded453aac143f15d842d8fcb984d803", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/NonEmpty/CaseInsensitive.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/NonEmpty/CodePoints.purs", + "vendored_sha256": "26f6c829cda09064888b07ada1989e44d5781c81d9927f9b3c2351e82b3077b0", + "vendored_lines": 138, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "26f6c829cda09064888b07ada1989e44d5781c81d9927f9b3c2351e82b3077b0", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/NonEmpty/CodePoints.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/NonEmpty/CodeUnits.purs", + "vendored_sha256": "305c39d652a011b9dbb723e22be8e87767bd91fb3f962cb511666828647fafc8", + "vendored_lines": 308, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "305c39d652a011b9dbb723e22be8e87767bd91fb3f962cb511666828647fafc8", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/NonEmpty/CodeUnits.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/NonEmpty/Internal.purs", + "vendored_sha256": "1efe2c4ec248665e11f8978bf5f44d6eec2c2c248586c47a6137e652d452d966", + "vendored_lines": 232, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "1efe2c4ec248665e11f8978bf5f44d6eec2c2c248586c47a6137e652d452d966", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/NonEmpty/Internal.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/NonEmpty.purs", + "vendored_sha256": "b369bd4f0f9ee7f1d8ced366cd82f0c8b17b99365517d9f305b2fe980c172134", + "vendored_lines": 9, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "b369bd4f0f9ee7f1d8ced366cd82f0c8b17b99365517d9f305b2fe980c172134", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/NonEmpty.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/Pattern.purs", + "vendored_sha256": "cd63d7bb03364c5f5e0b2fbe44746c81b2965300bbf890702331d146a5f3b90f", + "vendored_lines": 33, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "cd63d7bb03364c5f5e0b2fbe44746c81b2965300bbf890702331d146a5f3b90f", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Pattern.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/Regex/Flags.purs", + "vendored_sha256": "c764c8d36ccd74d5603dbfef8b62976788369c9844bceab91c319d61dea16c16", + "vendored_lines": 129, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "c764c8d36ccd74d5603dbfef8b62976788369c9844bceab91c319d61dea16c16", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Regex/Flags.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/Regex/Unsafe.purs", + "vendored_sha256": "9a0bf4c37a2ac844622b29a72d24c3b3aa1e0ad04bcfe1186962f6cd2e433e59", + "vendored_lines": 14, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "9a0bf4c37a2ac844622b29a72d24c3b3aa1e0ad04bcfe1186962f6cd2e433e59", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Regex/Unsafe.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/Regex.purs", + "vendored_sha256": "101f80a5bcba3f3bf1b6ec1ec68f48c18d149d9e4b7808eddf8e753eb15d1d88", + "vendored_lines": 131, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "101f80a5bcba3f3bf1b6ec1ec68f48c18d149d9e4b7808eddf8e753eb15d1d88", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Regex.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "showRegexImpl", + "line": 31, + "declaration": "foreign import showRegexImpl :: Regex -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "regexImpl", + "line": 36, + "declaration": "foreign import regexImpl :: (String -> Either String Regex) -> (Regex -> Either String Regex) -> String -> String -> Either String Regex", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "source", + "line": 49, + "declaration": "foreign import source :: Regex -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "flagsImpl", + "line": 56, + "declaration": "foreign import flagsImpl :: Regex -> RegexFlagsRec", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "test", + "line": 82, + "declaration": "foreign import test :: Regex -> String -> Boolean", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "_match", + "line": 84, + "declaration": "foreign import _match :: (forall r. r -> Maybe r) -> (forall r. Maybe r) -> Regex -> String -> Maybe (NonEmptyArray (Maybe String))", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "replace", + "line": 101, + "declaration": "foreign import replace :: Regex -> String -> String -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "_replaceBy", + "line": 103, + "declaration": "foreign import _replaceBy :: (forall r. r -> Maybe r) -> (forall r. Maybe r) -> Regex -> (String -> Array (Maybe String) -> String) -> String -> String", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "_search", + "line": 118, + "declaration": "foreign import _search :: (forall r. r -> Maybe r) -> (forall r. Maybe r) -> Regex -> String -> Maybe Int", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "split", + "line": 131, + "declaration": "foreign import split :: Regex -> String -> Array String", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String/Unsafe.purs", + "vendored_sha256": "b361c98410d61e2d110a4ae54cda75012679a6733b101dde378d3307ec69c702", + "vendored_lines": 15, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "b361c98410d61e2d110a4ae54cda75012679a6733b101dde378d3307ec69c702", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String/Unsafe.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "charAt", + "line": 10, + "declaration": "foreign import charAt :: Int -> String -> Char", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "char", + "line": 15, + "declaration": "foreign import char :: String -> Char", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/String.purs", + "vendored_sha256": "0cf63fa0bccd027bededff0647c1a4251d235be02c1df8a8310be2aea2eb367f", + "vendored_lines": 10, + "self_recursions": [], + "package": "purescript-strings", + "tag": "v6.0.1", + "commit": "3d3e2f7197d4f7aacb15e854ee9a645489555fff", + "upstream_sha256": "0cf63fa0bccd027bededff0647c1a4251d235be02c1df8a8310be2aea2eb367f", + "upstream_url": "https://github.com/purescript/purescript-strings/blob/3d3e2f7197d4f7aacb15e854ee9a645489555fff/src/Data/String.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Symbol.purs", + "vendored_sha256": "8302d378edfcd58fbe1aadcf95c22f8637533de188b99ca2a0e43147e8957ec4", + "vendored_lines": 24, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "8302d378edfcd58fbe1aadcf95c22f8637533de188b99ca2a0e43147e8957ec4", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Symbol.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "unsafeCoerce", + "line": 14, + "declaration": "foreign import unsafeCoerce :: forall a b. a -> b", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Traversable/Accum/Internal.purs", + "vendored_sha256": "03a2322dd8920092618ad5a9ef8bab31e740b384af88b2e9816abd25d34d1755", + "vendored_lines": 44, + "self_recursions": [], + "package": "purescript-foldable-traversable", + "tag": "v6.0.0", + "commit": "b3926f870532d287ea59e2d5cd3873b81ef2a93a", + "upstream_sha256": "03a2322dd8920092618ad5a9ef8bab31e740b384af88b2e9816abd25d34d1755", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/Traversable/Accum/Internal.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Traversable/Accum.purs", + "vendored_sha256": "a37609f85d2dc1e527a2cd969302f0bf0bd5c8735226f5fbc874f67a6f4b646d", + "vendored_lines": 5, + "self_recursions": [], + "package": "purescript-foldable-traversable", + "tag": "v6.0.0", + "commit": "b3926f870532d287ea59e2d5cd3873b81ef2a93a", + "upstream_sha256": "a37609f85d2dc1e527a2cd969302f0bf0bd5c8735226f5fbc874f67a6f4b646d", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/Traversable/Accum.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Traversable.purs", + "vendored_sha256": "605c2e3c31d3531ce3674d5fb48ab608981b1553f9272c234ad9ca792108bf77", + "vendored_lines": 257, + "self_recursions": [], + "package": "purescript-foldable-traversable", + "tag": "v6.0.0", + "commit": "b3926f870532d287ea59e2d5cd3873b81ef2a93a", + "upstream_sha256": "605c2e3c31d3531ce3674d5fb48ab608981b1553f9272c234ad9ca792108bf77", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/Traversable.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "traverseArrayImpl", + "line": 106, + "declaration": "foreign import traverseArrayImpl :: forall m a b . (forall x y. m (x -> y) -> m x -> m y) -> (forall x y. (x -> y) -> m x -> m y) -> (forall x. x -> m x) -> (a -> m b) -> Array a -> m (Array b)", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/TraversableWithIndex.purs", + "vendored_sha256": "aa2f2406e9ffbf49bb8b07a74b5c044eef9f4e3cef21b8cf7ba18a0edddc23ac", + "vendored_lines": 213, + "self_recursions": [], + "package": "purescript-foldable-traversable", + "tag": "v6.0.0", + "commit": "b3926f870532d287ea59e2d5cd3873b81ef2a93a", + "upstream_sha256": "aa2f2406e9ffbf49bb8b07a74b5c044eef9f4e3cef21b8cf7ba18a0edddc23ac", + "upstream_url": "https://github.com/purescript/purescript-foldable-traversable/blob/b3926f870532d287ea59e2d5cd3873b81ef2a93a/src/Data/TraversableWithIndex.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Tuple/Nested.purs", + "vendored_sha256": "6747d4c125c506fb22b831142513c54112fb3bf5908697534b540ce8e4cb0a4e", + "vendored_lines": 294, + "self_recursions": [], + "package": "purescript-tuples", + "tag": "v7.0.0", + "commit": "4f52da2729b448c8564369378f1232d8d2dc1d8b", + "upstream_sha256": "6747d4c125c506fb22b831142513c54112fb3bf5908697534b540ce8e4cb0a4e", + "upstream_url": "https://github.com/purescript/purescript-tuples/blob/4f52da2729b448c8564369378f1232d8d2dc1d8b/src/Data/Tuple/Nested.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Tuple.purs", + "vendored_sha256": "dc2ef1a90e52e851e02de1e8bb3bbbe9bfdbff8fe821473cbfda1998a82bc0d7", + "vendored_lines": 135, + "self_recursions": [], + "package": "purescript-tuples", + "tag": "v7.0.0", + "commit": "4f52da2729b448c8564369378f1232d8d2dc1d8b", + "upstream_sha256": "dc2ef1a90e52e851e02de1e8bb3bbbe9bfdbff8fe821473cbfda1998a82bc0d7", + "upstream_url": "https://github.com/purescript/purescript-tuples/blob/4f52da2729b448c8564369378f1232d8d2dc1d8b/src/Data/Tuple.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Unfoldable.purs", + "vendored_sha256": "c36722cb427b94e8d20b6a4df3afd8163ebf71cf88502cc20e68d8e3cad773f7", + "vendored_lines": 103, + "self_recursions": [], + "package": "purescript-unfoldable", + "tag": "v6.0.0", + "commit": "493dfe04ed590e20d8f69079df2f58486882748d", + "upstream_sha256": "c36722cb427b94e8d20b6a4df3afd8163ebf71cf88502cc20e68d8e3cad773f7", + "upstream_url": "https://github.com/purescript/purescript-unfoldable/blob/493dfe04ed590e20d8f69079df2f58486882748d/src/Data/Unfoldable.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "unfoldrArrayImpl", + "line": 47, + "declaration": "foreign import unfoldrArrayImpl :: forall a b . (forall x. Maybe x -> Boolean) -> (forall x. Maybe x -> x) -> (forall x y. Tuple x y -> x) -> (forall x y. Tuple x y -> y) -> (b -> Maybe (Tuple a b)) -> b -> Array a", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Unfoldable1.purs", + "vendored_sha256": "7e5bf042a5c7d47bc6b5c57bcbfec7eadfd72d2f33979fa0d5ab7c4605f8edc1", + "vendored_lines": 131, + "self_recursions": [], + "package": "purescript-unfoldable", + "tag": "v6.0.0", + "commit": "493dfe04ed590e20d8f69079df2f58486882748d", + "upstream_sha256": "7e5bf042a5c7d47bc6b5c57bcbfec7eadfd72d2f33979fa0d5ab7c4605f8edc1", + "upstream_url": "https://github.com/purescript/purescript-unfoldable/blob/493dfe04ed590e20d8f69079df2f58486882748d/src/Data/Unfoldable1.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "unfoldr1ArrayImpl", + "line": 48, + "declaration": "foreign import unfoldr1ArrayImpl :: forall a b . (forall x. Maybe x -> Boolean) -> (forall x. Maybe x -> x) -> (forall x y. Tuple x y -> x) -> (forall x y. Tuple x y -> y) -> (b -> Tuple a (Maybe b)) -> b -> Array a", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Unit.purs", + "vendored_sha256": "9da429dc5a86e009ed827b12b3b98106668d6d744c6f6ab7ee8d71414eb6ca0d", + "vendored_lines": 5, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "ce9f2d8fba25b6b392e5e9e81afe0fe33968dc15420765ab19bac8adb948e64f", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Unit.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "unit", + "line": 14, + "declaration": "foreign import unit :: Unit", + "vendored_status": "declaration_removed" + } + ], + "changed_original_code_outside_value_ffi": [ + { + "line": 1, + "text": "module Data.Unit where" + }, + { + "line": 11, + "text": "foreign import data Unit :: Type" + } + ] + }, + { + "path": "Data/Void.purs", + "vendored_sha256": "bf2d7931a857b5aeafc4e3a27c1425010c8c7710772b4e3ff8c102f5c3f84564", + "vendored_lines": 34, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "bf2d7931a857b5aeafc4e3a27c1425010c8c7710772b4e3ff8c102f5c3f84564", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Data/Void.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Data/Witherable.purs", + "vendored_sha256": "3250d0c8465691cc772887e09ef48f4b9554a236f614da7346abed3c0f5cfb93", + "vendored_lines": 162, + "self_recursions": [], + "package": "purescript-filterable", + "tag": "v5.0.0", + "commit": "7c5b8c72779997f2b17d12ce478ff81e7ddda285", + "upstream_sha256": "3250d0c8465691cc772887e09ef48f4b9554a236f614da7346abed3c0f5cfb93", + "upstream_url": "https://github.com/purescript/purescript-filterable/blob/7c5b8c72779997f2b17d12ce478ff81e7ddda285/src/Data/Witherable.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Effect/Class/Console.purs", + "vendored_sha256": "1aa9260f9b96c0d6b5721c5ad473dd0c518157d06da9c5d4cbbac6c28518558d", + "vendored_lines": 49, + "self_recursions": [], + "package": "purescript-console", + "tag": "v6.0.0", + "commit": "3b83d7b792d03872afeea5e62b4f686ab0f09842", + "upstream_sha256": "1aa9260f9b96c0d6b5721c5ad473dd0c518157d06da9c5d4cbbac6c28518558d", + "upstream_url": "https://github.com/purescript/purescript-console/blob/3b83d7b792d03872afeea5e62b4f686ab0f09842/src/Effect/Class/Console.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Effect/Class.purs", + "vendored_sha256": "a78c5876b0aad1123e30ca8a37cd79ca2a837fbccc3a9e92bea3793033cc5a03", + "vendored_lines": 19, + "self_recursions": [], + "package": "purescript-effect", + "tag": "v4.0.0", + "commit": "a192ddb923027d426d6ea3d8deb030c9aa7c7dda", + "upstream_sha256": "a78c5876b0aad1123e30ca8a37cd79ca2a837fbccc3a9e92bea3793033cc5a03", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Class.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Effect/Console.purs", + "vendored_sha256": "06eacf43a1b90e48e5e482805d7ca029c888b98adbd1440ccda4da3894a4ecc1", + "vendored_lines": 69, + "self_recursions": [], + "package": "purescript-console", + "tag": "v6.0.0", + "commit": "3b83d7b792d03872afeea5e62b4f686ab0f09842", + "upstream_sha256": "14a1820f28867485552db40f58826ceafb63fc899802a38a15ea82dd6974cce7", + "upstream_url": "https://github.com/purescript/purescript-console/blob/3b83d7b792d03872afeea5e62b4f686ab0f09842/src/Effect/Console.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "log", + "line": 9, + "declaration": "foreign import log :: String -> Effect Unit", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "warn", + "line": 19, + "declaration": "foreign import warn :: String -> Effect Unit", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "error", + "line": 29, + "declaration": "foreign import error :: String -> Effect Unit", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "info", + "line": 39, + "declaration": "foreign import info :: String -> Effect Unit", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "debug", + "line": 49, + "declaration": "foreign import debug :: String -> Effect Unit", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "time", + "line": 59, + "declaration": "foreign import time :: String -> Effect Unit", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "timeLog", + "line": 62, + "declaration": "foreign import timeLog :: String -> Effect Unit", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "timeEnd", + "line": 65, + "declaration": "foreign import timeEnd :: String -> Effect Unit", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "clear", + "line": 68, + "declaration": "foreign import clear :: Effect Unit", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Effect/Ref.purs", + "vendored_sha256": "d1e4e9c7037152b73f39a9a1369e9d6c00ca0d6ce6c437abe7ea457adb6d8774", + "vendored_lines": 73, + "self_recursions": [], + "package": "purescript-refs", + "tag": "v6.0.0", + "commit": "f8e6216da4cb9309fde1f20cd6f69ac3a3b7f9e8", + "upstream_sha256": "d1e4e9c7037152b73f39a9a1369e9d6c00ca0d6ce6c437abe7ea457adb6d8774", + "upstream_url": "https://github.com/purescript/purescript-refs/blob/f8e6216da4cb9309fde1f20cd6f69ac3a3b7f9e8/src/Effect/Ref.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "_new", + "line": 44, + "declaration": "foreign import _new :: forall s. s -> Effect (Ref s)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "newWithSelf", + "line": 51, + "declaration": "foreign import newWithSelf :: forall s. (Ref s -> s) -> Effect (Ref s)", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "read", + "line": 54, + "declaration": "foreign import read :: forall s. Ref s -> Effect s", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "modifyImpl", + "line": 61, + "declaration": "foreign import modifyImpl :: forall s b. (s -> { state :: s, value :: b }) -> Ref s -> Effect b", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "write", + "line": 73, + "declaration": "foreign import write :: forall s. s -> Ref s -> Effect Unit", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Effect/Uncurried.purs", + "vendored_sha256": "0c6db8591d2519d9b18bf2c5ec56f33b6c25ee6f69d3edfbce8482d48771214b", + "vendored_lines": 286, + "self_recursions": [], + "package": "purescript-effect", + "tag": "v4.0.0", + "commit": "a192ddb923027d426d6ea3d8deb030c9aa7c7dda", + "upstream_sha256": "0c6db8591d2519d9b18bf2c5ec56f33b6c25ee6f69d3edfbce8482d48771214b", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Uncurried.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "mkEffectFn1", + "line": 178, + "declaration": "foreign import mkEffectFn1 :: forall a r. (a -> Effect r) -> EffectFn1 a r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkEffectFn2", + "line": 180, + "declaration": "foreign import mkEffectFn2 :: forall a b r. (a -> b -> Effect r) -> EffectFn2 a b r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkEffectFn3", + "line": 182, + "declaration": "foreign import mkEffectFn3 :: forall a b c r. (a -> b -> c -> Effect r) -> EffectFn3 a b c r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkEffectFn4", + "line": 184, + "declaration": "foreign import mkEffectFn4 :: forall a b c d r. (a -> b -> c -> d -> Effect r) -> EffectFn4 a b c d r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkEffectFn5", + "line": 186, + "declaration": "foreign import mkEffectFn5 :: forall a b c d e r. (a -> b -> c -> d -> e -> Effect r) -> EffectFn5 a b c d e r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkEffectFn6", + "line": 188, + "declaration": "foreign import mkEffectFn6 :: forall a b c d e f r. (a -> b -> c -> d -> e -> f -> Effect r) -> EffectFn6 a b c d e f r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkEffectFn7", + "line": 190, + "declaration": "foreign import mkEffectFn7 :: forall a b c d e f g r. (a -> b -> c -> d -> e -> f -> g -> Effect r) -> EffectFn7 a b c d e f g r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkEffectFn8", + "line": 192, + "declaration": "foreign import mkEffectFn8 :: forall a b c d e f g h r. (a -> b -> c -> d -> e -> f -> g -> h -> Effect r) -> EffectFn8 a b c d e f g h r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkEffectFn9", + "line": 194, + "declaration": "foreign import mkEffectFn9 :: forall a b c d e f g h i r. (a -> b -> c -> d -> e -> f -> g -> h -> i -> Effect r) -> EffectFn9 a b c d e f g h i r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "mkEffectFn10", + "line": 196, + "declaration": "foreign import mkEffectFn10 :: forall a b c d e f g h i j r. (a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> Effect r) -> EffectFn10 a b c d e f g h i j r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runEffectFn1", + "line": 199, + "declaration": "foreign import runEffectFn1 :: forall a r. EffectFn1 a r -> a -> Effect r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runEffectFn2", + "line": 201, + "declaration": "foreign import runEffectFn2 :: forall a b r. EffectFn2 a b r -> a -> b -> Effect r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runEffectFn3", + "line": 203, + "declaration": "foreign import runEffectFn3 :: forall a b c r. EffectFn3 a b c r -> a -> b -> c -> Effect r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runEffectFn4", + "line": 205, + "declaration": "foreign import runEffectFn4 :: forall a b c d r. EffectFn4 a b c d r -> a -> b -> c -> d -> Effect r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runEffectFn5", + "line": 207, + "declaration": "foreign import runEffectFn5 :: forall a b c d e r. EffectFn5 a b c d e r -> a -> b -> c -> d -> e -> Effect r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runEffectFn6", + "line": 209, + "declaration": "foreign import runEffectFn6 :: forall a b c d e f r. EffectFn6 a b c d e f r -> a -> b -> c -> d -> e -> f -> Effect r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runEffectFn7", + "line": 211, + "declaration": "foreign import runEffectFn7 :: forall a b c d e f g r. EffectFn7 a b c d e f g r -> a -> b -> c -> d -> e -> f -> g -> Effect r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runEffectFn8", + "line": 213, + "declaration": "foreign import runEffectFn8 :: forall a b c d e f g h r. EffectFn8 a b c d e f g h r -> a -> b -> c -> d -> e -> f -> g -> h -> Effect r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runEffectFn9", + "line": 215, + "declaration": "foreign import runEffectFn9 :: forall a b c d e f g h i r. EffectFn9 a b c d e f g h i r -> a -> b -> c -> d -> e -> f -> g -> h -> i -> Effect r", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "runEffectFn10", + "line": 217, + "declaration": "foreign import runEffectFn10 :: forall a b c d e f g h i j r. EffectFn10 a b c d e f g h i j r -> a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> Effect r", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Effect/Unsafe.purs", + "vendored_sha256": "e20339c8aed22f342fa8d452dd186079b4324cd00933b9e9f4eeb59d3313cac0", + "vendored_lines": 8, + "self_recursions": [], + "package": "purescript-effect", + "tag": "v4.0.0", + "commit": "a192ddb923027d426d6ea3d8deb030c9aa7c7dda", + "upstream_sha256": "e20339c8aed22f342fa8d452dd186079b4324cd00933b9e9f4eeb59d3313cac0", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect/Unsafe.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "unsafePerformEffect", + "line": 8, + "declaration": "foreign import unsafePerformEffect :: forall a. Effect a -> a", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Effect.purs", + "vendored_sha256": "4095762286c11b91d19728e14e9cb260bda98e7982ee29a1b14566cee14faae7", + "vendored_lines": 72, + "self_recursions": [], + "package": "purescript-effect", + "tag": "v4.0.0", + "commit": "a192ddb923027d426d6ea3d8deb030c9aa7c7dda", + "upstream_sha256": "f5738a67511e72894c3784e44a334c94a6aa8e504fd3a29ddccc3617416b6f40", + "upstream_url": "https://github.com/purescript/purescript-effect/blob/a192ddb923027d426d6ea3d8deb030c9aa7c7dda/src/Effect.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "pureE", + "line": 29, + "declaration": "foreign import pureE :: forall a. a -> Effect a", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "bindE", + "line": 34, + "declaration": "foreign import bindE :: forall a b. Effect a -> (a -> Effect b) -> Effect b", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "untilE", + "line": 53, + "declaration": "foreign import untilE :: Effect Boolean -> Effect Unit", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "whileE", + "line": 60, + "declaration": "foreign import whileE :: forall a. Effect Boolean -> Effect a -> Effect Unit", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "forE", + "line": 66, + "declaration": "foreign import forE :: Int -> Int -> (Int -> Effect Unit) -> Effect Unit", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "foreachE", + "line": 72, + "declaration": "foreign import foreachE :: forall a. Array a -> (a -> Effect Unit) -> Effect Unit", + "vendored_status": "nonrecursive_replacement" + } + ], + "changed_original_code_outside_value_ffi": [ + { + "line": 16, + "text": "foreign import data Effect :: Type -> Type" + }, + { + "line": 18, + "text": "type role Effect representational" + }, + { + "line": 20, + "text": "instance functorEffect :: Functor Effect where" + }, + { + "line": 21, + "text": " map = liftA1" + }, + { + "line": 23, + "text": "instance applyEffect :: Apply Effect where" + }, + { + "line": 24, + "text": " apply = ap" + }, + { + "line": 26, + "text": "instance applicativeEffect :: Applicative Effect where" + }, + { + "line": 27, + "text": " pure = pureE" + }, + { + "line": 31, + "text": "instance bindEffect :: Bind Effect where" + }, + { + "line": 32, + "text": " bind = bindE" + }, + { + "line": 36, + "text": "instance monadEffect :: Monad Effect" + } + ] + }, + { + "path": "Partial/Unsafe.purs", + "vendored_sha256": "d3d70727652d25459d19f812d186cf24583de1be5eb1decd87b6bf4afc36a4bd", + "vendored_lines": 24, + "self_recursions": [], + "package": "purescript-partial", + "tag": "v4.0.0", + "commit": "0fa0646f5ea1ec5f0c46dcbd770c705a6c9ad3ec", + "upstream_sha256": "d3d70727652d25459d19f812d186cf24583de1be5eb1decd87b6bf4afc36a4bd", + "upstream_url": "https://github.com/purescript/purescript-partial/blob/0fa0646f5ea1ec5f0c46dcbd770c705a6c9ad3ec/src/Partial/Unsafe.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "_unsafePartial", + "line": 16, + "declaration": "foreign import _unsafePartial :: forall a b. a -> b", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Partial.purs", + "vendored_sha256": "33ec6baf7de02292b40b97ed63f91402ec09ebf49c97392bb77ea78680177cd6", + "vendored_lines": 15, + "self_recursions": [], + "package": "purescript-partial", + "tag": "v4.0.0", + "commit": "0fa0646f5ea1ec5f0c46dcbd770c705a6c9ad3ec", + "upstream_sha256": "33ec6baf7de02292b40b97ed63f91402ec09ebf49c97392bb77ea78680177cd6", + "upstream_url": "https://github.com/purescript/purescript-partial/blob/0fa0646f5ea1ec5f0c46dcbd770c705a6c9ad3ec/src/Partial.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "_crashWith", + "line": 15, + "declaration": "foreign import _crashWith :: forall a. String -> a", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Prelude.purs", + "vendored_sha256": "52f6778cf0da8490efb92963454d82a2b9863293d7a52c958572e2d161c3bbaf", + "vendored_lines": 104, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "af6b069885f179a179cc805e6ff7920e1c791dba6fc4d07cd2f52c5e7b91e59f", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Prelude.purs", + "status": "modified", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [ + { + "line": 14, + "text": " ( module Control.Applicative" + } + ] + }, + { + "path": "Record/Unsafe.purs", + "vendored_sha256": "21acab8a811f760d56be569f4e8c2ae30705b34711b26de1cc39b16f96ba3ac1", + "vendored_lines": 27, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "21acab8a811f760d56be569f4e8c2ae30705b34711b26de1cc39b16f96ba3ac1", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Record/Unsafe.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "unsafeHas", + "line": 10, + "declaration": "foreign import unsafeHas :: forall r1. String -> Record r1 -> Boolean", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "unsafeGet", + "line": 15, + "declaration": "foreign import unsafeGet :: forall r a. String -> Record r -> a", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "unsafeSet", + "line": 21, + "declaration": "foreign import unsafeSet :: forall r1 r2 a. String -> a -> Record r1 -> Record r2", + "vendored_status": "foreign_declaration_retained" + }, + { + "name": "unsafeDelete", + "line": 27, + "declaration": "foreign import unsafeDelete :: forall r1 r2. String -> Record r1 -> Record r2", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Safe/Coerce.purs", + "vendored_sha256": "67ab89bd8194231b886591fb03a5d8cd1cbb91e68e384a7e768c4405fa6d959e", + "vendored_lines": 27, + "self_recursions": [], + "package": "purescript-safe-coerce", + "tag": "v2.0.0", + "commit": "7fa799ae80a38b8d948efcb52608e58e198b3da7", + "upstream_sha256": "67ab89bd8194231b886591fb03a5d8cd1cbb91e68e384a7e768c4405fa6d959e", + "upstream_url": "https://github.com/purescript/purescript-safe-coerce/blob/7fa799ae80a38b8d948efcb52608e58e198b3da7/src/Safe/Coerce.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Test/Assert.purs", + "vendored_sha256": "6e04b61530ffab4c0dff75431b82aed94853c1207faf88e0531fa79b120c1a00", + "vendored_lines": 137, + "self_recursions": [], + "package": "purescript-assert", + "tag": "v6.0.0", + "commit": "27c0edb57d2ee497eb5fab664f5601c35b613eda", + "upstream_sha256": "666601c3d459503802027dca698b9bd05bacd42f48f7d320a834196ed4cb4127", + "upstream_url": "https://github.com/purescript/purescript-assert/blob/27c0edb57d2ee497eb5fab664f5601c35b613eda/src/Test/Assert.purs", + "status": "modified", + "upstream_value_foreign_declarations": [ + { + "name": "assertImpl", + "line": 29, + "declaration": "foreign import assertImpl :: String -> Boolean -> Effect Unit", + "vendored_status": "nonrecursive_replacement" + }, + { + "name": "checkThrows", + "line": 59, + "declaration": "foreign import checkThrows :: forall a . (Unit -> a) -> Effect Boolean", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Type/Data/Boolean.purs", + "vendored_sha256": "5bb19aeaddc6ea478b96f06025e742284c85f413637de56cb78198b86aa5fc87", + "vendored_lines": 66, + "self_recursions": [], + "package": "purescript-typelevel-prelude", + "tag": "v7.0.0", + "commit": "dca2fe3c8cfd5527d4fe70c4bedfda30148405bf", + "upstream_sha256": "5bb19aeaddc6ea478b96f06025e742284c85f413637de56cb78198b86aa5fc87", + "upstream_url": "https://github.com/purescript/purescript-typelevel-prelude/blob/dca2fe3c8cfd5527d4fe70c4bedfda30148405bf/src/Type/Data/Boolean.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Type/Data/Ordering.purs", + "vendored_sha256": "fcc7c02e66257c06c4647b929e187d214d653789fee6afc49346d4990ed14997", + "vendored_lines": 69, + "self_recursions": [], + "package": "purescript-typelevel-prelude", + "tag": "v7.0.0", + "commit": "dca2fe3c8cfd5527d4fe70c4bedfda30148405bf", + "upstream_sha256": "fcc7c02e66257c06c4647b929e187d214d653789fee6afc49346d4990ed14997", + "upstream_url": "https://github.com/purescript/purescript-typelevel-prelude/blob/dca2fe3c8cfd5527d4fe70c4bedfda30148405bf/src/Type/Data/Ordering.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Type/Data/Symbol.purs", + "vendored_sha256": "4c3283d0804ca9d2a1ba9fdfb7cf85aacdf2d0c8f6472b383d4b162d8940817b", + "vendored_lines": 35, + "self_recursions": [], + "package": "purescript-typelevel-prelude", + "tag": "v7.0.0", + "commit": "dca2fe3c8cfd5527d4fe70c4bedfda30148405bf", + "upstream_sha256": "4c3283d0804ca9d2a1ba9fdfb7cf85aacdf2d0c8f6472b383d4b162d8940817b", + "upstream_url": "https://github.com/purescript/purescript-typelevel-prelude/blob/dca2fe3c8cfd5527d4fe70c4bedfda30148405bf/src/Type/Data/Symbol.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Type/Equality.purs", + "vendored_sha256": "ac672615fd3612b596eda3ab9f2472006e7ee954c7880cc91ac4865f7c120f56", + "vendored_lines": 35, + "self_recursions": [], + "package": "purescript-type-equality", + "tag": "v4.0.1", + "commit": "0525b7d39e0fbd81b4209518139fb8ab02695774", + "upstream_sha256": "ac672615fd3612b596eda3ab9f2472006e7ee954c7880cc91ac4865f7c120f56", + "upstream_url": "https://github.com/purescript/purescript-type-equality/blob/0525b7d39e0fbd81b4209518139fb8ab02695774/src/Type/Equality.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Type/Function.purs", + "vendored_sha256": "d134b0394685faa83528c92f64c6827194b4531c956dd7d669436c30b2fd9812", + "vendored_lines": 23, + "self_recursions": [], + "package": "purescript-typelevel-prelude", + "tag": "v7.0.0", + "commit": "dca2fe3c8cfd5527d4fe70c4bedfda30148405bf", + "upstream_sha256": "d134b0394685faa83528c92f64c6827194b4531c956dd7d669436c30b2fd9812", + "upstream_url": "https://github.com/purescript/purescript-typelevel-prelude/blob/dca2fe3c8cfd5527d4fe70c4bedfda30148405bf/src/Type/Function.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Type/Prelude.purs", + "vendored_sha256": "2293b8cfd59325e393d51a047dcbaf4aaaf81a131c3f16c9b91ad49bb8f9732d", + "vendored_lines": 17, + "self_recursions": [], + "package": "purescript-typelevel-prelude", + "tag": "v7.0.0", + "commit": "dca2fe3c8cfd5527d4fe70c4bedfda30148405bf", + "upstream_sha256": "2293b8cfd59325e393d51a047dcbaf4aaaf81a131c3f16c9b91ad49bb8f9732d", + "upstream_url": "https://github.com/purescript/purescript-typelevel-prelude/blob/dca2fe3c8cfd5527d4fe70c4bedfda30148405bf/src/Type/Prelude.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Type/Proxy.purs", + "vendored_sha256": "e823c48b15006447de763d42a9f6b8c9e6913a3bd4b49b5e3b7cdce78d7d9b7c", + "vendored_lines": 53, + "self_recursions": [], + "package": "purescript-prelude", + "tag": "v6.0.1", + "commit": "f4cad0ae8106185c9ab407f43cf9abf05c256af4", + "upstream_sha256": "e823c48b15006447de763d42a9f6b8c9e6913a3bd4b49b5e3b7cdce78d7d9b7c", + "upstream_url": "https://github.com/purescript/purescript-prelude/blob/f4cad0ae8106185c9ab407f43cf9abf05c256af4/src/Type/Proxy.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Type/Row/Homogeneous.purs", + "vendored_sha256": "a3db3ece627f2d2c1ef76b76a0e6699934115dd52c6e56ecf50d95868e4eaab8", + "vendored_lines": 23, + "self_recursions": [], + "package": "purescript-typelevel-prelude", + "tag": "v7.0.0", + "commit": "dca2fe3c8cfd5527d4fe70c4bedfda30148405bf", + "upstream_sha256": "a3db3ece627f2d2c1ef76b76a0e6699934115dd52c6e56ecf50d95868e4eaab8", + "upstream_url": "https://github.com/purescript/purescript-typelevel-prelude/blob/dca2fe3c8cfd5527d4fe70c4bedfda30148405bf/src/Type/Row/Homogeneous.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Type/Row.purs", + "vendored_sha256": "b504404c46678bdbe8c49567a4fbbc1ba3cdddc25a7834539c23a1e99001132b", + "vendored_lines": 22, + "self_recursions": [], + "package": "purescript-typelevel-prelude", + "tag": "v7.0.0", + "commit": "dca2fe3c8cfd5527d4fe70c4bedfda30148405bf", + "upstream_sha256": "b504404c46678bdbe8c49567a4fbbc1ba3cdddc25a7834539c23a1e99001132b", + "upstream_url": "https://github.com/purescript/purescript-typelevel-prelude/blob/dca2fe3c8cfd5527d4fe70c4bedfda30148405bf/src/Type/Row.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Type/RowList.purs", + "vendored_sha256": "8e5b0112718c7a42104d5904dc8ad26619b04d826493fc818e375469010a24dd", + "vendored_lines": 82, + "self_recursions": [], + "package": "purescript-typelevel-prelude", + "tag": "v7.0.0", + "commit": "dca2fe3c8cfd5527d4fe70c4bedfda30148405bf", + "upstream_sha256": "8e5b0112718c7a42104d5904dc8ad26619b04d826493fc818e375469010a24dd", + "upstream_url": "https://github.com/purescript/purescript-typelevel-prelude/blob/dca2fe3c8cfd5527d4fe70c4bedfda30148405bf/src/Type/RowList.purs", + "status": "identical", + "upstream_value_foreign_declarations": [], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "Unsafe/Coerce.purs", + "vendored_sha256": "f0d052e7b2e2431860e6e995a91824c0d6437e2de773fd51c2324f626e10ed19", + "vendored_lines": 27, + "self_recursions": [], + "package": "purescript-unsafe-coerce", + "tag": "v6.0.0", + "commit": "ab956f82e66e633f647fb3098e8ddd3ec58d689f", + "upstream_sha256": "f0d052e7b2e2431860e6e995a91824c0d6437e2de773fd51c2324f626e10ed19", + "upstream_url": "https://github.com/purescript/purescript-unsafe-coerce/blob/ab956f82e66e633f647fb3098e8ddd3ec58d689f/src/Unsafe/Coerce.purs", + "status": "identical", + "upstream_value_foreign_declarations": [ + { + "name": "unsafeCoerce", + "line": 27, + "declaration": "foreign import unsafeCoerce :: forall a b. a -> b", + "vendored_status": "foreign_declaration_retained" + } + ], + "changed_original_code_outside_value_ffi": [] + }, + { + "path": "WASI/Clock.purs", + "vendored_sha256": "222f07399b2b881674691eff41d296a943824435ab13e60556f0117e048a7d0a", + "vendored_lines": 14, + "self_recursions": [], + "status": "platform_addition" + }, + { + "path": "WASI/Console.purs", + "vendored_sha256": "f49f4c7ae2e7c70f21bc35ce5080d000637a569e561567470ebf360c32c4beb9", + "vendored_lines": 33, + "self_recursions": [], + "status": "platform_addition" + }, + { + "path": "WASI/FileSystem.purs", + "vendored_sha256": "66747fef3108642b01290cd8323b7ef9ff4aaf08703370ffcd2d1654d92f5a32", + "vendored_lines": 270, + "self_recursions": [], + "status": "platform_addition" + }, + { + "path": "WASI/IO.purs", + "vendored_sha256": "e3df53b3966841930e01f4c89505172ed64996c60aae37118bb5d0d03d725c85", + "vendored_lines": 67, + "self_recursions": [], + "status": "platform_addition" + }, + { + "path": "WASI/Network.purs", + "vendored_sha256": "c2b769cf39bf4ba879024c5bcae388a0ee16bec143be81fb4bd199d6f3fd80d4", + "vendored_lines": 130, + "self_recursions": [], + "status": "platform_addition" + }, + { + "path": "WASI/Process.purs", + "vendored_sha256": "4cfd83ff897a6b3a1cea44aa6d4d5a3fd6e3d4d3a8ad30061ba80636de0bca7b", + "vendored_lines": 13, + "self_recursions": [], + "status": "platform_addition" + }, + { + "path": "WASI/Random.purs", + "vendored_sha256": "c05d4bf338e66e0358d9c74a494d8d84c2c4c99e37750a54c909600834175ee9", + "vendored_lines": 16, + "self_recursions": [], + "status": "platform_addition" + }, + { + "path": "WASI/Resource.purs", + "vendored_sha256": "8da4a53234de3f7d1ec6ee6aaead124a27201edba261da22da38f171fb167acc", + "vendored_lines": 14, + "self_recursions": [], + "status": "platform_addition" + }, + { + "path": "WASI.purs", + "vendored_sha256": "a1f72de13e9676ec66a87080e631ed4e31bf4a4392b10b6fdeabfc39f8d2bb11", + "vendored_lines": 102, + "self_recursions": [], + "status": "platform_addition" + } + ], + "absent_modules": [], + "limits": [ + "Only supplied upstream checkouts are compared; baseline_unavailable is not a pass.", + "Self-recursion detection covers exact top-level same-argument equations only.", + "The script records differences without approving target adaptations or proving semantic equivalence.", + "The official compiler support dependency ranges do not uniquely pin package patch versions." + ] +} diff --git a/docs/implementation/stdlib/vendor-restoration-2026-10-06/modules.md b/docs/implementation/stdlib/vendor-restoration-2026-10-06/modules.md new file mode 100644 index 00000000..21462131 --- /dev/null +++ b/docs/implementation/stdlib/vendor-restoration-2026-10-06/modules.md @@ -0,0 +1,221 @@ +# Vendored module inventory + +Generated by `audit-stdlib-vendor.py`. Status is comparison evidence, not approval. + +| Module path | Official package/tag | Comparison | Direct self-recursions | +| --- | --- | --- | --- | +| `Control/Alt.purs` | purescript-control v6.0.0 | identical | 0 | +| `Control/Alternative.purs` | purescript-control v6.0.0 | identical | 0 | +| `Control/Applicative.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Control/Apply.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Control/Biapplicative.purs` | purescript-bifunctors v6.1.0 | identical | 0 | +| `Control/Biapply.purs` | purescript-bifunctors v6.1.0 | identical | 0 | +| `Control/Bind.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Control/Category.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Control/Comonad.purs` | purescript-control v6.0.0 | identical | 0 | +| `Control/Extend.purs` | purescript-control v6.0.0 | identical | 0 | +| `Control/Lazy.purs` | purescript-control v6.0.0 | identical | 0 | +| `Control/Monad/Gen/Class.purs` | purescript-gen v4.0.0 | identical | 0 | +| `Control/Monad/Gen/Common.purs` | purescript-gen v4.0.0 | identical | 0 | +| `Control/Monad/Gen.purs` | purescript-gen v4.0.0 | identical | 0 | +| `Control/Monad/Rec/Class.purs` | purescript-tailrec v6.1.0 | identical | 0 | +| `Control/Monad/ST/Class.purs` | purescript-st v6.2.0 | identical | 0 | +| `Control/Monad/ST/Global.purs` | purescript-st v6.2.0 | identical | 0 | +| `Control/Monad/ST/Internal.purs` | purescript-st v6.2.0 | identical | 0 | +| `Control/Monad/ST/Ref.purs` | purescript-st v6.2.0 | identical | 0 | +| `Control/Monad/ST/Uncurried.purs` | purescript-st v6.2.0 | identical | 0 | +| `Control/Monad/ST.purs` | purescript-st v6.2.0 | identical | 0 | +| `Control/Monad.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Control/MonadPlus.purs` | purescript-control v6.0.0 | identical | 0 | +| `Control/Plus.purs` | purescript-control v6.0.0 | identical | 0 | +| `Control/Semigroupoid.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Array/NonEmpty/Internal.purs` | purescript-arrays v7.3.0 | identical | 0 | +| `Data/Array/NonEmpty.purs` | purescript-arrays v7.3.0 | identical | 0 | +| `Data/Array/Partial.purs` | purescript-arrays v7.3.0 | identical | 0 | +| `Data/Array/ST/Iterator.purs` | purescript-arrays v7.3.0 | identical | 0 | +| `Data/Array/ST/Partial.purs` | purescript-arrays v7.3.0 | identical | 0 | +| `Data/Array/ST.purs` | purescript-arrays v7.3.0 | identical | 0 | +| `Data/Array.purs` | purescript-arrays v7.3.0 | identical | 0 | +| `Data/Bifoldable.purs` | purescript-foldable-traversable v6.0.0 | identical | 0 | +| `Data/Bifunctor/Join.purs` | purescript-bifunctors v6.1.0 | identical | 0 | +| `Data/Bifunctor.purs` | purescript-bifunctors v6.1.0 | identical | 0 | +| `Data/Bitraversable.purs` | purescript-foldable-traversable v6.0.0 | identical | 0 | +| `Data/Boolean.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/BooleanAlgebra.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Bounded/Generic.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Bounded.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Char/Gen.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/Char.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/CommutativeRing.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Compactable.purs` | purescript-filterable v5.0.0 | identical | 0 | +| `Data/Comparison.purs` | purescript-contravariant v6.0.0 | identical | 0 | +| `Data/Const.purs` | purescript-const v6.0.0 | identical | 0 | +| `Data/Decidable.purs` | purescript-contravariant v6.0.0 | identical | 0 | +| `Data/Decide.purs` | purescript-contravariant v6.0.0 | identical | 0 | +| `Data/Distributive.purs` | purescript-distributive v6.0.0 | identical | 0 | +| `Data/Divide.purs` | purescript-contravariant v6.0.0 | identical | 0 | +| `Data/Divisible.purs` | purescript-contravariant v6.0.0 | identical | 0 | +| `Data/DivisionRing.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Either/Inject.purs` | purescript-either v6.1.0 | identical | 0 | +| `Data/Either/Nested.purs` | purescript-either v6.1.0 | identical | 0 | +| `Data/Either.purs` | purescript-either v6.1.0 | identical | 0 | +| `Data/Enum/Gen.purs` | purescript-enums v6.0.1 | identical | 0 | +| `Data/Enum/Generic.purs` | purescript-enums v6.0.1 | identical | 0 | +| `Data/Enum.purs` | purescript-enums v6.0.1 | identical | 0 | +| `Data/Eq/Generic.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Eq.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Equivalence.purs` | purescript-contravariant v6.0.0 | identical | 0 | +| `Data/EuclideanRing.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Exists.purs` | purescript-exists v6.0.0 | identical | 0 | +| `Data/Field.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Filterable.purs` | purescript-filterable v5.0.0 | identical | 0 | +| `Data/Foldable.purs` | purescript-foldable-traversable v6.0.0 | identical | 0 | +| `Data/FoldableWithIndex.purs` | purescript-foldable-traversable v6.0.0 | identical | 0 | +| `Data/Function/Uncurried.purs` | purescript-functions v6.0.0 | identical | 0 | +| `Data/Function.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Functor/App.purs` | purescript-functors v5.0.0 | identical | 0 | +| `Data/Functor/Clown.purs` | purescript-functors v5.0.0 | identical | 0 | +| `Data/Functor/Compose.purs` | purescript-functors v5.0.0 | identical | 0 | +| `Data/Functor/Contravariant.purs` | purescript-contravariant v6.0.0 | identical | 0 | +| `Data/Functor/Coproduct/Inject.purs` | purescript-functors v5.0.0 | identical | 0 | +| `Data/Functor/Coproduct/Nested.purs` | purescript-functors v5.0.0 | identical | 0 | +| `Data/Functor/Coproduct.purs` | purescript-functors v5.0.0 | identical | 0 | +| `Data/Functor/Costar.purs` | purescript-functors v5.0.0 | identical | 0 | +| `Data/Functor/Flip.purs` | purescript-functors v5.0.0 | identical | 0 | +| `Data/Functor/Invariant.purs` | purescript-invariant v6.0.0 | identical | 0 | +| `Data/Functor/Joker.purs` | purescript-functors v5.0.0 | identical | 0 | +| `Data/Functor/Product/Nested.purs` | purescript-functors v5.0.0 | identical | 0 | +| `Data/Functor/Product.purs` | purescript-functors v5.0.0 | identical | 0 | +| `Data/Functor/Product2.purs` | purescript-functors v5.0.0 | identical | 0 | +| `Data/Functor.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/FunctorWithIndex.purs` | purescript-foldable-traversable v6.0.0 | identical | 0 | +| `Data/Generic/Rep.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/HeytingAlgebra/Generic.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/HeytingAlgebra.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Identity.purs` | purescript-identity v6.0.0 | identical | 0 | +| `Data/Int/Bits.purs` | purescript-integers v6.0.0 | identical | 0 | +| `Data/Int.purs` | purescript-integers v6.0.0 | identical | 0 | +| `Data/Lazy.purs` | purescript-lazy v6.0.0 | identical | 0 | +| `Data/List/Internal.purs` | purescript-lists v7.0.0 | identical | 0 | +| `Data/List/Lazy/NonEmpty.purs` | purescript-lists v7.0.0 | identical | 0 | +| `Data/List/Lazy/Types.purs` | purescript-lists v7.0.0 | identical | 0 | +| `Data/List/Lazy.purs` | purescript-lists v7.0.0 | identical | 0 | +| `Data/List/NonEmpty.purs` | purescript-lists v7.0.0 | identical | 0 | +| `Data/List/Partial.purs` | purescript-lists v7.0.0 | identical | 0 | +| `Data/List/Types.purs` | purescript-lists v7.0.0 | identical | 0 | +| `Data/List/ZipList.purs` | purescript-lists v7.0.0 | identical | 0 | +| `Data/List.purs` | purescript-lists v7.0.0 | identical | 0 | +| `Data/Map/Gen.purs` | purescript-ordered-collections v3.2.0 | identical | 0 | +| `Data/Map/Internal.purs` | purescript-ordered-collections v3.2.0 | identical | 0 | +| `Data/Map.purs` | purescript-ordered-collections v3.2.0 | identical | 0 | +| `Data/Maybe/First.purs` | purescript-maybe v6.0.0 | identical | 0 | +| `Data/Maybe/Last.purs` | purescript-maybe v6.0.0 | identical | 0 | +| `Data/Maybe.purs` | purescript-maybe v6.0.0 | identical | 0 | +| `Data/Monoid/Additive.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Monoid/Alternate.purs` | purescript-control v6.0.0 | identical | 0 | +| `Data/Monoid/Conj.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Monoid/Disj.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Monoid/Dual.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Monoid/Endo.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Monoid/Generic.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Monoid/Multiplicative.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Monoid.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/NaturalTransformation.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Newtype.purs` | purescript-newtype v5.0.0 | identical | 0 | +| `Data/NonEmpty.purs` | purescript-nonempty v7.0.0 | identical | 0 | +| `Data/Number/Approximate.purs` | purescript-numbers v9.0.1 | identical | 0 | +| `Data/Number/Format.purs` | purescript-numbers v9.0.1 | identical | 0 | +| `Data/Number.purs` | purescript-numbers v9.0.1 | identical | 0 | +| `Data/Op.purs` | purescript-contravariant v6.0.0 | identical | 0 | +| `Data/Ord/Down.purs` | purescript-orders v6.0.0 | identical | 0 | +| `Data/Ord/Generic.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Ord/Max.purs` | purescript-orders v6.0.0 | identical | 0 | +| `Data/Ord/Min.purs` | purescript-orders v6.0.0 | identical | 0 | +| `Data/Ord.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Ordering.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Predicate.purs` | purescript-contravariant v6.0.0 | identical | 0 | +| `Data/Profunctor/Choice.purs` | purescript-profunctor v6.0.1 | identical | 0 | +| `Data/Profunctor/Closed.purs` | purescript-profunctor v6.0.1 | identical | 0 | +| `Data/Profunctor/Cochoice.purs` | purescript-profunctor v6.0.1 | identical | 0 | +| `Data/Profunctor/Costrong.purs` | purescript-profunctor v6.0.1 | identical | 0 | +| `Data/Profunctor/Join.purs` | purescript-profunctor v6.0.1 | identical | 0 | +| `Data/Profunctor/Split.purs` | purescript-profunctor v6.0.1 | identical | 0 | +| `Data/Profunctor/Star.purs` | purescript-profunctor v6.0.1 | identical | 0 | +| `Data/Profunctor/Strong.purs` | purescript-profunctor v6.0.1 | identical | 0 | +| `Data/Profunctor.purs` | purescript-profunctor v6.0.1 | identical | 0 | +| `Data/Reflectable.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Ring/Generic.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Ring.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Semigroup/First.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Semigroup/Foldable.purs` | purescript-foldable-traversable v6.0.0 | identical | 0 | +| `Data/Semigroup/Generic.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Semigroup/Last.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Semigroup/Traversable.purs` | purescript-foldable-traversable v6.0.0 | identical | 0 | +| `Data/Semigroup.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Semiring/Generic.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Semiring.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Set/NonEmpty.purs` | purescript-ordered-collections v3.2.0 | identical | 0 | +| `Data/Set.purs` | purescript-ordered-collections v3.2.0 | identical | 0 | +| `Data/Show/Generic.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Show.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/String/CaseInsensitive.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String/CodePoints.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String/CodeUnits.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String/Common.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String/Gen.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String/NonEmpty/CaseInsensitive.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String/NonEmpty/CodePoints.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String/NonEmpty/CodeUnits.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String/NonEmpty/Internal.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String/NonEmpty.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String/Pattern.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String/Regex/Flags.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String/Regex/Unsafe.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String/Regex.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String/Unsafe.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/String.purs` | purescript-strings v6.0.1 | identical | 0 | +| `Data/Symbol.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Traversable/Accum/Internal.purs` | purescript-foldable-traversable v6.0.0 | identical | 0 | +| `Data/Traversable/Accum.purs` | purescript-foldable-traversable v6.0.0 | identical | 0 | +| `Data/Traversable.purs` | purescript-foldable-traversable v6.0.0 | identical | 0 | +| `Data/TraversableWithIndex.purs` | purescript-foldable-traversable v6.0.0 | identical | 0 | +| `Data/Tuple/Nested.purs` | purescript-tuples v7.0.0 | identical | 0 | +| `Data/Tuple.purs` | purescript-tuples v7.0.0 | identical | 0 | +| `Data/Unfoldable.purs` | purescript-unfoldable v6.0.0 | identical | 0 | +| `Data/Unfoldable1.purs` | purescript-unfoldable v6.0.0 | identical | 0 | +| `Data/Unit.purs` | purescript-prelude v6.0.1 | modified | 0 | +| `Data/Void.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Data/Witherable.purs` | purescript-filterable v5.0.0 | identical | 0 | +| `Effect/Class/Console.purs` | purescript-console v6.0.0 | identical | 0 | +| `Effect/Class.purs` | purescript-effect v4.0.0 | identical | 0 | +| `Effect/Console.purs` | purescript-console v6.0.0 | modified | 0 | +| `Effect/Ref.purs` | purescript-refs v6.0.0 | identical | 0 | +| `Effect/Uncurried.purs` | purescript-effect v4.0.0 | identical | 0 | +| `Effect/Unsafe.purs` | purescript-effect v4.0.0 | identical | 0 | +| `Effect.purs` | purescript-effect v4.0.0 | modified | 0 | +| `Partial/Unsafe.purs` | purescript-partial v4.0.0 | identical | 0 | +| `Partial.purs` | purescript-partial v4.0.0 | identical | 0 | +| `Prelude.purs` | purescript-prelude v6.0.1 | modified | 0 | +| `Record/Unsafe.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Safe/Coerce.purs` | purescript-safe-coerce v2.0.0 | identical | 0 | +| `Test/Assert.purs` | purescript-assert v6.0.0 | modified | 0 | +| `Type/Data/Boolean.purs` | purescript-typelevel-prelude v7.0.0 | identical | 0 | +| `Type/Data/Ordering.purs` | purescript-typelevel-prelude v7.0.0 | identical | 0 | +| `Type/Data/Symbol.purs` | purescript-typelevel-prelude v7.0.0 | identical | 0 | +| `Type/Equality.purs` | purescript-type-equality v4.0.1 | identical | 0 | +| `Type/Function.purs` | purescript-typelevel-prelude v7.0.0 | identical | 0 | +| `Type/Prelude.purs` | purescript-typelevel-prelude v7.0.0 | identical | 0 | +| `Type/Proxy.purs` | purescript-prelude v6.0.1 | identical | 0 | +| `Type/Row/Homogeneous.purs` | purescript-typelevel-prelude v7.0.0 | identical | 0 | +| `Type/Row.purs` | purescript-typelevel-prelude v7.0.0 | identical | 0 | +| `Type/RowList.purs` | purescript-typelevel-prelude v7.0.0 | identical | 0 | +| `Unsafe/Coerce.purs` | purescript-unsafe-coerce v6.0.0 | identical | 0 | +| `WASI/Clock.purs` | unavailable | platform_addition | 0 | +| `WASI/Console.purs` | unavailable | platform_addition | 0 | +| `WASI/FileSystem.purs` | unavailable | platform_addition | 0 | +| `WASI/IO.purs` | unavailable | platform_addition | 0 | +| `WASI/Network.purs` | unavailable | platform_addition | 0 | +| `WASI/Process.purs` | unavailable | platform_addition | 0 | +| `WASI/Random.purs` | unavailable | platform_addition | 0 | +| `WASI/Resource.purs` | unavailable | platform_addition | 0 | +| `WASI.purs` | unavailable | platform_addition | 0 | diff --git a/docs/implementation/stdlib/vendor-restoration-2026-10-06/official-vs-vendored.diff b/docs/implementation/stdlib/vendor-restoration-2026-10-06/official-vs-vendored.diff new file mode 100644 index 00000000..32168725 --- /dev/null +++ b/docs/implementation/stdlib/vendor-restoration-2026-10-06/official-vs-vendored.diff @@ -0,0 +1,261 @@ +--- purescript-prelude@v6.0.1/src/Data/Unit.purs ++++ stdlib/lib/Data/Unit.purs +@@ -1,14 +1,5 @@ +-module Data.Unit where +- +--- | The `Unit` type has a single inhabitant, called `unit`. It represents +--- | values with no computational content. +--- | +--- | `Unit` is often used, wrapped in a monadic type constructor, as the +--- | return type of a computation where only the _effects_ are important. +--- | +--- | When returning a value of type `Unit` from an FFI function, it is +--- | recommended to use `undefined`, or not return a value at all. +-foreign import data Unit :: Type +- +--- | `unit` is the sole inhabitant of the `Unit` type. +-foreign import unit :: Unit ++-- | `Unit` is a compiler builtin, the same type as an unqualified ++-- | `Unit`. This module re-exports that builtin and the `unit` ++-- | primitive so `import Data.Unit` matches the official library ++-- | without declaring a second unit type. ++module Data.Unit (Unit, unit) where +--- purescript-console@v6.0.0/src/Effect/Console.purs ++++ stdlib/lib/Effect/Console.purs +@@ -1,14 +1,15 @@ + module Effect.Console where + + import Effect (Effect) ++import WASI.Console as Console + + import Data.Show (class Show, show) + import Data.Unit (Unit) + + -- | Write a message to the console. +-foreign import log +- :: String +- -> Effect Unit ++-- WASI routes log to its log stream operation. ++log :: String -> Effect Unit ++log = Console.log + + -- | Write a value to the console, using its `Show` instance to produce a + -- | `String`. +@@ -16,9 +17,9 @@ + logShow a = log (show a) + + -- | Write an warning to the console. +-foreign import warn +- :: String +- -> Effect Unit ++-- WASI routes warn to its warn stream operation. ++warn :: String -> Effect Unit ++warn = Console.warn + + -- | Write an warning value to the console, using its `Show` instance to produce + -- | a `String`. +@@ -26,9 +27,9 @@ + warnShow a = warn (show a) + + -- | Write an error to the console. +-foreign import error +- :: String +- -> Effect Unit ++-- WASI routes error to its error stream operation. ++error :: String -> Effect Unit ++error = Console.error + + -- | Write an error value to the console, using its `Show` instance to produce a + -- | `String`. +@@ -36,9 +37,9 @@ + errorShow a = error (show a) + + -- | Write an info message to the console. +-foreign import info +- :: String +- -> Effect Unit ++-- WASI routes info to its log stream operation. ++info :: String -> Effect Unit ++info = Console.log + + -- | Write an info value to the console, using its `Show` instance to produce a + -- | `String`. +@@ -46,9 +47,9 @@ + infoShow a = info (show a) + + -- | Write an debug message to the console. +-foreign import debug +- :: String +- -> Effect Unit ++-- WASI routes debug to its log stream operation. ++debug :: String -> Effect Unit ++debug = Console.log + + -- | Write an debug value to the console, using its `Show` instance to produce a + -- | `String`. +--- purescript-effect@v4.0.0/src/Effect.purs ++++ stdlib/lib/Effect.purs +@@ -13,27 +13,14 @@ + -- | A native effect. The type parameter denotes the return type of running the + -- | effect, that is, an `Effect Int` is a possibly-effectful computation which + -- | eventually produces a value of the type `Int` when it finishes. +-foreign import data Effect :: Type -> Type ++-- The Wasm state-token type and core instances are owned by Prelude. + +-type role Effect representational ++-- Target adapters retain the private upstream operation contracts. ++pureE :: forall a. a -> Effect a ++pureE = pure + +-instance functorEffect :: Functor Effect where +- map = liftA1 +- +-instance applyEffect :: Apply Effect where +- apply = ap +- +-instance applicativeEffect :: Applicative Effect where +- pure = pureE +- +-foreign import pureE :: forall a. a -> Effect a +- +-instance bindEffect :: Bind Effect where +- bind = bindE +- +-foreign import bindE :: forall a b. Effect a -> (a -> Effect b) -> Effect b +- +-instance monadEffect :: Monad Effect ++bindE :: forall a b. Effect a -> (a -> Effect b) -> Effect b ++bindE = bind + + -- | The `Semigroup` instance for effects allows you to run two effects, one + -- | after the other, and then combine their results using the result type's +@@ -50,23 +37,36 @@ + -- | + -- | `untilE b` is an effectful computation which repeatedly runs the effectful + -- | computation `b`, until its return value is `true`. +-foreign import untilE :: Effect Boolean -> Effect Unit ++untilE :: Effect Boolean -> Effect Unit ++untilE action = bind action \done -> ++ if done then pure unit else untilE action + + -- | Loop while a condition is `true`. + -- | + -- | `whileE b m` is effectful computation which runs the effectful computation + -- | `b`. If its result is `true`, it runs the effectful computation `m` and + -- | loops. If not, the computation ends. +-foreign import whileE :: forall a. Effect Boolean -> Effect a -> Effect Unit ++whileE :: forall a. Effect Boolean -> Effect a -> Effect Unit ++whileE condition action = bind condition \continue -> ++ if continue then bind action (\_ -> whileE condition action) else pure unit + + -- | Loop over a consecutive collection of numbers. + -- | + -- | `forE lo hi f` runs the computation returned by the function `f` for each + -- | of the inputs between `lo` (inclusive) and `hi` (exclusive). +-foreign import forE :: Int -> Int -> (Int -> Effect Unit) -> Effect Unit ++forE :: Int -> Int -> (Int -> Effect Unit) -> Effect Unit ++forE lower upper action = ++ if lower < upper then bind (action lower) (\_ -> forE (lower + 1) upper action) ++ else pure unit + + -- | Loop over an array of values. + -- | + -- | `foreachE xs f` runs the computation returned by the function `f` for each + -- | of the inputs `xs`. +-foreign import foreachE :: forall a. Array a -> (a -> Effect Unit) -> Effect Unit ++foreachE :: forall a. Array a -> (a -> Effect Unit) -> Effect Unit ++foreachE values action = go 0 ++ where ++ go index = ++ if intLt index (arrayLength values) ++ then bind (action (arrayIndex values index)) (\_ -> go (intAdd index 1)) ++ else pure unit +--- purescript-prelude@v6.0.1/src/Prelude.purs ++++ stdlib/lib/Prelude.purs +@@ -1,17 +1,20 @@ +--- | `Prelude` is a module that re-exports many other foundational modules from the `purescript-prelude` library +--- | (e.g. the Monad type class hierarchy, the Monoid type classes, Eq, Ord, etc.). ++-- | The primitive surface the rest of the library and the corpus build on. + -- | +--- | Typically, this module will be imported in most other libraries and projects as an open import. ++-- | The class hierarchy is the official `purescript-prelude` v6.0.1 re-export ++-- | list. `Effect` stays in this module: `check_run_effect_scope` resolves ++-- | `Prelude.runEffect`, and `psrs_core::effect::operations` synthesizes ++-- | `effectPure`, `effectBind`, `runEffect`, and `trap` from the `psrs:effect` ++-- | bindings declared here. The class methods `pure` and `bind` are the ++-- | official `Applicative` and `Bind` methods; the `Effect` instances call ++-- | those bindings, so creating an action still does not run it. + -- | +--- | ``` +--- | module MyModule where +--- | +--- | import Prelude -- open import +--- | +--- | import Data.Maybe (Maybe(..)) -- closed import +--- | ``` ++-- | `unit` is not declared here. `Unit` is a builtin, re-exported through ++-- | `Data.Unit`, and the one `Unit` value is `Intrinsic::Unit`. + module Prelude +- ( module Control.Applicative ++ ( Effect ++ , runEffect ++ , trap ++ , module Control.Applicative + , module Control.Apply + , module Control.Bind + , module Control.Category +@@ -68,3 +71,34 @@ + import Data.Show (class Show, show) + import Data.Unit (Unit, unit) + import Data.Void (Void, absurd) ++ ++foreign import data Effect :: Type -> Type ++type role Effect representational ++ ++-- | Builds an `Effect` that returns `value`. Lowering replaces this binding; ++-- | the `Applicative` instance is what user code calls `pure`. ++foreign import "psrs:effect#pure" effectPure :: forall a. a -> Effect a ++ ++-- | Sequences two effects. The `Bind` instance is what user code calls `bind`. ++foreign import "psrs:effect#bind" effectBind :: forall a b. Effect a -> (a -> Effect b) -> Effect b ++ ++foreign import "psrs:effect#run" runEffect :: forall a. Effect a -> a ++ ++-- | The effect that escapes instead of returning. An uncaught failure on this ++-- | target is a guest trap, so this is the one operation whose result never ++-- | exists; it is how a library reports an assertion that did not hold. ++foreign import "psrs:effect#trap" trap :: Effect Unit ++ ++instance functorEffect :: Functor Effect where ++ map = liftA1 ++ ++instance applyEffect :: Apply Effect where ++ apply = ap ++ ++instance applicativeEffect :: Applicative Effect where ++ pure = effectPure ++ ++instance bindEffect :: Bind Effect where ++ bind = effectBind ++ ++instance monadEffect :: Monad Effect +--- purescript-assert@v6.0.0/src/Test/Assert.purs ++++ stdlib/lib/Test/Assert.purs +@@ -26,10 +26,13 @@ + assert' :: String -> Boolean -> Effect Unit + assert' = assertImpl + +-foreign import assertImpl +- :: String +- -> Boolean +- -> Effect Unit ++-- Wasm/WASI implementation: failure writes its message before trapping. ++assertImpl :: String -> Boolean -> Effect Unit ++assertImpl message condition = ++ if condition then pure unit ++ else do ++ _ <- error message ++ trap + + -- | Throws a runtime exception with message "Assertion failed: An error should + -- | have been thrown", unless the argument throws an exception when evaluated. diff --git a/docs/implementation/stdlib/vendor-restoration-2026-10-06/report.md b/docs/implementation/stdlib/vendor-restoration-2026-10-06/report.md new file mode 100644 index 00000000..1e5b12d0 --- /dev/null +++ b/docs/implementation/stdlib/vendor-restoration-2026-10-06/report.md @@ -0,0 +1,122 @@ +# Standard-library restoration checkpoint + +This checkpoint describes the uncommitted restoration on top of revision +`67369ba`, on `stdlib/vendor-core-libraries`. It does not supersede the +[historical audit](../vendor-audit-2026-10-06/report.md), which records the +defects in that revision. The governing policy is +[standard-library source fidelity](../../../workflow/stdlib-vendoring.md). + +## Source evidence + +The [inventory](inventory.json) pins all 41 upstream packages by release tag, +commit, and per-module SHA-256. All 206 official modules are now present: +201 are byte-identical and five have target adaptations. The nine additional +modules are the existing WASI integration. No exact top-level same-argument +self-recursion remains; the detector previously found 200 such placeholders. +This syntactic check is not a general termination or semantic-equivalence proof. + +The four restored modules are `Effect.Class`, `Effect.Class.Console`, +`Effect.Uncurried`, and `Effect.Unsafe`. The official Tuple instances, Show +surface, assertion wrappers, console Show wrappers, pure combinators, and +instance method definitions have been restored. Original vendored source +length is preserved under the explicitly approved exception to the maintained +source-file limit. + +See [the exact remaining diff](official-vs-vendored.diff) and +[the module table](modules.md). An inventory difference is not approval of +an adaptation or proof that its implementation works. + +## Target adaptations and remaining obligations + +| Module | Target reason and owner | Preserved contract | Unverified obligations | +| --- | --- | --- | --- | +| `Prelude` | Existing state-token Effect operations and command entry, governed by the effects design | Ordinary official re-exports; Effect methods use the official combinators; representational role retained | Source ownership of Effect is relocated; execute sequencing and callback cases under mandatory Wasmtime | +| `Data.Unit` | Compiler-owned builtin Unit representation | Public Unit and unit are re-exported | Verify builtin ownership against the official public role and instance contract | +| `Effect` | Source wrappers around the target state-token operations | Official exports, loop signatures, Semigroup and Monoid bodies; core instances remain in Prelude | Execute all four loops, zero iterations, order, boundary inputs, and termination; source recursion may differ from the upstream iterative implementation in resource use | +| `Effect.Console` | WASI streams replace JavaScript console methods | All public values and official Show wrappers restored | Verify stream routing and output; time, timeLog, timeEnd, and clear retain foreign declarations without implementations | +| `Test.Assert` | WASI stderr plus the existing guest-trap failure protocol | All ten public values and ordinary comparisons/message construction restored | Execute successful and failed assertions; checkThrows and catch-based assertion behavior remain unsupported | + +The Effect implementation owner and protocol are described in +[effects](../../../design/backend/fp/effects.md). Unit ownership is described +in [scalars and primitives](../../../design/backend/fp/scalars-and-primitives.md). +Unicode target differences remain governed by DEC-16; no numeric formatting +approximation or pure API deletion is justified by UTF-8 storage. + +These adaptations are candidate implementations, not completed runtime +acceptance. In particular, retaining the `checkThrows` declaration preserves +the public source surface without pretending that guest traps can be caught. + +## Foreign declarations and compiler contracts + +Ordinary `foreign import` now retains its absence of an explicit WIT binding in +AST and its declaring module in HIR. Source declarations enter the ordinary +value namespace before fixity resolution. Imports and re-exports retain their +declaration symbol and signature and take precedence over bootstrap intrinsics. +THIR and Core require checked external signatures for ordinary library imports +as well as WIT imports. Foreign signatures with class constraints remain +rejected, matching the official compiler's restriction. + +P8 reports a missing target implementation explicitly, including declaration +ownership. No recursive source body or fabricated WIT import replaces it. +All 275 retained ordinary foreign declarations still need target implementations +and behavior evidence. The [binding inventory](foreign-bindings.json) links +every declaration to its pinned official source; the +[module summary](foreign-bindings.md) distinguishes the complete inventory from +the reproducer's import closure. + +Future target implementations must use an explicit binding contract, validate +the checked source signature, and retain producer/consumer evidence until the +owning lowering discharges it. Matching a spelling or guessing an arity cannot +establish representation compatibility. Scalar operations, polymorphic array +callbacks, mutable ST/Ref state, lazy values, uncurried functions, regexes, +number rendering, and failure behavior require their own observable tests. + +## Validation + +The pre-restoration import-only reproducer compiled in 21,707 ms, with the +recursive placeholders still present. After restoration and a fresh offline +CLI build, the same `/tmp/psrs-stdlib-all.purs` failed in 26,669 ms with +253 `P8 library linking` diagnostics and no earlier diagnostics. A subsequent +rebuild after completing THIR signature validation produced the same 253 +diagnostics in 21,996 ms. The first is +`Control.Apply.arrayApply`. The snapshot is `/tmp/psrs-stdlib-all.json`; its +complete input set is recorded, with trusted-library fingerprint +`fnv1a64:3f3081e2d1ea1032`. Source inputs intentionally changed, so this is +restoration evidence rather than a compatible-input compiler-only comparison. + +Focused validation: + +- `cargo test -p psrs-driver --lib let_constraints --offline`: 14 passed. +- Six passing library foreign tests cover checked evidence, source shadowing, imports, + fixities, forbidden constraints, and explicit missing-implementation errors. +- `PURESCRIPT_REPO=/Users/biu/Projects/purescript cargo test -p psrs-driver + --test upstream differential_library_foreign --offline`: passed against + official purs 0.15.16, including a same-name bootstrap collision. +- The trusted-order loader test passed. This validates loading/checking, not + target implementation or runtime behavior. +- `cargo test -p psrs-core --lib external_types --offline`: seven passed. +- THIR checked-external tests: six passed, including missing library evidence. + Backend binding projection tests: four passed. The existing class-constrained + WIT rejection test also passed with its original diagnostic. +- `cargo fmt --all --check` and strict workspace clippy with all targets passed. + +The filtered resolver command selected zero tests and supplies no independent +resolver coverage; the driver and official comparison exercise that boundary. +No workspace test suite, full runtime scoreboard, or full FFI behavior suite was +run. No D-04 or README measurement was changed. The restored stdlib does **not** +compile fully and does **not** have full runtime acceptance. + +## Reproduce the source comparison + +```sh +python3 docs/workflow/tools/audit-stdlib-vendor.py \ + --vendor stdlib/lib \ + --upstream /tmp/ps-pkgs \ + --upstream /tmp/purescript-prelude \ + --upstream /tmp/psrs-stdlib-audit-20261006/upstream \ + --out /tmp/psrs-stdlib-fidelity-current +``` + +Those directories must contain the exact clean tagged checkouts recorded in +the inventory. The comparison tool does not fetch dependencies or approve +changes. Preserve the historical audit separately from new measurements. diff --git a/docs/workflow/compiler-iteration-sop.md b/docs/workflow/compiler-iteration-sop.md new file mode 100644 index 00000000..0e10dd86 --- /dev/null +++ b/docs/workflow/compiler-iteration-sop.md @@ -0,0 +1,183 @@ +# Compiler Iteration SOP + +Use this workflow when investigating compile failures or making a compiler +change. It creates reproducible evidence, locates the earliest observed +boundary, and checks the same inputs after a change. Compile evidence and +runtime evidence answer different questions and must be recorded separately. + +For roadmap work, first select an issue using the phase and dependency rules in +[`AGENTS.md`](../../AGENTS.md#choosing-the-next-item), then read its references +and validation requirements. Diagnosis helps investigate the selected work; it +does not replace roadmap priority or acceptance evidence. + +## 1. Record the starting point and capture a baseline + +Record the branch, commit, and existing worktree changes. Choose one reproducer +or a small corpus cohort that reaches the relevant feature. Save reports and +bundles outside the source tree so they do not change the working-tree +fingerprint in a later report. + +```sh +# One file, including imported modules and a replay bundle. +cargo run -p psrs-cli -- diagnose path/to/Main.purs \ + --out /tmp/psrs-before.json --timeout 20 + +# A bounded, repeatable corpus cohort. The filter is applied before the limit. +cargo run -p psrs-cli -- diagnose --corpus passing --filter Functor --limit 20 \ + --out /tmp/psrs-before.json --timeout 20 +``` + +Use the default run first when pass outcomes and blocker attribution are enough. +Compiler rejections are recorded in the report and do not make the diagnosis +command fail. A nonzero command result indicates an operational problem such as +invalid arguments, missing inputs, or an output error. Timeouts, crashes, and +FFI exclusions are separate outcomes; none counts as a successful compile. + +## 2. Locate the first observed boundary + +Start with the case's first blocker and complete ordered diagnostics. Keep +source, trusted-library, and program-level origins distinct. A library-origin +diagnostic may be triggered by a user module, but does not establish which +implementation is wrong. + +The run manifest records actual pass executions and their input/output +artifacts. Follow those edges to find the first observed boundary that differs +from the expected contract. A matching first-blocker group is only a shared +diagnostic signature. Use group size to estimate reach, then inspect +representatives before treating one owner hypothesis as established. + +For a focused case where the IR itself is needed, request a trace: + +```sh +cargo run -p psrs-cli -- diagnose path/to/Main.purs --trace \ + --out /tmp/psrs-before-trace.json --timeout 20 + +# An explicit ordered input closure for a multi-file reproducer. +cargo run -p psrs-cli -- diagnose path/to/Main.purs \ + --input path/to/Imported.purs --trace \ + --out /tmp/psrs-before-trace.json --timeout 20 +``` + +Trace mode retains the selected Core, CC, and MIR dumps for accepted and +rejected cases and is preserved by the replay bundle. Open only artifacts whose +manifest record says they were produced. Read validation records separately: +artifact production does not establish verification. Pass entries may group +nested verifier work at an explicitly coarser observed boundary. A missing +summary or lineage relation means that evidence is unavailable; do not +reconstruct it from formatted text or overlapping spans. Querying by case, +symbol, or source span is planned; until it is implemented, inspect the saved +case, pass, and artifact records directly. + +`--compare` requires matching cohort selection and trusted-library fingerprints. +For each matching case it separately checks complete input fingerprints before +comparing pass records. The reported first difference is the first observed +execution-sequence difference, not an earliest artifact divergence or a root +cause. Canonical artifact-content comparison is unavailable until versioned +summaries exist. Host and installed tool versions are observations only; the +compiler build toolchain is not embedded in the current manifest. + +Use repeatable `--input FILE` when the reproducer's source set is explicit. +The primary file is first and each `--input` follows in order; this mode +compiles that exact list without resolving imports again. It applies only to +file diagnosis, and duplicate canonical paths or `--corpus` combined with +`--input` are invalid. A trace replay runs `diagnose` on the saved entry-first +list into a new `replay-trace.json`, then invokes the recorded `build` command +on the same files. The two runs are separate; the build command retains the +original compiler rejection's nonzero status. + +Failure bundles record the case and loaded inputs; `replay.sh` reruns with the +recorded compiler and trace mode. Override the executable when comparing a +different build: + +```sh +BUNDLE=/tmp/psrs-before.bundles/0000-Functor +PSRS_BIN="$PWD/target/debug/psrs" sh "$BUNDLE/replay.sh" +``` + +Use the bundle directory recorded in the report; the example name above is +illustrative. + +## 3. Reduce the case and identify the owner + +Make a small reproducer from the replay bundle while preserving required +imports and declarations. State the expected behavior, actual diagnostic or +output, and the earliest representation where they diverge. For generated +adapters or other compiler-created nodes, use explicit derivation records when +available. Source span overlap is a search hint, not lineage evidence. + +Read the governing design and identify the stage that owns the rule. Check the +input and output invariants of adjacent stages. Prefer repairing a shared +representation or operation when multiple forms rely on it. Avoid a +feature-specific backend workaround when an earlier representation or calling +contract is wrong. Keep a falsifiable next hypothesis; when an investigation +produces no next step, save the evidence and narrow the unanswered boundary +before adding speculative changes. + +For roadmap work, the issue and board rules still decide which work comes next. +Among cases within that work, use affected-case count and downstream +dependencies to choose investigation order; a large signature group alone +does not prove cause or raise an issue's roadmap priority. + +## 4. Validate the change at each affected layer + +Run a focused test or replay that covers the reduced case and relevant +rejection path. Then rerun the same diagnosis selection with the same filter, +limit, timeout, corpus, trusted-library setup, and trace mode: + +```sh +cargo run -p psrs-cli -- diagnose --corpus passing --filter Functor --limit 20 \ + --out /tmp/psrs-after.json --timeout 20 +cargo run -p psrs-cli -- diagnose --compare /tmp/psrs-before.json /tmp/psrs-after.json +``` + +Comparison checks cohort and observed input compatibility, then reports +per-case recovery, regression, blocker changes, input changes, and unmatched +paths. Compiler revision may differ and is shown as the subject of comparison. +If inputs are incomplete or environment metadata differs, keep that limitation +visible. Until compatible canonical summaries are available, artifact-content +and earliest-artifact difference are reported as unavailable; do not infer an +IR change from different Debug output. + +Compile diagnosis establishes compile acceptance only. If behavior reaches +Wasm execution, run a focused value-sensitive runtime check that observes the +relevant results, call counts, output order, or traps. Record the exact Wasm +artifact and runtime/tool version where the test supports it. Keep outcomes +separate, for example: + +```text +compile: accepted +runtime: trapped +value assertions: incomplete +``` + +A successful compile does not establish correct runtime behavior. For +runtime-gated driver tests that support it, require Wasmtime explicitly: + +```sh +PSRS_REQUIRE_WASMTIME=1 cargo test -p psrs-driver --lib focused_runtime_test_name +``` + +Run the issue's required official-suite or scoreboard validation when the +change affects a gate or its acceptance evidence. Update D-04 and README +measurements only after rerunning the relevant scoreboard with the documented +settings. Small filtered diagnosis reports are useful for locating regressions; +they are not replacements for official acceptance measurements. + +## 5. Report the evidence + +For each iteration, report: + +- the starting commit and whether the worktree already had changes; +- the exact baseline and comparison commands, selected cohort, and trace mode; +- compile outcomes by first-blocker stage/category, keeping exclusions, + timeouts, crashes, and incomplete inputs separate; +- the first observed pass/artifact boundary, its evidence coverage, the + reproducer, and the suspected semantic owner; +- focused compile checks, runtime behavior exercised, and required suite or + scoreboard runs; +- what changed, what remains unresolved, and which validations were not run. + +Do not turn a filtered cohort into a corpus-wide pass rate. Do not update a +scoreboard number from diagnostic output or from an unverified estimate. A +manifest, dump, or compile status does not substitute for missing lineage, +canonical artifact summaries, or value-sensitive runtime evidence. diff --git a/docs/workflow/component-linking.md b/docs/workflow/component-linking.md new file mode 100644 index 00000000..091393a8 --- /dev/null +++ b/docs/workflow/component-linking.md @@ -0,0 +1,106 @@ +# Explicit Component Linking + +**Feature:** [F-02](../feature/F-02-portable-programs.md). + +**Design:** [Linking and Runtime](../design/backend/wasm/linking-and-runtime.md). + +Use an already built synchronous application component and explicitly pinned +whole-interface guest providers: + +```sh +psrs link application.wasm --manifest providers.json -o linked.wasm --report link.json +``` + +The command composes components; it does not compile PureScript source, infer +providers from WIT files, download artifacts or discover providers by directory. +Source compilation can explicitly request the same checked composition: + +```sh +psrs build Main.purs --manifest providers.json -o linked.wasm --report build.json +``` + +`build` preserves source/stdlib checking and completes its core runtime plan +before composing the resulting application component. It does not discover +providers. For compiler-produced roots, `application_sha256` is optional: the +compiler pins the exact bytes it just emitted. If supplied, the pin must match. +Standalone `link` always requires the root pin. + +## Manifest version 1 + +```json +{ + "schema_version": 1, + "application_sha256": "<64 lowercase hexadecimal characters>", + "providers": [ + { + "id": "service", + "path": "components/service.wasm", + "sha256": "<64 lowercase hexadecimal characters>" + } + ], + "bindings": [ + { + "interface": "example:service/api@1.0.0", + "provider": "service" + } + ], + "permitted_host_interfaces": ["wasi:cli/stdout@0.2.12"] +} +``` + +Provider paths resolve relative to the manifest. The application path resolves +relative to the command's working directory. Artifact identities must be unique; +`application` is reserved for the root. Every live artifact's SHA-256 must match +its exact executable bytes. Unknown fields and schema versions are errors. + +A binding applies wherever its interface occurs in the live graph, including +transitive guest imports. An optional `export` field defaults to `interface`; +the initial format requires the two canonical names to match exactly. There is +one provider instance per selected artifact identity, shared by its consumers. +Bindings select whole interfaces, preserving the identity of resources and their +constructor, method and destructor operations. The component type checker rejects +incompatible function shapes and resource connections. + +Unbound live imports must be explicitly permitted host interfaces. The command +also checks WASI permissions against the compiler's stable target world and +capability profile. Other host interfaces form an explicit custom host contract; +the runtime must supply them. Unused candidate artifacts do not enter the graph +or report. Declaring an interface as permitted does not rescue a missing or +incompatible explicitly selected guest. + +Only instance imports and synchronous composition under the stable Wasm feature +profile are supported. Definition-only WIT packages, non-interface imports, +implicit version adaptation and cyclic instantiation cannot satisfy this entry +point. Guest executable core/component start functions are rejected: version 1 +has no provider initialization-effect contract. Declarative memory/data/global +initialization stays within the typed component; the compiler-owned application +may contain the already checked encoder's shim initialization. Provider memory stays inside that component; canonical lift/lower, +realloc and post-return carry values across the connection. + +## Report and evidence + +The optional report records schema version, selected component identities and +SHA-256 pins (including the application), live provider binding edges, validator +feature bits, the exact residual host interface imports, and the output digest. +A successful report and binary are written only after planning and validation. +With `build --report`, a rejected guest plan writes a `status: rejected` report +with its source lineage, exact application digest and composition diagnostic; +it produces no linked binary or successful output digest. Standalone `link` +reports remain successful-plan records. A report establishes artifact composition and validation, not successful execution or +source-language conformance. Standalone `link` reports are separate from +`psrs diagnose`. A `build --report` report includes the same diagnosis source/pass/artifact vocabulary, with a +`component_output` join naming the produced trace artifact and its exact SHA-256. +`application_sha256` matches that join and the composition plan's root pin; the +composition output digest matches the final file. It also records the exact +manifest digest. These links establish source-to-component lineage without +claiming runtime observations as compile verification. + +Run the resulting command component with a compatible runtime: + +```sh +wasmtime run linked.wasm +``` + +The linker tests execute cross-component strings, lists/results, post-return +buffer invalidation and resource lifetime behavior. CLI tests cover manifest +relative paths, digest rejection, closed imports and report/output agreement. diff --git a/docs/workflow/stdlib-conformance.md b/docs/workflow/stdlib-conformance.md new file mode 100644 index 00000000..527714ba --- /dev/null +++ b/docs/workflow/stdlib-conformance.md @@ -0,0 +1,354 @@ +# Standard-library conformance commands + +The library-owned Node component is `../psrs-stdlib/tools/conformance.mjs`. +It requires Node 22.7 or newer and has no npm dependencies. Run it directly: + +```sh +node ../psrs-stdlib/tools/conformance.mjs --help +``` + +A pinned complete source audit accepts the independent package and upstream +checkout directories. Supply the lock to reject a different upstream baseline: + +```sh +node ../psrs-stdlib/tools/conformance.mjs audit \ + --vendor ../psrs-stdlib/lib \ + --inventory ../psrs-stdlib/upstream-lock.json \ + --upstream /private/tmp/ps-pkgs \ + --upstream /private/tmp/purescript-prelude \ + --upstream /private/tmp/psrs-stdlib-audit-20261006/upstream \ + --out /tmp/psrs-stdlib-audit +``` + +The audit produces evidence for review; completing a report does not approve +its differences. Check the actual pinned checkout locations before running. +Generate and execute the currently implemented scalar cases: + +```sh +node ../psrs-stdlib/tools/conformance.mjs scalar-oracle \ + --vendor ../psrs-stdlib/lib \ + --inventory ../psrs-stdlib/upstream-lock.json \ + --cases ../psrs-stdlib/conformance/scalars.json \ + --upstream purescript-prelude=/private/tmp/purescript-prelude \ + --upstream purescript-integers=/private/tmp/ps-pkgs/purescript-integers \ + --out /tmp/psrs-stdlib-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-stdlib-oracle/Golden.purs \ + --input /tmp/psrs-stdlib-oracle/Main.purs \ + --expected-exit 42 --out /tmp/psrs-stdlib-runtime +``` + +`run.json` separates compilation and execution. The default expected output is +empty stdout/stderr; use `--expected-stdout` and `--expected-stderr` files for +other byte-exact goldens. `--wasmtime` selects an executable, never an optional +skip. No runner command publishes or modifies upstream checkouts. + +See [the repository boundary](../design/D-17-stdlib-and-conformance-boundaries.md) +for ownership, package locking, and the limits of this evidence. + +For public Bounded values and Enum range interactions: + +```sh +node ../psrs-stdlib/conformance/bounded.mjs \ + /private/tmp/purescript-prelude /tmp/psrs-bounded-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-bounded-oracle/Main.purs \ + --input /tmp/psrs-bounded-oracle/Target.purs \ + --expected-exit 42 --out /tmp/psrs-bounded-runtime +``` + +The generator verifies the six typed foreign-slot delegates against the pinned +source and reads its official JS bounds. Char's U+10FFFF target upper bound is +an explicit DEC-16 difference from JS's U+FFFF; the three Enum range observations +are integration checks. The library records these distinctions in +`docs/bounded.md` and `docs/evidence/bounded/`. + +The library owns non-scalar case generators as well. For array application: + +```sh +node ../psrs-stdlib/conformance/arrays.mjs \ + /private/tmp/purescript-prelude /tmp/psrs-array-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-array-oracle/Golden.purs \ + --input /tmp/psrs-array-oracle/Main.purs \ + --out /tmp/psrs-array-runtime +``` + +The generator verifies the clean upstream revision, evaluates the official JS +function, copies the actual vendored binding signature, and emits value checks. +Keep generators and cases in the library package; the independent runtime runner +continues to consume executable and source paths without compiler-internal APIs. + +Tool changes and their tests belong in `psrs-stdlib`; run `npm test` there. +The compiler retains its package lock and Rust regression tests. A cargo xtask +wrapper, if added, should delegate to this CLI. The former Python component and +compiler-local compatibility scripts were removed after report-equivalent Node +validation. Historical evidence retains the commands used at that time. + +For library array binding, generate the independent official observations and +run them through the same executable boundary: + +```sh +node ../psrs-stdlib/conformance/array-bind.mjs \ + /private/tmp/purescript-prelude /tmp/psrs-bind-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-bind-oracle/Golden.purs \ + --input /tmp/psrs-bind-oracle/Main.purs --out /tmp/psrs-bind-runtime +``` + +Both array algorithms are PureScript target library implementations. The oracle +fixtures copy the official public signature and import that implementation; +there is no whole-function compiler intrinsic for either operation. + +For array callback counts, visit order and short-circuiting: + +```sh +node ../psrs-stdlib/conformance/array-callbacks.mjs \ + /private/tmp/ps-pkgs/purescript-arrays /tmp/psrs-array-callback-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-array-callback-oracle/Main.purs \ + --out /tmp/psrs-array-callback-runtime +``` + +These observations execute target helpers. Public `Data.Array` wrapper execution +remains blocked by unsupported `Data.Array.ST` bindings in its import closure; +helper acceptance does not establish public API or whole-library acceptance. + +For scalar string length and slicing: + +```sh +node ../psrs-stdlib/conformance/string-slicing.mjs \ + /private/tmp/ps-pkgs/purescript-strings /tmp/psrs-string-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-string-oracle/Main.purs \ + --expected-exit 42 --timeout 240 --out /tmp/psrs-string-runtime +``` + +The generator verifies the five typed foreign-slot delegates and evaluates the +pinned official FFI on a one-BMP-unit-per-scalar projection. Scalar results map +back to original text; raw UTF-16 observations are recorded separately. +Negative-relative/clamping policies remain official, while DEC-16 intentionally +changes index units. This is a projected oracle, not raw-JS equality on +supplementary characters. Case engines and evidence live in the library package. + +For scalar character indexing and single-character conversion: + +```sh +node ../psrs-stdlib/conformance/string-characters.mjs \ + /private/tmp/ps-pkgs/purescript-strings /tmp/psrs-character-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-character-oracle/Main.purs \ + --expected-exit 42 --timeout 240 --out /tmp/psrs-character-runtime +``` + +The shared String source verifier checks all seven typed foreign-slot delegates. +The character engine records 269 pinned-FFI projection observations separately +from 6 target-helper rank-N builder checks. Supplementary scalars intentionally +differ from raw JS code units under DEC-16; raw official results remain in the +observations. Public charAt and toChar wrappers execute unchanged. + +For strict public integer parsing and Euclidean arithmetic: + +```sh +node ../psrs-stdlib/conformance/int-parsing.mjs \ + /private/tmp/ps-pkgs/purescript-integers /tmp/psrs-int-parsing-oracle +node ../psrs-stdlib/conformance/int-arithmetic.mjs \ + /private/tmp/purescript-prelude /tmp/psrs-int-arithmetic-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-int-parsing-oracle/Main.purs \ + --expected-exit 42 --timeout 240 --out /tmp/psrs-int-parsing-runtime +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-int-arithmetic-oracle/Main.purs \ + --expected-exit 42 --out /tmp/psrs-int-arithmetic-runtime +``` + +The parsing engine verifies the entire pinned Data.Int source, permitting only +its two typed foreign-slot adaptations. It records 836 actual official FFI +observations separately from 6 public Radix rejections and 4 target rank-N builder +checks. The arithmetic engine records 209 representable public degree/div/mod +observations. MIN / -1 produces an unrepresentable JS quotient and remains a +Wasm division trap; that observation is recorded separately from value agreement. +Both algorithms use raw truncating intQuot; the target library owns grammar, +overflow rejection and Euclidean sign correction. + +For public radix integer formatting and its shared decimal Show implementation: + +```sh +node ../psrs-stdlib/conformance/int-formatting.mjs \ + /private/tmp/ps-pkgs/purescript-integers /private/tmp/purescript-prelude \ + /tmp/psrs-int-formatting-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-int-formatting-oracle/Main.purs \ + --expected-exit 42 --timeout 240 --out /tmp/psrs-int-formatting-runtime +``` + +The generator compares 857 public formatting results and 15 showInt results +against their actual pinned official JS functions, and records 857 parse/format +round trips and 5 named-base checks separately. All 2..36 bases and signed-i32 +boundaries execute. The shared Data.Int source verifier now permits exactly +three typed foreign-slot adaptations; all official pure declarations remain. + +For checked public Number-to-Int conversion: + +```sh +node ../psrs-stdlib/conformance/int-number.mjs \ + /private/tmp/ps-pkgs/purescript-integers /tmp/psrs-int-number-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-int-number-oracle/Main.purs \ + --expected-exit 42 --out /tmp/psrs-int-number-runtime +``` + +The engine records 94 pinned official FFI observations, 11 toNumber/fromNumber +round trips and 6 target rank-N builder checks separately. It covers adjacent +IEEE-754 values at i32 boundaries, fractions, subnormals, signed zeros, overflow, +NaN and infinities. Target Int canonicalizes both Number zero signs to integer +zero; official negative-zero metadata is retained. The shared source verifier +now permits exactly four typed foreign-slot adaptations. + +For finite Number classification and Number/Int truncation: + +```sh +node ../psrs-stdlib/conformance/number-trunc.mjs \ + /private/tmp/ps-pkgs/purescript-numbers /private/tmp/ps-pkgs/purescript-integers \ + /tmp/psrs-number-trunc-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-number-trunc-oracle/Main.purs \ + --expected-exit 42 --out /tmp/psrs-number-trunc-runtime +``` + +The engine verifies both pinned full-module transformations. It executes actual +Number isFinite/trunc FFI for 49 inputs and composes Int clamp expectations from +unchanged official source rules, producing 147 public API checks. Reciprocal +observations distinguish Number zero signs; Int uses its single integer zero. +Raw snapshots and runtime reports remain local and regenerable; commit only +source, case engines and concise acceptance reports. + +For Number rounding and unchanged public Int clamping wrappers: + +```sh +node ../psrs-stdlib/conformance/number-rounding.mjs \ + /private/tmp/ps-pkgs/purescript-numbers \ + /private/tmp/ps-pkgs/purescript-integers /tmp/psrs-rounding-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-rounding-oracle/Main.purs \ + --expected-exit 42 --timeout 240 --out /tmp/psrs-rounding-runtime +``` + +The generator invokes the pinned official floor/ceil/round FFI, preserving +negative zero through reciprocal observations and testing adjacent values at +half-integer ties and Int bounds. Wasm floor/ceil are small checked primitives; +ECMAScript round tie behavior belongs to ordinary PureScript in PSRS.Number. +Official Data.Int pure wrappers remain unchanged. Raw generated reports are +local and ignored; summarized acceptance lives in the library's +`docs/number-rounding.md` and the compiler's topic report. + +For Number absolute value through direct and higher-order public calls: + +```sh +node ../psrs-stdlib/conformance/number-abs.mjs \ + /private/tmp/ps-pkgs/purescript-numbers /tmp/psrs-number-abs-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-number-abs-oracle/Main.purs \ + --expected-exit 42 --timeout 240 --out /tmp/psrs-number-abs-runtime +``` + +The generator checks the complete pinned Data.Number source transformation +and invokes its actual JS abs FFI. Reciprocal observations distinguish zero +signs. Cases include binary64 subnormals, nonfinite values, magnitudes beyond +i32, and deterministic generated bit patterns. The compiler's checked +numberAbs primitive selects f64.abs; the library retains its original public +Number signature and pure declarations. + +For Number square root through the same public-call shape: + +```sh +node ../psrs-stdlib/conformance/number-sqrt.mjs \ + /private/tmp/ps-pkgs/purescript-numbers /tmp/psrs-number-sqrt-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-number-sqrt-oracle/Main.purs \ + --expected-exit 42 --timeout 240 --out /tmp/psrs-number-sqrt-runtime +``` + +The generator invokes the pinned official sqrt FFI. Negative zero stays +negative zero, and negative inputs produce NaN. The compiler's checked +numberSqrt primitive selects f64.sqrt. + +For Number inverse cosine through the same public-call shape: + +```sh +node ../psrs-stdlib/conformance/number-acos.mjs \ + /private/tmp/ps-pkgs/purescript-numbers /tmp/psrs-number-acos-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-number-acos-oracle/Main.purs \ + --expected-exit 42 --timeout 240 --out /tmp/psrs-number-acos-runtime +``` + +The generator invokes the pinned official acos FFI. Finite inputs in [-1, 1] +match that FFI. Values outside the interval produce NaN. The compiler's +checked numberAcos primitive calls the scalar numeric-runtime export. Wasm +has no inverse-cosine instruction. + +For Number inverse sine through the same public-call shape: + +```sh +node ../psrs-stdlib/conformance/number-asin.mjs \ + /private/tmp/ps-pkgs/purescript-numbers /tmp/psrs-number-asin-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-number-asin-oracle/Main.purs \ + --expected-exit 42 --timeout 240 --out /tmp/psrs-number-asin-runtime +``` + +The generator invokes the pinned official asin FFI. Finite inputs in [-1, 1] +match that FFI, and negative zero stays negative zero. Values outside the +interval produce NaN. The compiler's checked numberAsin primitive calls the +scalar numeric-runtime export. Wasm has no inverse-sine instruction. + +For Number inverse tangent through the same public-call shape: + +```sh +node ../psrs-stdlib/conformance/number-atan.mjs \ + /private/tmp/ps-pkgs/purescript-numbers /tmp/psrs-number-atan-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-number-atan-oracle/Main.purs \ + --expected-exit 42 --timeout 240 --out /tmp/psrs-number-atan-runtime +``` + +The generator invokes the pinned official atan FFI. Every finite input and +both infinities match that FFI, and negative zero stays negative zero. NaN +produces NaN. The compiler's checked numberAtan primitive calls the scalar +numeric-runtime export. Wasm has no inverse-tangent instruction. + +For Number four-quadrant inverse tangent through the same public-call shape: + +```sh +node ../psrs-stdlib/conformance/number-atan2.mjs \ + /private/tmp/ps-pkgs/purescript-numbers /tmp/psrs-number-atan2-oracle +node ../psrs-stdlib/tools/conformance.mjs run \ + --compiler ./target/debug/psrs --stdlib-root ../psrs-stdlib \ + --input /tmp/psrs-number-atan2-oracle/Main.purs \ + --expected-exit 42 --timeout 240 --out /tmp/psrs-number-atan2-runtime +``` + +The generator invokes the pinned official atan2 FFI. The arguments are `y` +then `x`. Quadrant boundaries, both zero signs, both infinities, and large +ratios match that FFI. Either NaN produces NaN. The compiler's checked +numberAtan2 primitive calls one scalar numeric-runtime export with two +Number arguments. Wasm has no two-argument inverse-tangent instruction. diff --git a/docs/workflow/stdlib-vendoring.md b/docs/workflow/stdlib-vendoring.md new file mode 100644 index 00000000..cba61d46 --- /dev/null +++ b/docs/workflow/stdlib-vendoring.md @@ -0,0 +1,100 @@ +# Standard-library source fidelity + +The independent `psrs-stdlib` package preserves the official PureScript source contract. +The only permitted semantic differences have a concrete Wasm/WASI target or +DEC-16 Unicode scalar/UTF-8 representation justification. This policy governs +both new imports and repairs of the existing library. + +## Upstream provenance + +Record the upstream repository URL, exact release tag, commit, and source hash +for every package/module. Dependency ranges do not identify a reproducible +baseline. Include all package source modules; an omitted module is an explicit +gap, not an implicitly supported subset. Audit byte-level differences and +review changes to APIs and behavior separately. + +Official vendored `.purs` files retain upstream length even when they exceed +500 lines. The repository's 500-line rule continues to apply to maintained +compiler and tooling source. Do not split official modules to satisfy that rule. + +The initial complete audit and pinned comparison baselines are recorded in +[the source audit](../implementation/stdlib/vendor-audit-2026-10-06/report.md). +That audit describes revision 67369ba, including defects; it is not an approved +patch manifest. The current repeatable inventory tool is owned by `psrs-stdlib`: +see [the Node conformance commands](stdlib-conformance.md). Historical reports +retain their original Python commands; the Node port preserves their evidence +fields and uses Git for unified patch generation. + +The [restoration checkpoint](../implementation/stdlib/vendor-restoration-2026-10-06/report.md) +records restored sources, remaining target adaptations, and missing foreign +implementations separately from the historical defects. + +## Preserve ordinary PureScript + +Preserve official pure functions, type signatures, exports, type roles, classes, +instances, and modules. Do not remove a declaration because the compiler cannot +handle it. Do not eta-expand instance methods, replace a valid combinator, +change a record signature, or simplify a module to work around inference, +resolution, deriving, closure, or layout defects. Fix the owning compiler stage +and validate the unchanged source instead. + +Compiler-provided interfaces must have one declared semantic owner and validate +their public source contract. A primitive implementation must not arise from a +function-name heuristic, inferred arity, or a fabricated source body. + +## Foreign implementations and target adaptations + +An official foreign declaration states the public type; the target implements +that contract through an explicit primitive or host binding. The compiler must +retain checked binding identity and type evidence through lowering. Where a +binding is not implemented, compilation or linking reports unsupported target +support. No recursive equation, arbitrary constant, empty result, or permissive +fallback can stand in for the implementation. + +Each deliberate target adaptation records: + +1. The pinned original declaration and exact source difference. +2. The target requirement, with its governing design or decision. +3. The implementation owner and preserved source/API guarantees. +4. Behavior evidence for normal, boundary, and failure cases. +5. Any deliberately different observable behavior and remaining obligations. + +WASI routing and a declared trap-based failure protocol can justify changes to +host I/O and exception handling. They do not justify deleting comparison +assertions, tuple instances, console Show wrappers, or other pure APIs. UTF-8 +storage does not justify removing Show instances or approximating unrelated +numeric rendering. Target limitations must be recorded without redefining +official support around the implemented subset. + +## Verification and reporting + +Keep these acceptance layers separate: + +- Source fidelity: complete inventory, upstream hashes, exact diffs, and reviewed + target adaptations, including omitted module/API checks. +- Compilation: the restored source closure passes the compiler, with missing + support reported at its actual owning stage. +- Runtime: programs actually call the APIs and assert returned values, output, + mutation, sequencing, and failure behavior under mandatory Wasmtime. +- Foreign behavior: each implemented binding has type/ABI checks and observable + behavior comparisons with the official implementation or its explicit target + contract. Keep DEC-16 differences visible in Unicode cases. + +An import-only program with `main = 0` proves none of the runtime assertions. +Dead-code elimination can hide missing implementations and layouts. A runtime +scoreboard without output goldens proves execution without traps, not complete +semantic agreement. Missing or skipped execution is unverified. + +Follow [the compiler iteration SOP](compiler-iteration-sop.md), use focused +tests for each restored interaction, and capture comparable compile diagnoses. +Update roadmap measurements only after the required full scoreboard run. Do not +claim completion while any required API, binding, source-restoration obligation, +or behavior evidence remains missing. + +## Independent package workflow + +The library owns source revisions, upstream pins, target adaptations, and case +data. The compiler owns checked binding protocols and source loading. Follow +[the repository boundary](../design/D-17-stdlib-and-conformance-boundaries.md) +and [the conformance commands](stdlib-conformance.md) for locked consumption, +source audits, official scalar oracles, and mandatory runtime observations. diff --git a/stdlib.lock.json b/stdlib.lock.json new file mode 100644 index 00000000..ede8d76a --- /dev/null +++ b/stdlib.lock.json @@ -0,0 +1,6 @@ +{ + "schema_version": 1, + "path": "../psrs-stdlib", + "revision": "767ffd7f5bbdbf1a2e1bc568b2dce88deeb5ce88", + "source_fingerprint": "fnv1a64-v1:fe305ab57e585eca" +} diff --git a/stdlib/lib/Data/Either.purs b/stdlib/lib/Data/Either.purs deleted file mode 100644 index b22b6b88..00000000 --- a/stdlib/lib/Data/Either.purs +++ /dev/null @@ -1,21 +0,0 @@ - -module Data.Either where - -import Data.Maybe - -data Either a b = Left a | Right b - -either :: forall a b c. (a -> c) -> (b -> c) -> Either a b -> c -either onLeft onRight value = case value of - Left left -> onLeft left - Right right -> onRight right - -hush :: forall a b. Either a b -> Maybe b -hush value = case value of - Left _ -> Nothing - Right right -> Just right - -note :: forall a b. a -> Maybe b -> Either a b -note fallback value = case value of - Nothing -> Left fallback - Just inner -> Right inner diff --git a/stdlib/lib/Data/Eq.purs b/stdlib/lib/Data/Eq.purs deleted file mode 100644 index 97fce2e6..00000000 --- a/stdlib/lib/Data/Eq.purs +++ /dev/null @@ -1,41 +0,0 @@ --- | The `Eq` class and its `==` / `/=` operators. --- | --- | The `Int`, `Number`, `Boolean`, and `Char` instances are the compiler's --- | internal equality intrinsics; `Unit` has one value, so it is equal to --- | itself. The surface operators are library declarations over the internal --- | primitives, so the primitive names stay out of the source namespace. -module Data.Eq - ( class Eq - , eq - , notEq - , (==) - , (/=) - ) where - --- | A type whose values can be compared for equality. -class Eq a where - eq :: a -> a -> Boolean - notEq :: a -> a -> Boolean - -infix 4 eq as == -infix 4 notEq as /= - -instance eqInt :: Eq Int where - eq x y = intEq x y - notEq x y = intNe x y - -instance eqNumber :: Eq Number where - eq x y = numberEq x y - notEq x y = numberNe x y - -instance eqBoolean :: Eq Boolean where - eq x y = booleanEq x y - notEq x y = booleanNe x y - -instance eqChar :: Eq Char where - eq x y = charEq x y - notEq x y = charNe x y - -instance eqUnit :: Eq Unit where - eq _ _ = true - notEq _ _ = false diff --git a/stdlib/lib/Data/Foldable.purs b/stdlib/lib/Data/Foldable.purs deleted file mode 100644 index 5b54e8b0..00000000 --- a/stdlib/lib/Data/Foldable.purs +++ /dev/null @@ -1,66 +0,0 @@ --- | The `Foldable` class. --- | --- | This is the class surface deriving and the fundep work assume: `foldr`, --- | `foldl`, and `foldMap`, not a second traversal implementation. The `Array` --- | instance walks indexes with the compiler's `arrayLength` and `arrayIndex` --- | primitives. `Maybe` and `Either` fold by cases. `fold` is omitted. It would be `foldMap` of the identity under a --- | quantified -- | `Foldable f`, and projecting that rank-2 method from a dictionary parameter --- | fails CC verification, which rejects every program that does not prune --- | unreachable library declarations. Combinators that need --- | `Applicative`, `Alt`, or `Ord` stay out until those classes exist. -module Data.Foldable - ( class Foldable - , foldr - , foldl - , foldMap - ) where - -import Data.Either (Either(..)) -import Data.Maybe (Maybe(..)) -import Data.Monoid (class Monoid, mempty) -import Data.Semigroup (class Semigroup, append) - --- | A container that can be folded. -class Foldable f where - foldr :: forall a b. (a -> b -> b) -> b -> f a -> b - foldl :: forall a b. (b -> a -> b) -> b -> f a -> b - foldMap :: forall a m. Monoid m => (a -> m) -> f a -> m - -instance foldableArray :: Foldable Array where - foldr f z xs = foldrIndex f z xs 0 - foldl f z xs = foldlIndex f z xs 0 - foldMap f xs = foldr (\x acc -> append (f x) acc) mempty xs - -instance foldableMaybe :: Foldable Maybe where - foldr _ z Nothing = z - foldr f z (Just x) = f x z - foldl _ z Nothing = z - foldl f z (Just x) = f z x - foldMap _ Nothing = mempty - foldMap f (Just x) = f x - -instance foldableEither :: Foldable (Either a) where - foldr _ z (Left _) = z - foldr f z (Right x) = f x z - foldl _ z (Left _) = z - foldl f z (Right x) = f z x - foldMap _ (Left _) = mempty - foldMap f (Right x) = f x - --- | Left-to-right index, right-to-left combination: `go i` is --- | `f xs[i] (go (i + 1))`, which is `foldr`. `intLt` is the internal --- | comparison, so this module does not import the `Ord` operator. -foldrIndex :: forall a b. (a -> b -> b) -> b -> Array a -> Int -> b -foldrIndex f z xs i = - if intLt i (arrayLength xs) then - f (arrayIndex xs i) (foldrIndex f z xs (intAdd i 1)) - else - z - --- | Left-to-right walk. The recursive call is in tail position. -foldlIndex :: forall a b. (b -> a -> b) -> b -> Array a -> Int -> b -foldlIndex f z xs i = - if intLt i (arrayLength xs) then - foldlIndex f (f z (arrayIndex xs i)) xs (intAdd i 1) - else - z diff --git a/stdlib/lib/Data/Function.purs b/stdlib/lib/Data/Function.purs deleted file mode 100644 index b80a969e..00000000 --- a/stdlib/lib/Data/Function.purs +++ /dev/null @@ -1,47 +0,0 @@ --- | Function combinators and application operators. --- | --- | The operators are aliases for the value functions here, exactly as the --- | official `Data.Function` declares them: `$` applies a function to an --- | argument, so it is the definition that names `apply`, and the fixity --- | declaration gives the operator its right associativity and lowest --- | precedence. --- | --- | `identity` is **not** here. The official `Prelude` re-exports it from --- | `Control.Category`, where it is the `Category` class method, and defining a --- | private `identity` here first would be the narrower model that has to be --- | replaced when the class hierarchy lands in #94. -module Data.Function - ( apply - , applyFlipped - , const - , flip - , on - , (#) - , ($) - ) where - --- | Applies a function to an argument: `apply f x = f x`. -apply :: forall a b. (a -> b) -> a -> b -apply f x = f x - --- | Applies a function to an argument, with the argument first: --- | `applyFlipped x f = f x`. -applyFlipped :: forall a b. a -> (a -> b) -> b -applyFlipped x f = f x - --- | Returns its first argument and ignores the second. -const :: forall a b. a -> b -> a -const value _ = value - --- | Reverses the argument order of a two-argument function. -flip :: forall a b c. (a -> b -> c) -> b -> a -> c -flip f b a = f a b - --- | Applies a unary function to both arguments before a binary function: --- | `on f g x y = f (g x) (g y)`. -on :: forall a b c. (b -> b -> c) -> (a -> b) -> a -> a -> c -on f g x y = f (g x) (g y) - -infixr 0 apply as $ - -infixl 1 applyFlipped as # diff --git a/stdlib/lib/Data/Functor.purs b/stdlib/lib/Data/Functor.purs deleted file mode 100644 index 9f6a82a1..00000000 --- a/stdlib/lib/Data/Functor.purs +++ /dev/null @@ -1,41 +0,0 @@ --- | The `Functor` class and the `<$>` operator. --- | --- | `map` is the class method. `Prelude` re-exports it and supplies the --- | `Effect` instance, because this module cannot import `Prelude` without a --- | cycle. The `Array` instance walks indexes with `arrayIndex` and builds the --- | result with `arrayAppend`, the same primitives the rest of the library --- | uses for arrays. -module Data.Functor - ( class Functor - , map - , (<$>) - ) where - -import Data.Either (Either(..)) -import Data.Maybe (Maybe(..)) - --- | A type constructor that can apply a function to its contents. -class Functor f where - map :: forall a b. (a -> b) -> f a -> f b - -infixl 4 map as <$> - -instance functorArray :: Functor Array where - map f xs = mapFrom f xs 0 - -instance functorMaybe :: Functor Maybe where - map _ Nothing = Nothing - map f (Just value) = Just (f value) - -instance functorEither :: Functor (Either a) where - map _ (Left value) = Left value - map f (Right value) = Right (f value) - --- | `mapFrom f xs i` is `f xs[i]` followed by the rest. The recursive call is --- | an argument of `arrayAppend`, so this copies the tail at each index. -mapFrom :: forall a b. (a -> b) -> Array a -> Int -> Array b -mapFrom f xs index = - if intLt index (arrayLength xs) then - arrayAppend [f (arrayIndex xs index)] (mapFrom f xs (intAdd index 1)) - else - [] diff --git a/stdlib/lib/Data/Maybe.purs b/stdlib/lib/Data/Maybe.purs deleted file mode 100644 index 6fe76ef1..00000000 --- a/stdlib/lib/Data/Maybe.purs +++ /dev/null @@ -1,14 +0,0 @@ - -module Data.Maybe where - -data Maybe a = Nothing | Just a - -maybe :: forall a b. b -> (a -> b) -> Maybe a -> b -maybe fallback transform value = case value of - Nothing -> fallback - Just inner -> transform inner - -fromMaybe :: forall a. a -> Maybe a -> a -fromMaybe fallback value = case value of - Nothing -> fallback - Just inner -> inner diff --git a/stdlib/lib/Data/Monoid.purs b/stdlib/lib/Data/Monoid.purs deleted file mode 100644 index 9698f4d3..00000000 --- a/stdlib/lib/Data/Monoid.purs +++ /dev/null @@ -1,25 +0,0 @@ --- | The `Monoid` class. --- | --- | `Monoid` is the superclass constraint on `Data.Foldable.foldMap` and --- | `fold`. The class and its `mempty` method are the official surface; the --- | instances are the three types whose `Semigroup` instances already exist, --- | so folding those types does not invent a second appending operation. -module Data.Monoid - ( class Monoid - , mempty - ) where - -import Data.Semigroup (class Semigroup) - --- | A `Semigroup` with an identity element. -class Semigroup m <= Monoid m where - mempty :: m - -instance monoidString :: Monoid String where - mempty = "" - -instance monoidUnit :: Monoid Unit where - mempty = unit - -instance monoidArray :: Monoid (Array a) where - mempty = [] diff --git a/stdlib/lib/Data/Ord.purs b/stdlib/lib/Data/Ord.purs deleted file mode 100644 index 3c5d7fe7..00000000 --- a/stdlib/lib/Data/Ord.purs +++ /dev/null @@ -1,54 +0,0 @@ --- | The `Ord` class and its `<`, `<=`, `>`, and `>=` operators. --- | --- | `Ord` is a subclass of `Eq`. Each instance is defined by the four --- | comparison intrinsics for the ordered types; `Boolean` is ordered by --- | `false < true` and needs no intrinsic. -module Data.Ord - ( class Ord - , lessThan - , lessThanOrEq - , greaterThan - , greaterThanOrEq - , (<) - , (<=) - , (>) - , (>=) - ) where - -import Data.Eq (class Eq) - --- | A type with a total order. Its `Eq` instance agrees with the order. -class Eq a <= Ord a where - lessThan :: a -> a -> Boolean - lessThanOrEq :: a -> a -> Boolean - greaterThan :: a -> a -> Boolean - greaterThanOrEq :: a -> a -> Boolean - -infix 4 lessThan as < -infix 4 lessThanOrEq as <= -infix 4 greaterThan as > -infix 4 greaterThanOrEq as >= - -instance ordInt :: Ord Int where - lessThan x y = intLt x y - lessThanOrEq x y = intLe x y - greaterThan x y = intGt x y - greaterThanOrEq x y = intGe x y - -instance ordNumber :: Ord Number where - lessThan x y = numberLt x y - lessThanOrEq x y = numberLe x y - greaterThan x y = numberGt x y - greaterThanOrEq x y = numberGe x y - -instance ordChar :: Ord Char where - lessThan x y = charLt x y - lessThanOrEq x y = charLe x y - greaterThan x y = charGt x y - greaterThanOrEq x y = charGe x y - -instance ordBoolean :: Ord Boolean where - lessThan left right = if left then false else right - lessThanOrEq left right = if left then right else true - greaterThan left right = if right then false else left - greaterThanOrEq left right = if right then left else true diff --git a/stdlib/lib/Data/Semigroup.purs b/stdlib/lib/Data/Semigroup.purs deleted file mode 100644 index bdef6d10..00000000 --- a/stdlib/lib/Data/Semigroup.purs +++ /dev/null @@ -1,32 +0,0 @@ --- | The `Semigroup` class and its `<>` operator. --- | --- | This is the first class the library itself declares. The class, its --- | method, and the operator alias are all re-exported from `Prelude`, which --- | is what the corpus imports. --- | --- | `append` for `String` is built from the effect-free `stringToBytes` / --- | `arrayAppend` / `bytesToString` primitives rather than a second string --- | concatenation path. Joining two well-formed UTF-8 byte sequences always --- | yields well-formed UTF-8, so `bytesToString`'s validation is a no-op here --- | and the one string representation stays the only writer --- | ([DEC-16](../../../decision/DEC-16-scalar-strings-and-utf8-storage.md)). -module Data.Semigroup - ( class Semigroup - , append - , (<>) - ) where - --- | A type with an associative binary operation. -class Semigroup a where - append :: a -> a -> a - -infixr 5 append as <> - -instance semigroupString :: Semigroup String where - append left right = bytesToString (arrayAppend (stringToBytes left) (stringToBytes right)) - -instance semigroupUnit :: Semigroup Unit where - append _ _ = unit - -instance semigroupArray :: Semigroup (Array a) where - append left right = arrayAppend left right diff --git a/stdlib/lib/Data/Semiring.purs b/stdlib/lib/Data/Semiring.purs deleted file mode 100644 index 86fd6538..00000000 --- a/stdlib/lib/Data/Semiring.purs +++ /dev/null @@ -1,27 +0,0 @@ --- | The `Semiring` class and its `+` / `*` operators. --- | --- | The `Int` and `Number` instances are the compiler's internal arithmetic --- | intrinsics; the surface operators are library declarations over them. -module Data.Semiring - ( class Semiring - , add - , mul - , (+) - , (*) - ) where - --- | A type with addition and multiplication. -class Semiring a where - add :: a -> a -> a - mul :: a -> a -> a - -infixl 6 add as + -infixl 7 mul as * - -instance semiringInt :: Semiring Int where - add x y = intAdd x y - mul x y = intMul x y - -instance semiringNumber :: Semiring Number where - add x y = numberAdd x y - mul x y = numberMul x y diff --git a/stdlib/lib/Data/Show.purs b/stdlib/lib/Data/Show.purs deleted file mode 100644 index 40dae577..00000000 --- a/stdlib/lib/Data/Show.purs +++ /dev/null @@ -1,280 +0,0 @@ --- | The `Show` class. --- | --- | `show` renders a value as text. The `Int`, `Number`, `Boolean`, `Char`, --- | `String`, `Unit`, and `Array` instances are the ones the corpus actually --- | applies `show` to. `Show Unit` lives here rather than in `Data.Unit` --- | because that module is not part of this library yet; a record instance is --- | not here because it needs `reflectSymbol`, which has no runtime. --- | --- | `Boolean`, `Int`, `Char`, `String`, `Unit`, and `Array` match the official --- | spelling, including the `Char`/`String` escapes. `Number` uses the same --- | shape as the official instance — decimal digits, `.0` on an integer token, --- | scientific form outside `(1e-6, 1e21)` — but the digits come from the --- | numeric primitives, not from a correctly rounded ECMAScript conversion. --- | Integers whose absolute value is below `1e21`, powers of ten, and the --- | dyadic fractions the digit loop reaches exactly match; other fractions --- | print a deterministic expansion that can differ from `purs`. -module Data.Show - ( class Show - , show - ) where - -import Data.Semigroup ((<>)) - --- | A type that can be rendered as text. -class Show a where - show :: a -> String - -instance showBoolean :: Show Boolean where - show value = if value then "true" else "false" - -instance showInt :: Show Int where - show value = - if intEq value minInt then "-2147483648" - else if intLt value 0 then "-" <> showPositiveInt (intNeg value) - else showPositiveInt value - -instance showNumber :: Show Number where - show value = - if numberNe value value then "NaN" - else if numberEq value positiveInfinity then "Infinity" - else if numberEq value negativeInfinity then "-Infinity" - else if numberLt value 0.0 then "-" <> showPositiveNumber (numberNeg value) - else showPositiveNumber value - -instance showChar :: Show Char where - show value = "'" <> escapeCode (charToInt value) false false <> "'" - -instance showString :: Show String where - show value = "\"" <> escapeBytes (stringToBytes value) 0 <> "\"" - -instance showUnit :: Show Unit where - show _ = "unit" - -instance showArray :: Show a => Show (Array a) where - show value = "[" <> showElements value 0 <> "]" - -minInt :: Int -minInt = (0 - 2147483647) - 1 - -positiveInfinity :: Number -positiveInfinity = numberDiv 1.0 0.0 - -negativeInfinity :: Number -negativeInfinity = numberDiv (numberNeg 1.0) 0.0 - -showPositiveInt :: Int -> String -showPositiveInt value = - if intLt value 10 then digit value - else showPositiveInt (intDiv value 10) <> digit (intMod value 10) - -digit :: Int -> String -digit value = bytesToString [intAdd 48 value] - --- | A non-negative `Number`. Integers below `1e21` print as decimal digits --- | plus `.0`. Smaller magnitudes print as a fixed expansion. The rest use --- | scientific form, which is what the official `Show Number` does past --- | those thresholds. -showPositiveNumber :: Number -> String -showPositiveNumber value = - if numberEq value 0.0 then "0.0" - else if numberGe value 1.0e21 then showScientific value - else if numberLt value 1.0e-6 then showScientific value - else if numberEq value (floorNumber value) then showIntegerNumber value <> ".0" - else showMixed value - -showMixed :: Number -> String -showMixed value = - let - whole = floorNumber value - fraction = numberSub value whole - wholeText = if numberEq whole 0.0 then "0" else showIntegerNumber whole - fractionText = digitsToString (trimZeros (collectFraction fraction 17 [])) 0 - in - if intEq (arrayLength (stringToBytes fractionText)) 0 then wholeText <> ".0" - else wholeText <> "." <> fractionText - -showScientific :: Number -> String -showScientific value = - let - exponent = powerOfTen value 0 - mantissa = scaleToUnit value exponent - in - showMantissa mantissa <> showExponent exponent - -showMantissa :: Number -> String -showMantissa value = - let - whole = numberToInt (floorNumber value) - fraction = numberSub value (intToNumber whole) - fractionText = digitsToString (trimZeros (collectFraction fraction 17 [])) 0 - in - if intEq (arrayLength (stringToBytes fractionText)) 0 then showPositiveInt whole - else showPositiveInt whole <> "." <> fractionText - -showExponent :: Int -> String -showExponent value = - if intLt value 0 then "e-" <> showPositiveInt (intNeg value) - else "e+" <> showPositiveInt value - -powerOfTen :: Number -> Int -> Int -powerOfTen value exponent = - if numberEq value 0.0 then exponent - else if numberGe value 10.0 then powerOfTen (numberDiv value 10.0) (intAdd exponent 1) - else if numberLt value 1.0 then powerOfTen (numberMul value 10.0) (exponent - 1) - else exponent - -scaleToUnit :: Number -> Int -> Number -scaleToUnit value exponent = - if intEq exponent 0 then value - else if intGt exponent 0 then scaleToUnit (numberDiv value 10.0) (exponent - 1) - else scaleToUnit (numberMul value 10.0) (intAdd exponent 1) - --- | Floor of a non-negative number. Groups of nine digits stay inside `Int`, --- | so a value past `2^31` still floors without a wider primitive. -floorNumber :: Number -> Number -floorNumber value = - if numberLt value 1000000000.0 then intToNumber (numberToInt value) - else - let - billions = floorNumber (numberDiv value 1000000000.0) - base = numberMul billions 1000000000.0 - low = numberToInt (numberSub value base) - in - numberAdd base (intToNumber low) - -showIntegerNumber :: Number -> String -showIntegerNumber value = - if numberLt value 1000000000.0 then showPositiveInt (numberToInt value) - else - let - billions = floorNumber (numberDiv value 1000000000.0) - base = numberMul billions 1000000000.0 - low = numberToInt (numberSub value base) - in - showIntegerNumber billions <> pad9 low - -pad9 :: Int -> String -pad9 value = - if intLt value 10 then "00000000" <> showPositiveInt value - else if intLt value 100 then "0000000" <> showPositiveInt value - else if intLt value 1000 then "000000" <> showPositiveInt value - else if intLt value 10000 then "00000" <> showPositiveInt value - else if intLt value 100000 then "0000" <> showPositiveInt value - else if intLt value 1000000 then "000" <> showPositiveInt value - else if intLt value 10000000 then "00" <> showPositiveInt value - else if intLt value 100000000 then "0" <> showPositiveInt value - else showPositiveInt value - -collectFraction :: Number -> Int -> Array Int -> Array Int -collectFraction fraction remaining digits = - if intEq remaining 0 then digits - else if numberEq fraction 0.0 then digits - else - let - scaled = numberMul fraction 10.0 - digitValue = numberToInt scaled - rest = numberSub scaled (intToNumber digitValue) - in - collectFraction rest (remaining - 1) (arrayAppend digits [digitValue]) - -trimZeros :: Array Int -> Array Int -trimZeros digits = - let - length = arrayLength digits - in - if intEq length 0 then digits - else if intEq (arrayIndex digits (length - 1)) 0 then trimZeros (takePrefix digits (length - 1)) - else digits - -takePrefix :: Array Int -> Int -> Array Int -takePrefix digits count = copyPrefix digits count 0 - -copyPrefix :: Array Int -> Int -> Int -> Array Int -copyPrefix digits count index = - if intGe index count then [] - else arrayAppend [arrayIndex digits index] (copyPrefix digits count (intAdd index 1)) - -digitsToString :: Array Int -> Int -> String -digitsToString digits index = - if intGe index (arrayLength digits) then "" - else digit (arrayIndex digits index) <> digitsToString digits (intAdd index 1) - -showElements :: forall a. Show a => Array a -> Int -> String -showElements values index = - if intGe index (arrayLength values) then "" - else if intEq index 0 then show (arrayIndex values index) <> showElements values (intAdd index 1) - else "," <> show (arrayIndex values index) <> showElements values (intAdd index 1) - --- | The body of a `Char` or `String` escape, without the surrounding quotes. --- | `ampersand` is true when a numeric escape must not swallow a following --- | digit, which is the official `\&` rule. -escapeCode :: Int -> Boolean -> Boolean -> String -escapeCode code ampersand inString = - if intEq code 7 then "\\a" - else if intEq code 8 then "\\b" - else if intEq code 12 then "\\f" - else if intEq code 10 then "\\n" - else if intEq code 13 then "\\r" - else if intEq code 9 then "\\t" - else if intEq code 11 then "\\v" - else if intLt code 32 then "\\" <> showPositiveInt code <> emptyAmpersand ampersand - else if intEq code 127 then "\\127" <> emptyAmpersand ampersand - else if intEq code 92 then "\\\\" - else if intEq code 34 then if inString then "\\\"" else utf8 code - else if intEq code 39 then if inString then utf8 code else "\\'" - else utf8 code - -emptyAmpersand :: Boolean -> String -emptyAmpersand needed = if needed then "\\&" else "" - -escapeBytes :: Array Int -> Int -> String -escapeBytes bytes index = - if intGe index (arrayLength bytes) then "" - else - let - decoded = decode bytes index - code = arrayIndex decoded 0 - next = arrayIndex decoded 1 - in - escapeCode code (nextIsDigit bytes next) true <> escapeBytes bytes next - -nextIsDigit :: Array Int -> Int -> Boolean -nextIsDigit bytes index = - if intGe index (arrayLength bytes) then false - else - let - code = arrayIndex (decode bytes index) 0 - in - if intLt code 48 then false else intLe code 57 - --- | One Unicode scalar and the index of the byte after it. The bytes are --- | well-formed UTF-8 because they came from `stringToBytes`. -decode :: Array Int -> Int -> Array Int -decode bytes index = - let - first = arrayIndex bytes index - in - if intLt first 128 then [first, intAdd index 1] - else if intLt first 224 then - [ intAdd (intShl (intAnd first 31) 6) (continuation bytes (intAdd index 1)) - , intAdd index 2 - ] - else if intLt first 240 then - [ intAdd (intAdd (intShl (intAnd first 15) 12) (intShl (continuation bytes (intAdd index 1)) 6)) (continuation bytes (intAdd index 2)) - , intAdd index 3 - ] - else - [ intAdd (intAdd (intAdd (intShl (intAnd first 7) 18) (intShl (continuation bytes (intAdd index 1)) 12)) (intShl (continuation bytes (intAdd index 2)) 6)) (continuation bytes (intAdd index 3)) - , intAdd index 4 - ] - -continuation :: Array Int -> Int -> Int -continuation bytes index = intAnd (arrayIndex bytes index) 63 - -utf8 :: Int -> String -utf8 code = - if intLt code 128 then bytesToString [code] - else if intLt code 2048 then bytesToString [intOr 192 (intShr code 6), intOr 128 (intAnd code 63)] - else if intLt code 65536 then bytesToString [intOr 224 (intShr code 12), intOr 128 (intAnd (intShr code 6) 63), intOr 128 (intAnd code 63)] - else bytesToString [intOr 240 (intShr code 18), intOr 128 (intAnd (intShr code 12) 63), intOr 128 (intAnd (intShr code 6) 63), intOr 128 (intAnd code 63)] diff --git a/stdlib/lib/Data/Tuple.purs b/stdlib/lib/Data/Tuple.purs deleted file mode 100644 index 0f82c4ee..00000000 --- a/stdlib/lib/Data/Tuple.purs +++ /dev/null @@ -1,38 +0,0 @@ --- | A pair, as the closed record `{ _1, _2 }`. --- | --- | FE-06 already lowers tuple syntax to that record, and DEC-13 maps a WIT --- | `tuple` to the same labels. This module is that record, not a second --- | product: `Tuple a b` and `{ _1 :: a, _2 :: b }` are one type, and `(x, y)` --- | is a value of it. There is no algebraic `Tuple` constructor and no --- | `Eq` / `Ord` / `Show` / `Functor` instance; those would either duplicate --- | the record or belong to the Prelude class slices. -module Data.Tuple - ( Tuple - , fst - , snd - , curry - , uncurry - , swap - ) where - -type Tuple a b = { _1 :: a, _2 :: b } - --- | The first component. `fst (x, y)` is `x`. -fst :: forall a b. Tuple a b -> a -fst tuple = tuple._1 - --- | The second component. `snd (x, y)` is `y`. -snd :: forall a b. Tuple a b -> b -snd tuple = tuple._2 - --- | Turns a function of a pair into a function of two arguments. -curry :: forall a b c. (Tuple a b -> c) -> a -> b -> c -curry f x y = f { _1: x, _2: y } - --- | Turns a function of two arguments into a function of a pair. -uncurry :: forall a b c. (a -> b -> c) -> Tuple a b -> c -uncurry f tuple = f (tuple._1) (tuple._2) - --- | Exchanges the two components. -swap :: forall a b. Tuple a b -> Tuple b a -swap tuple = { _1: tuple._2, _2: tuple._1 } diff --git a/stdlib/lib/Effect.purs b/stdlib/lib/Effect.purs deleted file mode 100644 index d8934f39..00000000 --- a/stdlib/lib/Effect.purs +++ /dev/null @@ -1,23 +0,0 @@ --- | The `Effect` monad: the corpus-facing name for the abstract effect --- | interface this compiler already has. --- | --- | The operations themselves are anchored in `Prelude` rather than here, and --- | that is deliberate rather than an oversight. Two pieces of the compiler key --- | on that module by name: `check_run_effect_scope` resolves the trusted --- | `Prelude.runEffect` value, and `psrs_core::effect::operations` synthesizes --- | `pure`, `bind`, and `run` from the externals declared there. Moving the --- | foreign imports into this module would move the entry point with them. --- | --- | So this module is the public surface the corpus imports, and `Prelude` --- | remains the owner of the primitive interface. The dependency runs one way: --- | `Effect` imports `Prelude`, never the reverse. -module Effect - ( Effect - , pure - , bind - , discard - , map - , apply - ) where - -import Prelude (Effect, apply, bind, discard, map, pure) \ No newline at end of file diff --git a/stdlib/lib/Effect/Console.purs b/stdlib/lib/Effect/Console.purs deleted file mode 100644 index e336027e..00000000 --- a/stdlib/lib/Effect/Console.purs +++ /dev/null @@ -1,23 +0,0 @@ --- | The corpus's console surface, a thin binding over the platform layer --- | `WASI.Console` --- | ([DEC-11](../../../decision/DEC-11-primitive-ffi-stdlib-wrappers.md)). --- | --- | Nothing here chooses a stream or performs a write: `log` and `error` are the --- | platform functions under their corpus names, and `warn` is the platform's. --- | That keeps one place that decides where output goes, rather than a --- | wrapper that could drift from it. --- | --- | `logShow` adds no I/O of its own: it is `log` of `Data.Show.show`, so the --- | rendering is the library's `Show` and the destination is still the one --- | `WASI.Console.log` decides. It is a wrapper like `log`, not a second --- | stringifier. -module Effect.Console (log, warn, error, logShow) where - -import Prelude -import WASI.Console (error, log, warn) -import Data.Show (class Show, show) - --- | Writes the `Show` rendering of a value. `log` already writes the newline, --- | so this is `log` composed with the library's `show`. -logShow :: forall a. Show a => a -> Effect Unit -logShow value = log (show value) diff --git a/stdlib/lib/Prelude.purs b/stdlib/lib/Prelude.purs deleted file mode 100644 index 27aa8668..00000000 --- a/stdlib/lib/Prelude.purs +++ /dev/null @@ -1,94 +0,0 @@ --- | The primitive surface the rest of the library and the corpus build on. --- | --- | `Effect` is an abstract type here: the compiler owns its representation --- | rather than this file, and `psrs_core::effect::lower_effects` rewrites each --- | `Effect a` application into a closure over the runtime token after Typed --- | Core. The operations below are the externals that lowering supplies bodies --- | for, so they stay in this module by name — `check_run_effect_scope` --- | resolves the trusted `Prelude.runEffect` value, and --- | `psrs_core::effect::operations` dispatches on the `psrs:effect` bindings --- | declared here. --- | --- | `unit` is not declared here either. `Unit` is a builtin type with no --- | constructor table, so the one `Unit` value is a compiler primitive --- | (`psrs_hir::Intrinsic::Unit`) rather than a library declaration, exactly as --- | `true` and `false` are. --- | --- | The re-export list is explicit because the module now re-exports the --- | `Data.Function` operators, and this resolver's `module Data.Function` --- | re-export form is not selective: it would pull in `Data.Function.apply` --- | beside the Effect `apply` this module declares. The named entries re-export --- | exactly the four names the official `Prelude` takes from `Data.Function`. -module Prelude - ( Effect - , pure - , bind - , discard - , class Functor - , map - , (<$>) - , apply - , runEffect - , trap - , class Semigroup - , append - , (<>) - , class Eq - , eq - , notEq - , (==) - , (/=) - , class Ord - , lessThan - , lessThanOrEq - , greaterThan - , greaterThanOrEq - , (<) - , (<=) - , (>) - , (>=) - , class Semiring - , add - , mul - , (+) - , (*) - , class Show - , show - , const - , flip - , ($) - , (#) - ) where - -import Data.Function (const, flip, (#), ($)) -import Data.Functor (class Functor, map, (<$>)) -import Data.Semigroup (class Semigroup, append, (<>)) -import Data.Eq (class Eq, eq, notEq, (==), (/=)) -import Data.Ord (class Ord, lessThan, lessThanOrEq, greaterThan, greaterThanOrEq, (<), (<=), (>), (>=)) -import Data.Semiring (class Semiring, add, mul, (+), (*)) -import Data.Show (class Show, show) - -foreign import data Effect :: Type -> Type - -foreign import "psrs:effect#pure" pure :: forall a. a -> Effect a - -foreign import "psrs:effect#bind" bind :: forall a b. Effect a -> (a -> Effect b) -> Effect b - -discard :: forall a b. Effect a -> (a -> Effect b) -> Effect b -discard first next = bind first next - --- | `map` on `Effect` is this instance, not a separate function. The body is --- | the previous `Effect`-only `map`: it builds a new action and does not run --- | `action` until that action is run. -instance functorEffect :: Functor Effect where - map f action = bind action (\value -> pure (f value)) - -apply :: forall a b. Effect (a -> b) -> Effect a -> Effect b -apply f x = bind f (\g -> bind x (\v -> pure (g v))) - -foreign import "psrs:effect#run" runEffect :: forall a. Effect a -> a - --- | The effect that escapes instead of returning. An uncaught failure on this --- | target is a guest trap, so this is the one operation whose result never --- | exists; it is how a library reports an assertion that did not hold. -foreign import "psrs:effect#trap" trap :: Effect Unit diff --git a/stdlib/lib/Test/Assert.purs b/stdlib/lib/Test/Assert.purs deleted file mode 100644 index 477c987f..00000000 --- a/stdlib/lib/Test/Assert.purs +++ /dev/null @@ -1,70 +0,0 @@ --- | The corpus's assertion surface: the `Test.Assert` module the `passing` --- | suite imports. --- | --- | A failed assertion must be visible to whatever runs the program, and this --- | target's only such signal is a guest trap — a non-zero exit code is a --- | recorded result, not a failure. So the failure path writes the message to --- | standard error and then escapes through `Prelude.trap` --- | ([DEC-11](../../../decision/DEC-11-primitive-ffi-stdlib-wrappers.md): the --- | wrapper owns the corpus name, the primitive stays in `Prelude`). --- | --- | **Deliberately absent**, with the reason recorded rather than approximated: --- | --- | - `assertEqual` and `assertEqual'` compare with `Eq` and print with `Show`. --- | Both classes are declared (`Data.Eq`, `Data.Show`), so the surface itself --- | is writable — the official signature is what this compiler cannot yet --- | elaborate. The corpus calls the record form --- | (`assertEqual' "label" { expected: e, actual: a }`), whose official type is --- | `forall a. Eq a => Show a => String -> { actual :: a, expected :: a } -> --- | Effect Unit`. A constraint whose quantified variable appears inside a --- | record type is elaborated with the *record* as the constraint's argument, --- | so `Eq a` is wanted for `{ actual :: a, expected :: a }` and the --- | declaration is rejected with `NoInstanceFound`. The same signature with a --- | type synonym for the record fails identically, so it is constraint --- | elaboration rather than the record syntax. Approximating the signature --- | would change the official API the corpus calls, so the functions stay out --- | until that is fixed. #137 carries the minimal reproduction and the probes --- | that separate this defect from record syntax. --- | - `assertThrows` and `assertThrows'` need to observe that evaluating an --- | argument failed. A trap is not observable from inside the guest without --- | the Wasm exceptions proposal, which is outside the target profile --- | ([DEC-05](../../../decision/DEC-05-wasmtime-feature-set.md)), so there is --- | no honest implementation to write yet. -module Test.Assert (assert, assert', assertTrue, assertFalse) where - -import Prelude -import Effect (Effect) -import Effect.Console (error) - --- | Escapes when the boolean is false. The message is written to standard --- | error first, so the trap carries the diagnostic with it. -assert' :: String -> Boolean -> Effect Unit -assert' message condition = - if condition - then pure unit - else abortWith message - --- | Escapes with the default message when the boolean is false. -assert :: Boolean -> Effect Unit -assert = assert' "Assertion failed" - --- | Escapes unless the value is `true`, naming both values. -assertTrue :: Boolean -> Effect Unit -assertTrue actual = - if actual - then pure unit - else abortWith "Assertion failed: Expected: true\nActual: false" - --- | Escapes unless the value is `false`, naming both values. -assertFalse :: Boolean -> Effect Unit -assertFalse actual = - if actual - then abortWith "Assertion failed: Expected: false\nActual: true" - else pure unit - --- | The escape itself. Kept private: a caller reports a failure by choosing the --- | message, not by reaching for the trap. -abortWith :: String -> Effect Unit -abortWith message = do - _ <- error message - trap diff --git a/stdlib/lib/WASI.purs b/stdlib/lib/WASI.purs deleted file mode 100644 index 42e78885..00000000 --- a/stdlib/lib/WASI.purs +++ /dev/null @@ -1,102 +0,0 @@ --- | The WASI umbrella. A program that wants the common capabilities can --- | `import WASI` and use the consolidated public API. The filesystem and --- | socket surfaces are re-exported explicitly so their `error-code` enums do --- | not collide; their constructors stay available from the focused modules. -module WASI - ( module WASI.Resource - , module WASI.IO - , module WASI.Console - , module WASI.Clock - , module WASI.Random - , module WASI.Process - , Descriptor - , DirectoryEntryStream - , FileError(..) - , DescriptorType(..) - , Advice(..) - , NewTimestamp(..) - , preopens - , preopen - , openAt - , openRead - , openWrite - , openAppend - , readFile - , writeFile - , readViaStream - , writeViaStream - , appendViaStream - , writeString - , readString - , metadataHash - , metadataHashAt - , getFlags - , getType - , readlinkAt - , isSameObject - , createDirectoryAt - , removeDirectoryAt - , unlinkFileAt - , renameAt - , symlinkAt - , sync - , syncData - , setSize - , setTimes - , setTimesAt - , advise - , linkAt - , stat - , statAt - , readDirectory - , readDirectoryEntry - , dropDirectoryEntryStream - , filesystemErrorCode - , dropDescriptor - , withDescriptor - , Network - , TcpSocket - , UdpSocket - , IncomingDatagramStream - , OutgoingDatagramStream - , NetworkError(..) - , IpAddressFamily(..) - , IpSocketAddress(..) - , ShutdownType(..) - , instanceNetwork - , createTcpSocket - , createUdpSocket - , tcpStartBind - , tcpFinishBind - , tcpStartConnect - , tcpFinishConnect - , tcpStartListen - , tcpFinishListen - , tcpAccept - , tcpIsListening - , tcpAddressFamily - , tcpLocalAddress - , tcpRemoteAddress - , tcpShutdown - , tcpKeepAliveIdleTime - , udpStartBind - , udpFinishBind - , udpAddressFamily - , udpLocalAddress - , udpRemoteAddress - , udpSocketStream - , dropNetwork - , dropTcpSocket - , dropUdpSocket - , dropIncomingDatagramStream - , dropOutgoingDatagramStream - ) where - -import WASI.Resource -import WASI.IO -import WASI.Console -import WASI.Clock -import WASI.Random -import WASI.Process -import WASI.FileSystem -import WASI.Network diff --git a/stdlib/lib/WASI/Clock.purs b/stdlib/lib/WASI/Clock.purs deleted file mode 100644 index 1c96ea65..00000000 --- a/stdlib/lib/WASI/Clock.purs +++ /dev/null @@ -1,14 +0,0 @@ --- | Idiomatic wrappers over `wasi:clocks`: the monotonic and wall clocks, and --- | the two clock subscriptions that yield a `Resource Pollable`. -module WASI.Clock (now, wallNow, wallResolution, subscribeInstant, subscribeDuration) where - -import Prelude -import WASI.IO (Pollable) -import WASI.Resource (Resource) - -foreign import "wasi:clocks/monotonic-clock#now" now :: Effect (Int) -foreign import "wasi:clocks/wall-clock#now" wallNow :: Effect ({ seconds :: Int, nanoseconds :: Int }) -foreign import "wasi:clocks/wall-clock#resolution" wallResolution :: Effect ({ seconds :: Int, nanoseconds :: Int }) -foreign import "wasi:clocks/monotonic-clock#subscribe-instant" subscribeInstant :: Int -> Effect (Resource Pollable) -foreign import "wasi:clocks/monotonic-clock#subscribe-duration" subscribeDuration :: Int -> Effect (Resource Pollable) - diff --git a/stdlib/lib/WASI/Console.purs b/stdlib/lib/WASI/Console.purs deleted file mode 100644 index eb713d30..00000000 --- a/stdlib/lib/WASI/Console.purs +++ /dev/null @@ -1,33 +0,0 @@ --- | Convenience output over `WASI.IO`: write a line to standard output or --- | standard error and drop the stream. A failed write is ignored so the --- | public signature stays `String -> Effect Unit`. --- | --- | A WIT `list` is `Array Int`, not `String`, so the wrapper converts the --- | message through the canonical UTF-8 bytes --- | ([DEC-16](../../../decision/DEC-16-scalar-strings-and-utf8-storage.md)). --- --- | `warn` joins `error` on the same stream. The platform layer owns the choice --- | of stream so `Effect.Console` stays a thin binding over it rather than a --- | second place that decides where a diagnostic goes --- | ([DEC-11](../../../decision/DEC-11-primitive-ffi-stdlib-wrappers.md)). -module WASI.Console (log, warn, error) where - -import Prelude -import WASI.IO (blockingWriteAndFlush, dropOutputStream, getStderr, getStdout) - -log :: String -> Effect Unit -log s = do - handle <- getStdout - _ <- blockingWriteAndFlush handle (stringToBytes s) - _ <- blockingWriteAndFlush handle (stringToBytes "\n") - dropOutputStream handle - -warn :: String -> Effect Unit -warn s = error s - -error :: String -> Effect Unit -error s = do - handle <- getStderr - _ <- blockingWriteAndFlush handle (stringToBytes s) - _ <- blockingWriteAndFlush handle (stringToBytes "\n") - dropOutputStream handle diff --git a/stdlib/lib/WASI/FileSystem.purs b/stdlib/lib/WASI/FileSystem.purs deleted file mode 100644 index 834f871e..00000000 --- a/stdlib/lib/WASI/FileSystem.purs +++ /dev/null @@ -1,270 +0,0 @@ --- | Idiomatic wrappers over `wasi:filesystem`. Raw imports stay module --- | private; the public API is expressed with `Resource a` handles and library --- | types (`Maybe`, `Either`, closed records, and an `error-code` ADT). Every --- | `result` maps to `Either` with the error on `Left`. -module WASI.FileSystem - ( Descriptor - , DirectoryEntryStream - , FileError(..) - , DescriptorType(..) - , Advice(..) - , NewTimestamp(..) - , preopens - , preopen - , openAt - , openRead - , openWrite - , openAppend - , readFile - , writeFile - , readViaStream - , writeViaStream - , appendViaStream - , writeString - , readString - , metadataHash - , metadataHashAt - , getFlags - , getType - , readlinkAt - , isSameObject - , createDirectoryAt - , removeDirectoryAt - , unlinkFileAt - , renameAt - , symlinkAt - , sync - , syncData - , setSize - , setTimes - , setTimesAt - , advise - , linkAt - , stat - , statAt - , readDirectory - , readDirectoryEntry - , dropDirectoryEntryStream - , filesystemErrorCode - , dropDescriptor - , withDescriptor - ) where - -import Prelude -import Data.Either (Either(..)) -import Data.Maybe (Maybe(..)) -import WASI.IO (Error, InputStream, OutputStream, StreamError(..), blockingRead, blockingWriteAndFlush, dropError, dropInputStream, dropOutputStream) -import WASI.Resource (Resource) - --- | The opaque phantom naming a filesystem descriptor resource. -foreign import data Descriptor :: Type - --- | The opaque phantom naming a directory-entry stream resource. -foreign import data DirectoryEntryStream :: Type - --- | `error-code` returned by filesystem operations, mapped from the WIT enum. -data FileError - = Access - | WouldBlock - | Already - | BadDescriptor - | Busy - | Deadlock - | Quota - | Exist - | FileTooLarge - | IllegalByteSequence - | InProgress - | Interrupted - | Invalid - | Io - | IsDirectory - | Loop - | TooManyLinks - | MessageSize - | NameTooLong - | NoDevice - | NoEntry - | NoLock - | InsufficientMemory - | InsufficientSpace - | NotDirectory - | NotEmpty - | NotRecoverable - | Unsupported - | NoTty - | NoSuchDevice - | Overflow - | NotPermitted - | Pipe - | ReadOnly - | InvalidSeek - | TextFileBusy - | CrossDevice - --- | `descriptor-type`, mapped from the WIT enum. -data DescriptorType - = Unknown - | BlockDevice - | CharacterDevice - | Directory - | Fifo - | SymbolicLink - | RegularFile - | Socket - --- | `advice`, mapped from the WIT enum. -data Advice = Normal | Sequential | Random | WillNeed | DontNeed | NoReuse - --- | `new-timestamp` taken by `setTimes` and `setTimesAt`: leave the timestamp --- | unchanged, set it to now, or set it to a given `datetime`. -data NewTimestamp - = NoChange - | Now - | Timestamp { seconds :: Int, nanoseconds :: Int } - -defaultPathFlags :: { symlinkFollow :: Boolean } -defaultPathFlags = { symlinkFollow: true } - -defaultOpenFlags :: { create :: Boolean, directory :: Boolean, exclusive :: Boolean, truncate :: Boolean } -defaultOpenFlags = { create: false, directory: false, exclusive: false, truncate: false } - -defaultDescriptorFlags :: { read :: Boolean, write :: Boolean, fileIntegritySync :: Boolean, dataIntegritySync :: Boolean, requestedWriteSync :: Boolean, mutateDirectory :: Boolean } -defaultDescriptorFlags = { read: false, write: false, fileIntegritySync: false, dataIntegritySync: false, requestedWriteSync: false, mutateDirectory: false } - -foreign import "wasi:filesystem/preopens#get-directories" preopens :: Effect (Array { _1 :: Resource Descriptor, _2 :: String }) - --- | The preopened directories. The canonical ABI fixes a tuple's field names --- | to `_1`/`_2`, so each element carries the descriptor first and its path --- | second; `preopen` returns the friendlier named record for one entry. --- | The preopened directory at `index`, as a named record. -preopen :: Int -> Effect { descriptor :: Resource Descriptor, path :: String } -preopen index = - map (\entries -> - let entry = arrayIndex entries index in - { descriptor: entry._1, path: entry._2 } - ) preopens - -foreign import "wasi:filesystem/types#[method]descriptor.open-at" openAt :: Resource Descriptor -> { symlinkFollow :: Boolean } -> String -> { create :: Boolean, directory :: Boolean, exclusive :: Boolean, truncate :: Boolean } -> { read :: Boolean, write :: Boolean, fileIntegritySync :: Boolean, dataIntegritySync :: Boolean, requestedWriteSync :: Boolean, mutateDirectory :: Boolean } -> Effect (Either FileError (Resource Descriptor)) - -openRead :: Resource Descriptor -> String -> Effect (Either FileError (Resource Descriptor)) -openRead descriptor path = - openAt descriptor defaultPathFlags path defaultOpenFlags (defaultDescriptorFlags { read = true }) - -openWrite :: Resource Descriptor -> String -> Effect (Either FileError (Resource Descriptor)) -openWrite descriptor path = - openAt descriptor defaultPathFlags path (defaultOpenFlags { create = true, truncate = true }) (defaultDescriptorFlags { write = true }) - -openAppend :: Resource Descriptor -> String -> Effect (Either FileError (Resource Descriptor)) -openAppend descriptor path = - openAt descriptor defaultPathFlags path defaultOpenFlags (defaultDescriptorFlags { write = true }) - --- | Reads up to `length` bytes at `offset`. A WIT `list` is `Array Int`, --- | not `String`, so the payload carries uninterpreted bytes --- | ([DEC-16](../../../decision/DEC-16-scalar-strings-and-utf8-storage.md)). -foreign import "wasi:filesystem/types#[method]descriptor.read" readRaw :: Resource Descriptor -> Int -> Int -> Effect (Either FileError { _1 :: Array Int, _2 :: Boolean }) - --- | Reads up to `length` bytes at `offset`. `Right` is the decoded byte --- | sequence, `Left` a `FileError`. -readFile :: Resource Descriptor -> Int -> Int -> Effect (Either FileError String) -readFile descriptor length offset = - bind (readRaw descriptor length offset) (\result -> - case result of - Right value -> - let bytes = value._1 - in pure (Right (bytesToString bytes)) - Left err -> pure (Left err)) - -foreign import "wasi:filesystem/types#[method]descriptor.write" writeFile :: Resource Descriptor -> Array Int -> Int -> Effect (Either FileError Int) - -foreign import "wasi:filesystem/types#[method]descriptor.read-via-stream" readViaStream :: Resource Descriptor -> Int -> Effect (Either FileError (Resource InputStream)) -foreign import "wasi:filesystem/types#[method]descriptor.write-via-stream" writeViaStream :: Resource Descriptor -> Int -> Effect (Either FileError (Resource OutputStream)) -foreign import "wasi:filesystem/types#[method]descriptor.append-via-stream" appendViaStream :: Resource Descriptor -> Effect (Either FileError (Resource OutputStream)) - -foreign import "wasi:filesystem/types#filesystem-error-code" filesystemErrorCode :: Resource Error -> Effect (Maybe FileError) - --- | Recovers a filesystem `error-code` from a stream operation failure. The --- | `Error` handle is borrowed by the WIT call. --- | Writes `contents` through an output stream. `Nothing` means the write --- | succeeded; `Just` is the error from opening the stream. The `Either` --- | returned by `blockingWriteAndFlush` is ignored here. -writeString :: Resource Descriptor -> String -> Effect (Maybe FileError) -writeString descriptor contents = - bind (writeViaStream descriptor 0) (\opened -> - case opened of - Left err -> pure (Just err) - Right stream -> - bind (blockingWriteAndFlush stream (stringToBytes contents)) (\ignored -> - bind (dropOutputStream stream) (\ignoredStream -> - pure Nothing))) - --- | Reads up to `length` bytes through an input stream. -readString :: Resource Descriptor -> Int -> Effect (Either FileError String) -readString descriptor length = - bind (readViaStream descriptor 0) (\opened -> - case opened of - Left err -> pure (Left err) - Right stream -> - bind (blockingRead stream length) (\contents -> - bind (dropInputStream stream) (\ignored -> - case contents of - Right bytes -> pure (Right (bytesToString bytes)) - Left (LastOperationFailed err) -> - bind (filesystemErrorCode err) (\code -> - bind (dropError err) (\ignoredError -> - case code of - Just fileError -> pure (Left fileError) - Nothing -> pure (Left Io))) - Left Closed -> pure (Left Io)))) - -foreign import "wasi:filesystem/types#[method]descriptor.metadata-hash" metadataHash :: Resource Descriptor -> Effect (Either FileError { lower :: Int, upper :: Int }) -foreign import "wasi:filesystem/types#[method]descriptor.metadata-hash-at" metadataHashAt :: Resource Descriptor -> { symlinkFollow :: Boolean } -> String -> Effect (Either FileError { lower :: Int, upper :: Int }) - -foreign import "wasi:filesystem/types#[method]descriptor.get-flags" getFlags :: Resource Descriptor -> Effect (Either FileError { read :: Boolean, write :: Boolean, fileIntegritySync :: Boolean, dataIntegritySync :: Boolean, requestedWriteSync :: Boolean, mutateDirectory :: Boolean }) - -foreign import "wasi:filesystem/types#[method]descriptor.get-type" getType :: Resource Descriptor -> Effect (Either FileError DescriptorType) - -foreign import "wasi:filesystem/types#[method]descriptor.readlink-at" readlinkAt :: Resource Descriptor -> String -> Effect (Either FileError String) - -foreign import "wasi:filesystem/types#[method]descriptor.is-same-object" isSameObject :: Resource Descriptor -> Resource Descriptor -> Effect (Boolean) - -foreign import "wasi:filesystem/types#[method]descriptor.stat" stat :: Resource Descriptor -> Effect (Either FileError { type :: DescriptorType, linkCount :: Int, size :: Int, dataAccessTimestamp :: Maybe { seconds :: Int, nanoseconds :: Int }, dataModificationTimestamp :: Maybe { seconds :: Int, nanoseconds :: Int }, statusChangeTimestamp :: Maybe { seconds :: Int, nanoseconds :: Int } }) -foreign import "wasi:filesystem/types#[method]descriptor.stat-at" statAt :: Resource Descriptor -> { symlinkFollow :: Boolean } -> String -> Effect (Either FileError { type :: DescriptorType, linkCount :: Int, size :: Int, dataAccessTimestamp :: Maybe { seconds :: Int, nanoseconds :: Int }, dataModificationTimestamp :: Maybe { seconds :: Int, nanoseconds :: Int }, statusChangeTimestamp :: Maybe { seconds :: Int, nanoseconds :: Int } }) - --- | The attributes of an open file or directory. --- | The attributes of a file or directory named by a relative path. -foreign import "wasi:filesystem/types#[method]descriptor.read-directory" readDirectory :: Resource Descriptor -> Effect (Either FileError (Resource DirectoryEntryStream)) -foreign import "wasi:filesystem/types#[method]directory-entry-stream.read-directory-entry" readDirectoryEntry :: Resource DirectoryEntryStream -> Effect (Either FileError (Maybe { type :: DescriptorType, name :: String })) -foreign import "wasi:filesystem/types#[resource-drop]directory-entry-stream" dropDirectoryEntryStream :: Resource DirectoryEntryStream -> Effect (Unit) - --- | Opens a fresh stream over the entries of a directory. --- | Reads the next entry from a directory stream. `Nothing` reports the end of --- | the stream. --- The remaining operations report `result<_, error-code>`, which DEC-13 maps --- to `Either FileError Unit`: `Right unit` on success, `Left` the error. -foreign import "wasi:filesystem/types#[method]descriptor.create-directory-at" createDirectoryAt :: Resource Descriptor -> String -> Effect (Either FileError Unit) -foreign import "wasi:filesystem/types#[method]descriptor.remove-directory-at" removeDirectoryAt :: Resource Descriptor -> String -> Effect (Either FileError Unit) -foreign import "wasi:filesystem/types#[method]descriptor.unlink-file-at" unlinkFileAt :: Resource Descriptor -> String -> Effect (Either FileError Unit) -foreign import "wasi:filesystem/types#[method]descriptor.rename-at" renameAt :: Resource Descriptor -> String -> Resource Descriptor -> String -> Effect (Either FileError Unit) -foreign import "wasi:filesystem/types#[method]descriptor.symlink-at" symlinkAt :: Resource Descriptor -> String -> String -> Effect (Either FileError Unit) -foreign import "wasi:filesystem/types#[method]descriptor.sync" sync :: Resource Descriptor -> Effect (Either FileError Unit) -foreign import "wasi:filesystem/types#[method]descriptor.sync-data" syncData :: Resource Descriptor -> Effect (Either FileError Unit) -foreign import "wasi:filesystem/types#[method]descriptor.set-size" setSize :: Resource Descriptor -> Int -> Effect (Either FileError Unit) -foreign import "wasi:filesystem/types#[method]descriptor.advise" advise :: Resource Descriptor -> Int -> Int -> Advice -> Effect (Either FileError Unit) -foreign import "wasi:filesystem/types#[method]descriptor.link-at" linkAt :: Resource Descriptor -> { symlinkFollow :: Boolean } -> String -> Resource Descriptor -> String -> Effect (Either FileError Unit) - -foreign import "wasi:filesystem/types#[method]descriptor.set-times" setTimes :: Resource Descriptor -> NewTimestamp -> NewTimestamp -> Effect (Either FileError Unit) -foreign import "wasi:filesystem/types#[method]descriptor.set-times-at" setTimesAt :: Resource Descriptor -> { symlinkFollow :: Boolean } -> String -> NewTimestamp -> NewTimestamp -> Effect (Either FileError Unit) - --- | Adjusts the access and modification timestamps of an open file or --- | directory. `Right unit` reports success; `Left` is the `FileError`. --- | Adjusts the timestamps of a file or directory named by a relative path. -foreign import "wasi:filesystem/types#[resource-drop]descriptor" dropDescriptor :: Resource Descriptor -> Effect (Unit) - --- | Runs `action` with `descriptor`, then drops it. The descriptor is always --- | released once `action` completes. -withDescriptor :: forall a. Resource Descriptor -> (Resource Descriptor -> Effect a) -> Effect a -withDescriptor descriptor action = - bind (action descriptor) \result -> - bind (dropDescriptor descriptor) \_ -> - pure result diff --git a/stdlib/lib/WASI/IO.purs b/stdlib/lib/WASI/IO.purs deleted file mode 100644 index 2c31a297..00000000 --- a/stdlib/lib/WASI/IO.purs +++ /dev/null @@ -1,67 +0,0 @@ --- | Idiomatic wrappers over `wasi:io`. This module merges the former --- | `WASI.Streams`, `WASI.Poll`, `WASI.Error`, and `WASI.Stdin`: the streams, --- | the pollable, and the error resource. Raw imports stay module private; the --- | public API uses `Resource a` for every handle and `Data.Either` for every --- | `result`, with the error on `Left`. -module WASI.IO - ( InputStream - , OutputStream - , Pollable - , Error - , StreamError(..) - , getStdin - , getStdout - , getStderr - , read - , blockingRead - , skip - , blockingSkip - , write - , blockingWriteAndFlush - , flush - , subscribeInputStream - , subscribeOutputStream - , toDebugString - , poll - , ready - , block - , dropInputStream - , dropOutputStream - , dropPollable - , dropError - ) where - -import Prelude -import Data.Either (Either) -import WASI.Resource (Resource) - --- | Opaque phantom types naming the resources a `Resource` may wrap. They are --- | not handle types themselves: every value is a `Resource X`. -foreign import data InputStream :: Type -foreign import data OutputStream :: Type -foreign import data Pollable :: Type -foreign import data Error :: Type - -data StreamError = LastOperationFailed (Resource Error) | Closed - -foreign import "wasi:cli/stdin#get-stdin" getStdin :: Effect (Resource InputStream) -foreign import "wasi:cli/stdout#get-stdout" getStdout :: Effect (Resource OutputStream) -foreign import "wasi:cli/stderr#get-stderr" getStderr :: Effect (Resource OutputStream) -foreign import "wasi:io/streams#[method]input-stream.read" read :: Resource InputStream -> Int -> Effect (Either StreamError (Array Int)) -foreign import "wasi:io/streams#[method]input-stream.blocking-read" blockingRead :: Resource InputStream -> Int -> Effect (Either StreamError (Array Int)) -foreign import "wasi:io/streams#[method]input-stream.skip" skip :: Resource InputStream -> Int -> Effect (Either StreamError Int) -foreign import "wasi:io/streams#[method]input-stream.blocking-skip" blockingSkip :: Resource InputStream -> Int -> Effect (Either StreamError Int) -foreign import "wasi:io/streams#[method]output-stream.write" write :: Resource OutputStream -> Array Int -> Effect (Either StreamError Unit) -foreign import "wasi:io/streams#[method]output-stream.blocking-write-and-flush" blockingWriteAndFlush :: Resource OutputStream -> Array Int -> Effect (Either StreamError Unit) -foreign import "wasi:io/streams#[method]output-stream.flush" flush :: Resource OutputStream -> Effect (Either StreamError Unit) -foreign import "wasi:io/streams#[method]input-stream.subscribe" subscribeInputStream :: Resource InputStream -> Effect (Resource Pollable) -foreign import "wasi:io/streams#[method]output-stream.subscribe" subscribeOutputStream :: Resource OutputStream -> Effect (Resource Pollable) -foreign import "wasi:io/error#[method]error.to-debug-string" toDebugString :: Resource Error -> Effect (String) -foreign import "wasi:io/poll#poll" poll :: Array (Resource Pollable) -> Effect (Array Int) -foreign import "wasi:io/poll#[method]pollable.ready" ready :: Resource Pollable -> Effect (Boolean) -foreign import "wasi:io/poll#[method]pollable.block" block :: Resource Pollable -> Effect (Unit) -foreign import "wasi:io/streams#[resource-drop]input-stream" dropInputStream :: Resource InputStream -> Effect (Unit) -foreign import "wasi:io/streams#[resource-drop]output-stream" dropOutputStream :: Resource OutputStream -> Effect (Unit) -foreign import "wasi:io/poll#[resource-drop]pollable" dropPollable :: Resource Pollable -> Effect (Unit) -foreign import "wasi:io/error#[resource-drop]error" dropError :: Resource Error -> Effect (Unit) - diff --git a/stdlib/lib/WASI/Network.purs b/stdlib/lib/WASI/Network.purs deleted file mode 100644 index ba1a55e0..00000000 --- a/stdlib/lib/WASI/Network.purs +++ /dev/null @@ -1,130 +0,0 @@ --- | Idiomatic wrappers over `wasi:sockets`. Raw imports stay module private; --- | the public API is expressed with `Resource a` handles and library types --- | (`Maybe`, `Either`, closed records, and ADTs). Every `result` maps to --- | `Either` with the error on `Left`. --- | --- | The address-returning methods (`local-address`, `remote-address`) and the --- | datagram stream operation are exposed; their canonical memory layout is --- | modeled by the backend. -module WASI.Network - ( Network - , TcpSocket - , UdpSocket - , IncomingDatagramStream - , OutgoingDatagramStream - , NetworkError(..) - , IpAddressFamily(..) - , IpSocketAddress(..) - , ShutdownType(..) - , instanceNetwork - , createTcpSocket - , createUdpSocket - , tcpStartBind - , tcpFinishBind - , tcpStartConnect - , tcpFinishConnect - , tcpStartListen - , tcpFinishListen - , tcpAccept - , tcpIsListening - , tcpAddressFamily - , tcpLocalAddress - , tcpRemoteAddress - , tcpShutdown - , tcpKeepAliveIdleTime - , udpStartBind - , udpFinishBind - , udpAddressFamily - , udpLocalAddress - , udpRemoteAddress - , udpSocketStream - , dropNetwork - , dropTcpSocket - , dropUdpSocket - , dropIncomingDatagramStream - , dropOutgoingDatagramStream - ) where - -import Prelude -import Data.Either (Either(..)) -import Data.Maybe (Maybe) -import WASI.IO (InputStream, OutputStream) -import WASI.Resource (Resource) - --- | Opaque phantom types naming the socket resources a `Resource` may wrap. -foreign import data Network :: Type -foreign import data TcpSocket :: Type -foreign import data UdpSocket :: Type -foreign import data IncomingDatagramStream :: Type -foreign import data OutgoingDatagramStream :: Type - --- | `error-code`, mapped from the WIT enum. -data NetworkError - = Unknown - | AccessDenied - | NotSupported - | InvalidArgument - | OutOfMemory - | Timeout - | ConcurrencyConflict - | NotInProgress - | WouldBlock - | InvalidState - | NewSocketLimit - | AddressNotBindable - | AddressInUse - | RemoteUnreachable - | ConnectionRefused - | ConnectionReset - | ConnectionAborted - | DatagramTooLarge - | NameUnresolvable - | TemporaryResolverFailure - | PermanentResolverFailure - --- | `ip-address-family`, mapped from the WIT enum. -data IpAddressFamily = Ipv4 | Ipv6 - --- | `ip-socket-address`, mapped from the WIT variant. The constructor names --- | are library-chosen; the ABI matches variant cases by position. -data IpSocketAddress - = IpV4SocketAddress { port :: Int, address :: { _1 :: Int, _2 :: Int, _3 :: Int, _4 :: Int } } - | IpV6SocketAddress { port :: Int, flowInfo :: Int, address :: { _1 :: Int, _2 :: Int, _3 :: Int, _4 :: Int, _5 :: Int, _6 :: Int, _7 :: Int, _8 :: Int }, scopeId :: Int } - --- | `shutdown-type`, mapped from the WIT enum. -data ShutdownType = Receive | Send | Both - -foreign import "wasi:sockets/instance-network#instance-network" instanceNetwork :: Effect (Resource Network) - -foreign import "wasi:sockets/tcp-create-socket#create-tcp-socket" createTcpSocket :: IpAddressFamily -> Effect (Either NetworkError (Resource TcpSocket)) -foreign import "wasi:sockets/udp-create-socket#create-udp-socket" createUdpSocket :: IpAddressFamily -> Effect (Either NetworkError (Resource UdpSocket)) - -foreign import "wasi:sockets/tcp#[method]tcp-socket.start-bind" tcpStartBind :: Resource TcpSocket -> Resource Network -> IpSocketAddress -> Effect (Either NetworkError Unit) -foreign import "wasi:sockets/tcp#[method]tcp-socket.finish-bind" tcpFinishBind :: Resource TcpSocket -> Effect (Either NetworkError Unit) -foreign import "wasi:sockets/tcp#[method]tcp-socket.start-connect" tcpStartConnect :: Resource TcpSocket -> Resource Network -> IpSocketAddress -> Effect (Either NetworkError Unit) -foreign import "wasi:sockets/tcp#[method]tcp-socket.start-listen" tcpStartListen :: Resource TcpSocket -> Effect (Either NetworkError Unit) -foreign import "wasi:sockets/tcp#[method]tcp-socket.finish-listen" tcpFinishListen :: Resource TcpSocket -> Effect (Either NetworkError Unit) -foreign import "wasi:sockets/tcp#[method]tcp-socket.shutdown" tcpShutdown :: Resource TcpSocket -> ShutdownType -> Effect (Either NetworkError Unit) -foreign import "wasi:sockets/tcp#[method]tcp-socket.finish-connect" tcpFinishConnect :: Resource TcpSocket -> Effect (Either NetworkError { _1 :: Resource InputStream, _2 :: Resource OutputStream }) -foreign import "wasi:sockets/tcp#[method]tcp-socket.accept" tcpAccept :: Resource TcpSocket -> Effect (Either NetworkError { _1 :: Resource TcpSocket, _2 :: Resource InputStream, _3 :: Resource OutputStream }) -foreign import "wasi:sockets/tcp#[method]tcp-socket.is-listening" tcpIsListening :: Resource TcpSocket -> Effect (Boolean) -foreign import "wasi:sockets/tcp#[method]tcp-socket.address-family" tcpAddressFamily :: Resource TcpSocket -> Effect (IpAddressFamily) -foreign import "wasi:sockets/tcp#[method]tcp-socket.local-address" tcpLocalAddress :: Resource TcpSocket -> Effect (Either NetworkError IpSocketAddress) -foreign import "wasi:sockets/tcp#[method]tcp-socket.remote-address" tcpRemoteAddress :: Resource TcpSocket -> Effect (Either NetworkError IpSocketAddress) -foreign import "wasi:sockets/tcp#[method]tcp-socket.keep-alive-idle-time" tcpKeepAliveIdleTime :: Resource TcpSocket -> Effect (Either NetworkError Int) - -foreign import "wasi:sockets/udp#[method]udp-socket.start-bind" udpStartBind :: Resource UdpSocket -> Resource Network -> IpSocketAddress -> Effect (Either NetworkError Unit) -foreign import "wasi:sockets/udp#[method]udp-socket.finish-bind" udpFinishBind :: Resource UdpSocket -> Effect (Either NetworkError Unit) -foreign import "wasi:sockets/udp#[method]udp-socket.address-family" udpAddressFamily :: Resource UdpSocket -> Effect (IpAddressFamily) -foreign import "wasi:sockets/udp#[method]udp-socket.local-address" udpLocalAddress :: Resource UdpSocket -> Effect (Either NetworkError IpSocketAddress) -foreign import "wasi:sockets/udp#[method]udp-socket.remote-address" udpRemoteAddress :: Resource UdpSocket -> Effect (Either NetworkError IpSocketAddress) -foreign import "wasi:sockets/udp#[method]udp-socket.stream" udpSocketStream :: Resource UdpSocket -> Maybe IpSocketAddress -> Effect (Either NetworkError { _1 :: Resource IncomingDatagramStream, _2 :: Resource OutgoingDatagramStream }) - --- | Connects the UDP socket to an optional remote address and returns the --- | incoming and outgoing datagram streams. -foreign import "wasi:sockets/network#[resource-drop]network" dropNetwork :: Resource Network -> Effect (Unit) -foreign import "wasi:sockets/tcp#[resource-drop]tcp-socket" dropTcpSocket :: Resource TcpSocket -> Effect (Unit) -foreign import "wasi:sockets/udp#[resource-drop]udp-socket" dropUdpSocket :: Resource UdpSocket -> Effect (Unit) -foreign import "wasi:sockets/udp#[resource-drop]incoming-datagram-stream" dropIncomingDatagramStream :: Resource IncomingDatagramStream -> Effect (Unit) -foreign import "wasi:sockets/udp#[resource-drop]outgoing-datagram-stream" dropOutgoingDatagramStream :: Resource OutgoingDatagramStream -> Effect (Unit) - diff --git a/stdlib/lib/WASI/Process.purs b/stdlib/lib/WASI/Process.purs deleted file mode 100644 index 4eedc626..00000000 --- a/stdlib/lib/WASI/Process.purs +++ /dev/null @@ -1,13 +0,0 @@ --- | Idiomatic wrappers over `wasi:cli`: process exit and the command-line --- | arguments and environment. This module merges the former `WASI.Exit` and --- | `WASI.Environment`. -module WASI.Process (exitWithCode, arguments, environment) where - -import Prelude - -foreign import "wasi:cli/exit#exit-with-code" exitWithCode :: Int -> Effect (Unit) -foreign import "wasi:cli/environment#get-arguments" arguments :: Effect (Array String) -foreign import "wasi:cli/environment#get-environment" environment :: Effect (Array { _1 :: String, _2 :: String }) - --- | The environment variables, each a `(name, value)` pair. The canonical ABI --- | fixes a tuple's field names to `_1`/`_2`. diff --git a/stdlib/lib/WASI/Random.purs b/stdlib/lib/WASI/Random.purs deleted file mode 100644 index a10c7e8f..00000000 --- a/stdlib/lib/WASI/Random.purs +++ /dev/null @@ -1,16 +0,0 @@ -module WASI.Random - ( randomBytes - , randomU64 - , insecureBytes - , insecureU64 - , insecureSeed - ) where - -import Prelude - -foreign import "wasi:random/random#get-random-bytes" randomBytes :: Int -> Effect (Array Int) -foreign import "wasi:random/random#get-random-u64" randomU64 :: Effect (Int) -foreign import "wasi:random/insecure#get-insecure-random-bytes" insecureBytes :: Int -> Effect (Array Int) -foreign import "wasi:random/insecure#get-insecure-random-u64" insecureU64 :: Effect (Int) -foreign import "wasi:random/insecure-seed#insecure-seed" insecureSeed :: Effect ({ _1 :: Int, _2 :: Int }) - diff --git a/stdlib/lib/WASI/Resource.purs b/stdlib/lib/WASI/Resource.purs deleted file mode 100644 index eaec8e64..00000000 --- a/stdlib/lib/WASI/Resource.purs +++ /dev/null @@ -1,14 +0,0 @@ -module WASI.Resource (Resource(..), unResource, withResource) where - -import Prelude - -newtype Resource a = Resource Int - -unResource :: forall a. Resource a -> Int -unResource (Resource index) = index - -withResource :: forall a b. (Resource a -> Effect Unit) -> Resource a -> (Resource a -> Effect b) -> Effect b -withResource release resource action = - bind (action resource) \result -> - bind (release resource) \_ -> - pure result diff --git a/stdlib/lib/trusted b/stdlib/lib/trusted deleted file mode 100644 index 08b7eb17..00000000 --- a/stdlib/lib/trusted +++ /dev/null @@ -1,27 +0,0 @@ -# Trusted standard-library modules, in prefix order. -# Dots are directory separators under this directory. -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